feat: surface codex app server hook governance
This commit is contained in:
@@ -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/,
|
||||
|
||||
@@ -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, "Hook:2 个 · 启用 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");
|
||||
});
|
||||
|
||||
57
tests/fixtures/codex-app-server-runtime.mjs
vendored
57
tests/fixtures/codex-app-server-runtime.mjs
vendored
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user