Skip to content

Audit

The proxy records two things for every call:

  1. A run — one row per call. Carries identity, timing, final effect.
  2. 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

FieldNotes
idUUID
instance_idJoins to the auto-claimed instance
started_atWhen prepare() opened the run
finished_atWhen the run closed (Allow, Block, or stream end)
final_effectAllow, Flag, Block, or null if still open
(joined) class_id, class_slugThe class this call was for
(joined) principal_idWho called
(subquery) step_countHow 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

FieldNotes
seqOrder within the run (0-indexed). Request-side first, response-side later.
directionrequest or response
detectorDetector name, or budget for budget gate steps
effectThe verdict effect (Allow, Flag, Block, …)
scoreFloat in [0, 1] for detectors that emit scores
reasonFree-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=budget if a cap is hit (action block, flag, or throttle).

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

Terminal window
# Last 50 runs across the tenant
curl http://localhost:8000/api/v1/audit/runs?limit=50
# Just blocks
curl "http://localhost:8000/api/v1/audit/runs?final_effect=Block&limit=20"
# Runs for one class
curl "http://localhost:8000/api/v1/audit/runs?class_id=<UUID>"
# One run with its steps
curl http://localhost:8000/api/v1/audit/runs/<run_id>

Postgres directly

Common queries against the schema:

-- Recent blocks across the fleet, with the blocking detector
SELECT 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_detector
FROM runs r JOIN instances i ON r.instance_id = i.id
JOIN classes c ON i.class_id = c.id
WHERE r.final_effect = 'Block'
ORDER BY r.started_at DESC LIMIT 50;
-- p50/p95/p99 latency per class
SELECT 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 p99
FROM runs r JOIN instances i ON r.instance_id = i.id
JOIN classes c ON i.class_id = c.id
WHERE 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.