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 @@
|
||||
/**
|
||||
* Audit Log Extension
|
||||
*
|
||||
* WHAT: Records session starts and slash-command usage to the local audit log
|
||||
* (~/.config/pi-kit/audit.jsonl). Guardrail blocks are recorded by the
|
||||
* guardrail extensions themselves via the shared audit() helper. If an
|
||||
* audit endpoint is configured it is forwarded best-effort (see below).
|
||||
*
|
||||
* CONFIGURE: set `auditEndpoint` in config/company.json, or the PI_KIT_AUDIT_URL
|
||||
* env var, to forward events. Turn everything off with PI_KIT_AUDIT_OFF=1.
|
||||
*
|
||||
* DISABLE: remove/rename this file from the package `extensions/` folder.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { audit } from "./lib/audit.ts";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_PATH = path.join(HERE, "..", "config", "company.json");
|
||||
|
||||
/** Wire company.json `auditEndpoint` into the env var read by audit() (unless already set). */
|
||||
function initEndpointFromConfig(): void {
|
||||
if (process.env.PI_KIT_AUDIT_URL) return;
|
||||
try {
|
||||
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
if (typeof cfg.auditEndpoint === "string" && cfg.auditEndpoint) {
|
||||
process.env.PI_KIT_AUDIT_URL = cfg.auditEndpoint;
|
||||
}
|
||||
} catch {
|
||||
// No config / unreadable — local-only auditing.
|
||||
}
|
||||
}
|
||||
|
||||
export default function auditLogExtension(pi: ExtensionAPI) {
|
||||
initEndpointFromConfig();
|
||||
|
||||
pi.on("session_start", async (event) => {
|
||||
audit({ kind: "session_start", reason: (event as { reason?: string }).reason });
|
||||
});
|
||||
|
||||
// Record slash-command usage (name only, no arguments — args may contain secrets).
|
||||
pi.on("input", async (event) => {
|
||||
const text = (event as { text?: string }).text ?? "";
|
||||
const m = text.match(/^\/([a-z0-9:_-]+)/i);
|
||||
if (m) audit({ kind: "command", command: m[1] });
|
||||
return { action: "continue" };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user