mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -103,6 +103,10 @@ describe('codexMcpConfig', () => {
|
||||
expect(args[1]).toContain('hooks.SessionStart=[');
|
||||
expect(args[1]).toContain('type = "command"');
|
||||
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:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* configuration and developer instructions at runtime.
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
|
||||
|
||||
/**
|
||||
@@ -51,6 +52,47 @@ function shellJoin(parts: string[]): string {
|
||||
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[] {
|
||||
const { command, args } = getHappyCliCommand([
|
||||
'hook-forwarder',
|
||||
@@ -62,7 +104,10 @@ export function buildSessionStartHookConfigArgs(port: number, token: string): st
|
||||
const hookCommand = shellJoin([command, ...args]);
|
||||
const escapedHookCommand = escapeTomlString(hookCommand);
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user