flex-auth/docs/action-bound-authorization-contract.md
tegwick 74bfb3b664
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 40s
Accept ActionAuthorization deferral; fix the state-hub authority constant
approval-engine filed APPROVAL-IN-0002: secrets-engine built its PEP
validator against our ActionAuthorization schema, pointed it at
GET /v1/approvals/{id}/claim, and it rejects every response. Both
envelopes declare schema_version 0.1, so it fails late and reads like an
approval-engine outage rather than a contract mismatch.

FLEX-DEC-2026-006 accepts the deferral and argues against flex-auth's own
proposal. The composed object had the PIP republish our decision, which
crosses the same layer boundary we invoked to decline authentication
evidence and to win section 17's schema. The claim-plus-DecisionEnvelope
split drops no check; each verification lands on the layer that owns it.

approval-engine asked, before the decision, whether the open G3 finding
argues for ratifying now. It does not: G3 is already closed the other
way. FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required
on every allow by schema conditional, published 2026-09-02. The trigger
resolved by adding a field rather than by composition, so the decision
stands alone and needs no bundle.

The provenance.authority == state-hub constant is our defect and is
fixed at source. It came from examples/caring/action_authorization.json,
which contradicted the same contract's ownership section. That fixture
now names approval-engine as the approval fact's authority and flex-auth
as the decision's, and its stale secrets-engine.lifecycle pin is
corrected to the reserved coordinate from FLEX-DEC-2026-005.

The contract doc and schema are marked deferred-not-withdrawn so no
other consumer builds a validator against them. The execute-time half is
untouched: /v1/check, binding, the canonical digest, and
flex-auth.decision-record.v1 stay published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTbVXpEiXA7mNJVpDnEPcB

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 412054@bnt-lap001
Assistant-Session: 3968fae1-8d59-4209-9bd6-c22594b8ab19
2026-09-06 01:30:04 +02:00

6.5 KiB

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:

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

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