# Action-bound authorization contract Status: **execute-time request binding is implemented and published.** The composed `ActionAuthorization` object in this document is **deferred, not withdrawn** — see "Deferral of the composed object" below. Do not build a validator against it. Date: 2026-08-23 (deferral recorded 2026-09-06, `FLEX-DEC-2026-006`) ## Ownership and endpoints flex-auth owns authorization evaluation and the canonical request/decision vocabulary. A protected system obtains a current decision from: ```text POST /v1/check ``` The response now includes `binding`, the exact normalized subject, action, resource, context, and full SHA-256 request digest evaluated by the policy. How that digest is computed, what is excluded, and when replay is permitted are in [`canonical-request-digest.md`](canonical-request-digest.md). Consumers must compare structured fields; `reason`, `diagnostics`, titles, and other prose are never an authorization contract. The decision envelope itself is the published PDP contract [`decision-record-contract.md`](decision-record-contract.md). `approval-engine` owns the durable approval object, authenticated approval evidence, storage, supersession, consumption, and availability. State Hub decision records are coordination and provenance evidence; they are not the runtime approval authority. Production consumers must not infer action binding or dual control from State Hub prose or a free-form `decided_by` value. ## Deferral of the composed object `ActionAuthorization` was never ratified. `GH-DEC-2026-003` (2026-08-29) named `approval-engine`'s **approval-claim** as the step-1 artifact, and flex-auth accepts that split (`FLEX-DEC-2026-006`): - **Step 1 — approval fact:** `approval-engine`'s approval-claim, served from `GET /v1/approvals/{id}/claim`. `approval-engine` is its authority. - **Step 2 — decision:** flex-auth's `DecisionEnvelope`, with exact `CheckRequest` match via `binding.request_digest` and the policy pin. A PEP validates **across both**. No check from the composed object is dropped; each one lands on the layer that owns it. The composed object would have had the PIP republish flex-auth's decision, which is the part that was wrong. The `PROPOSED` framing below is retained as the record of what was proposed. It is not a contract, and a consumer that validates a `GET /v1/approvals/{id}/claim` response against it will reject every response — both envelopes declare `schema_version: 0.1`, so the mismatch surfaces late and reads like an `approval-engine` outage. That is exactly what happened to secrets-engine. ## Durable object (deferred — proposed, never ratified) [`../schemas/action_authorization.schema.json`](../schemas/action_authorization.schema.json) defines the proposed `ActionAuthorization` storage and transport object. It combines: - a canonical UUID and explicit lifecycle status; - one complete `CheckRequest` containing the actor, action, resource, exact target attributes, stage, purpose, and other policy inputs; - a bounded validity window; - approval count and individual approval evidence; - the flex-auth `DecisionEnvelope` whose structured binding and digest match that request. The schema is generic. For secrets-engine, use this profile: | Requirement | Canonical field | | --- | --- | | catalog id | `request.resource.id` | | stage | `request.resource.attributes.stage` | | lifecycle operation | `request.action` | | exact secret fields | `request.resource.attributes.fields` | | policy/auth targets | `request.resource.attributes.policy_targets` / `auth_targets` | | requesting actor | `request.subject` | | purpose | `request.context.purpose` | | execution window | `validity.not_before` / `validity.expires_at` | | dual control | `approvals.required_count` and distinct `entries[].subject_id` | Array values that express exact targets are sets: producers sort and de-duplicate them before evaluation; consumers require exact set equality. Wildcards are policy inputs and should be denied for destructive production actions. ## Required verification A production consumer may execute only when all of the following hold: 1. The durable object resolves from `approval-engine`, its canonical authority; an outage fails the action closed. 2. `status` is `approved`, `superseded_by` is absent, and the current time is inside the validity window. 3. The caller's proposed action exactly matches `request`, including resource id, system, stage, target sets, actor, and purpose. 4. `decision.effect` is `allow`; `decision.binding` matches the request and its digest; the policy package/version are accepted for the deployment. 5. Approval entries contain at least `required_count` distinct authenticated subjects. Repeated entries from one subject count once. Local fixtures, CCR labels, workplan ids, prose, and a caller-supplied flex-auth decision id are provenance only. They cannot unlock a production action. **State Hub is never the authority here.** A validator must not require `provenance.authority == "state-hub"`, or accept it as evidence. State Hub decision records are coordination and provenance evidence, as the Ownership section above states. The authority of the approval fact is `approval-engine`; the authority of the decision is flex-auth. The stale `"authority": "state-hub"` value in `examples/caring/action_authorization.json` was the source of that constant and is corrected. ## Outage and supersession semantics - `approval-engine` unreachable: privileged live action denied; read-only inspection and dry-run may continue with a clear non-authoritative status. - flex-auth unavailable when a fresh decision is required: privileged live action denied. A previously stored decision is usable only inside its bounded validity window and only if the authoritative lifecycle object can still be checked for supersession or revocation. - `superseded`, `expired`, `revoked`, `denied`, or unknown status: deny. - `superseded_by` present regardless of status: deny and resolve the replacement explicitly; never follow it silently during execution. ## Remaining external work `approval-engine` needs a structured object/endpoint capable of storing this schema or equivalent fields, authenticated approval entries, atomic supersession, and single-consumption semantics. Until that lands and the consumer proof passes, secrets-engine is correct to keep live destructive actions disabled. The standalone evaluator and the Topaz, relationship, rule, and Keycloak delegated adapter boundaries now all populate the same binding.