feat: surface codex app server hook governance

This commit is contained in:
AI Bot
2026-06-03 11:03:45 +08:00
parent 3c6a0c546b
commit 0071dec860
13 changed files with 154 additions and 13 deletions

View File

@@ -38,6 +38,7 @@ if (args[0] === "app-server" && args[1] === "generate-json-schema") {
{ properties: { method: { const: "thread/start" } } },
{ properties: { method: { const: "thread/inject_items" } } },
{ properties: { method: { const: "skills/extraRoots/set" } } },
{ properties: { method: { const: "hooks/list" } } },
{ properties: { method: { const: "turn/start" } } }
]
}, null, 2));
@@ -45,7 +46,7 @@ if (args[0] === "app-server" && args[1] === "generate-json-schema") {
}
if (args[0] === "app-server" && args[1] === "generate-ts") {
const out = args[args.indexOf("--out") + 1];
writeGenerated(out, "ClientRequest.ts", 'export type ClientRequest = { "method": "thread/start" } | { "method": "skills/extraRoots/set" } | { "method": "turn/start" };\\n');
writeGenerated(out, "ClientRequest.ts", 'export type ClientRequest = { "method": "thread/start" } | { "method": "skills/extraRoots/set" } | { "method": "hooks/list" } | { "method": "turn/start" };\\n');
process.exit(0);
}
console.error("unexpected args " + args.join(" "));
@@ -78,7 +79,14 @@ process.exit(2);
assert.equal(manifest.supports.unixTransport, true);
assert.equal(manifest.supports.threadInjectItems, true);
assert.equal(manifest.supports.skillsExtraRoots, true);
assert.deepEqual(manifest.methods, ["skills/extraRoots/set", "thread/inject_items", "thread/start", "turn/start"]);
assert.equal(manifest.supports.hooksList, true);
assert.deepEqual(manifest.methods, [
"hooks/list",
"skills/extraRoots/set",
"thread/inject_items",
"thread/start",
"turn/start",
]);
assert.match(
await readFile(path.join(outDir, "0.135.0-alpha.1", "app-server-help.txt"), "utf8"),
/ws:\/\/IP:PORT/,

View File

@@ -105,6 +105,19 @@ test("device detail exposes Codex App Server discovered model and extension summ
rootCount: 2,
rootLabels: ["boss-shared-skills", "team-skills"],
},
hookSummary: {
workspaceCount: 1,
hookCount: 2,
enabledHookCount: 1,
managedHookCount: 1,
trustedHookCount: 1,
modifiedHookCount: 1,
untrustedHookCount: 0,
warningCount: 1,
errorCount: 1,
eventNames: ["PreToolUse", "SessionStart"],
handlerTypes: ["command", "prompt"],
},
threadSummary: {
threadCount: 3,
loadedThreadCount: 2,
@@ -156,6 +169,7 @@ test("device detail exposes Codex App Server discovered model and extension summ
assert.equal(cards.capabilities.items.codexAccount, "账号chatgpt · 套餐 pro · 额度 42%");
assert.equal(cards.capabilities.items.codexConfig, "配置App 2 个 · 已启用 1 个 · 托管要求 2 个 · 外部迁移 3 项");
assert.equal(cards.capabilities.items.codexSkillRoots, "共享 Skill 根2 个 · 已下发");
assert.equal(cards.capabilities.items.codexHooks, "Hook2 个 · 启用 1 个 · 警告 1 个");
assert.equal(cards.capabilities.items.codexThreads, "线程3 个 · 已加载 2 个 · 活跃 1 个 · 最新 2026-06-03 16:20");
assert.equal(cards.capabilities.items.codexTurns, "轮次3 个 · 运行中 1 个 · 完成 2 个 · 最新 2026-06-03 16:21");
});

View File

@@ -120,6 +120,63 @@ rl.on("line", (line) => {
return;
}
if (message.method === "hooks/list") {
send({
id: message.id,
result: {
data: [
{
cwd: "/Users/kris/code/boss",
hooks: [
{
key: "session-start-private-key-should-not-leak",
eventName: "SessionStart",
handlerType: "command",
matcher: null,
command: "echo token=sk-secret-should-not-leak",
timeoutSec: 30,
statusMessage: "private hook status should not leak",
sourcePath: "/Users/kris/code/boss/.codex/hooks/private-hook.toml",
source: "project",
pluginId: null,
displayOrder: 1,
enabled: true,
isManaged: true,
currentHash: "hash-secret-should-not-leak",
trustStatus: "trusted",
},
{
key: "pre-tool-private-key-should-not-leak",
eventName: "PreToolUse",
handlerType: "prompt",
matcher: "Bash",
command: null,
timeoutSec: 10,
statusMessage: "modified hook status should not leak",
sourcePath: "/Users/kris/.codex/hooks/private-user-hook.toml",
source: "user",
pluginId: "private-plugin-should-not-leak",
displayOrder: 2,
enabled: false,
isManaged: false,
currentHash: "modified-hash-secret-should-not-leak",
trustStatus: "modified",
},
],
warnings: ["private hook warning should not leak"],
errors: [
{
path: "/Users/kris/code/boss/.codex/hooks/broken-hook.toml",
message: "private hook error token=sk-secret-should-not-leak",
},
],
},
],
},
});
return;
}
if (message.method === "plugin/list") {
send({
id: message.id,

View File

@@ -73,6 +73,19 @@ test("codex app-server discovery includes governance and MCP summaries without l
rootCount: 2,
rootLabels: ["boss-shared-skills", "team-skills"],
});
assert.deepEqual(metadata.hookSummary, {
workspaceCount: 1,
hookCount: 2,
enabledHookCount: 1,
managedHookCount: 1,
trustedHookCount: 1,
modifiedHookCount: 1,
untrustedHookCount: 0,
warningCount: 1,
errorCount: 1,
eventNames: ["PreToolUse", "SessionStart"],
handlerTypes: ["command", "prompt"],
});
assert.equal(metadata.threadSummary.threadCount, 3);
assert.equal(metadata.threadSummary.loadedThreadCount, 2);
assert.equal(metadata.threadSummary.activeThreadCount, 1);
@@ -117,6 +130,14 @@ test("codex app-server discovery includes governance and MCP summaries without l
assert.equal(serialized.includes("private active turn text should not leak"), false);
assert.equal(serialized.includes("private item content should not leak"), false);
assert.equal(serialized.includes("private idle turn text should not leak"), false);
assert.equal(serialized.includes("private-hook.toml"), false);
assert.equal(serialized.includes("private-user-hook.toml"), false);
assert.equal(serialized.includes("broken-hook.toml"), false);
assert.equal(serialized.includes("private hook status"), false);
assert.equal(serialized.includes("private hook warning"), false);
assert.equal(serialized.includes("private hook error"), false);
assert.equal(serialized.includes("session-start-private-key"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
});
function encodeWsTextFrame(value) {