From a9d8d3eb79bf5d806411e6084418ec3341d43361 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 4 May 2026 15:37:56 -0500 Subject: [PATCH] Harden LLM wiki source redaction Co-Authored-By: Paperclip --- packages/plugins/plugin-llm-wiki/src/ui/index.tsx | 1 + packages/plugins/plugin-llm-wiki/src/wiki.ts | 12 ++++++++++-- .../plugins/plugin-llm-wiki/tests/plugin.spec.ts | 6 +++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/plugins/plugin-llm-wiki/src/ui/index.tsx b/packages/plugins/plugin-llm-wiki/src/ui/index.tsx index 5d6918f7c8c..c94ebb4871a 100644 --- a/packages/plugins/plugin-llm-wiki/src/ui/index.tsx +++ b/packages/plugins/plugin-llm-wiki/src/ui/index.tsx @@ -76,6 +76,7 @@ const DEFAULT_ROUTE_SIDEBAR_EXPANDED_PATHS = [ "wiki", "wiki/sources", "wiki/projects", + "wiki/areas", "wiki/entities", "wiki/concepts", "wiki/synthesis", diff --git a/packages/plugins/plugin-llm-wiki/src/wiki.ts b/packages/plugins/plugin-llm-wiki/src/wiki.ts index e866ef168dd..6581144c496 100644 --- a/packages/plugins/plugin-llm-wiki/src/wiki.ts +++ b/packages/plugins/plugin-llm-wiki/src/wiki.ts @@ -398,7 +398,7 @@ function redactDistillationSensitiveText(input: string): string { function protectDistillationSourceBody(input: { issue: Issue; - sourceKind: "comment" | "document"; + sourceKind: "issue" | "comment" | "document"; sourceId: string; body: string; }): DistillationSourceProtectionResult { @@ -1465,6 +1465,13 @@ export async function assemblePaperclipSourceBundle(ctx: PluginContext, input: P const remaining = { value: maxCharacters - lines.join("\n").length }; for (const issue of issues) { + const protectedIssue = protectDistillationSourceBody({ + issue, + sourceKind: "issue", + sourceId: issue.id, + body: issue.description?.trim() ? issue.description.trim() : "_No issue description._", + }); + if (protectedIssue.warning) warnings.push(protectedIssue.warning); const issueBody = [ `- Issue ID: ${issue.id}`, issue.identifier ? `- Identifier: ${issue.identifier}` : null, @@ -1474,7 +1481,7 @@ export async function assemblePaperclipSourceBundle(ctx: PluginContext, input: P issue.projectId ? `- Project ID: ${issue.projectId}` : null, `- Updated at: ${isoString(issue.updatedAt) ?? "unknown"}`, "", - issue.description?.trim() ? issue.description.trim() : "_No issue description._", + protectedIssue.body, ].filter((line): line is string => line !== null).join("\n"); appendBoundedSection({ lines, @@ -1488,6 +1495,7 @@ export async function assemblePaperclipSourceBundle(ctx: PluginContext, input: P projectId: issue.projectId ?? null, title: issue.title, updatedAt: isoString(issue.updatedAt) ?? undefined, + ...protectedIssue.refPatch, }, remaining, perSourceLimit, diff --git a/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts b/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts index 9a9af52d8ad..de7a1b17a28 100644 --- a/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts +++ b/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts @@ -1294,7 +1294,7 @@ Route sidebar state stays attached to the selected wiki page. identifier: "PAP-4002", title: "Sensitive source issue", projectId: "77777777-7777-4777-8777-777777777777", - description: "Keep the project page current without copying credential material into the wiki.", + description: "Keep the project page current without copying OPENAI_API_KEY=sk-supersecretissuevalue1234567890 into the wiki.", updatedAt: new Date("2026-05-04T10:00:00Z"), }); harness.seed({ @@ -1335,11 +1335,14 @@ Route sidebar state stays attached to the selected wiki page. expect(run.bundle.markdown).toContain("Suppressed by LLM Wiki distillation security policy"); expect(run.bundle.markdown).not.toContain("ghp_supersecretcommenttoken1234567890"); expect(run.bundle.markdown).not.toContain("sk-supersecretdocumentvalue1234567890"); + expect(run.bundle.markdown).not.toContain("sk-supersecretissuevalue1234567890"); expect(run.bundle.warnings).toEqual(expect.arrayContaining([ + expect.stringContaining("Suppressed issue content"), expect.stringContaining("Suppressed comment content"), expect.stringContaining("Suppressed document content"), ])); expect(run.bundle.sourceRefs).toEqual(expect.arrayContaining([ + expect.objectContaining({ kind: "issue", redactionStatus: "suppressed_sensitive_content" }), expect.objectContaining({ kind: "comment", redactionStatus: "suppressed_sensitive_content" }), expect.objectContaining({ kind: "document", redactionStatus: "suppressed_sensitive_content" }), ])); @@ -1351,6 +1354,7 @@ Route sidebar state stays attached to the selected wiki page. expect(storedMarkdown).toContain("Suppressed by LLM Wiki distillation security policy"); expect(storedMarkdown).not.toContain("ghp_supersecretcommenttoken1234567890"); expect(storedMarkdown).not.toContain("sk-supersecretdocumentvalue1234567890"); + expect(storedMarkdown).not.toContain("sk-supersecretissuevalue1234567890"); }); it("creates source snapshots and only advances cursors after successful distillation outcomes", async () => {