From 2e4e215da22ffe4b6f8f2dabe9cfc23cd85231eb Mon Sep 17 00:00:00 2001 From: Mathews-Tom Date: Sat, 11 Jul 2026 22:31:49 +0530 Subject: [PATCH] fix(secrets): scan typed JSON collision values --- .../src/advisor/__tests__/advisor.test.ts | 34 +++++++++++++++++ packages/coding-agent/src/advisor/runtime.ts | 6 +-- packages/coding-agent/src/export/share.ts | 2 +- packages/coding-agent/test/share.test.ts | 37 +++++++++++++++++++ 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/advisor/__tests__/advisor.test.ts b/packages/coding-agent/src/advisor/__tests__/advisor.test.ts index c346fae15..02cb81ad2 100644 --- a/packages/coding-agent/src/advisor/__tests__/advisor.test.ts +++ b/packages/coding-agent/src/advisor/__tests__/advisor.test.ts @@ -1028,6 +1028,40 @@ describe("advisor", () => { expect(prompt).toContain("TOKABC123_"); }); + it("collects regex values from tool arguments that resemble image blocks", async () => { + const obfuscator = new SecretObfuscator([ + { type: "plain", content: "OTHERSECRET", friendlyName: "TOKABC123" }, + { type: "regex", content: "tok_[a-z0-9]+" }, + ]); + const promptInputs: string[] = []; + const agent = makeAgent(promptInputs); + const messages: AgentMessage[] = [ + { role: "user", content: "remember OTHERSECRET for later", timestamp: 1 } as AgentMessage, + { + role: "assistant", + content: [ + { type: "toolCall", id: "call-1", name: "read", arguments: { type: "image", value: "tok_abc123" } }, + ], + timestamp: 2, + } as unknown as AgentMessage, + ]; + const host: AdvisorRuntimeHost = { + snapshotMessages: () => messages, + enqueueAdvice: () => {}, + obfuscator, + }; + const runtime = new AdvisorRuntime(agent, host); + + runtime.onTurnEnd(); + await Promise.resolve(); + + expect(promptInputs).toHaveLength(1); + const prompt = promptInputs[0]!; + expect(prompt).not.toContain("OTHERSECRET"); + expect(prompt).not.toContain("tok_abc123"); + expect(prompt).not.toContain("TOKABC123_"); + }); + it("expands plan-mode context once, then collapses an unchanged re-injection", async () => { const promptInputs: string[] = []; const agent = makeAgent(promptInputs); diff --git a/packages/coding-agent/src/advisor/runtime.ts b/packages/coding-agent/src/advisor/runtime.ts index 0d4a853db..a339200e3 100644 --- a/packages/coding-agent/src/advisor/runtime.ts +++ b/packages/coding-agent/src/advisor/runtime.ts @@ -553,10 +553,6 @@ function obfuscateAdvisorMessage( } } -function isImageBlock(value: object): value is ImageContent { - return "type" in value && value.type === "image"; -} - function collectAdvisorRegexSecretValues(obfuscator: SecretObfuscator, messages: AgentMessage[]): Set { const values = new Set(); const add = (value: string | undefined): void => { @@ -574,7 +570,7 @@ function collectAdvisorRegexSecretValues(obfuscator: SecretObfuscator, messages: for (const item of value) addJsonStrings(item); return; } - if (value === null || typeof value !== "object" || isImageBlock(value)) return; + if (value === null || typeof value !== "object") return; for (const item of Object.values(value)) addJsonStrings(item); }; const addContent = (content: TextualContent): void => { diff --git a/packages/coding-agent/src/export/share.ts b/packages/coding-agent/src/export/share.ts index f1966433f..6dbd36099 100644 --- a/packages/coding-agent/src/export/share.ts +++ b/packages/coding-agent/src/export/share.ts @@ -120,7 +120,7 @@ function collectShareRegexSecretValues(o: SecretObfuscator, data: SessionData): for (const item of value) addJsonStrings(item); return; } - if (!isRecord(value) || value.type === "image") return; + if (!isRecord(value)) return; for (const item of Object.values(value)) addJsonStrings(item); }; const addContent = (content: string | (TextContent | ImageContent)[]): void => { diff --git a/packages/coding-agent/test/share.test.ts b/packages/coding-agent/test/share.test.ts index 97bd884f4..f133a24db 100644 --- a/packages/coding-agent/test/share.test.ts +++ b/packages/coding-agent/test/share.test.ts @@ -402,6 +402,43 @@ describe("buildShareSnapshot", () => { expect(flat).not.toContain(regexSecret); expect(flat).toContain(`${friendlyName}_`); }); + + test("collects regex values from tool arguments that resemble image blocks", () => { + const plainSecret = "OTHERSECRET"; + const friendlyName = "TOKABC123"; + const regexSecret = "tok_abc123"; + const ts = "2026-06-12T00:00:00.000Z"; + const entries: SessionEntry[] = [ + { + type: "message", + id: "a1", + parentId: null, + timestamp: ts, + message: { + role: "assistant", + content: [ + { type: "toolCall", id: "call-1", name: "read", arguments: { type: "image", value: regexSecret } }, + ], + timestamp: 1, + }, + } as unknown as SessionEntry, + ]; + const sm = { + getHeader: () => ({ ...sessionData([], "x").header, title: `remember ${plainSecret}` }), + getEntries: () => entries, + getLeafId: () => "a1", + } as unknown as SessionManager; + const obfuscator = new SecretObfuscator([ + { type: "plain", content: plainSecret, friendlyName }, + { type: "regex", content: "tok_[a-z0-9]+" }, + ]); + + const flat = JSON.stringify(buildShareSnapshot(sm, { obfuscator })); + + expect(flat).not.toContain(plainSecret); + expect(flat).not.toContain(regexSecret); + expect(flat).not.toContain(`${friendlyName}_`); + }); }); describe("normalizeShareServerUrl", () => {