From 317b408ff43b14249326874311df38863bcbc1e5 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 28 Jul 2026 19:04:58 -0400 Subject: [PATCH 1/8] feat(pi): guard write/edit content against leaked model transport tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gpt-5.x can bleed its own function-call tokens into a string argument mid-stream: run 9704a73e wrote `.functions.complete_task (commentary ...` plus a DEL byte into a customer's global-error.tsx, and exact-match edits could not remove it before the task deadline — the run shipped a syntax-broken file. Reject write content and edit newText carrying transport signatures before they reach disk; oldText stays unguarded so repairs of already-corrupted files keep working. Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../pi/__tests__/content-guard.test.ts | 120 ++++++++++++++++++ .../agent/runner/harness/pi/content-guard.ts | 100 +++++++++++++++ src/lib/agent/runner/harness/pi/index.ts | 15 ++- src/lib/agent/runner/harness/pi/task.ts | 17 ++- 4 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts create mode 100644 src/lib/agent/runner/harness/pi/content-guard.ts diff --git a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts new file mode 100644 index 00000000..8f5a0ffa --- /dev/null +++ b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + contentLeak, + withContentGuard, + pickEditContent, + pickWriteContent, +} from '../content-guard'; + +vi.mock('@utils/analytics', () => ({ + analytics: { wizardCapture: vi.fn() }, +})); +vi.mock('@utils/debug', () => ({ logToFile: vi.fn() })); + +// The exact garbage run 9704a73e wrote into a customer's global-error.tsx. +const LEAKED_LINE = + "' }#+#+#+#+.functions.complete_task (commentary json.functions.complete_taskjson>tagger历山大发 { "; + +describe('contentLeak', () => { + it('flags the observed transport-token leak', () => { + expect(contentLeak(LEAKED_LINE)).toBe('leaked tool-call tokens'); + }); + + it('flags channel markers and control characters', () => { + expect(contentLeak('x<|channel|>y')).toBe('leaked channel markers'); + expect(contentLeak('trailing\x7f')).toBe('control characters'); + expect(contentLeak('null\0byte')).toBe('control characters'); + }); + + it('passes real source content, including Firebase functions.* code', () => { + expect(contentLeak('export default function GlobalError() {}')).toBe( + undefined, + ); + expect(contentLeak('const f = functions.https.onRequest(app);')).toBe( + undefined, + ); + expect(contentLeak('line one\n\ttabbed\r\nline two')).toBe(undefined); + }); +}); + +function makeTool() { + const execute = vi.fn().mockResolvedValue({ content: [], details: {} }); + return { tool: { name: 'write', execute }, execute }; +} + +describe('withContentGuard', () => { + it('blocks a write whose content carries the leak and never executes', async () => { + const { tool, execute } = makeTool(); + const guarded = withContentGuard(tool, pickWriteContent); + const result = (await guarded.execute( + ...([ + 'id1', + { content: LEAKED_LINE }, + undefined, + undefined, + {}, + ] as never[]), + )) as { isError?: boolean; content: [{ text: string }] }; + expect(execute).not.toHaveBeenCalled(); + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/NOT written/); + }); + + it('passes clean writes through to the real execute', async () => { + const { tool, execute } = makeTool(); + const guarded = withContentGuard(tool, pickWriteContent); + await guarded.execute( + ...([ + 'id1', + { content: 'clean file\n' }, + undefined, + undefined, + {}, + ] as never[]), + ); + expect(execute).toHaveBeenCalledOnce(); + }); + + it('guards edit newText but lets oldText match existing garbage (repairs stay possible)', async () => { + const { tool, execute } = makeTool(); + const guarded = withContentGuard(tool, pickEditContent); + // Repair edit: garbage in oldText, clean newText — must pass. + await guarded.execute( + ...([ + 'id1', + { path: 'x.tsx', edits: [{ oldText: LEAKED_LINE, newText: '\n' }] }, + undefined, + undefined, + {}, + ] as never[]), + ); + expect(execute).toHaveBeenCalledOnce(); + // Corrupted newText — must block. + const result = (await guarded.execute( + ...([ + 'id2', + { path: 'x.tsx', edits: [{ oldText: '\n', newText: LEAKED_LINE }] }, + undefined, + undefined, + {}, + ] as never[]), + )) as { isError?: boolean }; + expect(result.isError).toBe(true); + expect(execute).toHaveBeenCalledOnce(); + }); + + it('lets malformed params fall through to the tool own validation', async () => { + const { tool, execute } = makeTool(); + const guarded = withContentGuard(tool, pickEditContent); + await guarded.execute( + ...([ + 'id1', + { edits: 'not-an-array' }, + undefined, + undefined, + {}, + ] as never[]), + ); + expect(execute).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts new file mode 100644 index 00000000..0576cecb --- /dev/null +++ b/src/lib/agent/runner/harness/pi/content-guard.ts @@ -0,0 +1,100 @@ +/** + * File-content guard for the pi harness's write/edit tools. + * + * gpt-5.x can leak its own function-call transport tokens into a string + * argument mid-stream: run 9704a73e wrote + * `' }#+#+#+#+.functions.complete_task (commentary …json.functions.complete_taskjson>tagger…` + * plus a DEL byte into a customer's global-error.tsx. Once on disk the + * garbage is nearly unremovable by exact-match edits, so the run shipped a + * syntax-broken file. Reject the call before it reaches disk and tell the + * model to re-emit — only NEW content is guarded; `edit.oldText` must stay + * free to match (and remove) garbage already in a file. + */ +import { analytics } from '@utils/analytics'; +import { logToFile } from '@utils/debug'; + +// Transport-token shapes that never belong in written file content. Scoped to +// the wizard's own orchestrator tool names — `functions.` alone would +// false-positive on real code (e.g. Firebase's `functions.config()`). +const LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ + { + pattern: /functions\.(?:complete_task|enqueue_task|read_handoffs)/, + label: 'leaked tool-call tokens', + }, + { + pattern: /<\|(?:channel|constrain|message|call|end|start|return)\|>/, + label: 'leaked channel markers', + }, + // C0 controls and DEL, minus tab/newline/CR — never valid in source text. + // eslint-disable-next-line no-control-regex + { pattern: /[\0-\x08\x0B\f\x0E-\x1F\x7F]/, label: 'control characters' }, +]; + +/** The leak label when content is corrupted, else undefined. */ +export function contentLeak(content: string): string | undefined { + return LEAK_PATTERNS.find(({ pattern }) => pattern.test(content))?.label; +} + +type GuardableTool = { + name: string; + execute: (...args: never[]) => Promise; +}; + +/** + * Wrap a pi write/edit tool so corrupted content is rejected with a + * corrective error instead of reaching disk. `pick` extracts the strings + * that will be WRITTEN (write content, edit newText) from the tool params. + */ +export function withContentGuard( + tool: T, + pick: (params: unknown) => readonly string[], +): T { + const execute = tool.execute.bind(tool) as ( + ...args: unknown[] + ) => Promise; + tool.execute = ((...args: unknown[]) => { + const params = args[1]; + let leak: string | undefined; + try { + leak = pick(params).map(contentLeak).find(Boolean); + } catch { + leak = undefined; // malformed params: let the tool's own validation speak + } + if (leak) { + analytics.wizardCapture('file content guard tripped', { + tool: tool.name, + leak, + }); + logToFile(`[content-guard] blocked ${tool.name}: ${leak}`); + return Promise.resolve({ + content: [ + { + type: 'text', + text: + `Error: the new file content contains ${leak} from the model transport — ` + + 'it was NOT written. Re-issue the call with only the intended file content.', + }, + ], + details: {}, + isError: true, + }); + } + return execute(...args); + }) as T['execute']; + return tool; +} + +/** Strings a `write` call would put on disk. */ +export function pickWriteContent(params: unknown): readonly string[] { + const content = (params as { content?: unknown })?.content; + return typeof content === 'string' ? [content] : []; +} + +/** Strings an `edit` call would put on disk (newText only — oldText must match existing bytes). */ +export function pickEditContent(params: unknown): readonly string[] { + const edits = (params as { edits?: unknown })?.edits; + if (!Array.isArray(edits)) return []; + return edits + .map((e: unknown) => (e as { newText?: unknown })?.newText) + .filter((t): t is string => typeof t === 'string'); +} diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index b9db3cf2..7decb6a7 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -28,6 +28,11 @@ import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent/signals'; import { AgentOutputSignals } from '@lib/agent/output-signals'; import { assembleCommandments } from '../../switchboard/commandments'; import { buildGatewayProvider, GATEWAY_PROVIDER } from './gateway'; +import { + withContentGuard, + pickEditContent, + pickWriteContent, +} from './content-guard'; import type { AgentResult, AgentHarness, @@ -360,8 +365,14 @@ export const piBackend: AgentHarness = { // are the stock definitions. Reads run in parallel so a batched turn of // independent reads executes at once; edit/write/bash stay sequential. withMode(createReadToolDefinition(session.installDir), 'parallel'), - withMode(createEditToolDefinition(session.installDir), 'sequential'), - withMode(createWriteToolDefinition(session.installDir), 'sequential'), + withContentGuard( + withMode(createEditToolDefinition(session.installDir), 'sequential'), + pickEditContent, + ), + withContentGuard( + withMode(createWriteToolDefinition(session.installDir), 'sequential'), + pickWriteContent, + ), scrubbedBash, // Native ls/find/grep so the agent explores with proper tools instead // of fence-blocked `bash {ls/find}` (the profiled retry-spirals came diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 37398e19..c9c0dfbb 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -44,6 +44,11 @@ import { lastStatusLine, withMode, } from './index'; +import { + withContentGuard, + pickEditContent, + pickWriteContent, +} from './content-guard'; /** wizard tool vocabulary → the pi tool definitions it unlocks. */ const CODING_TOOL_MAP: Record = { @@ -263,8 +268,16 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { const dir = session.installDir; const codingToolFactories = { read: () => withMode(createReadToolDefinition(dir), 'parallel'), - edit: () => withMode(createEditToolDefinition(dir), 'sequential'), - write: () => withMode(createWriteToolDefinition(dir), 'sequential'), + edit: () => + withContentGuard( + withMode(createEditToolDefinition(dir), 'sequential'), + pickEditContent, + ), + write: () => + withContentGuard( + withMode(createWriteToolDefinition(dir), 'sequential'), + pickWriteContent, + ), bash: () => withMode( createBashToolDefinition(dir, { From a55aa7307ebd6caa27d9e34682f52936658b382c Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 09:30:13 -0400 Subject: [PATCH 2/8] style(pi): one-line comments in the content guard Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../agent/runner/harness/pi/content-guard.ts | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts index 0576cecb..ce39f00b 100644 --- a/src/lib/agent/runner/harness/pi/content-guard.ts +++ b/src/lib/agent/runner/harness/pi/content-guard.ts @@ -1,21 +1,8 @@ -/** - * File-content guard for the pi harness's write/edit tools. - * - * gpt-5.x can leak its own function-call transport tokens into a string - * argument mid-stream: run 9704a73e wrote - * `' }#+#+#+#+.functions.complete_task (commentary …json.functions.complete_taskjson>tagger…` - * plus a DEL byte into a customer's global-error.tsx. Once on disk the - * garbage is nearly unremovable by exact-match edits, so the run shipped a - * syntax-broken file. Reject the call before it reaches disk and tell the - * model to re-emit — only NEW content is guarded; `edit.oldText` must stay - * free to match (and remove) garbage already in a file. - */ +// Rejects pi write/edit content carrying leaked model transport tokens (run 9704a73e shipped them into a customer file) before it reaches disk. import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; -// Transport-token shapes that never belong in written file content. Scoped to -// the wizard's own orchestrator tool names — `functions.` alone would -// false-positive on real code (e.g. Firebase's `functions.config()`). +// Scoped to the wizard's own tool names — a generic `functions.*` match would false-positive on real code like Firebase's `functions.config()`. const LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ { pattern: /functions\.(?:complete_task|enqueue_task|read_handoffs)/, @@ -25,7 +12,7 @@ const LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ pattern: /<\|(?:channel|constrain|message|call|end|start|return)\|>/, label: 'leaked channel markers', }, - // C0 controls and DEL, minus tab/newline/CR — never valid in source text. + // C0 controls and DEL minus tab/newline/CR — never valid in source text. // eslint-disable-next-line no-control-regex { pattern: /[\0-\x08\x0B\f\x0E-\x1F\x7F]/, label: 'control characters' }, ]; @@ -40,11 +27,7 @@ type GuardableTool = { execute: (...args: never[]) => Promise; }; -/** - * Wrap a pi write/edit tool so corrupted content is rejected with a - * corrective error instead of reaching disk. `pick` extracts the strings - * that will be WRITTEN (write content, edit newText) from the tool params. - */ +/** Wraps a pi write/edit tool; `pick` extracts the strings that would be WRITTEN, and a leak returns a corrective error instead of executing. */ export function withContentGuard( tool: T, pick: (params: unknown) => readonly string[], @@ -90,7 +73,7 @@ export function pickWriteContent(params: unknown): readonly string[] { return typeof content === 'string' ? [content] : []; } -/** Strings an `edit` call would put on disk (newText only — oldText must match existing bytes). */ +/** Strings an `edit` call would put on disk — newText only, oldText must stay free to match existing garbage. */ export function pickEditContent(params: unknown): readonly string[] { const edits = (params as { edits?: unknown })?.edits; if (!Array.isArray(edits)) return []; From 0b3aeb77907e8ffc28ba26ed810645559e325355 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 10:34:41 -0400 Subject: [PATCH 3/8] docs(pi): cite the upstream leak reports the content guard works around Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../agent/runner/harness/pi/content-guard.ts | 19 ++++++++++++++++++- src/lib/agent/runner/harness/pi/index.ts | 1 + src/lib/agent/runner/harness/pi/task.ts | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts index ce39f00b..3b4a5b2f 100644 --- a/src/lib/agent/runner/harness/pi/content-guard.ts +++ b/src/lib/agent/runner/harness/pi/content-guard.ts @@ -1,14 +1,31 @@ -// Rejects pi write/edit content carrying leaked model transport tokens (run 9704a73e shipped them into a customer file) before it reaches disk. +/** + * Workaround, not a feature: rejects pi write/edit content carrying leaked + * model transport tokens before it reaches disk. gpt-5.x streaming with tools + * leaks its function-call grammar into string values — run 9704a73e wrote + * `' }#+#+#+#+.functions.complete_task (commentary …json>tagger…` plus a DEL + * byte into a customer's global-error.tsx, and exact-match edits could not + * remove it. Unfixed upstream; delete this module when the stack stops leaking: + * - litellm#14260 ("gpt5 with streaming and tool calls randomly produces + * garbage in response": `functions.name_of_some_function ` + * in content — closed stale, not planned) + * https://github.com/BerriAI/litellm/issues/14260 + * - OpenAI community 1386422 (gpt-5.6-luna, Responses API: "garbage tokens + * (foreign scripts / leaked reasoning) inside string values right before + * the closing quote"; identical Chat Completions request is clean) + * https://community.openai.com/t/1386422 + */ import { analytics } from '@utils/analytics'; import { logToFile } from '@utils/debug'; // Scoped to the wizard's own tool names — a generic `functions.*` match would false-positive on real code like Firebase's `functions.config()`. const LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ { + // The litellm#14260 signature: `functions.` emitted as text. pattern: /functions\.(?:complete_task|enqueue_task|read_handoffs)/, label: 'leaked tool-call tokens', }, { + // Harmony channel markers (openai/harmony#27-class leaks). pattern: /<\|(?:channel|constrain|message|call|end|start|return)\|>/, label: 'leaked channel markers', }, diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index f8fdb151..f97d2a1a 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -373,6 +373,7 @@ export const piBackend: AgentHarness = { // are the stock definitions. Reads run in parallel so a batched turn of // independent reads executes at once; edit/write/bash stay sequential. withMode(createReadToolDefinition(session.installDir), 'parallel'), + // Guards against gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts. withContentGuard( withMode(createEditToolDefinition(session.installDir), 'sequential'), pickEditContent, diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index e00f57d0..03abbb47 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -277,6 +277,7 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { const codingToolFactories = { read: () => withMode(createReadToolDefinition(dir), 'parallel'), edit: () => + // Guards against gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts. withContentGuard( withMode(createEditToolDefinition(dir), 'sequential'), pickEditContent, From 99691277588448b3d87af3321cd81cffe3990c97 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 10:41:20 -0400 Subject: [PATCH 4/8] feat(pi): report the matched leak evidence in guard analytics, never the content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trip event now carries the leaked token itself (escaped, capped), its offset, the content length, and an at_end flag — enough to profile the upstream failure without ever capturing customer code. Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../pi/__tests__/content-guard.test.ts | 16 ++++--- .../agent/runner/harness/pi/content-guard.ts | 42 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts index 8f5a0ffa..2bf66489 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts @@ -16,14 +16,20 @@ const LEAKED_LINE = "' }#+#+#+#+.functions.complete_task (commentary json.functions.complete_taskjson>tagger历山大发 { "; describe('contentLeak', () => { - it('flags the observed transport-token leak', () => { - expect(contentLeak(LEAKED_LINE)).toBe('leaked tool-call tokens'); + it('flags the observed transport-token leak with its evidence, not the content', () => { + const finding = contentLeak(LEAKED_LINE); + expect(finding?.label).toBe('leaked tool-call tokens'); + expect(finding?.token).toBe('"functions.complete_task"'); + expect(finding?.offset).toBe(LEAKED_LINE.indexOf('functions.')); + expect(finding?.contentLength).toBe(LEAKED_LINE.length); }); it('flags channel markers and control characters', () => { - expect(contentLeak('x<|channel|>y')).toBe('leaked channel markers'); - expect(contentLeak('trailing\x7f')).toBe('control characters'); - expect(contentLeak('null\0byte')).toBe('control characters'); + expect(contentLeak('x<|channel|>y')?.label).toBe('leaked channel markers'); + const ctl = contentLeak('trailing\x7f'); + expect(ctl?.label).toBe('control characters'); + expect(ctl?.token).toBe('"\\u007f"'); + expect(contentLeak('null\0byte')?.label).toBe('control characters'); }); it('passes real source content, including Firebase functions.* code', () => { diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts index 3b4a5b2f..bde90a1d 100644 --- a/src/lib/agent/runner/harness/pi/content-guard.ts +++ b/src/lib/agent/runner/harness/pi/content-guard.ts @@ -34,9 +34,30 @@ const LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ { pattern: /[\0-\x08\x0B\f\x0E-\x1F\x7F]/, label: 'control characters' }, ]; -/** The leak label when content is corrupted, else undefined. */ -export function contentLeak(content: string): string | undefined { - return LEAK_PATTERNS.find(({ pattern }) => pattern.test(content))?.label; +/** What the guard reports about a leak — the pattern's evidence, never the surrounding file content. */ +export interface LeakFinding { + label: string; + /** The leaked token itself (JSON-escaped, capped) — transport grammar, not customer code. */ + token: string; + /** Offset of the match within the content, and the content's length — locates the leak (end-of-string is the upstream signature) without carrying the string. */ + offset: number; + contentLength: number; +} + +/** The leak finding when content is corrupted, else undefined. */ +export function contentLeak(content: string): LeakFinding | undefined { + for (const { pattern, label } of LEAK_PATTERNS) { + const match = pattern.exec(content); + if (!match) continue; + // JSON.stringify escapes C0 but not DEL — force \uXXXX for every control char. + const token = JSON.stringify(match[0].slice(0, 40)).replace( + // eslint-disable-next-line no-control-regex + /[\x7F]/g, + (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`, + ); + return { label, token, offset: match.index, contentLength: content.length }; + } + return undefined; } type GuardableTool = { @@ -54,7 +75,7 @@ export function withContentGuard( ) => Promise; tool.execute = ((...args: unknown[]) => { const params = args[1]; - let leak: string | undefined; + let leak: LeakFinding | undefined; try { leak = pick(params).map(contentLeak).find(Boolean); } catch { @@ -63,15 +84,22 @@ export function withContentGuard( if (leak) { analytics.wizardCapture('file content guard tripped', { tool: tool.name, - leak, + leak: leak.label, + leak_token: leak.token, + leak_offset: leak.offset, + content_length: leak.contentLength, + // End-of-string leaks are the upstream decoder signature (see header). + at_end: leak.contentLength - leak.offset < 80, }); - logToFile(`[content-guard] blocked ${tool.name}: ${leak}`); + logToFile( + `[content-guard] blocked ${tool.name}: ${leak.label} token=${leak.token} at ${leak.offset}/${leak.contentLength}`, + ); return Promise.resolve({ content: [ { type: 'text', text: - `Error: the new file content contains ${leak} from the model transport — ` + + `Error: the new file content contains ${leak.label} from the model transport — ` + 'it was NOT written. Re-issue the call with only the intended file content.', }, ], From 681d2b0ab5f42c7a62a356996df02c112db665cc Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 10:53:11 -0400 Subject: [PATCH 5/8] feat(pi): observe transport-token leaks passively instead of blocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaking writes now proceed: the finding is logged, captured as 'file content leak observed', and appended to the task's handoff so the review stage checks the touched files. No error is returned to the model — pure observability while the upstream leak is profiled. Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../pi/__tests__/content-guard.test.ts | 47 +++++++++++-------- .../agent/runner/harness/pi/content-guard.ts | 32 +++++-------- src/lib/agent/runner/harness/pi/index.ts | 2 +- .../runner/harness/pi/orchestrator-tools.ts | 24 ++++++---- src/lib/agent/runner/harness/pi/task.ts | 21 +++++++-- 5 files changed, 72 insertions(+), 54 deletions(-) diff --git a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts index 2bf66489..4c3b1c3a 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts @@ -4,6 +4,7 @@ import { withContentGuard, pickEditContent, pickWriteContent, + type LeakFinding, } from '../content-guard'; vi.mock('@utils/analytics', () => ({ @@ -48,11 +49,14 @@ function makeTool() { return { tool: { name: 'write', execute }, execute }; } -describe('withContentGuard', () => { - it('blocks a write whose content carries the leak and never executes', async () => { +describe('withContentGuard (passive observation)', () => { + it('observes a leaking write, reports it, and still executes', async () => { const { tool, execute } = makeTool(); - const guarded = withContentGuard(tool, pickWriteContent); - const result = (await guarded.execute( + const leaks: LeakFinding[] = []; + const guarded = withContentGuard(tool, pickWriteContent, (f) => + leaks.push(f), + ); + await guarded.execute( ...([ 'id1', { content: LEAKED_LINE }, @@ -60,15 +64,18 @@ describe('withContentGuard', () => { undefined, {}, ] as never[]), - )) as { isError?: boolean; content: [{ text: string }] }; - expect(execute).not.toHaveBeenCalled(); - expect(result.isError).toBe(true); - expect(result.content[0].text).toMatch(/NOT written/); + ); + expect(execute).toHaveBeenCalledOnce(); + expect(leaks).toHaveLength(1); + expect(leaks[0].label).toBe('leaked tool-call tokens'); }); - it('passes clean writes through to the real execute', async () => { + it('passes clean writes through without reporting', async () => { const { tool, execute } = makeTool(); - const guarded = withContentGuard(tool, pickWriteContent); + const leaks: LeakFinding[] = []; + const guarded = withContentGuard(tool, pickWriteContent, (f) => + leaks.push(f), + ); await guarded.execute( ...([ 'id1', @@ -79,12 +86,15 @@ describe('withContentGuard', () => { ] as never[]), ); expect(execute).toHaveBeenCalledOnce(); + expect(leaks).toHaveLength(0); }); - it('guards edit newText but lets oldText match existing garbage (repairs stay possible)', async () => { + it('observes edit newText but ignores oldText (repairs stay silent)', async () => { const { tool, execute } = makeTool(); - const guarded = withContentGuard(tool, pickEditContent); - // Repair edit: garbage in oldText, clean newText — must pass. + const leaks: LeakFinding[] = []; + const guarded = withContentGuard(tool, pickEditContent, (f) => + leaks.push(f), + ); await guarded.execute( ...([ 'id1', @@ -94,9 +104,8 @@ describe('withContentGuard', () => { {}, ] as never[]), ); - expect(execute).toHaveBeenCalledOnce(); - // Corrupted newText — must block. - const result = (await guarded.execute( + expect(leaks).toHaveLength(0); + await guarded.execute( ...([ 'id2', { path: 'x.tsx', edits: [{ oldText: '\n', newText: LEAKED_LINE }] }, @@ -104,9 +113,9 @@ describe('withContentGuard', () => { undefined, {}, ] as never[]), - )) as { isError?: boolean }; - expect(result.isError).toBe(true); - expect(execute).toHaveBeenCalledOnce(); + ); + expect(leaks).toHaveLength(1); + expect(execute).toHaveBeenCalledTimes(2); }); it('lets malformed params fall through to the tool own validation', async () => { diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts index bde90a1d..864821df 100644 --- a/src/lib/agent/runner/harness/pi/content-guard.ts +++ b/src/lib/agent/runner/harness/pi/content-guard.ts @@ -1,10 +1,10 @@ /** - * Workaround, not a feature: rejects pi write/edit content carrying leaked - * model transport tokens before it reaches disk. gpt-5.x streaming with tools - * leaks its function-call grammar into string values — run 9704a73e wrote - * `' }#+#+#+#+.functions.complete_task (commentary …json>tagger…` plus a DEL - * byte into a customer's global-error.tsx, and exact-match edits could not - * remove it. Unfixed upstream; delete this module when the stack stops leaking: + * Passive observability for an unfixed upstream failure: gpt-5.x streaming + * with tools leaks its function-call grammar into string values — run 9704a73e + * wrote `' }#+#+#+#+.functions.complete_task (commentary …json>tagger…` plus a + * DEL byte into a customer's global-error.tsx. Writes are observed and logged, + * never blocked; a leak is surfaced to the review stage via the task handoff. + * Delete this module when the stack stops leaking: * - litellm#14260 ("gpt5 with streaming and tool calls randomly produces * garbage in response": `functions.name_of_some_function ` * in content — closed stale, not planned) @@ -65,10 +65,11 @@ type GuardableTool = { execute: (...args: never[]) => Promise; }; -/** Wraps a pi write/edit tool; `pick` extracts the strings that would be WRITTEN, and a leak returns a corrective error instead of executing. */ +/** Wraps a pi write/edit tool with passive leak observation: `pick` extracts the strings that would be WRITTEN; a leak is logged, captured, and handed to `onLeak` — the call always executes. */ export function withContentGuard( tool: T, pick: (params: unknown) => readonly string[], + onLeak?: (finding: LeakFinding) => void, ): T { const execute = tool.execute.bind(tool) as ( ...args: unknown[] @@ -82,7 +83,7 @@ export function withContentGuard( leak = undefined; // malformed params: let the tool's own validation speak } if (leak) { - analytics.wizardCapture('file content guard tripped', { + analytics.wizardCapture('file content leak observed', { tool: tool.name, leak: leak.label, leak_token: leak.token, @@ -92,20 +93,9 @@ export function withContentGuard( at_end: leak.contentLength - leak.offset < 80, }); logToFile( - `[content-guard] blocked ${tool.name}: ${leak.label} token=${leak.token} at ${leak.offset}/${leak.contentLength}`, + `[content-guard] observed ${tool.name}: ${leak.label} token=${leak.token} at ${leak.offset}/${leak.contentLength}`, ); - return Promise.resolve({ - content: [ - { - type: 'text', - text: - `Error: the new file content contains ${leak.label} from the model transport — ` + - 'it was NOT written. Re-issue the call with only the intended file content.', - }, - ], - details: {}, - isError: true, - }); + onLeak?.(leak); } return execute(...args); }) as T['execute']; diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index f97d2a1a..77a44263 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -373,7 +373,7 @@ export const piBackend: AgentHarness = { // are the stock definitions. Reads run in parallel so a batched turn of // independent reads executes at once; edit/write/bash stay sequential. withMode(createReadToolDefinition(session.installDir), 'parallel'), - // Guards against gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts. + // Observes gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts. withContentGuard( withMode(createEditToolDefinition(session.installDir), 'sequential'), pickEditContent, diff --git a/src/lib/agent/runner/harness/pi/orchestrator-tools.ts b/src/lib/agent/runner/harness/pi/orchestrator-tools.ts index 6c6fc3a0..53074b27 100644 --- a/src/lib/agent/runner/harness/pi/orchestrator-tools.ts +++ b/src/lib/agent/runner/harness/pi/orchestrator-tools.ts @@ -64,9 +64,10 @@ const HANDOFF_PARAMS = Type.Object({ ), }); -/** The three queue tools bound to one agent's orchestrator context. */ +/** The three queue tools bound to one agent's orchestrator context. `leakNote` reports transport-token leaks observed in this task's writes (see content-guard.ts); the note rides the handoff so the review stage checks the touched files. */ export function createPiOrchestratorTools( ctx: OrchestratorToolsContext, + leakNote?: () => string | undefined, ): ToolDefinition[] { const enqueueTask = defineTool({ name: 'enqueue_task', @@ -126,14 +127,19 @@ export function createPiOrchestratorTools( remark: Type.Optional(Type.String({ description: REMARK_ASK })), }), execute(_id, args) { - const res = applyComplete( - ctx, - args as { - status: 'done' | 'failed' | 'not needed'; - handoff: TaskHandoff; - remark?: string; - }, - ); + const complete = args as { + status: 'done' | 'failed' | 'not needed'; + handoff: TaskHandoff; + remark?: string; + }; + const note = leakNote?.(); + if (note) { + complete.handoff.forNextAgent = [ + complete.handoff.forNextAgent, + note, + ].join('\n'); + } + const res = applyComplete(ctx, complete); if (!res.ok) return Promise.resolve(text(`Error: ${res.message}`)); return Promise.resolve(text('ok')); }, diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 03abbb47..7a9cb420 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -48,6 +48,7 @@ import { withContentGuard, pickEditContent, pickWriteContent, + type LeakFinding, } from './content-guard'; import { createAioCapture } from '@lib/agent/aio-capture'; @@ -274,18 +275,22 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { // in parallel; mutating tools stay sequential. Bash subprocesses get the // scrubbed env, same as the linear run. const dir = session.installDir; + // Observes gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts; findings ride the handoff to the review stage. + const observedLeaks: LeakFinding[] = []; + const recordLeak = (f: LeakFinding) => observedLeaks.push(f); const codingToolFactories = { read: () => withMode(createReadToolDefinition(dir), 'parallel'), edit: () => - // Guards against gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts. withContentGuard( withMode(createEditToolDefinition(dir), 'sequential'), pickEditContent, + recordLeak, ), write: () => withContentGuard( withMode(createWriteToolDefinition(dir), 'sequential'), pickWriteContent, + recordLeak, ), bash: () => withMode( @@ -316,9 +321,17 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { ); const { createPiOrchestratorTools } = await import('./orchestrator-tools'); - const queueTools = createPiOrchestratorTools(orchestrator).filter((t) => - orchestratorTools.has(t.name), - ); + const queueTools = createPiOrchestratorTools(orchestrator, () => + observedLeaks.length + ? `Note (added by the wizard, not the agent): ${ + observedLeaks.length + } file write(s) in this task matched model transport-leak patterns (${[ + ...new Set(observedLeaks.map((f) => f.label)), + ].join( + ', ', + )}) — the touched files may contain garbled lines; verify them.` + : undefined, + ).filter((t) => orchestratorTools.has(t.name)); const customTools = [...codingToolDefs, ...wizardTools, ...queueTools]; const { session: agentSession } = await createAgentSession({ From 27191549dfdc0ae375284bf6b2276fd85a0f922d Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 10:57:40 -0400 Subject: [PATCH 6/8] fix(pi): handoff names the suspected-corrupt files in fixed wording Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../harness/pi/__tests__/content-guard.test.ts | 3 ++- .../agent/runner/harness/pi/content-guard.ts | 10 +++++++++- src/lib/agent/runner/harness/pi/task.ts | 18 +++++++----------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts index 4c3b1c3a..5588e529 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts @@ -59,7 +59,7 @@ describe('withContentGuard (passive observation)', () => { await guarded.execute( ...([ 'id1', - { content: LEAKED_LINE }, + { path: 'app/global-error.tsx', content: LEAKED_LINE }, undefined, undefined, {}, @@ -68,6 +68,7 @@ describe('withContentGuard (passive observation)', () => { expect(execute).toHaveBeenCalledOnce(); expect(leaks).toHaveLength(1); expect(leaks[0].label).toBe('leaked tool-call tokens'); + expect(leaks[0].path).toBe('app/global-error.tsx'); }); it('passes clean writes through without reporting', async () => { diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts index 864821df..4cdc9927 100644 --- a/src/lib/agent/runner/harness/pi/content-guard.ts +++ b/src/lib/agent/runner/harness/pi/content-guard.ts @@ -42,6 +42,8 @@ export interface LeakFinding { /** Offset of the match within the content, and the content's length — locates the leak (end-of-string is the upstream signature) without carrying the string. */ offset: number; contentLength: number; + /** The file the suspect content was written to — names the file in the handoff note. */ + path?: string; } /** The leak finding when content is corrupted, else undefined. */ @@ -92,8 +94,14 @@ export function withContentGuard( // End-of-string leaks are the upstream decoder signature (see header). at_end: leak.contentLength - leak.offset < 80, }); + const path = (params as { path?: unknown })?.path; + if (typeof path === 'string') leak.path = path; logToFile( - `[content-guard] observed ${tool.name}: ${leak.label} token=${leak.token} at ${leak.offset}/${leak.contentLength}`, + `[content-guard] observed ${tool.name}: ${leak.label} token=${ + leak.token + } at ${leak.offset}/${leak.contentLength}${ + leak.path ? ` path=${leak.path}` : '' + }`, ); onLeak?.(leak); } diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 7a9cb420..7daec16f 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -321,17 +321,13 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { ); const { createPiOrchestratorTools } = await import('./orchestrator-tools'); - const queueTools = createPiOrchestratorTools(orchestrator, () => - observedLeaks.length - ? `Note (added by the wizard, not the agent): ${ - observedLeaks.length - } file write(s) in this task matched model transport-leak patterns (${[ - ...new Set(observedLeaks.map((f) => f.label)), - ].join( - ', ', - )}) — the touched files may contain garbled lines; verify them.` - : undefined, - ).filter((t) => orchestratorTools.has(t.name)); + const queueTools = createPiOrchestratorTools(orchestrator, () => { + if (!observedLeaks.length) return undefined; + const files = [ + ...new Set(observedLeaks.map((f) => f.path ?? 'an unnamed file')), + ].join(', '); + return `There is a suspected corruption of the file ${files}. Please correct these before continuing.`; + }).filter((t) => orchestratorTools.has(t.name)); const customTools = [...codingToolDefs, ...wizardTools, ...queueTools]; const { session: agentSession } = await createAgentSession({ From fb73b4b4a2108b00cb0fc9a0114572fed0adb2c9 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 11:17:55 -0400 Subject: [PATCH 7/8] refactor(pi): fold transport-leak telemetry into the security extension Analytics-only now: the leak patterns run where the security extension already extracts write/edit content, one event per leaking call, no blocking, no handoff amendment. Deletes the content-guard module, its tool wrapping, and the leakNote threading through the queue tools. Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../pi/__tests__/content-guard.test.ts | 136 ------------------ .../harness/pi/__tests__/security.test.ts | 51 +++++++ .../agent/runner/harness/pi/content-guard.ts | 126 ---------------- src/lib/agent/runner/harness/pi/index.ts | 16 +-- .../runner/harness/pi/orchestrator-tools.ts | 24 ++-- src/lib/agent/runner/harness/pi/security.ts | 56 ++++++++ src/lib/agent/runner/harness/pi/task.ts | 33 +---- 7 files changed, 123 insertions(+), 319 deletions(-) delete mode 100644 src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts delete mode 100644 src/lib/agent/runner/harness/pi/content-guard.ts diff --git a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts b/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts deleted file mode 100644 index 5588e529..00000000 --- a/src/lib/agent/runner/harness/pi/__tests__/content-guard.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { - contentLeak, - withContentGuard, - pickEditContent, - pickWriteContent, - type LeakFinding, -} from '../content-guard'; - -vi.mock('@utils/analytics', () => ({ - analytics: { wizardCapture: vi.fn() }, -})); -vi.mock('@utils/debug', () => ({ logToFile: vi.fn() })); - -// The exact garbage run 9704a73e wrote into a customer's global-error.tsx. -const LEAKED_LINE = - "' }#+#+#+#+.functions.complete_task (commentary json.functions.complete_taskjson>tagger历山大发 { "; - -describe('contentLeak', () => { - it('flags the observed transport-token leak with its evidence, not the content', () => { - const finding = contentLeak(LEAKED_LINE); - expect(finding?.label).toBe('leaked tool-call tokens'); - expect(finding?.token).toBe('"functions.complete_task"'); - expect(finding?.offset).toBe(LEAKED_LINE.indexOf('functions.')); - expect(finding?.contentLength).toBe(LEAKED_LINE.length); - }); - - it('flags channel markers and control characters', () => { - expect(contentLeak('x<|channel|>y')?.label).toBe('leaked channel markers'); - const ctl = contentLeak('trailing\x7f'); - expect(ctl?.label).toBe('control characters'); - expect(ctl?.token).toBe('"\\u007f"'); - expect(contentLeak('null\0byte')?.label).toBe('control characters'); - }); - - it('passes real source content, including Firebase functions.* code', () => { - expect(contentLeak('export default function GlobalError() {}')).toBe( - undefined, - ); - expect(contentLeak('const f = functions.https.onRequest(app);')).toBe( - undefined, - ); - expect(contentLeak('line one\n\ttabbed\r\nline two')).toBe(undefined); - }); -}); - -function makeTool() { - const execute = vi.fn().mockResolvedValue({ content: [], details: {} }); - return { tool: { name: 'write', execute }, execute }; -} - -describe('withContentGuard (passive observation)', () => { - it('observes a leaking write, reports it, and still executes', async () => { - const { tool, execute } = makeTool(); - const leaks: LeakFinding[] = []; - const guarded = withContentGuard(tool, pickWriteContent, (f) => - leaks.push(f), - ); - await guarded.execute( - ...([ - 'id1', - { path: 'app/global-error.tsx', content: LEAKED_LINE }, - undefined, - undefined, - {}, - ] as never[]), - ); - expect(execute).toHaveBeenCalledOnce(); - expect(leaks).toHaveLength(1); - expect(leaks[0].label).toBe('leaked tool-call tokens'); - expect(leaks[0].path).toBe('app/global-error.tsx'); - }); - - it('passes clean writes through without reporting', async () => { - const { tool, execute } = makeTool(); - const leaks: LeakFinding[] = []; - const guarded = withContentGuard(tool, pickWriteContent, (f) => - leaks.push(f), - ); - await guarded.execute( - ...([ - 'id1', - { content: 'clean file\n' }, - undefined, - undefined, - {}, - ] as never[]), - ); - expect(execute).toHaveBeenCalledOnce(); - expect(leaks).toHaveLength(0); - }); - - it('observes edit newText but ignores oldText (repairs stay silent)', async () => { - const { tool, execute } = makeTool(); - const leaks: LeakFinding[] = []; - const guarded = withContentGuard(tool, pickEditContent, (f) => - leaks.push(f), - ); - await guarded.execute( - ...([ - 'id1', - { path: 'x.tsx', edits: [{ oldText: LEAKED_LINE, newText: '\n' }] }, - undefined, - undefined, - {}, - ] as never[]), - ); - expect(leaks).toHaveLength(0); - await guarded.execute( - ...([ - 'id2', - { path: 'x.tsx', edits: [{ oldText: '\n', newText: LEAKED_LINE }] }, - undefined, - undefined, - {}, - ] as never[]), - ); - expect(leaks).toHaveLength(1); - expect(execute).toHaveBeenCalledTimes(2); - }); - - it('lets malformed params fall through to the tool own validation', async () => { - const { tool, execute } = makeTool(); - const guarded = withContentGuard(tool, pickEditContent); - await guarded.execute( - ...([ - 'id1', - { edits: 'not-an-array' }, - undefined, - undefined, - {}, - ] as never[]), - ); - expect(execute).toHaveBeenCalledOnce(); - }); -}); diff --git a/src/lib/agent/runner/harness/pi/__tests__/security.test.ts b/src/lib/agent/runner/harness/pi/__tests__/security.test.ts index 9c86f959..2ffa63cd 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/security.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/security.test.ts @@ -6,10 +6,16 @@ import { evaluateToolCall, createSecurityExtension, isScopedFileRemoval, + observeTransportLeak, overwriteShrinkReason, MAX_TOOL_CALLS, type PiExtensionApiLike, } from '../security'; +import { analytics } from '@utils/analytics'; + +vi.mock('@utils/analytics', () => ({ + analytics: { wizardCapture: vi.fn() }, +})); // @posthog/warlock resolves to __mocks__/@posthog/warlock.ts (ESM + WASM can't // load under the CJS test runner). Default: scan matches nothing; tests @@ -687,3 +693,48 @@ describe('pi-security: overwrite shrink guard (destructive whole-file rewrite)', expect(decision.block).toBe(false); }); }); + +describe('observeTransportLeak (passive telemetry)', () => { + // The exact garbage run 9704a73e wrote into a customer's global-error.tsx. + const LEAKED_LINE = + "' }#+#+#+#+.functions.complete_task (commentary json.functions.complete_taskjson>tagger历山大发 { "; + + it('captures the leak evidence, never the content', () => { + observeTransportLeak('Write', LEAKED_LINE, 'app/global-error.tsx'); + const call = vi + .mocked(analytics.wizardCapture) + .mock.calls.find(([e]) => e === 'file content leak observed'); + expect(call?.[1]).toMatchObject({ + tool: 'Write', + leak: 'leaked tool-call tokens', + leak_token: '"functions.complete_task"', + content_length: LEAKED_LINE.length, + }); + }); + + it('escapes DEL in the reported token', () => { + vi.mocked(analytics.wizardCapture).mockClear(); + observeTransportLeak('Edit', 'trailing\x7f', 'x.ts'); + const call = vi + .mocked(analytics.wizardCapture) + .mock.calls.find(([e]) => e === 'file content leak observed'); + expect(call?.[1]).toMatchObject({ + leak: 'control characters', + leak_token: '"\\u007f"', + }); + }); + + it('stays silent on real source, including Firebase functions.* code', () => { + vi.mocked(analytics.wizardCapture).mockClear(); + observeTransportLeak( + 'Write', + 'const f = functions.https.onRequest(app);\nline\n\ttabbed\r\n', + 'index.ts', + ); + expect( + vi + .mocked(analytics.wizardCapture) + .mock.calls.some(([e]) => e === 'file content leak observed'), + ).toBe(false); + }); +}); diff --git a/src/lib/agent/runner/harness/pi/content-guard.ts b/src/lib/agent/runner/harness/pi/content-guard.ts deleted file mode 100644 index 4cdc9927..00000000 --- a/src/lib/agent/runner/harness/pi/content-guard.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Passive observability for an unfixed upstream failure: gpt-5.x streaming - * with tools leaks its function-call grammar into string values — run 9704a73e - * wrote `' }#+#+#+#+.functions.complete_task (commentary …json>tagger…` plus a - * DEL byte into a customer's global-error.tsx. Writes are observed and logged, - * never blocked; a leak is surfaced to the review stage via the task handoff. - * Delete this module when the stack stops leaking: - * - litellm#14260 ("gpt5 with streaming and tool calls randomly produces - * garbage in response": `functions.name_of_some_function ` - * in content — closed stale, not planned) - * https://github.com/BerriAI/litellm/issues/14260 - * - OpenAI community 1386422 (gpt-5.6-luna, Responses API: "garbage tokens - * (foreign scripts / leaked reasoning) inside string values right before - * the closing quote"; identical Chat Completions request is clean) - * https://community.openai.com/t/1386422 - */ -import { analytics } from '@utils/analytics'; -import { logToFile } from '@utils/debug'; - -// Scoped to the wizard's own tool names — a generic `functions.*` match would false-positive on real code like Firebase's `functions.config()`. -const LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ - { - // The litellm#14260 signature: `functions.` emitted as text. - pattern: /functions\.(?:complete_task|enqueue_task|read_handoffs)/, - label: 'leaked tool-call tokens', - }, - { - // Harmony channel markers (openai/harmony#27-class leaks). - pattern: /<\|(?:channel|constrain|message|call|end|start|return)\|>/, - label: 'leaked channel markers', - }, - // C0 controls and DEL minus tab/newline/CR — never valid in source text. - // eslint-disable-next-line no-control-regex - { pattern: /[\0-\x08\x0B\f\x0E-\x1F\x7F]/, label: 'control characters' }, -]; - -/** What the guard reports about a leak — the pattern's evidence, never the surrounding file content. */ -export interface LeakFinding { - label: string; - /** The leaked token itself (JSON-escaped, capped) — transport grammar, not customer code. */ - token: string; - /** Offset of the match within the content, and the content's length — locates the leak (end-of-string is the upstream signature) without carrying the string. */ - offset: number; - contentLength: number; - /** The file the suspect content was written to — names the file in the handoff note. */ - path?: string; -} - -/** The leak finding when content is corrupted, else undefined. */ -export function contentLeak(content: string): LeakFinding | undefined { - for (const { pattern, label } of LEAK_PATTERNS) { - const match = pattern.exec(content); - if (!match) continue; - // JSON.stringify escapes C0 but not DEL — force \uXXXX for every control char. - const token = JSON.stringify(match[0].slice(0, 40)).replace( - // eslint-disable-next-line no-control-regex - /[\x7F]/g, - (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`, - ); - return { label, token, offset: match.index, contentLength: content.length }; - } - return undefined; -} - -type GuardableTool = { - name: string; - execute: (...args: never[]) => Promise; -}; - -/** Wraps a pi write/edit tool with passive leak observation: `pick` extracts the strings that would be WRITTEN; a leak is logged, captured, and handed to `onLeak` — the call always executes. */ -export function withContentGuard( - tool: T, - pick: (params: unknown) => readonly string[], - onLeak?: (finding: LeakFinding) => void, -): T { - const execute = tool.execute.bind(tool) as ( - ...args: unknown[] - ) => Promise; - tool.execute = ((...args: unknown[]) => { - const params = args[1]; - let leak: LeakFinding | undefined; - try { - leak = pick(params).map(contentLeak).find(Boolean); - } catch { - leak = undefined; // malformed params: let the tool's own validation speak - } - if (leak) { - analytics.wizardCapture('file content leak observed', { - tool: tool.name, - leak: leak.label, - leak_token: leak.token, - leak_offset: leak.offset, - content_length: leak.contentLength, - // End-of-string leaks are the upstream decoder signature (see header). - at_end: leak.contentLength - leak.offset < 80, - }); - const path = (params as { path?: unknown })?.path; - if (typeof path === 'string') leak.path = path; - logToFile( - `[content-guard] observed ${tool.name}: ${leak.label} token=${ - leak.token - } at ${leak.offset}/${leak.contentLength}${ - leak.path ? ` path=${leak.path}` : '' - }`, - ); - onLeak?.(leak); - } - return execute(...args); - }) as T['execute']; - return tool; -} - -/** Strings a `write` call would put on disk. */ -export function pickWriteContent(params: unknown): readonly string[] { - const content = (params as { content?: unknown })?.content; - return typeof content === 'string' ? [content] : []; -} - -/** Strings an `edit` call would put on disk — newText only, oldText must stay free to match existing garbage. */ -export function pickEditContent(params: unknown): readonly string[] { - const edits = (params as { edits?: unknown })?.edits; - if (!Array.isArray(edits)) return []; - return edits - .map((e: unknown) => (e as { newText?: unknown })?.newText) - .filter((t): t is string => typeof t === 'string'); -} diff --git a/src/lib/agent/runner/harness/pi/index.ts b/src/lib/agent/runner/harness/pi/index.ts index 77a44263..20300423 100644 --- a/src/lib/agent/runner/harness/pi/index.ts +++ b/src/lib/agent/runner/harness/pi/index.ts @@ -28,11 +28,6 @@ import { AgentSignals, REMARK_INSTRUCTION } from '@lib/agent/signals'; import { AgentOutputSignals } from '@lib/agent/output-signals'; import { assembleCommandments } from '../../switchboard/commandments'; import { buildGatewayProvider, GATEWAY_PROVIDER } from './gateway'; -import { - withContentGuard, - pickEditContent, - pickWriteContent, -} from './content-guard'; import { createAioCapture } from '@lib/agent/aio-capture'; import type { AgentResult, @@ -373,15 +368,8 @@ export const piBackend: AgentHarness = { // are the stock definitions. Reads run in parallel so a batched turn of // independent reads executes at once; edit/write/bash stay sequential. withMode(createReadToolDefinition(session.installDir), 'parallel'), - // Observes gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts. - withContentGuard( - withMode(createEditToolDefinition(session.installDir), 'sequential'), - pickEditContent, - ), - withContentGuard( - withMode(createWriteToolDefinition(session.installDir), 'sequential'), - pickWriteContent, - ), + withMode(createEditToolDefinition(session.installDir), 'sequential'), + withMode(createWriteToolDefinition(session.installDir), 'sequential'), scrubbedBash, // Native ls/find/grep so the agent explores with proper tools instead // of fence-blocked `bash {ls/find}` (the profiled retry-spirals came diff --git a/src/lib/agent/runner/harness/pi/orchestrator-tools.ts b/src/lib/agent/runner/harness/pi/orchestrator-tools.ts index 53074b27..6c6fc3a0 100644 --- a/src/lib/agent/runner/harness/pi/orchestrator-tools.ts +++ b/src/lib/agent/runner/harness/pi/orchestrator-tools.ts @@ -64,10 +64,9 @@ const HANDOFF_PARAMS = Type.Object({ ), }); -/** The three queue tools bound to one agent's orchestrator context. `leakNote` reports transport-token leaks observed in this task's writes (see content-guard.ts); the note rides the handoff so the review stage checks the touched files. */ +/** The three queue tools bound to one agent's orchestrator context. */ export function createPiOrchestratorTools( ctx: OrchestratorToolsContext, - leakNote?: () => string | undefined, ): ToolDefinition[] { const enqueueTask = defineTool({ name: 'enqueue_task', @@ -127,19 +126,14 @@ export function createPiOrchestratorTools( remark: Type.Optional(Type.String({ description: REMARK_ASK })), }), execute(_id, args) { - const complete = args as { - status: 'done' | 'failed' | 'not needed'; - handoff: TaskHandoff; - remark?: string; - }; - const note = leakNote?.(); - if (note) { - complete.handoff.forNextAgent = [ - complete.handoff.forNextAgent, - note, - ].join('\n'); - } - const res = applyComplete(ctx, complete); + const res = applyComplete( + ctx, + args as { + status: 'done' | 'failed' | 'not needed'; + handoff: TaskHandoff; + remark?: string; + }, + ); if (!res.ok) return Promise.resolve(text(`Error: ${res.message}`)); return Promise.resolve(text('ok')); }, diff --git a/src/lib/agent/runner/harness/pi/security.ts b/src/lib/agent/runner/harness/pi/security.ts index 9c550466..aa74a401 100644 --- a/src/lib/agent/runner/harness/pi/security.ts +++ b/src/lib/agent/runner/harness/pi/security.ts @@ -31,6 +31,7 @@ import { } from '@lib/yara-hooks'; import { scanVerdict, type ScanContext } from '@lib/yara-policy'; import { logToFile } from '@utils/debug'; +import { analytics } from '@utils/analytics'; /** warlock ScanMatch → the report shape `recordExternalScan` expects. */ function toReportViolation(m: ScanMatch) { @@ -45,6 +46,60 @@ function toReportViolation(m: ScanMatch) { /** Runaway backstop: hard cap on tool calls per (sub)agent session. */ export const MAX_TOOL_CALLS = 250; +/** + * Passive telemetry for an unfixed upstream failure: gpt-5.x streaming with + * tools leaks its function-call grammar into string values, and the leak lands + * on disk (run 9704a73e shipped `…functions.complete_task (commentary…` plus a + * DEL byte inside a customer's global-error.tsx). Observe-only — never blocks. + * Prior art: https://github.com/BerriAI/litellm/issues/14260 (closed stale), + * https://community.openai.com/t/1386422 (gpt-5.6-luna, no fix). + */ +const TRANSPORT_LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ + // The litellm#14260 signature, scoped to the wizard's own tool names — a + // generic `functions.*` would false-positive on real code (Firebase). + { + pattern: /functions\.(?:complete_task|enqueue_task|read_handoffs)/, + label: 'leaked tool-call tokens', + }, + { + pattern: /<\|(?:channel|constrain|message|call|end|start|return)\|>/, + label: 'leaked channel markers', + }, + // C0 controls and DEL minus tab/newline/CR — never valid in source text. + // eslint-disable-next-line no-control-regex + { pattern: /[\0-\x08\x0B\f\x0E-\x1F\x7F]/, label: 'control characters' }, +]; + +/** Capture one event per leaking write/edit: the matched token and where it sat, never the file content. */ +export function observeTransportLeak( + tool: string, + content: string, + filePath: string, +): void { + for (const { pattern, label } of TRANSPORT_LEAK_PATTERNS) { + const match = pattern.exec(content); + if (!match) continue; + // JSON.stringify escapes C0 but not DEL. + const token = JSON.stringify(match[0].slice(0, 40)).replace( + /\x7f/g, // eslint-disable-line no-control-regex + '\\u007f', + ); + analytics.wizardCapture('file content leak observed', { + tool, + leak: label, + leak_token: token, + leak_offset: match.index, + content_length: content.length, + // End-of-string leaks are the upstream decoder signature. + at_end: content.length - match.index < 80, + }); + logToFile( + `[transport-leak] observed ${tool}: ${label} token=${token} at ${match.index}/${content.length} path=${filePath}`, + ); + return; + } +} + export interface ToolGateContext { disallowedTools?: readonly string[]; /** True while a wizard_ask overlay is open (interactive); blocks Write/Edit. */ @@ -270,6 +325,7 @@ async function preExecutionYaraBlock( return undefined; } if (!content) return undefined; + if (ctx === 'output') observeTransportLeak(tool, content, str(input.path)); let matches = await scanAndTriage(content, ctx, triage); if (ctx === 'output' && isWizardDocumentationPath(str(input.path))) { diff --git a/src/lib/agent/runner/harness/pi/task.ts b/src/lib/agent/runner/harness/pi/task.ts index 7daec16f..0855e2b4 100644 --- a/src/lib/agent/runner/harness/pi/task.ts +++ b/src/lib/agent/runner/harness/pi/task.ts @@ -44,12 +44,6 @@ import { lastStatusLine, withMode, } from './index'; -import { - withContentGuard, - pickEditContent, - pickWriteContent, - type LeakFinding, -} from './content-guard'; import { createAioCapture } from '@lib/agent/aio-capture'; /** wizard tool vocabulary → the pi tool definitions it unlocks. */ @@ -275,23 +269,10 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { // in parallel; mutating tools stay sequential. Bash subprocesses get the // scrubbed env, same as the linear run. const dir = session.installDir; - // Observes gpt-5.x transport-token leaks (litellm#14260) — see content-guard.ts; findings ride the handoff to the review stage. - const observedLeaks: LeakFinding[] = []; - const recordLeak = (f: LeakFinding) => observedLeaks.push(f); const codingToolFactories = { read: () => withMode(createReadToolDefinition(dir), 'parallel'), - edit: () => - withContentGuard( - withMode(createEditToolDefinition(dir), 'sequential'), - pickEditContent, - recordLeak, - ), - write: () => - withContentGuard( - withMode(createWriteToolDefinition(dir), 'sequential'), - pickWriteContent, - recordLeak, - ), + edit: () => withMode(createEditToolDefinition(dir), 'sequential'), + write: () => withMode(createWriteToolDefinition(dir), 'sequential'), bash: () => withMode( createBashToolDefinition(dir, { @@ -321,13 +302,9 @@ export async function runPiTask(inputs: TaskRunInputs): Promise { ); const { createPiOrchestratorTools } = await import('./orchestrator-tools'); - const queueTools = createPiOrchestratorTools(orchestrator, () => { - if (!observedLeaks.length) return undefined; - const files = [ - ...new Set(observedLeaks.map((f) => f.path ?? 'an unnamed file')), - ].join(', '); - return `There is a suspected corruption of the file ${files}. Please correct these before continuing.`; - }).filter((t) => orchestratorTools.has(t.name)); + const queueTools = createPiOrchestratorTools(orchestrator).filter((t) => + orchestratorTools.has(t.name), + ); const customTools = [...codingToolDefs, ...wizardTools, ...queueTools]; const { session: agentSession } = await createAgentSession({ From 7e3678bed46cad79d897f216836780e70f778990 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 29 Jul 2026 11:25:44 -0400 Subject: [PATCH 8/8] fix(pi): leak telemetry reports the pattern only, never matched text Generated-By: PostHog Code Task-Id: affdea28-7761-4d50-8335-6b32376b1a82 --- .../harness/pi/__tests__/security.test.ts | 17 +++++++---------- src/lib/agent/runner/harness/pi/security.ts | 18 ++++-------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/lib/agent/runner/harness/pi/__tests__/security.test.ts b/src/lib/agent/runner/harness/pi/__tests__/security.test.ts index 2ffa63cd..0dcce5c6 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/security.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/security.test.ts @@ -699,29 +699,27 @@ describe('observeTransportLeak (passive telemetry)', () => { const LEAKED_LINE = "' }#+#+#+#+.functions.complete_task (commentary json.functions.complete_taskjson>tagger历山大发 { "; - it('captures the leak evidence, never the content', () => { - observeTransportLeak('Write', LEAKED_LINE, 'app/global-error.tsx'); + it('reports which pattern fired and where — never any matched text', () => { + observeTransportLeak('Write', LEAKED_LINE); const call = vi .mocked(analytics.wizardCapture) .mock.calls.find(([e]) => e === 'file content leak observed'); expect(call?.[1]).toMatchObject({ tool: 'Write', leak: 'leaked tool-call tokens', - leak_token: '"functions.complete_task"', + leak_offset: LEAKED_LINE.indexOf('functions.'), content_length: LEAKED_LINE.length, }); + expect(JSON.stringify(call?.[1])).not.toContain('complete_task'); }); - it('escapes DEL in the reported token', () => { + it('flags control characters by pattern only', () => { vi.mocked(analytics.wizardCapture).mockClear(); - observeTransportLeak('Edit', 'trailing\x7f', 'x.ts'); + observeTransportLeak('Edit', 'trailing\x7f'); const call = vi .mocked(analytics.wizardCapture) .mock.calls.find(([e]) => e === 'file content leak observed'); - expect(call?.[1]).toMatchObject({ - leak: 'control characters', - leak_token: '"\\u007f"', - }); + expect(call?.[1]).toMatchObject({ leak: 'control characters' }); }); it('stays silent on real source, including Firebase functions.* code', () => { @@ -729,7 +727,6 @@ describe('observeTransportLeak (passive telemetry)', () => { observeTransportLeak( 'Write', 'const f = functions.https.onRequest(app);\nline\n\ttabbed\r\n', - 'index.ts', ); expect( vi diff --git a/src/lib/agent/runner/harness/pi/security.ts b/src/lib/agent/runner/harness/pi/security.ts index aa74a401..8e87b170 100644 --- a/src/lib/agent/runner/harness/pi/security.ts +++ b/src/lib/agent/runner/harness/pi/security.ts @@ -70,31 +70,21 @@ const TRANSPORT_LEAK_PATTERNS: readonly { pattern: RegExp; label: string }[] = [ { pattern: /[\0-\x08\x0B\f\x0E-\x1F\x7F]/, label: 'control characters' }, ]; -/** Capture one event per leaking write/edit: the matched token and where it sat, never the file content. */ -export function observeTransportLeak( - tool: string, - content: string, - filePath: string, -): void { +/** Capture one event per leaking write/edit: which pattern fired and where it sat — no matched text, ever. */ +export function observeTransportLeak(tool: string, content: string): void { for (const { pattern, label } of TRANSPORT_LEAK_PATTERNS) { const match = pattern.exec(content); if (!match) continue; - // JSON.stringify escapes C0 but not DEL. - const token = JSON.stringify(match[0].slice(0, 40)).replace( - /\x7f/g, // eslint-disable-line no-control-regex - '\\u007f', - ); analytics.wizardCapture('file content leak observed', { tool, leak: label, - leak_token: token, leak_offset: match.index, content_length: content.length, // End-of-string leaks are the upstream decoder signature. at_end: content.length - match.index < 80, }); logToFile( - `[transport-leak] observed ${tool}: ${label} token=${token} at ${match.index}/${content.length} path=${filePath}`, + `[transport-leak] observed ${tool}: ${label} at ${match.index}/${content.length}`, ); return; } @@ -325,7 +315,7 @@ async function preExecutionYaraBlock( return undefined; } if (!content) return undefined; - if (ctx === 'output') observeTransportLeak(tool, content, str(input.path)); + if (ctx === 'output') observeTransportLeak(tool, content); let matches = await scanAndTriage(content, ctx, triage); if (ctx === 'output' && isWizardDocumentationPath(str(input.path))) {