Audit
The proxy records two things for every call:
- A run — one row per call. Carries identity, timing, final effect.
- A list of steps — one row per detector inspection within that run.
Both live in Postgres and are surfaced via GET /api/v1/audit/runs and GET /api/v1/audit/runs/{run_id}.
Run
| Field | Notes |
|---|---|
id | UUID |
instance_id | Joins to the auto-claimed instance |
started_at | When prepare() opened the run |
finished_at | When the run closed (Allow, Block, or stream end) |
final_effect | Allow, Flag, Block, or null if still open |
(joined) class_id, class_slug | The class this call was for |
(joined) principal_id | Who called |
(subquery) step_count | How many step rows are attached |
A run is opened after identity resolves but before the cascade starts, so a budget-blocked or detector-blocked call still has a run.
Step
| Field | Notes |
|---|---|
seq | Order within the run (0-indexed). Request-side first, response-side later. |
direction | request or response |
detector | Detector name, or budget for budget gate steps |
effect | The verdict effect (Allow, Flag, Block, …) |
score | Float in [0, 1] for detectors that emit scores |
reason | Free-text — what the detector saw and why it decided |
When a step is written
- Request-side cascade — one step per detector per stage. Written before forwarding upstream.
- Response-side cascade — one step per detector per window-crossing pass during streaming, plus one final-pass step at stream end if there’s any tail.
- Budget gate — one step with
detector=budgetif a cap is hit (actionblock,flag, orthrottle).
What you can do with it
Dashboard
/audit shows recent runs with filters by effect and class. Click into a run for the full step list. Each step shows the direction, detector, effect dot, score, and reason.
HTTP
# Last 50 runs across the tenantcurl http://localhost:8000/api/v1/audit/runs?limit=50
# Just blockscurl "http://localhost:8000/api/v1/audit/runs?final_effect=Block&limit=20"
# Runs for one classcurl "http://localhost:8000/api/v1/audit/runs?class_id=<UUID>"
# One run with its stepscurl http://localhost:8000/api/v1/audit/runs/<run_id>Postgres directly
Common queries against the schema:
-- Recent blocks across the fleet, with the blocking detectorSELECT r.id, c.slug AS class, r.started_at, (SELECT detector FROM steps WHERE run_id = r.id AND effect='Block' LIMIT 1) AS by_detectorFROM runs r JOIN instances i ON r.instance_id = i.id JOIN classes c ON i.class_id = c.idWHERE r.final_effect = 'Block'ORDER BY r.started_at DESC LIMIT 50;
-- p50/p95/p99 latency per classSELECT c.slug, percentile_disc(0.50) WITHIN GROUP (ORDER BY r.finished_at - r.started_at) AS p50, percentile_disc(0.95) WITHIN GROUP (ORDER BY r.finished_at - r.started_at) AS p95, percentile_disc(0.99) WITHIN GROUP (ORDER BY r.finished_at - r.started_at) AS p99FROM runs r JOIN instances i ON r.instance_id = i.id JOIN classes c ON i.class_id = c.idWHERE r.finished_at IS NOT NULL AND r.started_at > now() - INTERVAL '24 hours'GROUP BY c.slug;What’s retained
Currently: forever. There’s no retention policy built in. For production, point Postgres at a backup / retention solution that fits your compliance shape, or add a periodic DELETE FROM runs WHERE started_at < now() - INTERVAL '90 days' (and cascade by FK).
Audit as a contract
The audit log is the mechanism for “what did the system do, and why?” Build dashboards, reports, alerts, and compliance evidence on it.