From df79c48ecffcc13bc7a18f89b5a1166f9e120b6f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:38:29 -0700 Subject: [PATCH] fix(pty): validate evidence field types, not just verdicts, in the inspection-evidence reader A foreign host can put any JSON in processEvidence. An observed verdict carrying a non-string processName rode through normalization into recognizeAgentProcess and counted toward the 'foreground is not a recognized agent' leg of the positive-exited gate; non-string unverifiable reasons were forwarded as-is. Out-of-type payloads now degrade to unverifiable (malformed-evidence reason), and non-string reasons fall back to 'unspecified'. --- .../pty-process-inspection-evidence.test.ts | 31 +++++++++++++++++++ src/shared/pty-process-inspection-evidence.ts | 16 ++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) 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' } }