2026-09-08 17:01:42 +02:00
|
|
|
# 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.
|
|
|
|
|
|
2026-09-10 20:36:21 +02:00
|
|
|
## 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.
|
|
|
|
|
|
2026-09-08 17:01:42 +02:00
|
|
|
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
|
|
|
|
|
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.
|