Migrations:
- 0004_investigation_bureau.sql: 7 new tables (investigation_jobs + evidence,
hypotheses, contradictions, witnesses, gaps, residual_uncertainties), id
sequences, pg_notify trigger on investigation_jobs, RLS read-only public,
investigator role with least-privilege grants (no service_role).
- 0005_investigator_write_policies.sql: fixup adding RLS INSERT/UPDATE
policies bound to investigator + service_role + postgres (RLS with only a
SELECT policy was silently blocking the worker's claim UPDATE).
investigator-runtime/ (new Bun + TS container):
- src/main.ts: LISTEN/NOTIFY poller, claim-with-SKIP-LOCKED, drain pool,
healthcheck file, graceful SIGTERM shutdown.
- src/orchestrator.ts: chief-detective dispatch (evidence_chain → Locard).
Marks job failed when all per-item outputs error; surfaces first errors.
- src/lib/{env,pg,audit,ids,claude}.ts: typed config (gate #8), pool +
dedicated LISTEN client, NDJSON audit, sequence allocator (E-NNNN etc),
claude -p subprocess with quota detection (api_error_status=429).
- src/tools/write_evidence.ts: schema-validate (grade A/B/C custody steps),
resolve chunk_pk via FK, verify verbatim_excerpt actually appears in
chunk content, INSERT + render case/evidence/E-NNNN.md + audit.
- src/detectives/locard.ts: load chunk → call Claude with locard.md system
prompt → parse strict JSON → call writeEvidence locally.
- Dockerfile installs `claude` CLI (OAuth) at build time.
Compose:
- new `investigator` service builds from investigator-runtime/, connects
with low-privilege role, mounts case/ RW and wiki/+raw/ RO, 512m mem cap.
Web:
- /api/admin/investigate/test (POST+GET) gated by middleware (W0-F1).
POST creates a job, GET polls status. For W3.6 it becomes the chat tool.
End-to-end smoke: INSERT job → pg_notify → claim → Locard dispatch →
claude subprocess invoked. Auth works (CLI v2.1.150). Currently quota
exhausted (weekly limit · resets 3pm UTC) — pipeline catches the typed
isQuota error, marks job failed with surfaced reason. Architecture proven;
quota reset enables real evidence creation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38 lines
1.2 KiB
TypeScript
38 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}`);
|
|
}
|
|
}
|