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
148
docs/contracts/approval-consumption.md
Normal file
148
docs/contracts/approval-consumption.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# 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
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
```text
|
||||
POST /v1/approvals/{id}/consume
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"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`](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`](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-engine` reads the claim and never mutates.
|
||||
- Not implemented until `approval-engine` wires `POST /consume` to 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue