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.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user