Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
5.4 KiB
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
POST /v1/approvals/{id}/consume
{
"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
approvedtoconsumed, and inserts oneapproval.useoutbox row in the same transaction. - Same digest after consumption:
200idempotent 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.
Authoritative time and fractional precision
New source preserves the actual aware UTC clock's microseconds rather than
rounding down to whole seconds. The first consume samples one instant after
BEGIN IMMEDIATE acquires its write lock, then uses that instant for the
not-before/inclusive and expiry/exclusive check, persisted consumed_at and
updated_at, and use-event occurrence/creation time. Lock wait cannot borrow
an earlier validity instant or stamp evidence before actual admission.
Existing whole-second ISO strings remain valid and are never backfilled with invented fractions. Idempotent responses and object readback retain the original recorded timestamp/decision exactly, even after expiry; they do not renew execution authority. The store schema, API keys, CAS/digest correspondence and no-unconsume rule are unchanged. Claim observation/freshness and other new source timestamps also retain actual clock precision. Consumers must parse aware ISO/RFC3339 timestamps including fractional seconds instead of assuming a fixed-width whole-second string. No clock skew or early-use tolerance is added.
The claim response is not an ActionAuthorization
GET /v1/approvals/{id}/claim serves the approval-claim envelope defined in
approval-claim.md. It is not the ActionAuthorization
object that secrets-engine's validate_action_authorization currently expects,
and a PEP that points that validator at this URL fails closed for the wrong
reason.
Both envelopes carry schema_version: "0.1", so the version check passes and
the mismatch surfaces later as a missing-field or wrong-authority error. Do not
read that failure as an approval-engine outage.
| Validator expectation | What the claim actually serves |
|---|---|
id (canonical UUID) |
approval_id — the object id, same value, different key |
status == "approved" |
state (approved, consumed, revoked, superseded, expired, requested) plus the valid_now predicate |
superseded_by |
absent; supersession appears as state: "superseded" |
provenance.authority == "state-hub" |
issuer: "approval-engine" — this engine is the authority for the approval object; the State Hub is a read model and never issues one |
request (full CheckRequest) |
binding (action, actor, principal, purpose, target) plus binding.digest, and binding.pdp_digest when recorded at issue |
approvals.required_count / entries |
not exposed; the distinct-approver threshold is already folded into valid_now, with reason_code: "insufficient_approvers" when unmet |
| policy package/version pin | not carried; the policy pin belongs to the access-engine decision, not to the approval fact |
The omissions are deliberate. A claim is a fact about an approval object, not
a decision and not a permission; approver identities and policy pins are not
republished to consumers. The consumer checks in
approval-claim.md ("Required verification") are the
supported validation path.
Settled 2026-09-06 by GH-DEC-2026-005 (with FLEX-DEC-2026-006). The
approval-claim is the step-1 artifact; ActionAuthorization is not required on
this path and MUST NOT be served from the claim endpoint. A PEP validates
across two artifacts: the claim for the approval fact, and the step-2 flex-auth
DecisionEnvelope for exact CheckRequest match and the policy pin.
Gate House recorded the split as doctrine rather than as an implementation
convenience: each artifact is validated against the layer that owns its data,
and a PIP MUST NOT republish the PDP's decision. The
provenance.authority == "state-hub" requirement was struck explicitly — State
Hub is a read model holding no runtime approval authority, so a validator
requiring it fails closed against every correctly issued claim.
This engine's claim schema is unchanged by the ruling.