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
6.3 KiB
Approval consumption ordering
Owner: gate-house
PIP: approval-engine
PDP: access-engine (currently flex-auth)
Status: normative for GH-WP-0002-T06; recorded as GH-DEC-2026-003
Date: 2026-08-29
Statute: net-kingdom/canon/standards/security-layer-model_v0.7.md §9.4, §9.7.3–4, §16
Blocks: APPROVAL-WP-0001-T05, FLEX-WP-0017-T05
This contract settles who marks an approval consumed, at what point relative
to the decision, and who owns each of the three named failure modes. Nothing
in approval-engine may implement a public consume by guessing this.
Sequence
1. PIP GET /v1/approvals/{id}/claim → valid_now (a fact, not permission)
2. PDP Check / decide → ALLOW | DENY | …
3. PEP POST /v1/approvals/{id}/consume → CAS valid → consumed
4. PEP protected side effect → only after consume succeeds
The PDP never mutates. Staff never calls consume. audit-core never
calls consume. The signaler is the PEP-shaped consumer that is about to cause
the protected side effect named in the binding — today secrets-engine for
OpenBao writes, ops-warden for the SSH signing lane, and any other consumer
companion §5 already treats as PEP-shaped.
Holding a claim with valid_now: true, or an ALLOW against that claim, is not
authority to act. The consume is.
Why consume is before the side effect
§9.7.3 says consumption MUST NOT be inferred from a decision record. That stands. A decision record proves an intent to act, not an act. The consume mutation is the act's evidence at this engine.
The same paragraph currently reads as if the action must precede the
consume call. That reading is a protocol, and it is the wrong one. If the
PEP acts and then consumes, two racing PEPs can both observe valid_now,
both receive ALLOW, and both act; CAS then prevents only the second
record, not the second side effect. Single consumption would be theatre.
The PEP MUST obtain a successful consume before the protected side effect.
In-flight duplicate ALLOWs are expected; the CAS serializes use. A later
CheckRequest sees valid_now: false and cannot mint a new ALLOW against the
same object.
This is a protocol clarification of §9.7.3, not a retraction of the forensic claim. It lands in statute v0.8. Until then this contract governs the implementers who were blocked on it.
Consume request
POST /v1/approvals/{id}/consume
{
"request_digest": "<canonical request digest from the decision binding>",
"decision_id": "<optional; the ALLOW this consume discharges>"
}
request_digest is the same digest companion §5.2 already uses as the
mechanical replay test (NewDecisionBinding.request_digest in flex-auth).
Go's json.Marshal of a CheckRequest is not this digest. Compare to the
binding the PDP already computed, not to a re-serialized CheckRequest.
The mutation is compare-and-swap:
| Current status | Digest on the object | Result |
|---|---|---|
approved, inside validity window |
none | consumed; store this digest; emit use |
consumed |
same digest | idempotent success — same logical request, a retry |
consumed |
different digest | conflict — already used for another request |
revoked / superseded / expired / outside window / not approved |
— | conflict; MUST NOT act |
The use outbox row is inserted in the same transaction as the status change
(approval-outbox.md). A failed insert rolls the
consume back. An audit-core outage does not.
There is no unconsume. There is no reserve/release. An approval authorizes one attempt, not one success.
The three failure modes
1. Allow rendered, never consumed
Cause. PEP crash or abandonment between ALLOW and consume.
Effect. The object stays approved. A later PEP can still consume it,
inside the remaining validity window and inside the ALLOW's own lifetime
(§9.7.1). That is a delayed first use, not a replay of a completed one.
Owner. The PEP is obligated to consume before the side effect. The
approval-engine does not auto-consume from the existence of a decision.
The PDP bounds the window with the decision lifetime. Detection of a
stale ALLOW with no matching use is a finding on this surface
(approval-emission-detection.md), not a
consume.
2. Double consumption by racing callers
Cause. Two PEPs, two ALLOWs in flight, both presenting consume.
Effect. CAS: one success; the other is idempotent success if and only if the digest matches, otherwise conflict.
Owner. approval-engine performs the CAS. The PEP that receives conflict
MUST NOT perform the side effect. The PDP that rendered the second ALLOW has
done nothing wrong — it decided on a still-valid claim.
Same-digest idempotent success is a retry of one logical request. Companion §5.2 already permits reusing an ALLOW inside its binding and lifetime. The side effect MUST itself be idempotent under that digest; that is the PEP's obligation, not this engine's.
3. Consumed, then the authorized action fails
Cause. Consume succeeded; the protected side effect then failed or was never reached (crash after consume).
Effect. The approval is spent. Retry requires a new approval object.
Owner. Accepted here as the cost of closing (2). Unconsume would reopen replay. A flake is an issuance problem, not a reason to make consumption reversible. The PEP MAY request a new approval; it MUST NOT infer that the spent object is still valid.
What this is not
- Not a second decision. Consume answers "did this engine accept the use", never "may this actor do X".
- Not an inference from
audit-core. The archive reports what it received. - Not a PDP duty.
access-enginereads the claim and never mutates. - Not implemented until
approval-enginewiresPOST /consumeto the CAS that already exists internally, with the digest column this contract adds.
Acceptance
APPROVAL-WP-0001-T05 may implement the public consume against this
document. FLEX-WP-0017-T05 may require the PEP (secrets-engine, for that
task) to consume before the OpenBao call. Canon T-06 consume-side replay —
a second, different digest against a consumed object — becomes in-scope with
that implementation.