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
4.4 KiB
Platform approval audit sender
Source return for APPROVAL-WP-0002-T01/T04 and AUDIT-WP-0009-T09,
2026-09-08. This records the existing platform store's required registration;
it neither provisions a credential nor admits another tenant.
| Field | Required value |
|---|---|
| Sender and permitted source | approval-engine (exact) |
| Tenants | ["tenant:platform"] (exact) |
| Write / read | true / false |
| Evidence kind | load-bearing |
| Completeness trade | none |
| Secret policy | redact |
| Consumer mount | Secret approval-engine/approval-engine-audit, key audit-token |
The accepted platform tenant decision is already implemented in the Engine
default, production CLI and deployment arguments. The audit envelope carries
that same store tenant without normalization. This is the tenant list Audit
Core requested; neither platform, tenant:coulomb, nor * is required.
A future store serving another tenant needs a separate registration review.
Choose redact explicitly: the receiver should retain a legitimate revocation
record with an accidentally secret-shaped field removed, rather than reject
the event and leave its delivery pending. Approval payloads must still contain
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. Receiver identity/scope/ingress and credential admission precede live drain proof. Attestation freshness, omission detection and reconciliation retain their separate AUDIT-WP-0009-T02/T04/T06 owners and bounds; this registration does not claim their completion.
First provisioning still requires the platform-owned custody record linking the receiver's protected sender registry and this mounted credential. No credential value belongs in this document, Git, Hub or a rollout receipt.