From 16c4e2bd40368352f674d7889ceee42a98dd58b3 Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Tue, 29 Sep 2026 20:21:55 +0200 Subject: [PATCH] fix(pi): redact private blocks before truncating prompts --- plugin/pi/index.ts | 4 ++- plugin/pi/test/native-tool-contract.test.mjs | 36 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/plugin/pi/index.ts b/plugin/pi/index.ts index 0450ecf33..bd0aee708 100644 --- a/plugin/pi/index.ts +++ b/plugin/pi/index.ts @@ -1972,7 +1972,9 @@ export default function registerEngram(pi: ExtensionAPI) { if (knownSessions.has(`\u0000closing:${effectiveID}`)) return { systemPrompt }; const body: PromptBody = { session_id: effectiveID, - content: stripPrivateTags(truncate(finalContent, 2000)), + // Redact before truncating: a block straddling the limit + // would otherwise lose its closing tag and leak. + content: truncate(stripPrivateTags(finalContent), 2000), project, }; if (state && (state.closing || state.epoch !== epoch)) return { systemPrompt }; diff --git a/plugin/pi/test/native-tool-contract.test.mjs b/plugin/pi/test/native-tool-contract.test.mjs index 61fe628fd..eca4c33c2 100644 --- a/plugin/pi/test/native-tool-contract.test.mjs +++ b/plugin/pi/test/native-tool-contract.test.mjs @@ -2945,3 +2945,39 @@ test("registered Pi-native mem_pin and mem_unpin target the observation pin rout else process.env.ENGRAM_URL = originalUrl; } }); + +test("automatic prompt capture redacts a private block that straddles the truncation limit", async () => { + const originalFetch = globalThis.fetch; + const originalUrl = process.env.ENGRAM_URL; + const calls = []; + process.env.ENGRAM_URL = "http://127.0.0.1:17437"; + globalThis.fetch = async (url, init = {}) => { + const path = new URL(url).pathname; + const body = init.body ? JSON.parse(init.body) : undefined; + calls.push({ method: init.method ?? "GET", path, body }); + if (path === "/health") return new Response(JSON.stringify({ status: "ok" })); + if (path === "/project/current") return new Response(JSON.stringify({ project: "engram" })); + if (path === "/sessions") return new Response(JSON.stringify({ status: "created" }), { status: 201 }); + if (path === "/prompts") return new Response(JSON.stringify({ id: 1 }), { status: 201 }); + return new Response(JSON.stringify({})); + }; + + try { + await withPluginSandbox("engram-pi-contract-", async ({ sandbox }) => { + const { eventHandlers } = await loadPluginHarness(sandbox); + await eventHandlers.get("before_agent_start")( + { systemPrompt: "base", prompt: `${"a".repeat(1980)}PIN=42 trailing` }, + runtimeContext("straddle-session"), + ); + }); + + const prompts = calls.filter((call) => call.method === "POST" && call.path === "/prompts"); + assert.equal(prompts.length, 1, "the prompt must still be captured"); + assert.equal(JSON.stringify(prompts[0].body).includes("PIN=42"), false, "private content must never reach the wire"); + assert.match(prompts[0].body.content, /\[REDACTED\]/); + } finally { + globalThis.fetch = originalFetch; + if (originalUrl === undefined) delete process.env.ENGRAM_URL; + else process.env.ENGRAM_URL = originalUrl; + } +});