mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix Codex hook trust hash for matcherless events (UserPromptSubmit/Stop) (#7110)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -150,6 +150,46 @@ describe('computeTrustedHash', () => {
|
||||
expect(a).not.toBe(b)
|
||||
})
|
||||
|
||||
it('drops the matcher on user_prompt_submit/stop like matcher_pattern_for_event', () => {
|
||||
// Why: Codex ignores matchers on these two events and hashes the
|
||||
// identity WITHOUT the matcher field. A definition carrying
|
||||
// `"matcher": ""` (common in third-party installers) must therefore
|
||||
// hash identically to one without a matcher, or the system-trust
|
||||
// mirror misses and the stale-entry sweep deletes the trust Codex
|
||||
// wrote — re-prompting the user on every launch.
|
||||
for (const eventLabel of ['user_prompt_submit', 'stop'] as const) {
|
||||
const base: CodexTrustEntry = {
|
||||
sourcePath: '/x/hooks.json',
|
||||
eventLabel,
|
||||
groupIndex: 0,
|
||||
handlerIndex: 0,
|
||||
command: 'foo'
|
||||
}
|
||||
const bare = computeTrustedHash(base)
|
||||
expect(computeTrustedHash({ ...base, matcher: '' })).toBe(bare)
|
||||
expect(computeTrustedHash({ ...base, matcher: 'anything' })).toBe(bare)
|
||||
}
|
||||
})
|
||||
|
||||
it('pins the matcher-omitted hash for a Stop entry that carries an empty matcher', () => {
|
||||
// Why: regression pin for the shape observed in the wild (a real
|
||||
// Codex 0.140 config.toml, path anonymized): Codex stores the
|
||||
// matcher-omitted hash for Stop even when hooks.json carries
|
||||
// `"matcher": ""`, so we must never fold the matcher into this
|
||||
// identity. If serialization or normalization drifts, this constant
|
||||
// fails loudly.
|
||||
expect(
|
||||
computeTrustedHash({
|
||||
sourcePath: '/home/user/.codex/hooks.json',
|
||||
eventLabel: 'stop',
|
||||
groupIndex: 0,
|
||||
handlerIndex: 0,
|
||||
command: '/home/user/.tma1/hooks/agent-hook.sh',
|
||||
matcher: ''
|
||||
})
|
||||
).toBe('sha256:f8b48c31eabfba63f117b8570b839a5f6efc1d67867512d661775b5312df946f')
|
||||
})
|
||||
|
||||
it('produces a different hash when statusMessage is set', () => {
|
||||
const a = computeTrustedHash({
|
||||
sourcePath: '/x/hooks.json',
|
||||
|
||||
@@ -100,10 +100,37 @@ function canonicalize(value: unknown): unknown {
|
||||
return value
|
||||
}
|
||||
|
||||
// Why: reproduces matcher_pattern_for_event (codex-rs
|
||||
// hooks/src/events/common.rs). Codex ignores matchers on
|
||||
// UserPromptSubmit/Stop and drops them from the hook identity BEFORE
|
||||
// hashing, so a hooks.json entry carrying `"matcher": ""` on those
|
||||
// events must hash the same as one without it. Including it computes a
|
||||
// hash Codex never writes: system trust then looks stale, and
|
||||
// removeStaleRuntimeHookTrustEntries deletes the entry Codex wrote on
|
||||
// every launch — an endless re-trust prompt for those two events.
|
||||
function matcherPatternForEvent(
|
||||
eventLabel: CodexEventLabel,
|
||||
matcher: string | undefined
|
||||
): string | undefined {
|
||||
switch (eventLabel) {
|
||||
case 'user_prompt_submit':
|
||||
case 'stop':
|
||||
return undefined
|
||||
case 'pre_tool_use':
|
||||
case 'permission_request':
|
||||
case 'post_tool_use':
|
||||
case 'pre_compact':
|
||||
case 'post_compact':
|
||||
case 'session_start':
|
||||
return matcher
|
||||
}
|
||||
}
|
||||
|
||||
// Why: reproduces command_hook_hash. NormalizedHookIdentity has `group:
|
||||
// MatcherGroup` flattened in, so the wire shape is { event_name, matcher?,
|
||||
// hooks: [<normalized handler>] }. `matcher` is omitted (not null) when
|
||||
// absent — Rust's Option<String>=None drops through the TOML→JSON path.
|
||||
// absent — Rust's Option<String>=None drops through the TOML→JSON path —
|
||||
// and normalized per event first (see matcherPatternForEvent).
|
||||
// Handler is normalized to timeout=600 (or explicit, min 1) and async=false.
|
||||
export function computeTrustedHash(entry: CodexTrustEntry): string {
|
||||
const handler: Record<string, unknown> = {
|
||||
@@ -119,8 +146,9 @@ export function computeTrustedHash(entry: CodexTrustEntry): string {
|
||||
event_name: entry.eventLabel,
|
||||
hooks: [handler]
|
||||
}
|
||||
if (entry.matcher !== undefined) {
|
||||
identity.matcher = entry.matcher
|
||||
const matcher = matcherPatternForEvent(entry.eventLabel, entry.matcher)
|
||||
if (matcher !== undefined) {
|
||||
identity.matcher = matcher
|
||||
}
|
||||
const serialized = JSON.stringify(canonicalize(identity))
|
||||
return `sha256:${createHash('sha256').update(serialized).digest('hex')}`
|
||||
|
||||
Reference in New Issue
Block a user