gate-house resolved APPROVAL-IN-0002. Two changes fell to this repo.
1. Split validate_action_authorization. The claim from approval-engine now
carries the approval fact (issuer, valid_now, consumption, binding digest,
freshness, reason_code) via approval_claim.validate_approval_claim; the
flex-auth DecisionEnvelope carries the decision (effect, binding match,
request digest, lifetime, policy pin) via validate_decision_envelope.
ActionAuthorization is deferred and never ratified (FLEX-DEC-2026-006) and
cannot be served from a step-1 call; nothing validates it now.
2. Dropped AUTHORITY = "state-hub" and the provenance.authority requirement.
State Hub is a read model with no runtime approval authority, so the check
failed closed against every correctly issued record. flex-auth traced the
constant to their own fixture and fixed it at source.
Two consequences recorded rather than buried: there are now two distinct
digests over the same action (approval-engine native over
{action,actor,principal,purpose,target}, and the flex-auth CheckRequest
digest) which are never compared to each other; and the distinct-approver
threshold is no longer checked here, since the claim exposes no approver
entries and approval-engine folds it into valid_now.
The canonical request digest is unchanged and its contract test is preserved
verbatim. Production still fails closed. 251 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
119 lines
5.6 KiB
Markdown
119 lines
5.6 KiB
Markdown
# Approval consumption (PEP)
|
|
|
|
Status: engine consumer and the PIP claim/validate join are implemented; live
|
|
production remains fail-closed until approval-engine and access-engine actually
|
|
serve the durable objects.
|
|
|
|
Normative protocol: `gate-house/docs/contracts/approval-consumption.md`
|
|
(`GH-DEC-2026-003`). Implementation surface:
|
|
`approval-engine/docs/approval-consumption.md`. This document records how
|
|
secrets-engine, as the PEP for OpenBao writes, consumes that protocol. It
|
|
does not redefine it.
|
|
|
|
## Sequence
|
|
|
|
```text
|
|
1. PIP GET /v1/approvals/{id}/claim
|
|
2. PDP access-engine Check / decide → ALLOW
|
|
3. PEP POST /v1/approvals/{id}/consume → CAS
|
|
4. PEP OpenBao call → only after consume succeeds
|
|
```
|
|
|
|
**Each artifact is validated by the layer that owns its data**
|
|
(`GH-DEC-2026-005`). There is no single bundled object:
|
|
|
|
| Step | Artifact | Owner | Validated by |
|
|
| --- | --- | --- | --- |
|
|
| 1 | approval-claim | approval-engine | `approval_claim.validate_approval_claim` |
|
|
| 2 | DecisionEnvelope | flex-auth | `authorization.validate_decision_envelope` |
|
|
|
|
The claim carries the *approval fact*: issuer, `valid_now`, consumption state,
|
|
binding digest, freshness, `reason_code`. The envelope carries the *decision*:
|
|
effect, exact CheckRequest binding, canonical request digest, lifetime, and the
|
|
policy package/version pin. Neither republishes the other's data.
|
|
|
|
`ActionAuthorization` is **deferred and was never ratified**
|
|
(`FLEX-DEC-2026-006`). It cannot be served from a step-1 call, and nothing here
|
|
validates it. A ratified post-decision form remains a deferred option.
|
|
|
|
There are **two different digests** over the same proposed action, and they are
|
|
never compared to each other:
|
|
|
|
- `claim.binding.digest` — approval-engine native, `sha256` over canonical JSON
|
|
of `{action, actor, principal, purpose, target}`.
|
|
- `decision.binding.request_digest` — flex-auth canonical CheckRequest digest.
|
|
|
|
When the issuer recorded `claim.binding.pdp_digest`, that comparison is
|
|
preferred. The CheckRequest mapping onto the claim binding is published in
|
|
`approval-engine/docs/approval-claim.md`.
|
|
|
|
The approval-engine object id is never inferred from a State Hub decision UUID.
|
|
It comes from catalog `approval.authorization_id`, and is the value in the
|
|
`{id}` path segment echoed back as `approval_id`.
|
|
|
|
**There is no authority constant.** The engine previously required
|
|
`provenance.authority == "state-hub"`, which contradicted its own source: State
|
|
Hub is a read model and holds no runtime approval authority, so that check
|
|
failed closed against every correctly issued record. The approval fact's
|
|
authority is approval-engine (checked as `issuer`); the decision's is flex-auth.
|
|
|
|
Every live privileged production handler passes `_require_lane_approval`,
|
|
which calls `require_production_consume` before `OpenBaoClient.resolve`.
|
|
Dry-run and `plan` do not consume. Build/test remain fail-open relative to
|
|
approval-engine. The three-factor unsafe-demo exception is not a consume
|
|
path.
|
|
|
|
## Fail closed
|
|
|
|
| Condition | Result |
|
|
| --- | --- |
|
|
| No served consume binding | refuse; no OpenBao |
|
|
| URL/token/authorization id all unset | binding is `None` → refuse; no OpenBao |
|
|
| Partially configured join (missing subject or policy pin) | raise; never degrade to "unconfigured" |
|
|
| Claim digest, action, or field set mismatch | refuse; no OpenBao |
|
|
| Missing `SECRETS_ENGINE_APPROVAL_URL` or token file | refuse; no OpenBao |
|
|
| HTTP 409 / different digest | refuse; no OpenBao |
|
|
| Same digest after consume | idempotent success; OpenBao may proceed |
|
|
| 401/403/404/503/unreachable | refuse; no OpenBao |
|
|
| Side effect fails after consume | approval is spent; no unconsume |
|
|
|
|
The consume response is mutation evidence (`status=consumed` plus the
|
|
presented digest). It is not a permission. Evidence records approval id,
|
|
digest, idempotence, and consumed-at only. No token, secret, or accessor.
|
|
|
|
## Required configuration
|
|
|
|
The join is absent by default, so an unconfigured engine behaves exactly as it
|
|
did before. Production additionally needs:
|
|
|
|
| Variable | Meaning |
|
|
| --- | --- |
|
|
| `SECRETS_ENGINE_APPROVAL_URL` | approval-engine base URL (claim + consume) |
|
|
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | mode-0600 credential, outside Git |
|
|
| `SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID` / `_SUBJECT_TYPE` | the acting principal |
|
|
| `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` | the live pin (step 2) |
|
|
|
|
The distinct-approver threshold is no longer a consumer-side check. The claim
|
|
does not expose approver entries; approval-engine folds that requirement into
|
|
`valid_now`, which is true only when enough distinct authenticated approvers
|
|
have been recorded and the object is not consumed, superseded, revoked, or
|
|
expired.
|
|
|
|
There is deliberately no default policy pin. The reserved coordinate is
|
|
`secrets-engine.catalog-lane.lifecycle` / `v1`, but that is a *reservation, not
|
|
a publication* (`FLEX-DEC-2026-005`) and must not be configured until
|
|
`FLEX-WP-0021-T02` publishes the package. `docs/gated-actions.md` supplies the
|
|
action vocabulary that package is built from.
|
|
|
|
Step 2 additionally needs the `flex-auth-secrets-engine` Service DNS. No
|
|
estate-wide PDP exists by design — flex-auth runs per-consumer cluster-local
|
|
pins — and that pin has not been created, so the PDP call is not wired yet.
|
|
|
|
## What this does not do
|
|
|
|
- It does not enable live production. Unreachable-engine stance still
|
|
fail-closes production until an access-engine decision record is served
|
|
(`SECRETS-WP-0007-T04` / `SECRETS-WP-0008-T02`).
|
|
- It does not render or cache an authorization decision.
|
|
- It does not infer consumption from a decision record or from local
|
|
evidence.
|