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'.
This commit is contained in:
Brennan Benson
2026-08-28 05:19:39 -07:00
parent 8f8fe97524
commit df79c48ecf
2 changed files with 44 additions and 3 deletions
@@ -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 })
+13 -3
View File
@@ -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' }
}