Files
pi-kit/extensions/audit-log.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.9 KiB
TypeScript

/**
* 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" };
});
}