feat(audit): publish sequenced heartbeat and reconciliation evidence

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06ec5-7e2b-7743-ac08-719e1b0f42e2
This commit is contained in:
tegwick 2026-09-05 01:39:48 +02:00
parent fce60099c4
commit b9349782f4
32 changed files with 839 additions and 111 deletions

View file

@ -9,6 +9,7 @@ Phase 1 ships:
- policy-gated `GET /v1/transactions`
- policy-gated `GET /v1/snapshot`
- structured audit events without secrets
- sequenced audit heartbeat and reconciliation evidence
- env-backed or OpenBao-CLI-backed credential loading
Spend, transfer, card, invoicing, payment-link, and other volume-cost actions
@ -207,6 +208,32 @@ That consumer-side write remains outside this repo.
the current dogfood path remains `legacy_api_key` because that is the proven
BINKY-WP-0005 header mode.
## Audit deny-stream reconciliation
`audit.deny` is a low-volume, load-bearing observation class. Do not infer
completeness from a minimum event rate. The service emits `audit.heartbeat` at
startup, every `QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS` while the process is
active (default `86400`), and best-effort at shutdown. Each request event and
heartbeat carries `stream_instance_id` plus a monotonic `stream_sequence`.
For each instance, compare received `audit.allow` / `audit.deny` totals with
the heartbeat's `source_transition_counts` and reject sequence gaps. Window
counts describe transitions since the prior heartbeat. A quiet deny window has
`assertion: nothing-to-report`; a non-quiet one has
`assertion: transitions-reported`. Counters are process-local and reset only
when `stream_instance_id` changes.
The authenticated diagnostic view is:
```text
GET /v1/audit/reconciliation
```
It returns only stream identity, timestamps, sequence, and counts. It does not
write State Hub, query an observer, include actor/bank data, or increment the
stream it describes. The exact contract is declared in
`specs/audit-emission-cadence.yaml`.
## Live authorization gate (flex-auth + tenant-engine)
Off by default (no `QONTO_FLEX_AUTH_URL` set). When configured, every