fix(coding-agent): redacted nested advisor custom details

Walked custom/hook `details` recursively through the obfuscator so nested renderer fields (e.g. async-result `jobs[].label`) cannot leak configured secrets into the advisor prompt.

Fixes #3237
This commit is contained in:
roboomp
2026-06-22 07:24:51 +00:00
parent df71f9bc18
commit 426ca4bf03
2 changed files with 38 additions and 14 deletions
@@ -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);
+6 -14
View File
@@ -346,23 +346,15 @@ function obfuscateAssistantMessage(obfuscator: SecretObfuscator, message: Assist
return changed ? { ...message, content } : message;
}
function obfuscateStringDetails(
function obfuscateDetails(
obfuscator: SecretObfuscator,
details: Record<string, unknown> | undefined,
): Record<string, unknown> | undefined {
if (!details) return details;
let changed = false;
const result: Record<string, unknown> = {};
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<string, unknown>;
};
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;
}