Gate House confirmed all three requested dispositions as GH-DEC-2026-005 (GH-IN-0002, closed); flex-auth accepted as FLEX-DEC-2026-006. The approval-claim is the step-1 artifact, ActionAuthorization is not required and MUST NOT be served from the claim endpoint, and a PEP validates across the claim and the step-2 DecisionEnvelope. Gate House recorded the split as doctrine rather than convenience -- a PIP must not republish the PDP's decision -- and struck the provenance.authority == state-hub requirement explicitly. This engine's claim schema is unchanged. Correct the deferred option D trigger list. The G3 trigger was written conditional on G3 being settled by composition; flex-auth reports FLEX-WP-0019 closed it by adding a lifetime field to DecisionEnvelope on 2026-09-02, so a decision now states its own end without borrowing ActionAuthorizationValidity. That was the one structural thing the bundle did that the split does not, so the trigger is not merely spent -- it resolved against ratification. Struck with reasons; a future revisit needs a fresh argument. The trigger came from a stale row in a dated 2026-08-29 review record that flex-auth does not rewrite. Also record the root cause of the bad authority constant: it came from a flex-auth fixture, not prose. A contract whose examples contradict its prose will be implemented as its examples. Close APPROVAL-IN-0002. Docs only; 84 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND Assistant: claude-code Assistant-Model: opus Assistant-Process: 411227@bnt-lap001 Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
4.3 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.
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.