Heartbeat per event class, not per source
audit-core's completeness contract landed, and our single per-source beat is the shape it rules inadequate: it is discharged by whichever class is busy, so a revocation stream that has gone silent looks identical to a quiet one — and revocation is the only silence here that matters. Emit one nothing-to-report assertion per declared class, all four in one transaction so a partial emission cannot report some classes healthy and others stalled. Carry type audit-core.heartbeat with class and assertion on data. Pin that the first beat goes out at startup rather than an interval later, since a declared-but-never-sent class is their no_heartbeat_since_registration finding and not a skip. Declare heartbeat_classes and the reconciliation surface in the source registration, including the residual neither control covers: a compromised emitter suppresses the event and its own heartbeat together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275879@bnt-lap001 Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
This commit is contained in:
parent
c8f85c6d76
commit
bfb1e66646
6 changed files with 213 additions and 18 deletions
|
|
@ -27,6 +27,52 @@ no credentials. Redaction findings do not excuse a producer defect.
|
|||
Audit Core's scope overlay does not set this field; the protected sender
|
||||
registry entry must explicitly carry `secret_policy: redact` when provisioned.
|
||||
|
||||
## Heartbeat classes
|
||||
|
||||
Per class, not per source (`AUDIT-WP-0009-T04`, audit-core
|
||||
`docs/stream-completeness.md`). A per-source heartbeat is discharged by
|
||||
whichever class happens to be busy and says nothing about the quiet one — here
|
||||
that is `revocation`, whose silence is the only silence that matters. Requested
|
||||
`heartbeat_classes`, all four at the same bound:
|
||||
|
||||
| Class | Longest gap that is not yet a finding |
|
||||
| --- | --- |
|
||||
| `issuance` | 172800s (48h) |
|
||||
| `use` | 172800s (48h) |
|
||||
| `supersession` | 172800s (48h) |
|
||||
| `revocation` | 172800s (48h) |
|
||||
|
||||
Twice the deployed `--heartbeat-seconds` default of 86400, so one missed cycle
|
||||
is not a finding and two consecutive ones are. The engine emits all four in a
|
||||
single transaction: a partial emission would report some classes healthy and
|
||||
others stalled, which is a worse signal than none.
|
||||
|
||||
The first beat is emitted at startup rather than one interval later —
|
||||
`heartbeat_due` is true when no heartbeat exists — because audit-core raises
|
||||
`no_heartbeat_since_registration` for a declared-but-never-sent class, which is
|
||||
the shape a naive compare-against-last-seen would drop silently.
|
||||
|
||||
Heartbeat events carry `type: audit-core.heartbeat` with `class` and
|
||||
`assertion` on `data`. They are ordinary events on the same append-only chain
|
||||
and the same durable drain; there is no separate heartbeat path here either.
|
||||
|
||||
## Reconciliation
|
||||
|
||||
`GET /v1/reconciliation?source=&tenant=&since=&until=` returns per-class counts
|
||||
for this source's own events. `may_read: false` still holds and is not
|
||||
weakened: a source asking how many of its own events the archive holds learns
|
||||
nothing it did not itself emit. Both bounds must be supplied — a count whose
|
||||
window this engine did not choose is not comparable against
|
||||
`transition_counts()`. A request for another source's counts returns `403`, not
|
||||
zero; a zero would be a false answer to a completeness question.
|
||||
|
||||
**The bound, restated so it is not lost:** agreement on counts proves neither
|
||||
completeness nor that any event occurred, and a missing heartbeat is not proof
|
||||
of suppression. Both controls cover loss, outage, drain failure and accident.
|
||||
Neither covers this engine lying about itself — a compromised emitter suppresses
|
||||
the event and its own heartbeat together. That is the residual, and it is not
|
||||
closed by anything in this document.
|
||||
|
||||
The transactional outbox emits issuance, use, supersession and revocation;
|
||||
heartbeat uses the same durable drain. Its atomicity and retry contract remain
|
||||
in [outbox-contract.md](outbox-contract.md). Receiver identity/scope/ingress and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue