Finish approval engine spine

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a05e2e-805b-7042-a750-71f473bceea2
This commit is contained in:
tegwick 2026-09-01 23:45:48 +02:00
parent e7c210bf56
commit c3f1dfbc07
18 changed files with 526 additions and 75 deletions

View file

@ -0,0 +1,43 @@
# Approval consumption API
Status: implemented under Gate House decision `GH-DEC-2026-003`.
The normative protocol is
`gate-house/docs/contracts/approval-consumption.md`. This document records the
approval-engine implementation surface; it does not redefine the protocol.
## Endpoint
```text
POST /v1/approvals/{id}/consume
```
```json
{
"request_digest": "sha256:<64 lowercase hex>",
"decision_id": "decision:optional-provenance"
}
```
The caller is the PEP that is about to perform the protected side effect. It
calls consume after an ALLOW and before that side effect. `request_digest` is
the canonical digest from the PDP decision binding, not a newly serialized
request and not approval-engine's native binding digest.
## Results
- First valid consume: atomically stores the digest, changes `approved` to
`consumed`, and inserts one `approval.use` outbox row in the same transaction.
- Same digest after consumption: `200` idempotent success and no second outbox
row.
- Different digest after consumption: `409 conflict`; the PEP must not act.
- Revoked, superseded, expired, outside-window, requested, or unknown object:
conflict or not-found; the PEP must not act.
- Outbox insert/store failure: `503`; the transaction rolls back and the PEP
must not act.
There is no unconsume, release, or reserve. If the protected side effect fails
after consumption, the approval remains spent and a retry needs a new approval.
The response is mutation evidence, not a permission decision. It contains no
`effect`, `allow`, `deny`, or decision result.

View file

@ -8,6 +8,8 @@
- Claim contract: [`approval-claim.md`](approval-claim.md)
- Introspection: `GET /v1/approvals/{id}/claim`
- Object: `POST /v1/approvals`, entries, revoke, supersede
- Consumption: `POST /v1/approvals/{id}/consume`, called by the PEP before the
protected side effect under `GH-DEC-2026-003`
- Native `binding.digest` plus optional `binding.pdp_digest` for
`NewDecisionBinding.request_digest`
@ -15,12 +17,15 @@ T03 is unblocked on the **object**, not on a hub substitute. Validate the
claim before privileged production actions. Fail closed if this engine is
unreachable.
## What does not exist yet
## Consumption contract
Consumption ordering (`GH-WP-0002-T06` / `APPROVAL-WP-0001-T05`). There is
no public `consume`. `FLEX-WP-0017` T05 stays blocked **only** on that
contract, not on a missing object or a missing digest.
Gate House settled ordering in `GH-DEC-2026-003` and
`docs/contracts/approval-consumption.md`. The PEP presents the decision
binding's `request_digest` and consumes by CAS before the protected side
effect. A same-digest retry is idempotent success; a different digest conflicts;
there is no unconsume. `access-engine` remains read-only and never calls this
mutation.
Canon T-06 against this implementation: `tests/test_t06_replay.py` (wrong
target, wrong action, later time, revoked, superseded). Consume-side replay
is out of scope until T05.
target, wrong action, later time, revoked, superseded, same-digest retry, and
different-digest consume replay).

View file

@ -26,7 +26,7 @@ object mutation.
| Class | When | `audit-core` `action` |
| --- | --- | --- |
| `issuance` | object becomes `approved` (threshold met) | `approval.issuance` |
| `use` | object becomes `consumed` (internal CAS; not a public API until `GH-WP-0002-T06`) | `approval.use` |
| `use` | public CAS accepts first `request_digest` and object becomes `consumed` | `approval.use` |
| `supersession` | object becomes `superseded` | `approval.supersession` |
| `revocation` | object becomes `revoked` | `approval.revocation` |
| `heartbeat` | signed *nothing to report* plus counts | `approval.heartbeat` |