fix(secrets): scan typed JSON collision values

This commit is contained in:
Mathews-Tom
2026-07-11 22:31:49 +05:30
parent 331e7bea65
commit 2e4e215da2
4 changed files with 73 additions and 6 deletions
@@ -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);
+1 -5
View File
@@ -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<string> {
const values = new Set<string>();
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 => {
+1 -1
View File
@@ -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 => {
+37
View File
@@ -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", () => {