diff --git a/packages/coding-agent/src/advisor/__tests__/advisor.test.ts b/packages/coding-agent/src/advisor/__tests__/advisor.test.ts index 9c240c019..7c62c86cf 100644 --- a/packages/coding-agent/src/advisor/__tests__/advisor.test.ts +++ b/packages/coding-agent/src/advisor/__tests__/advisor.test.ts @@ -529,6 +529,38 @@ describe("advisor", () => { expect(promptInputs[0]).not.toContain(secret); }); + it("redacts nested async-result job labels before formatting", async () => { + const secret = "JOB_LABEL_SECRET_TOKEN_123"; + const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]); + const placeholder = obfuscator.obfuscate(secret); + const promptInputs: string[] = []; + const agent = makeAgent(promptInputs); + const messages: AgentMessage[] = [ + { + role: "custom", + customType: "async-result", + content: "", + details: { jobs: [{ label: `bash: echo ${secret}`, jobId: "j1" }] }, + display: true, + attribution: "agent", + timestamp: 1, + } 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); + expect(promptInputs[0]).toContain(placeholder); + expect(promptInputs[0]).not.toContain(secret); + }); + 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 7d4b671af..794c4bd00 100644 --- a/packages/coding-agent/src/advisor/runtime.ts +++ b/packages/coding-agent/src/advisor/runtime.ts @@ -346,23 +346,15 @@ function obfuscateAssistantMessage(obfuscator: SecretObfuscator, message: Assist return changed ? { ...message, content } : message; } -function obfuscateStringDetails( +function obfuscateDetails( obfuscator: SecretObfuscator, details: Record | undefined, ): Record | undefined { if (!details) return details; - let changed = false; - const result: Record = {}; - for (const [key, value] of Object.entries(details)) { - if (typeof value === "string") { - const text = obfuscator.obfuscate(value); - if (text !== value) changed = true; - result[key] = text; - } else { - result[key] = value; - } - } - return changed ? result : details; + // Walk strings at every depth: `customOneLiner` renders nested fields + // (e.g. `async-result` reads `details.jobs[].label`/`jobId`), so a shallow + // pass leaks any secret a background job's label happens to contain. + return obfuscateToolArguments(obfuscator, details); } function obfuscateAdvisorMessage(obfuscator: SecretObfuscator, message: AgentMessage): AgentMessage { @@ -382,7 +374,7 @@ function obfuscateAdvisorMessage(obfuscator: SecretObfuscator, message: AgentMes details?: Record; }; const content = obfuscateTextualContent(obfuscator, msg.content); - const details = obfuscateStringDetails(obfuscator, msg.details); + const details = obfuscateDetails(obfuscator, msg.details); if (content === msg.content && details === msg.details) return message; return { ...(message as object), content, details } as AgentMessage; }