approval-engine/docs/approval-consumption.md
tegwick 115f309e0a Record the claim vs ActionAuthorization envelope divergence
secrets-engine reports its PIP join is implemented and blocked on
deployment rather than contract. Reviewing its
validate_action_authorization against what this engine actually serves
shows that is not the whole story: it expects a state-hub-authority
ActionAuthorization (id, status, superseded_by, request,
approvals.entries, policy pin) while GET /v1/approvals/{id}/claim serves
the governed approval-claim (approval_id, state/valid_now, binding,
freshness, reason_code, issuer approval-engine).

Both envelopes declare schema_version 0.1, so the version check passes
and the mismatch surfaces as a field or authority error that reads like
an approval-engine outage.

Document the field-by-field divergence and why the omissions are
deliberate: a claim is a fact about an approval object, not a decision,
so approver identities and policy pins are not republished. Reconciling
the envelopes is a GH-DEC-2026-003 cross-repo change, so the governed
claim schema is left unchanged here.

No code change; 84 tests still 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
2026-09-06 01:09:23 +02:00

3.7 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 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.

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.

Reconciling the two envelopes is a cross-repo contract change under GH-DEC-2026-003, not a unilateral edit here. Until it is decided, this engine keeps serving the approval-claim shape and does not emit a state-hub authority it does not have.