Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
---
|
|
|
|
|
id: secrets-engine.catalog-lane.lifecycle
|
|
|
|
|
name: secrets-engine catalog-lane lifecycle authorization
|
|
|
|
|
namespace: secrets-engine:secret-catalog-lane
|
|
|
|
|
version: v1
|
|
|
|
|
status: ready
|
|
|
|
|
package: flexauth.secrets_engine.catalog_lane
|
|
|
|
|
allow_ttl: 15m
|
|
|
|
|
actions:
|
|
|
|
|
- apply
|
|
|
|
|
- provision
|
|
|
|
|
- rotate
|
|
|
|
|
- verify
|
|
|
|
|
- handoff
|
|
|
|
|
- wrap
|
|
|
|
|
- exec
|
|
|
|
|
- deactivate
|
|
|
|
|
- suspend
|
|
|
|
|
- destroy
|
|
|
|
|
- compromise
|
|
|
|
|
- reactivate
|
|
|
|
|
owner: team:platform-security
|
|
|
|
|
fixtures:
|
|
|
|
|
- policy_fixtures.yaml
|
|
|
|
|
caring:
|
|
|
|
|
profile: caring-0.4.0-rc2
|
|
|
|
|
enforce: false
|
|
|
|
|
canonical_roles:
|
|
|
|
|
- Operator
|
|
|
|
|
organization_relations:
|
|
|
|
|
- ServiceProvider
|
|
|
|
|
scopes:
|
|
|
|
|
- level: Platform
|
|
|
|
|
id: platform:secrets-engine
|
|
|
|
|
tenant: tenant:platform
|
|
|
|
|
planes:
|
|
|
|
|
- Secret
|
|
|
|
|
- Policy
|
|
|
|
|
- Audit
|
|
|
|
|
capabilities:
|
|
|
|
|
- Create
|
|
|
|
|
- EditAny
|
|
|
|
|
- Execute
|
|
|
|
|
- Archive
|
|
|
|
|
- Restore
|
|
|
|
|
- View
|
|
|
|
|
- Audit
|
|
|
|
|
exposure_modes:
|
|
|
|
|
- Metadata
|
|
|
|
|
conditions:
|
|
|
|
|
- DualApprovalRequired
|
|
|
|
|
restrictions:
|
|
|
|
|
- PrivilegeEscalationBlocked
|
|
|
|
|
activation:
|
|
|
|
|
mode: local
|
|
|
|
|
metadata:
|
|
|
|
|
source: examples/secrets-engine/policy_package.md
|
|
|
|
|
flex_auth_contract: protected-system-v0
|
|
|
|
|
action_vocabulary: docs/secrets-engine-action-vocabulary.md
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# secrets-engine catalog-lane lifecycle authorization
|
|
|
|
|
|
|
|
|
|
This package authorizes `secrets-engine`'s gated CLI operations over a secret
|
|
|
|
|
catalog lane. `secrets-engine` keeps custody of the secret material, the
|
|
|
|
|
OpenBao objects, and the local delivery overlay; flex-auth decides whether a
|
|
|
|
|
specific lifecycle operation is allowed now.
|
|
|
|
|
|
|
|
|
|
Opened by `FLEX-DEC-2026-005` and carried by `FLEX-WP-0021`.
|
|
|
|
|
|
|
|
|
|
## The vocabulary is theirs, not ours
|
|
|
|
|
|
|
|
|
|
The twelve action values below were **delivered by secrets-engine**
|
|
|
|
|
(`FLEX-WP-0021-T01`, their `docs/gated-actions.md`), read out of `cli.py`
|
|
|
|
|
rather than derived from the `secrets-engine.lifecycle/v1` example vocabulary
|
|
|
|
|
that this package replaces. `FLEX-WP-0021-T01` made an inferred action a
|
|
|
|
|
blocker rather than a default, and four of the twelve would have been inferred
|
|
|
|
|
wrongly. They are recorded in
|
|
|
|
|
[`../../docs/secrets-engine-action-vocabulary.md`](../../docs/secrets-engine-action-vocabulary.md).
|
|
|
|
|
|
|
|
|
|
The four that an inferred list would have gotten wrong are encoded here, not
|
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
2026-09-06 08:11:14 +02:00
|
|
|
assumed — see "The four traps" below.
|
|
|
|
|
|
|
|
|
|
## Correction, 2026-09-06 — the dual-control rule was written against an invented shape
|
|
|
|
|
|
|
|
|
|
The first published version of this rule required `context.approval.status ==
|
|
|
|
|
"approved"` and counted `context.approval.approvals[].subject_id`. **Neither
|
|
|
|
|
field exists.** The claim's field is `state` (whose `approved` is not the
|
|
|
|
|
operative value — `valid` is), and it carries no approver list at all. That 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. It was, however, a policy written
|
|
|
|
|
against an invented shape rather than a published one — the same class of error
|
|
|
|
|
`approval-engine` had flagged one message earlier, committed while flagging it.
|
|
|
|
|
Recorded here rather than quietly rewritten. The rule now consumes
|
|
|
|
|
`valid_now` from the published claim; see "Dual control on `destroy`".
|
|
|
|
|
|
|
|
|
|
## The four traps
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
|
|
|
|
|
1. **`revoke` is not an action.** The CLI verb `revoke` gates as `deactivate`,
|
|
|
|
|
which is also reached from `lifecycle deactivate`. There is no `revoke`
|
|
|
|
|
value and one must not be added — `test_revoke_is_not_an_action` asserts it
|
|
|
|
|
denies `unknown_action`.
|
|
|
|
|
2. **`destroy` is defined but not reachable live.** Its handler raises before
|
|
|
|
|
the gate, so only `--dry-run` renders today. It stays in the vocabulary as
|
|
|
|
|
the dual-control case; no live `destroy` Check arrives until
|
|
|
|
|
`SECRETS-WP-0007-T04` lands.
|
|
|
|
|
3. **`compromise` and `reactivate` touch no OpenBao object.** They mutate local
|
|
|
|
|
overlay state only. They are gated because they change delivery posture, not
|
|
|
|
|
because they write to the backend.
|
|
|
|
|
4. **Seven CLI surfaces never reach the gate** and must not appear here:
|
|
|
|
|
`plan`, `apply --dry-run`, `route`, `audit`, `catalog`, `decision inspect`,
|
|
|
|
|
and `evidence`. `test_ungated_surfaces_are_not_actions` asserts they deny.
|
|
|
|
|
|
|
|
|
|
On the fourth: `apply` *is* an action and `apply --dry-run` is not, but that
|
|
|
|
|
distinction is invisible to flex-auth — both would arrive as `apply`. The PEP
|
|
|
|
|
does not call the gate for a dry run, and that is the only thing keeping them
|
|
|
|
|
apart. flex-auth cannot enforce it and does not pretend to.
|
|
|
|
|
|
|
|
|
|
## Request shape
|
|
|
|
|
|
|
|
|
|
`resource.type` is `secret-catalog-lane`, `resource.system` is
|
|
|
|
|
`secrets-engine`, and the catalog id is `resource.id`
|
|
|
|
|
(`FLEX-DEC-2026-005`; secrets-engine implemented this in their commit
|
|
|
|
|
`627810b`). `resource.attributes` carries `stage` plus sorted `fields`,
|
|
|
|
|
`policy_targets`, and `auth_targets`. `fields` is populated for `provision`,
|
|
|
|
|
`rotate`, `verify`, and `exec`; the rest send an empty list rather than a
|
|
|
|
|
guess.
|
|
|
|
|
|
|
|
|
|
## Allow lifetime
|
|
|
|
|
|
|
|
|
|
`allow_ttl: 15m`, stated explicitly rather than inherited from the engine
|
|
|
|
|
default. These decisions authorize live secret operations, so the window in
|
|
|
|
|
which one may be relied on is a property worth writing down in the package
|
|
|
|
|
where a reviewer sees it, not one to leave implicit.
|
|
|
|
|
|
|
|
|
|
Per `FLEX-DEC-2026-004`, that lifetime is authority to *issue* the operation,
|
|
|
|
|
not authority to keep using anything the operation produced. A wrapped or
|
|
|
|
|
delivered secret's own lifetime is secrets-engine's to bound.
|
|
|
|
|
|
|
|
|
|
## Dual control on `destroy`
|
|
|
|
|
|
|
|
|
|
`destroy` is the one action that requires more than a known caller. It requires
|
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
2026-09-06 08:11:14 +02:00
|
|
|
an **approval-claim** on `context.approval`, issued by `approval-engine`, whose
|
|
|
|
|
`valid_now` is `true`.
|
|
|
|
|
|
|
|
|
|
That is the whole check, and the shape is
|
|
|
|
|
`approval-engine/schemas/approval_claim.schema.json` — not a shape of
|
|
|
|
|
flex-auth's devising. `valid_now` is a summary predicate: true only when the
|
|
|
|
|
object is approved, inside its validity window, and not consumed, superseded,
|
|
|
|
|
revoked, or expired. **The distinct-approver threshold is folded into it**, and
|
|
|
|
|
`reason_code: insufficient_approvers` is how a claim that failed the threshold
|
|
|
|
|
comes back.
|
|
|
|
|
|
|
|
|
|
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 this decision is step 2; the PEP
|
|
|
|
|
validates across both.
|
|
|
|
|
|
|
|
|
|
### What this package deliberately does not do
|
|
|
|
|
|
|
|
|
|
- **It does not count approvers.** The claim carries no approver list, and
|
|
|
|
|
re-deriving the threshold here is exactly the duplication the step-1/step-2
|
|
|
|
|
split removes (`GH-DEC-2026-005`). The compensating property is
|
|
|
|
|
reconstructability at the issuer under §9.6 — detection, not prevention —
|
|
|
|
|
and it is `approval-engine`'s, not ours.
|
|
|
|
|
- **It does not re-derive validity, freshness, signature, or supersession.**
|
|
|
|
|
Those are `approval-engine`'s to assert and the PEP's 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.
|
|
|
|
|
- **It does not compare `binding.pdp_digest`.** That comparison is real and
|
|
|
|
|
preferred (`approval-engine/docs/approval-claim.md`), but the request digest
|
|
|
|
|
is computed by the engine *after* policy evaluation, so a Rego rule cannot
|
|
|
|
|
see it. It belongs in the PEP and in the engine, not here.
|
|
|
|
|
|
|
|
|
|
### Open item: no published binding mapping
|
|
|
|
|
|
|
|
|
|
The claim's `binding.action` and `binding.target` use `approval-engine`'s
|
|
|
|
|
vocabulary (`secrets.kv.destroy`, `{"id": "lane-openbao-root", "stage": "prod"}`),
|
|
|
|
|
not this package's (`destroy`, `lane:...`). There is no published mapping
|
|
|
|
|
between the two, so this package makes **no** cross-check that the claim was
|
|
|
|
|
approved for *this* action and target — and it must not invent one, because a
|
|
|
|
|
wrong mapping would silently accept a claim approved for something else.
|
|
|
|
|
|
|
|
|
|
Until a mapping is published, that correspondence is the PEP's to enforce via
|
|
|
|
|
`binding.pdp_digest`. This is worth closing before `destroy` becomes reachable
|
|
|
|
|
under `SECRETS-WP-0007-T04`.
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
|
|
|
|
|
### Decision: one subject, so no `action_not_granted` branch
|
|
|
|
|
|
|
|
|
|
**Decision (`FLEX-WP-0021-T02`, 2026-09-06): the denial ladder stops at
|
|
|
|
|
`unknown_subject` plus `dual_control_required`.**
|
|
|
|
|
|
|
|
|
|
secrets-engine named exactly one calling identity. With one subject holding all
|
|
|
|
|
twelve actions, an `action_not_granted` branch could never fire. Following the
|
|
|
|
|
precedent set in `tenant-engine.write-api.mutate` (`FLEX-WP-0010-T02`), a rule
|
|
|
|
|
that cannot fail is worse than no rule: it reads to a later reviewer as though
|
|
|
|
|
per-action grants were separately controlled when they are not.
|
|
|
|
|
|
|
|
|
|
`dual_control_required` is a real branch — it fires whenever `destroy` arrives
|
|
|
|
|
without a satisfying claim, and the fixtures exercise both sides.
|
|
|
|
|
|
|
|
|
|
**Revisit when** a second calling identity is registered against
|
|
|
|
|
`system: "secrets-engine"`, or when secrets-engine splits its CLI identity by
|
|
|
|
|
lane or stage. Adding the branch then is additive to this package: no consumer
|
|
|
|
|
change and no request-shape change.
|
|
|
|
|
|
|
|
|
|
## Rules
|
|
|
|
|
|
|
|
|
|
```rego
|
|
|
|
|
import future.keywords.contains
|
|
|
|
|
import future.keywords.if
|
|
|
|
|
import future.keywords.in
|
|
|
|
|
|
|
|
|
|
valid_actions := {
|
|
|
|
|
"apply",
|
|
|
|
|
"provision",
|
|
|
|
|
"rotate",
|
|
|
|
|
"verify",
|
|
|
|
|
"handoff",
|
|
|
|
|
"wrap",
|
|
|
|
|
"exec",
|
|
|
|
|
"deactivate",
|
|
|
|
|
"suspend",
|
|
|
|
|
"destroy",
|
|
|
|
|
"compromise",
|
|
|
|
|
"reactivate",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dual_control_actions := {"destroy"}
|
|
|
|
|
|
|
|
|
|
known_subjects := {"secrets-engine"}
|
|
|
|
|
|
|
|
|
|
decision := {"effect": "allow", "reason": "catalog_lane_policy_matched"} if {
|
|
|
|
|
allowed
|
|
|
|
|
} else := {"effect": "deny", "reason": first_denial} if {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
well_formed if {
|
|
|
|
|
input.resource.system == "secrets-engine"
|
|
|
|
|
input.resource.type == "secret-catalog-lane"
|
|
|
|
|
input.action in valid_actions
|
|
|
|
|
input.subject.type == "service"
|
|
|
|
|
input.subject.id in known_subjects
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allowed if {
|
|
|
|
|
well_formed
|
|
|
|
|
not input.action in dual_control_actions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allowed if {
|
|
|
|
|
well_formed
|
|
|
|
|
input.action in dual_control_actions
|
|
|
|
|
dual_control_satisfied
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dual_control_satisfied if {
|
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
2026-09-06 08:11:14 +02:00
|
|
|
claim := input.context.approval
|
|
|
|
|
claim.kind == "approval-claim"
|
|
|
|
|
claim.issuer == "approval-engine"
|
|
|
|
|
claim.valid_now == true
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default first_denial := "no_matching_rule"
|
|
|
|
|
|
|
|
|
|
first_denial := "wrong_system" if {
|
|
|
|
|
input.resource.system != "secrets-engine"
|
|
|
|
|
} else := "wrong_resource_type" if {
|
|
|
|
|
input.resource.type != "secret-catalog-lane"
|
|
|
|
|
} else := "unknown_action" if {
|
|
|
|
|
not input.action in valid_actions
|
|
|
|
|
} else := "wrong_subject_type" if {
|
|
|
|
|
input.subject.type != "service"
|
|
|
|
|
} else := "unknown_subject" if {
|
|
|
|
|
not input.subject.id in known_subjects
|
|
|
|
|
} else := "dual_control_required" if {
|
|
|
|
|
input.action in dual_control_actions
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Tests
|
|
|
|
|
|
|
|
|
|
```rego test
|
|
|
|
|
package flexauth.secrets_engine.catalog_lane_test
|
|
|
|
|
|
|
|
|
|
import future.keywords.every
|
|
|
|
|
import future.keywords.if
|
|
|
|
|
import future.keywords.in
|
|
|
|
|
import data.flexauth.secrets_engine.catalog_lane
|
|
|
|
|
|
|
|
|
|
lane(action) := {
|
|
|
|
|
"id": "check:secrets-engine",
|
|
|
|
|
"tenant": "tenant:platform",
|
|
|
|
|
"subject": {"id": "secrets-engine", "type": "service"},
|
|
|
|
|
"action": action,
|
|
|
|
|
"resource": {
|
|
|
|
|
"id": "lane:glas-primary",
|
|
|
|
|
"type": "secret-catalog-lane",
|
|
|
|
|
"system": "secrets-engine",
|
|
|
|
|
"attributes": {"stage": "prod", "fields": [], "policy_targets": [], "auth_targets": []}
|
|
|
|
|
},
|
|
|
|
|
"context": {}
|
|
|
|
|
}
|
|
|
|
|
|
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
2026-09-06 08:11:14 +02:00
|
|
|
valid_claim := {
|
|
|
|
|
"schema_version": "0.1",
|
|
|
|
|
"kind": "approval-claim",
|
|
|
|
|
"issuer": "approval-engine",
|
|
|
|
|
"approval_id": "3d1c0a8e-6b7f-4c21-9a0e-1f2b3c4d5e6f",
|
|
|
|
|
"state": "valid",
|
|
|
|
|
"valid_now": true,
|
|
|
|
|
"consumed": false,
|
|
|
|
|
"reason_code": "ok"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destroy_with(claim) := object.union(lane("destroy"), {"context": {"approval": claim}})
|
|
|
|
|
|
|
|
|
|
approved_destroy := destroy_with(valid_claim)
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
|
|
|
|
|
test_apply_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("apply")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_provision_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("provision")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_rotate_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("rotate")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_verify_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("verify")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_handoff_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("handoff")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_wrap_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("wrap")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_exec_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("exec")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_deactivate_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("deactivate")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_suspend_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("suspend")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_compromise_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("compromise")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_reactivate_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as lane("reactivate")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_destroy_with_dual_control_allowed if {
|
|
|
|
|
catalog_lane.decision.effect == "allow" with input as approved_destroy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_destroy_without_claim_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as lane("destroy")
|
|
|
|
|
}
|
|
|
|
|
|
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
2026-09-06 08:11:14 +02:00
|
|
|
test_destroy_insufficient_approvers_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as destroy_with(
|
|
|
|
|
object.union(valid_claim, {"state": "requested", "valid_now": false, "reason_code": "insufficient_approvers"})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_destroy_consumed_claim_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as destroy_with(
|
|
|
|
|
object.union(valid_claim, {"state": "consumed", "valid_now": false, "consumed": true, "reason_code": "consumed"})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_destroy_revoked_claim_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as destroy_with(
|
|
|
|
|
object.union(valid_claim, {"state": "revoked", "valid_now": false, "reason_code": "revoked"})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_destroy_approved_but_not_valid_now_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as destroy_with(
|
|
|
|
|
object.union(valid_claim, {"state": "approved", "valid_now": false, "reason_code": "not_yet_valid"})
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
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
2026-09-06 08:11:14 +02:00
|
|
|
test_destroy_foreign_issuer_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as destroy_with(
|
|
|
|
|
object.union(valid_claim, {"issuer": "some-other-engine"})
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
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
2026-09-06 08:11:14 +02:00
|
|
|
test_destroy_wrong_kind_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "dual_control_required" with input as destroy_with(
|
|
|
|
|
object.union(valid_claim, {"kind": "action-authorization"})
|
Publish secrets-engine.catalog-lane.lifecycle v1 (FLEX-WP-0021 T01, T02)
secrets-engine delivered the action vocabulary T01 asked for: twelve
actions read out of cli.py, not the example vocabulary. The gate earned
its keep -- four would have been inferred wrongly, and revoke, the
obvious thirteenth, does not exist as an action at all. It gates as
deactivate, which is also reached from lifecycle deactivate.
docs/secrets-engine-action-vocabulary.md records the list and the four
traps. examples/secrets-engine/ carries the package, both manifests, a
loadable registry snapshot, 26 fixtures, five check requests, and a
README. validate -kind policy is valid with 22/22 Rego tests and 26/26
fixtures.
allow_ttl is 15m, stated in the package rather than inherited from the
engine default: these decisions authorize live secret operations, so the
reliance window belongs where a reviewer sees it. Per FLEX-DEC-2026-004
it is authority to issue, not authority to keep using what the operation
produced.
destroy is the dual-control case, gated on a context.approval claim
marked approved with two distinct approvers, repeated entries counting
once. flex-auth checks what the claim says and deliberately does not
re-derive its temporal validity, signature, or supersession -- those are
approval-engine's to assert and the PEP's to verify against the live
claim, per the split accepted in FLEX-DEC-2026-006. It stays in the
package though its handler raises before the gate, so the rule is
reviewed and fixtured before SECRETS-WP-0007-T04 opens the path.
Following the FLEX-WP-0010-T02 precedent the denial ladder has no
action_not_granted branch: one subject holding all twelve actions could
never reach it, and a rule that cannot fail reads as control that is not
there. Registering a second calling identity is the revisit trigger.
Not deployed. No flex-auth-secrets-engine pin exists yet (T04), so their
policy pin stays unset and fail-closed until T05.
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 08:02:52 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_revoke_is_not_an_action if {
|
|
|
|
|
catalog_lane.decision.reason == "unknown_action" with input as lane("revoke")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_ungated_surfaces_are_not_actions if {
|
|
|
|
|
every surface in ["plan", "route", "audit", "catalog", "decision", "evidence", "inspect"] {
|
|
|
|
|
catalog_lane.decision.reason == "unknown_action" with input as lane(surface)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_unknown_subject_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "unknown_subject" with input as object.union(
|
|
|
|
|
lane("rotate"),
|
|
|
|
|
{"subject": {"id": "some-other-service", "type": "service"}}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_wrong_subject_type_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "wrong_subject_type" with input as object.union(
|
|
|
|
|
lane("rotate"),
|
|
|
|
|
{"subject": {"id": "secrets-engine", "type": "human"}}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_wrong_system_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "wrong_system" with input as object.union(
|
|
|
|
|
lane("rotate"),
|
|
|
|
|
{"resource": {"id": "lane:glas-primary", "type": "secret-catalog-lane", "system": "some-other-system"}}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test_wrong_resource_type_denied if {
|
|
|
|
|
catalog_lane.decision.reason == "wrong_resource_type" with input as object.union(
|
|
|
|
|
lane("rotate"),
|
|
|
|
|
{"resource": {"id": "lane:glas-primary", "type": "secret", "system": "secrets-engine"}}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
```
|