2026-08-29 14:51:53 +02:00
|
|
|
|
# Approval consumption ordering
|
|
|
|
|
|
|
|
|
|
|
|
**Owner:** gate-house
|
|
|
|
|
|
**PIP:** approval-engine
|
|
|
|
|
|
**PDP:** access-engine (currently flex-auth)
|
2026-09-06 01:29:28 +02:00
|
|
|
|
**Status:** normative for GH-WP-0002-T06; recorded as `GH-DEC-2026-003`,
|
|
|
|
|
|
amended by `GH-DEC-2026-005` (step-1 artifact and split validation)
|
|
|
|
|
|
**Date:** 2026-09-06
|
2026-08-29 14:51:53 +02:00
|
|
|
|
**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.
|
|
|
|
|
|
|
2026-09-06 01:29:28 +02:00
|
|
|
|
## What each step's artifact is
|
|
|
|
|
|
|
|
|
|
|
|
Amended by `GH-DEC-2026-005`.
|
|
|
|
|
|
|
|
|
|
|
|
**Step 1 serves the approval-claim** (`approval-engine/docs/approval-claim.md`).
|
|
|
|
|
|
That is the artifact `valid_now` belongs to, and it is the only object the claim
|
|
|
|
|
|
endpoint serves. flex-auth's proposed `ActionAuthorization` is **not** required
|
|
|
|
|
|
here, is not served here, and MUST NOT be served from this endpoint: a step-1
|
|
|
|
|
|
response cannot contain a step-2 decision.
|
|
|
|
|
|
|
|
|
|
|
|
**A PEP validates across both artifacts it already fetches**, and each against
|
|
|
|
|
|
the layer that owns its data:
|
|
|
|
|
|
|
|
|
|
|
|
| Artifact | Source | What the PEP checks |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| approval-claim | step 1, `approval-engine` (PIP) | binding digest, validity window, consumption state, observation freshness, issuer |
|
|
|
|
|
|
| `DecisionEnvelope` | step 2, `access-engine` (PDP) | exact `CheckRequest` match, policy package and version pin |
|
|
|
|
|
|
|
|
|
|
|
|
A PIP MUST NOT republish the PDP's decision, and a PEP MUST NOT accept the
|
|
|
|
|
|
approval fact from the decision artifact or the decision from the approval
|
|
|
|
|
|
artifact. Composing the two into one served object is a separate, post-decision
|
|
|
|
|
|
artifact at a separate endpoint if it is ever ratified — not a change to the
|
|
|
|
|
|
claim.
|
|
|
|
|
|
|
|
|
|
|
|
**Claim provenance is not State Hub.** A PEP MUST NOT require
|
|
|
|
|
|
`provenance.authority == "state-hub"` on the claim. State Hub is a read model and
|
|
|
|
|
|
holds no runtime approval authority; the issuer to check is the engine that
|
|
|
|
|
|
served the claim.
|
|
|
|
|
|
|
2026-08-29 14:51:53 +02:00
|
|
|
|
## 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.
|