import { appendFile, mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { expandHomePath } from "../../src/core/paths"; import { configLoader } from "../../src/shared/config"; import { GUARDRAILS_ACTION_BLOCKED_EVENT, GUARDRAILS_ACTION_PROMPTED_EVENT, GUARDRAILS_APPROVAL_REPLY_EVENT, GUARDRAILS_APPROVAL_REQUEST_EVENT, GUARDRAILS_RISK_DETECTED_EVENT, GUARDRAILS_YOLO_CHANGED_EVENT, } from "../../src/shared/events"; export interface AuditEntry { kind: | "blocked" | "prompted" | "risk" | "approval-request" | "approval-reply" | "yolo"; timestamp: string; cwd: string; feature?: string; reason?: string; tool?: string; blockSource?: string; requestId?: string; decision?: string; command?: string; yoloEnabled?: boolean; } export function toAuditEntry( kind: AuditEntry["kind"], data: unknown, ): AuditEntry { const payload = (data ?? {}) as Record; const context = payload.context as { toolName?: string } | undefined; const block = payload.block as { source?: string } | undefined; const risk = payload.risk as { reason?: string } | undefined; return { kind, timestamp: typeof payload.timestamp === "string" ? payload.timestamp : new Date().toISOString(), cwd: process.cwd(), ...(typeof payload.feature === "string" ? { feature: payload.feature } : {}), ...(typeof payload.reason === "string" ? { reason: payload.reason } : typeof risk?.reason === "string" ? { reason: risk.reason } : {}), ...(typeof context?.toolName === "string" ? { tool: context.toolName } : {}), ...(typeof block?.source === "string" ? { blockSource: block.source } : {}), ...(typeof payload.requestId === "string" ? { requestId: payload.requestId } : {}), ...(typeof payload.decision === "string" ? { decision: payload.decision } : {}), ...(typeof payload.command === "string" ? { command: payload.command } : {}), ...(typeof payload.enabled === "boolean" ? { yoloEnabled: payload.enabled } : {}), }; } /** * Append guardrails decisions (blocks, prompts, detected risks) to a JSONL * audit log so the user can review what the agent attempted. Auditing must * never break the session: all failures are swallowed. */ export function registerAuditLog(pi: ExtensionAPI): void { const record = (kind: AuditEntry["kind"]) => (data: unknown) => { void (async () => { try { const config = configLoader.getConfig(); if (!config.enabled || !config.audit.enabled) return; const path = expandHomePath(config.audit.path); await mkdir(dirname(path), { recursive: true }); await appendFile( path, `${JSON.stringify(toAuditEntry(kind, data))}\n`, "utf8", ); } catch { // Never let auditing interfere with the session. } })(); }; pi.events.on(GUARDRAILS_ACTION_BLOCKED_EVENT, record("blocked")); pi.events.on(GUARDRAILS_ACTION_PROMPTED_EVENT, record("prompted")); pi.events.on(GUARDRAILS_RISK_DETECTED_EVENT, record("risk")); pi.events.on(GUARDRAILS_APPROVAL_REQUEST_EVENT, record("approval-request")); pi.events.on(GUARDRAILS_APPROVAL_REPLY_EVENT, record("approval-reply")); pi.events.on(GUARDRAILS_YOLO_CHANGED_EVENT, record("yolo")); }