/** * Shared local audit log used by the guardrail extensions. * * WHAT: appends JSONL events to ~/.config/pi-kit/audit.jsonl (chmod 600). * Never logs secret values — only pattern names, paths, command shape. * If PI_KIT_AUDIT_URL is set (or company.json auditEndpoint, wired by * audit-log.ts into that env var), each event is also POSTed best-effort * with a few retries. Posting never blocks or breaks the agent. * * DISABLE: remove the audit-log.ts extension, or set PI_KIT_AUDIT_OFF=1. */ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; const DIR = path.join(os.homedir(), ".config", "pi-kit"); const FILE = path.join(DIR, "audit.jsonl"); const POST_TIMEOUT_MS = 1500; const POST_ATTEMPTS = 3; export interface AuditEntry { kind: string; // e.g. "guardrail_block", "command", "session_start" [key: string]: unknown; } /** POST a line best-effort with a couple of retries (never awaited). */ function postWithRetry(url: string, body: string): void { const attempt = (n: number): Promise => fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body, signal: AbortSignal.timeout(POST_TIMEOUT_MS), }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); }) .catch((err) => { if (n < POST_ATTEMPTS) { const backoff = 200 * 2 ** (n - 1); return new Promise((resolve) => setTimeout(resolve, backoff)).then(() => attempt(n + 1)); } throw err; }); attempt(1).catch(() => {}); // fire-and-forget; failures are ignored } /** Append an audit entry locally and optionally forward it to an endpoint. */ export function audit(entry: AuditEntry): void { if (process.env.PI_KIT_AUDIT_OFF === "1") return; const line = JSON.stringify({ t: new Date().toISOString(), ...entry }); try { fs.mkdirSync(DIR, { recursive: true, mode: 0o700 }); fs.appendFileSync(FILE, `${line}\n`, { mode: 0o600 }); } catch { // Auditing must never break the agent; swallow local write errors. } const url = process.env.PI_KIT_AUDIT_URL; if (url) { postWithRetry(url, line); } }