diff --git a/src/shared/pty-process-inspection-evidence.test.ts b/src/shared/pty-process-inspection-evidence.test.ts index e99511f947f..dd05d806018 100644 --- a/src/shared/pty-process-inspection-evidence.test.ts +++ b/src/shared/pty-process-inspection-evidence.test.ts @@ -52,6 +52,37 @@ describe('readPtyProcessInspectionEvidence normalization', () => { expect(evidence.children).toEqual({ verdict: 'unverifiable', reason: 'unspecified' }) }) + it('coerces an observed verdict with a non-string processName to unverifiable', () => { + // A number here would flow into recognizeAgentProcess and count toward the + // "foreground is not a recognized agent" leg — malformed foreign data must + // never feed the positive-exited path. + const evidence = readPtyProcessInspectionEvidence({ + foregroundProcess: null, + hasChildProcesses: false, + processEvidence: { + foreground: { verdict: 'observed', processName: 42 } as never, + children: { verdict: 'exited' } + } + }) + expect(evidence.foreground).toEqual({ + verdict: 'unverifiable', + reason: 'malformed foreground inspection evidence' + }) + }) + + it('replaces a non-string unverifiable reason instead of forwarding it', () => { + const evidence = readPtyProcessInspectionEvidence({ + foregroundProcess: null, + hasChildProcesses: false, + processEvidence: { + foreground: { verdict: 'unverifiable', reason: 42 } as never, + children: { verdict: 'unverifiable', reason: { deep: true } } as never + } + }) + expect(evidence.foreground).toEqual({ verdict: 'unverifiable', reason: 'unspecified' }) + expect(evidence.children).toEqual({ verdict: 'unverifiable', reason: 'unspecified' }) + }) + it('synthesizes the legacy reading when the host predates evidence', () => { expect( readPtyProcessInspectionEvidence({ foregroundProcess: 'codex', hasChildProcesses: true }) diff --git a/src/shared/pty-process-inspection-evidence.ts b/src/shared/pty-process-inspection-evidence.ts index e501738aa85..d2736617417 100644 --- a/src/shared/pty-process-inspection-evidence.ts +++ b/src/shared/pty-process-inspection-evidence.ts @@ -78,14 +78,24 @@ export function readPtyProcessInspectionEvidence(result: { } } +// Field types are validated, not just verdicts: a foreign host can put any +// JSON in these slots, and an out-of-type payload must degrade to +// `unverifiable` — never ride an `observed` verdict into the exit gate. +function normalizeReason(reason: unknown): string { + return typeof reason === 'string' ? reason : 'unspecified' +} + function normalizeForegroundEvidence( evidence: PtyForegroundProcessEvidence | undefined ): PtyForegroundProcessEvidence { if (evidence?.verdict === 'observed') { - return { verdict: 'observed', processName: evidence.processName ?? null } + const processName = evidence.processName ?? null + if (processName === null || typeof processName === 'string') { + return { verdict: 'observed', processName } + } } if (evidence?.verdict === 'unverifiable') { - return { verdict: 'unverifiable', reason: evidence.reason ?? 'unspecified' } + return { verdict: 'unverifiable', reason: normalizeReason(evidence.reason) } } return { verdict: 'unverifiable', reason: 'malformed foreground inspection evidence' } } @@ -97,7 +107,7 @@ function normalizeChildrenEvidence( return { verdict: evidence.verdict } } if (evidence?.verdict === 'unverifiable') { - return { verdict: 'unverifiable', reason: evidence.reason ?? 'unspecified' } + return { verdict: 'unverifiable', reason: normalizeReason(evidence.reason) } } return { verdict: 'unverifiable', reason: 'malformed child-process inspection evidence' } }