Fix the destroy rule: it was written against an invented claim shape
approval-engine flagged the class one message earlier -- a contract whose examples contradict its prose gets implemented as its examples -- and yesterday's package was a fresh instance of it, committed while flagging it. The published rule required context.approval.status == "approved" and counted context.approval.approvals[].subject_id. Neither field exists. approval-engine's approval_claim.schema.json has `state` (whose operative value is `valid`, not `approved`) and carries no approver list at all. The rule was unsatisfiable: every live destroy would have denied dual_control_required no matter how good the approval was. It failed closed, so it was never a hole, but it was policy written against a shape of our own devising rather than a published one. The rule now consumes valid_now from the real claim, guarded on kind and issuer. valid_now is the summary predicate that already folds in the distinct-approver threshold, with reason_code insufficient_approvers for a claim that failed it -- so this is also the correct layering, not just the correct shape. Counting approvers here is exactly the duplication GH-DEC-2026-005 removes; the compensating property is reconstructability at the issuer under 9.6, which is approval-engine's. Recorded as a correction section in the package and the vocabulary doc rather than quietly rewritten. 25 Rego tests and 29 fixtures pass, covering insufficient_approvers, consumed, revoked, approved-but-not-yet- valid, foreign issuer, and wrong kind. Two things the package deliberately does not do, both now written down: it does not compare binding.pdp_digest, because the request digest is computed after policy evaluation and a Rego rule cannot see it; and it makes no cross-check that the claim was approved for this action and target, because the claim's binding uses approval-engine's vocabulary and no mapping between the two is published. Inventing one would silently accept a claim approved for something else. Both belong to the PEP until a mapping exists, and that is worth closing before SECRETS-WP-0007-T04 makes destroy reachable. Also swept the other published fixtures on approval-engine's reasoning. One more instance: the inner decision in examples/caring/action_authorization.json declared contract_version flex-auth.decision-record.v1 while its provenance omitted policy_package_digest, registry_snapshot_digest, and input_claim_digests -- all published contract fields since 2026-09-02. Completed. The remaining example context vocabularies are consumer-owned and match their integrations. 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
This commit is contained in:
parent
6bf5eb7ff9
commit
68ad039a3f
6 changed files with 311 additions and 99 deletions
|
|
@ -78,14 +78,44 @@ it does not have.
|
|||
|
||||
## Dual control on `destroy`
|
||||
|
||||
`destroy` requires `context.approval` marked `approved` with at least two
|
||||
distinct approver subject ids. Repeated entries from one subject count once.
|
||||
`destroy` requires an **approval-claim** on `context.approval`, issued by
|
||||
`approval-engine`, whose `valid_now` is `true`. That is the whole check.
|
||||
|
||||
flex-auth consumes that claim as an input claim and never mutates it
|
||||
The shape is `approval-engine/schemas/approval_claim.schema.json`.
|
||||
`valid_now` is a summary predicate — true only when the object is approved,
|
||||
inside its validity window, and not consumed, superseded, revoked, or expired —
|
||||
and **the distinct-approver threshold is folded into it**. A claim that failed
|
||||
the threshold returns `reason_code: insufficient_approvers`.
|
||||
|
||||
flex-auth consumes the claim as an input claim and never mutates it
|
||||
(`security-layer-model_v0.7` §9.4). Per `FLEX-DEC-2026-006` the approval fact is
|
||||
`approval-engine`'s step-1 artifact and the decision is step 2; the PEP
|
||||
validates across both. This package checks that the claim *says* approved with
|
||||
distinct approvers. It does **not** re-derive the claim's temporal validity,
|
||||
signature, or supersession state — those belong to `approval-engine` to assert
|
||||
and to the PEP to verify against the live claim. A PDP re-deriving them from a
|
||||
caller-supplied blob would be inventing an authority it does not have.
|
||||
validates across both.
|
||||
|
||||
The package does **not** count approvers, re-derive validity/freshness/
|
||||
signature/supersession, or compare `binding.pdp_digest`. Counting approvers is
|
||||
the duplication the split removes (`GH-DEC-2026-005`); the compensating property
|
||||
is reconstructability at the issuer under §9.6, which is `approval-engine`'s.
|
||||
The digest comparison is real and preferred but the request digest is computed
|
||||
after policy evaluation, so a Rego rule cannot see it — it belongs in the PEP.
|
||||
|
||||
### Correction, 2026-09-06
|
||||
|
||||
The first published rule required `context.approval.status == "approved"` and
|
||||
counted `context.approval.approvals[].subject_id`. Neither field exists: the
|
||||
claim's field is `state` (and `valid`, not `approved`, is the operative value),
|
||||
and it carries no approver list. The rule was unsatisfiable — every live
|
||||
`destroy` would have denied `dual_control_required` regardless of the approval.
|
||||
It failed closed, so it was never a hole, but it was a policy written against an
|
||||
invented shape. Corrected the same day.
|
||||
|
||||
### Open item: no published binding mapping
|
||||
|
||||
The claim's `binding.action` and `binding.target` use `approval-engine`'s
|
||||
vocabulary (`secrets.kv.destroy`, `{"id": ..., "stage": ...}`), not this one
|
||||
(`destroy`, `lane:...`). No mapping between them is published, so the package
|
||||
makes no cross-check that the claim was approved for *this* action and target,
|
||||
and must not invent one — a wrong mapping would silently accept a claim approved
|
||||
for something else. Until a mapping exists that correspondence is the PEP's, via
|
||||
`binding.pdp_digest`. Worth closing before `SECRETS-WP-0007-T04` makes `destroy`
|
||||
reachable.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue