- 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.
52 lines
1.7 KiB
TypeScript
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 };
|
|
});
|
|
}
|