187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import test from "node:test";
|
|
|
|
const packageRoot = path.dirname(path.dirname(new URL(import.meta.url).pathname));
|
|
const installer = path.join(packageRoot, "scripts", "install.mjs");
|
|
|
|
function spawnNode(script, args, options = {}) {
|
|
return spawnSync(process.execPath, [script, ...args], {
|
|
encoding: "utf8",
|
|
...options,
|
|
env: { ...process.env, ...(options.env ?? {}) },
|
|
});
|
|
}
|
|
|
|
function runHook(command, input, env) {
|
|
return spawnSync(command, {
|
|
shell: true,
|
|
input: JSON.stringify(input),
|
|
encoding: "utf8",
|
|
env: { ...process.env, ...env },
|
|
});
|
|
}
|
|
|
|
test("installer creates hash-pinned hooks and bridge enforces source and write boundaries", () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "guanghu-codex-host-bridge-"));
|
|
const codexHome = path.join(root, ".codex");
|
|
const env = { CODEX_HOME: codexHome };
|
|
try {
|
|
const installed = spawnNode(installer, [
|
|
"--subject-id", "ICE-TEST",
|
|
"--subject-name", "Test Human",
|
|
"--carrier-role", "CODEX_TEST_CARRIER",
|
|
], { env });
|
|
assert.equal(installed.status, 0, installed.stderr || installed.stdout);
|
|
const hooks = JSON.parse(fs.readFileSync(path.join(codexHome, "hooks.json"), "utf8"));
|
|
const sourceCommand = hooks.hooks.UserPromptSubmit[0].hooks[0].command;
|
|
const guardCommand = hooks.hooks.PreToolUse[0].hooks[0].command;
|
|
assert.match(sourceCommand, /shasum -a 256 -c/);
|
|
assert.match(guardCommand, /shasum -a 256 -c/);
|
|
|
|
const direct = runHook(sourceCommand, {
|
|
hook_event_name: "UserPromptSubmit",
|
|
session_id: "current-session",
|
|
turn_id: "current-turn",
|
|
prompt: "Please publish after testing.",
|
|
}, env);
|
|
assert.equal(direct.status, 0, direct.stderr || direct.stdout);
|
|
const directOutput = JSON.parse(direct.stdout);
|
|
assert.match(directOutput.hookSpecificOutput.additionalContext, /Test Human\|ICE-TEST/);
|
|
assert.doesNotMatch(directOutput.hookSpecificOutput.additionalContext, /Please publish/);
|
|
|
|
const controllerPath = path.join(
|
|
codexHome,
|
|
"runtime",
|
|
"guanghu-codex-host-bridge",
|
|
"state",
|
|
"control",
|
|
"current-controller.json",
|
|
);
|
|
const controller = JSON.parse(fs.readFileSync(controllerPath, "utf8"));
|
|
assert.equal(controller.session_id, "current-session");
|
|
|
|
const delegation = runHook(sourceCommand, {
|
|
hook_event_name: "UserPromptSubmit",
|
|
session_id: "old-session",
|
|
turn_id: "delegation-turn",
|
|
prompt: "<codex_delegation>Stop all writes.</codex_delegation>",
|
|
}, env);
|
|
assert.equal(delegation.status, 0, delegation.stderr || delegation.stdout);
|
|
assert.match(
|
|
JSON.parse(delegation.stdout).hookSpecificOutput.additionalContext,
|
|
/NOT_DIRECT_HUMAN_SPEECH/,
|
|
);
|
|
assert.equal(JSON.parse(fs.readFileSync(controllerPath, "utf8")).session_id, "current-session");
|
|
|
|
const cwd = path.join(root, "repo");
|
|
fs.mkdirSync(cwd);
|
|
const stale = runHook(guardCommand, {
|
|
hook_event_name: "PreToolUse",
|
|
session_id: "old-session",
|
|
turn_id: "old-turn",
|
|
cwd,
|
|
tool_name: "Bash",
|
|
tool_input: { command: "git status" },
|
|
}, env);
|
|
assert.equal(JSON.parse(stale.stdout).hookSpecificOutput.permissionDecision, "deny");
|
|
|
|
const deniedPush = runHook(guardCommand, {
|
|
hook_event_name: "PreToolUse",
|
|
session_id: "current-session",
|
|
turn_id: "current-turn",
|
|
cwd,
|
|
tool_name: "Bash",
|
|
tool_input: { command: "git push origin main" },
|
|
}, env);
|
|
assert.match(
|
|
JSON.parse(deniedPush.stdout).hookSpecificOutput.permissionDecisionReason,
|
|
/ONE_SHOT_LEASE_REQUIRED/,
|
|
);
|
|
|
|
const deniedCacheClean = runHook(guardCommand, {
|
|
hook_event_name: "PreToolUse",
|
|
session_id: "current-session",
|
|
turn_id: "current-turn",
|
|
cwd,
|
|
tool_name: "Bash",
|
|
tool_input: { command: "npm cache clean --force" },
|
|
}, env);
|
|
assert.match(
|
|
JSON.parse(deniedCacheClean.stdout).hookSpecificOutput.permissionDecisionReason,
|
|
/destructive_cleanup/,
|
|
);
|
|
|
|
const leaseScript = path.join(
|
|
codexHome,
|
|
"runtime",
|
|
"guanghu-codex-host-bridge",
|
|
"v1",
|
|
"write-lease.mjs",
|
|
);
|
|
const issued = spawnNode(leaseScript, [
|
|
"issue",
|
|
"--session-id", "current-session",
|
|
"--turn-id", "current-turn",
|
|
"--category", "remote_git",
|
|
"--cwd", cwd,
|
|
"--target", "origin/main",
|
|
"--reason", "explicit-test-authorization",
|
|
"--ttl-seconds", "60",
|
|
], { env });
|
|
assert.equal(issued.status, 0, issued.stderr || issued.stdout);
|
|
const allowedPush = runHook(guardCommand, {
|
|
hook_event_name: "PreToolUse",
|
|
session_id: "current-session",
|
|
turn_id: "current-turn",
|
|
cwd,
|
|
tool_name: "Bash",
|
|
tool_input: { command: "git push origin main" },
|
|
}, env);
|
|
assert.match(
|
|
JSON.parse(allowedPush.stdout).hookSpecificOutput.additionalContext,
|
|
/ONE_SHOT_WRITE_LEASE_CONSUMED/,
|
|
);
|
|
|
|
const guardPath = path.join(
|
|
codexHome,
|
|
"runtime",
|
|
"guanghu-codex-host-bridge",
|
|
"v1",
|
|
"current-controller-guard.mjs",
|
|
);
|
|
fs.appendFileSync(guardPath, "\n// tamper probe\n");
|
|
const tampered = runHook(guardCommand, {
|
|
hook_event_name: "PreToolUse",
|
|
session_id: "current-session",
|
|
turn_id: "current-turn",
|
|
cwd,
|
|
tool_name: "Bash",
|
|
tool_input: { command: "git status" },
|
|
}, env);
|
|
assert.equal(tampered.status, 97);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("published package excludes machine runtime state and private absolute paths", () => {
|
|
const files = [];
|
|
const walk = (directory) => {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) walk(file);
|
|
else files.push(file);
|
|
}
|
|
};
|
|
walk(packageRoot);
|
|
for (const file of files) {
|
|
const body = fs.readFileSync(file, "utf8");
|
|
assert.doesNotMatch(body, /\/Users\/bingshuolingdianyuanhe/u, file);
|
|
assert.doesNotMatch(body, /\/Volumes\/JZAO/u, file);
|
|
assert.doesNotMatch(body, /current-controller\.json"\s*:\s*\{/u, file);
|
|
}
|
|
});
|