Files
pi-kit/extensions/llm-redaction.ts
Aleksey Shakhmatov 7e50043984 feat(security): secret-scanner, commit-guard, llm-redaction, audit-log
- lib/secrets.ts: shared SECRET_PATTERNS + scanSecrets/redact (AWS/GitLab/GitHub/Slack tokens, private keys, JWT, generic credentials)
- lib/audit.ts: local JSONL audit (~/.config/pi-kit/audit.jsonl, 600), optional PI_KIT_AUDIT_URL POST; never logs secret values
- secret-scanner: block write/edit whose content matches a secret (complements protected-paths)
- commit-guard: enforce Conventional Commits on 'git commit -m' + scan message for secrets
- llm-redaction: redact secrets from outbound payload via before_provider_request
- audit-log: session_start + slash-command usage; wires company.json auditEndpoint into env
- config: auditEndpoint (null); package.json: npm test; test/guardrails.test.ts (16 cases, all pass)

Verified: all extensions load via pi (lib/ not loaded as extensions); npm test green.
2026-07-16 13:46:22 +03:00

52 lines
1.7 KiB
TypeScript

/**
* LLM Redaction Extension
*
* WHAT: Before each provider request, redacts secrets from the outbound
* payload so tokens/keys never leave the machine in a prompt. Uses the
* `before_provider_request` hook and returns a modified payload.
*
* CONFIGURE: secret patterns live in extensions/lib/secrets.ts.
*
* DISABLE: remove/rename this file from the package `extensions/` folder.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { redact } from "./lib/secrets.ts";
import { audit } from "./lib/audit.ts";
/** Recursively redact secrets in every string within a JSON-like value. */
function redactDeep(value: unknown, counter: { n: number }): unknown {
if (typeof value === "string") {
const cleaned = redact(value);
if (cleaned !== value) counter.n++;
return cleaned;
}
if (Array.isArray(value)) return value.map((v) => redactDeep(v, counter));
if (value && typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = redactDeep(v, counter);
}
return out;
}
return value;
}
export default function llmRedactionExtension(pi: ExtensionAPI) {
pi.on("before_provider_request", async (event) => {
const payload = event.payload;
if (!payload) return undefined;
const counter = { n: 0 };
// Only rewrite the message content to avoid disturbing payload metadata.
const messages = (payload as { messages?: unknown }).messages;
if (messages === undefined) return undefined;
const redactedMessages = redactDeep(messages, counter);
if (counter.n === 0) return undefined;
audit({ kind: "redaction", occurrences: counter.n });
return { ...payload, messages: redactedMessages };
});
}