39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
|
|
/**
|
||
|
|
* audit.ts — append-only NDJSON audit log of every meaningful step.
|
||
|
|
*
|
||
|
|
* Every write tool, every detective dispatch, every gate failure goes here.
|
||
|
|
* The chief-detective trail is what makes the Investigation Bureau auditable
|
||
|
|
* (per ADR-002 + agentic-layer-spec sec 5).
|
||
|
|
*/
|
||
|
|
import { appendFile, mkdir } from "node:fs/promises";
|
||
|
|
import path from "node:path";
|
||
|
|
import { env } from "./env";
|
||
|
|
|
||
|
|
const ensured = new Set<string>();
|
||
|
|
|
||
|
|
async function ensureDir(p: string): Promise<void> {
|
||
|
|
if (ensured.has(p)) return;
|
||
|
|
await mkdir(p, { recursive: true });
|
||
|
|
ensured.add(p);
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface AuditEvent {
|
||
|
|
event: string;
|
||
|
|
job_id?: string;
|
||
|
|
detective?: string;
|
||
|
|
/** Free-form payload — keep keys snake_case. */
|
||
|
|
[k: string]: unknown;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function audit(ev: AuditEvent): Promise<void> {
|
||
|
|
const row = { ts: new Date().toISOString(), worker_id: env.WORKER_ID, ...ev };
|
||
|
|
const dir = path.dirname(env.AUDIT_LOG);
|
||
|
|
try {
|
||
|
|
await ensureDir(dir);
|
||
|
|
await appendFile(env.AUDIT_LOG, JSON.stringify(row) + "\n", "utf-8");
|
||
|
|
} catch (e) {
|
||
|
|
// Audit must never break the worker. Log to stderr and move on.
|
||
|
|
console.error(`[audit] append failed: ${(e as Error).message}`);
|
||
|
|
}
|
||
|
|
}
|