Publish approval evidence-integrity contracts for GH-WP-0002
Adopt approval-engine's outbox wire as Gate House doctrine, specify the heartbeat-and-reconciliation detection surface, and settle consumption ordering: the PEP consumes by CAS before the side effect. T05's §11 check is written here and queued for statute v0.8. Assistant: grok Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
This commit is contained in:
parent
2ae611d2ad
commit
7408ff9234
5 changed files with 441 additions and 10 deletions
105
docs/contracts/approval-outbox.md
Normal file
105
docs/contracts/approval-outbox.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Approval-engine transactional outbox contract
|
||||
|
||||
**Owner:** gate-house
|
||||
**Implementer:** approval-engine
|
||||
**Evidence engine:** audit-core
|
||||
**Status:** normative for GH-WP-0002-T02
|
||||
**Date:** 2026-08-29
|
||||
**Statute:** `net-kingdom/canon/standards/security-layer-model_v0.7.md` §9.4, §9.6
|
||||
**Failure mode:** `GH-DEC-2026-002`
|
||||
**Source draft:** `approval-engine/docs/outbox-contract.md` (adopted)
|
||||
|
||||
This is the wire. Boundary and locality are in the statute and in
|
||||
`approval-engine/INTENT.md`. An implementer MUST NOT satisfy this contract by
|
||||
emitting synchronously to `audit-core` inside the state-change transaction.
|
||||
That path is atomic and is forbidden: an audit outage would become an inability
|
||||
to revoke.
|
||||
|
||||
## Rule
|
||||
|
||||
Every issuance, use, supersession, and revocation **inserts an outbox row in
|
||||
the same transaction** that mutates the approval object. The durable queue
|
||||
lives in `approval-engine`'s own store. Drain is asynchronous, at-least-once.
|
||||
`audit-core` dedupes on `event_id`; a replay does not fork the chain.
|
||||
|
||||
If the outbox insert cannot be committed, the mutation does not commit.
|
||||
Emit-after-commit is a defect.
|
||||
|
||||
Heartbeat rows (see [`approval-emission-detection.md`](approval-emission-detection.md))
|
||||
use the same table and the same at-least-once drain. They are not coupled to an
|
||||
object mutation.
|
||||
|
||||
## Event classes
|
||||
|
||||
| Class | When | `audit-core` `action` |
|
||||
| --- | --- | --- |
|
||||
| `issuance` | object becomes `approved` (threshold met) | `approval.issuance` |
|
||||
| `use` | object becomes `consumed` (public consume under [`approval-consumption.md`](approval-consumption.md)) | `approval.use` |
|
||||
| `supersession` | object becomes `superseded` | `approval.supersession` |
|
||||
| `revocation` | object becomes `revoked` | `approval.revocation` |
|
||||
| `heartbeat` | signed *nothing to report* plus counts | `approval.heartbeat` |
|
||||
|
||||
Expiry is a clock crossing, persisted on observation, and is **not** an
|
||||
emitted class. The validity window is already on the object.
|
||||
|
||||
## Outbox row
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `event_id` | UUID | Stable. Drain retries reuse it. |
|
||||
| `class` | enum above | |
|
||||
| `approval_id` | UUID or null | Null only for heartbeat. |
|
||||
| `payload` | object | The `audit-core.event.v1alpha1` record, ready to POST. |
|
||||
| `created_at` | RFC 3339 UTC | |
|
||||
| `drained_at` | RFC 3339 UTC or null | Set after a successful `audit-core` ack. |
|
||||
|
||||
## Payload (`audit-core` v1alpha1)
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "audit-core.event.v1alpha1",
|
||||
"event_id": "<same as outbox.event_id>",
|
||||
"observed_at": "<created_at>",
|
||||
"tenant": "platform",
|
||||
"scope": "netkingdom-approvals",
|
||||
"source": "approval-engine",
|
||||
"actor": "<actor or null for heartbeat>",
|
||||
"action": "approval.revocation",
|
||||
"resource": "approval:<approval_id>",
|
||||
"outcome": "success",
|
||||
"reason": null,
|
||||
"details": {
|
||||
"class": "revocation",
|
||||
"approval_id": "<uuid>",
|
||||
"binding_digest": "sha256:…",
|
||||
"request_digest": null,
|
||||
"superseded_by": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
No secret values. `details` may add non-secret identifiers; it MUST NOT add a
|
||||
validity verdict for consumers to branch on. `audit-core` MUST NOT expose an
|
||||
approval-validity query; this payload does not invite one.
|
||||
|
||||
For `use` events, `details.request_digest` is the digest the PEP presented at
|
||||
consume (see the consumption contract). It is null on other classes.
|
||||
|
||||
## Drain
|
||||
|
||||
1. Select undrained rows, oldest first.
|
||||
2. POST each payload to `audit-core`.
|
||||
3. On success, set `drained_at`.
|
||||
4. On failure, leave the row; retry later. **Do not** roll back the object
|
||||
mutation — it already committed with the row.
|
||||
|
||||
An `audit-core` outage therefore cannot block a revocation. This engine's
|
||||
own store being unavailable can, and should: the change could not have been
|
||||
recorded anyway. That is the whole of `GH-DEC-2026-002`.
|
||||
|
||||
## Forbidden shapes
|
||||
|
||||
- `audit-core` HTTP (or any client) inside `BEGIN` … `COMMIT` of a mutation.
|
||||
- Best-effort publish after commit with no row.
|
||||
- A second, non-local queue as the durability mechanism.
|
||||
- Deduping in `approval-engine` instead of relying on `event_id` at `audit-core`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue