fix(cli): trust injected Codex session hook

Codex now requires hook trust before non-managed hooks can run. HAPI relies on the runtime-injected SessionStart hook to receive the Codex thread/session id, so leaving that hook untrusted breaks local Codex startup without manual /hooks review.\n\nGenerate the same trusted_hash Codex derives for the injected SessionStart command and pass it through the runtime hooks.state override. The trust is scoped to the synthetic session-flags hook key and the exact generated command, so user, project, and plugin hooks still go through Codex review normally.\n\nAlso cover the generated config args so future changes keep both the hook declaration and its trust state together.\n\nValidation:\n- bun test cli/src/codex/utils/codexMcpConfig.test.ts\n- bun typecheck
This commit is contained in:
weishu
2026-05-08 19:09:38 +08:00
parent 991b01cd10
commit 2fe1a2ed45
2 changed files with 50 additions and 1 deletions
@@ -103,6 +103,10 @@ describe('codexMcpConfig', () => {
expect(args[1]).toContain('hooks.SessionStart=['); expect(args[1]).toContain('hooks.SessionStart=[');
expect(args[1]).toContain('type = "command"'); expect(args[1]).toContain('type = "command"');
expect(args[1]).toContain('hook-forwarder --port 4312 --token secret-token'); expect(args[1]).toContain('hook-forwarder --port 4312 --token secret-token');
expect(args[2]).toBe('-c');
expect(args[3]).toContain('hooks.state={');
expect(args[3]).toContain(':session_start:0:0');
expect(args[3]).toContain('trusted_hash="sha256:');
}); });
}); });
}); });
+46 -1
View File
@@ -7,6 +7,7 @@
* configuration and developer instructions at runtime. * configuration and developer instructions at runtime.
*/ */
import { createHash } from 'node:crypto';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI'; import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
/** /**
@@ -51,6 +52,47 @@ function shellJoin(parts: string[]): string {
return parts.map(shellQuote).join(' '); return parts.map(shellQuote).join(' ');
} }
function canonicalJson(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(canonicalJson);
}
if (value && typeof value === 'object') {
const entries = Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([key, entryValue]) => [key, canonicalJson(entryValue)]);
return Object.fromEntries(entries);
}
return value;
}
function versionForTomlLikeValue(value: unknown): string {
const serialized = JSON.stringify(canonicalJson(value));
return `sha256:${createHash('sha256').update(serialized).digest('hex')}`;
}
function buildSessionStartHookTrustedHash(command: string): string {
return versionForTomlLikeValue({
event_name: 'session_start',
hooks: [
{
async: false,
command,
timeout: 600,
type: 'command'
}
]
});
}
function sessionFlagsHookStateKey(): string {
const sourcePath = process.platform === 'win32'
? 'C:\\<session-flags>\\config.toml'
: '/<session-flags>/config.toml';
return `${sourcePath}:session_start:0:0`;
}
export function buildSessionStartHookConfigArgs(port: number, token: string): string[] { export function buildSessionStartHookConfigArgs(port: number, token: string): string[] {
const { command, args } = getHappyCliCommand([ const { command, args } = getHappyCliCommand([
'hook-forwarder', 'hook-forwarder',
@@ -62,7 +104,10 @@ export function buildSessionStartHookConfigArgs(port: number, token: string): st
const hookCommand = shellJoin([command, ...args]); const hookCommand = shellJoin([command, ...args]);
const escapedHookCommand = escapeTomlString(hookCommand); const escapedHookCommand = escapeTomlString(hookCommand);
const hookConfig = `hooks.SessionStart=[{ hooks = [{ type = "command", command = "${escapedHookCommand}" }] }]`; const hookConfig = `hooks.SessionStart=[{ hooks = [{ type = "command", command = "${escapedHookCommand}" }] }]`;
return ['-c', hookConfig]; const trustedHash = buildSessionStartHookTrustedHash(hookCommand);
const escapedStateKey = escapeTomlString(sessionFlagsHookStateKey());
const hookState = `hooks.state={"${escapedStateKey}"={trusted_hash="${trustedHash}"}}`;
return ['-c', hookConfig, '-c', hookState];
} }
/** /**