diff --git a/packages/coding-agent/src/secrets/obfuscator.ts b/packages/coding-agent/src/secrets/obfuscator.ts index cb3b0a444..f6f66ec2a 100644 --- a/packages/coding-agent/src/secrets/obfuscator.ts +++ b/packages/coding-agent/src/secrets/obfuscator.ts @@ -1874,21 +1874,18 @@ export function deobfuscateAgentMessages( const result = messages.map((message): AgentMessage => { switch (message.role) { case "assistant": { - if (!allowLegacyAliases) return message; const content = deobfuscateAssistantContent(obfuscator, message.content, allowLegacyAliases); if (content === message.content) return message; changed = true; return { ...message, content }; } case "branchSummary": { - if (!allowLegacyAliases) return message; const summary = deob(message.summary); if (summary === message.summary) return message; changed = true; return { ...message, summary }; } case "compactionSummary": { - if (!allowLegacyAliases) return message; const summary = deob(message.summary); const shortSummary = message.shortSummary === undefined ? undefined : deob(message.shortSummary); const blocks = diff --git a/packages/coding-agent/test/sdk-session-isolation.test.ts b/packages/coding-agent/test/sdk-session-isolation.test.ts index 34f461d0e..afcff92e5 100644 --- a/packages/coding-agent/test/sdk-session-isolation.test.ts +++ b/packages/coding-agent/test/sdk-session-isolation.test.ts @@ -223,7 +223,7 @@ describe("createAgentSession session storage isolation", () => { }); }); - it("keeps restored assistant placeholders obfuscated across reloads", async () => { + it("restores keyed assistant placeholders across reloads", async () => { await withClearedSecretEnv(async () => { await withTempConfigRoot(async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `pi-sdk-session-secrets-${Snowflake.next()}-`)); @@ -284,9 +284,13 @@ describe("createAgentSession session storage isolation", () => { enableLsp: false, }); try { - expect(getAssistantText(session.messages.at(-1) as AssistantMessage | undefined)).toBe(placeholder); + expect(getAssistantText(session.messages.at(-1) as AssistantMessage | undefined)).toBe( + "token sdk-secret-token-123456", + ); await session.reload(); - expect(getAssistantText(session.messages.at(-1) as AssistantMessage | undefined)).toBe(placeholder); + expect(getAssistantText(session.messages.at(-1) as AssistantMessage | undefined)).toBe( + "token sdk-secret-token-123456", + ); } finally { await session.dispose(); } diff --git a/packages/coding-agent/test/secrets-obfuscator.test.ts b/packages/coding-agent/test/secrets-obfuscator.test.ts index 583ef4399..6fa4e5dd9 100644 --- a/packages/coding-agent/test/secrets-obfuscator.test.ts +++ b/packages/coding-agent/test/secrets-obfuscator.test.ts @@ -2355,7 +2355,7 @@ describe("SecretObfuscator friendlyName placeholders", () => { expect(obfuscator.deobfuscateObject({ cmd: "cat #XRRS#" })).toEqual({ cmd: "cat #XRRS#" }); }); - it("never restores legacy aliases on agent-feeding replay, only on display transcripts", () => { + it("restores keyed placeholders but never legacy aliases on agent-feeding replay", () => { // deobfuscateSessionContext has two kinds of consumers: agent-feeding paths // (resume, history rewrite, branch switch) whose output is re-obfuscated and // sent to the provider, and a display-only transcript (allowLegacyAliases). @@ -2395,8 +2395,26 @@ describe("SecretObfuscator friendlyName placeholders", () => { isError: false, timestamp: 2, }; + const branchSummary: AgentMessage = { + role: "branchSummary", + summary: `branch #XRRS# and echoed ${keyedToken}`, + fromId: "branch-1", + timestamp: 3, + }; + const compactionSummary: AgentMessage = { + role: "compactionSummary", + summary: `compaction #XRRS# and echoed ${keyedToken}`, + tokensBefore: 0, + timestamp: 4, + }; + const contextMessages: AgentMessage[] = [ + assistant as AgentMessage, + toolResult as AgentMessage, + branchSummary, + compactionSummary, + ]; const ctx = { - messages: [assistant, toolResult], + messages: contextMessages, models: {}, injectedTtsrRules: [], selectedMCPToolNames: [], @@ -2404,14 +2422,18 @@ describe("SecretObfuscator friendlyName placeholders", () => { mode: "none", }; - // Agent-feeding default: assistant-authored content stays obfuscated, and no - // legacy `#XRRS#` is restored, so an echoed placeholder is not expanded into a - // raw secret before the next provider turn. + // Agent-feeding default restores keyed placeholders authored by this + // obfuscator but leaves a prompt-injected legacy alias inert before the + // next provider turn. const fed = deobfuscateSessionContext(ctx, obfuscator); const fedAssistant = (fed.messages[0] as Extract).content[0] as { text: string }; const fedTool = (fed.messages[1] as Extract).content[0] as { text: string }; - expect(fedAssistant.text).toBe(`attacker planted #XRRS# and echoed ${keyedToken}`); + expect(fedAssistant.text).toBe("attacker planted #XRRS# and echoed legacy-secret"); expect(fedTool.text).toBe("bash stdout #XRRS#"); + const fedBranch = fed.messages[2] as Extract; + const fedCompaction = fed.messages[3] as Extract; + expect(fedBranch.summary).toBe("branch #XRRS# and echoed legacy-secret"); + expect(fedCompaction.summary).toBe("compaction #XRRS# and echoed legacy-secret"); // Display-only transcript: legacy aliases ARE restored so a genuinely // pre-keyed session renders its secrets. This output is never re-obfuscated. @@ -2422,6 +2444,10 @@ describe("SecretObfuscator friendlyName placeholders", () => { const shownTool = (shown.messages[1] as Extract).content[0] as { text: string }; expect(shownAssistant.text).toBe("attacker planted legacy-secret and echoed legacy-secret"); expect(shownTool.text).toBe("bash stdout #XRRS#"); + const shownBranch = shown.messages[2] as Extract; + const shownCompaction = shown.messages[3] as Extract; + expect(shownBranch.summary).toBe("branch legacy-secret and echoed legacy-secret"); + expect(shownCompaction.summary).toBe("compaction legacy-secret and echoed legacy-secret"); }); it("deobfuscates placeholders after friendlyName changes", () => { @@ -3502,11 +3528,7 @@ describe("deobfuscateAgentMessages (display restore)", () => { timestamp: 4, }; - const restored = deobfuscateAgentMessages( - obfuscator, - [userMsg, assistantMsg, branchSummary, compactionSummary], - true, - ); + const restored = deobfuscateAgentMessages(obfuscator, [userMsg, assistantMsg, branchSummary, compactionSummary]); // Assistant text and tool-call args/intent are restored to the real secret. const restoredAssistant = restored[1] as AssistantMessage;