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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user