# secrets-engine action vocabulary Status: published Date: 2026-09-06 Workplan: `FLEX-WP-0021-T01` Source: secrets-engine `docs/gated-actions.md`, read out of their `cli.py` Package: `secrets-engine.catalog-lane.lifecycle` v1 (`examples/secrets-engine/policy_package.md`) `FLEX-WP-0021-T01` required secrets-engine to **name** this list rather than letting flex-auth derive it, and made an inferred action a blocker rather than a default. That was not ceremony: four of the twelve would have been inferred wrongly, and one action that looks obvious does not exist at all. The example vocabulary `secrets-engine.lifecycle/v1` this replaces is **not** this list and was never published (`FLEX-DEC-2026-005`). ## Request shape | Field | Value | | --- | --- | | `resource.type` | `secret-catalog-lane` | | `resource.system` | `secrets-engine` | | `resource.id` | the catalog id | | `resource.attributes.stage` | deployment stage | | `resource.attributes.fields` | sorted; populated for `provision`, `rotate`, `verify`, `exec`; empty list otherwise | | `resource.attributes.policy_targets` / `auth_targets` | sorted | | `context.approval` | approval claim; required for `destroy` only | Empty lists are sent rather than omitted or guessed. ## The twelve actions | Action | Reaches OpenBao | Notes | | --- | --- | --- | | `apply` | yes | `apply --dry-run` does not reach the gate | | `provision` | yes | carries `fields` | | `rotate` | yes | carries `fields` | | `verify` | yes | carries `fields` | | `handoff` | yes | | | `wrap` | yes | | | `exec` | yes | carries `fields` | | `deactivate` | yes | also the gate for the CLI verb `revoke` | | `suspend` | yes | | | `destroy` | yes | dual control; not reachable live yet | | `compromise` | no | local delivery overlay only | | `reactivate` | no | local delivery overlay only | ## Four things an inferred list gets wrong **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. The package asserts this (`test_revoke_is_not_an_action` → `unknown_action`). **2. `destroy` is defined but not reachable live.** Its handler raises before the gate, so only `--dry-run` renders. It stays in the vocabulary as the dual-control case; no live `destroy` Check arrives until `SECRETS-WP-0007-T04` lands. Keeping it in the package now means the rule is reviewed and fixtured *before* the path opens rather than in the same change that opens it. **3. `compromise` and `reactivate` touch no OpenBao object.** They mutate local delivery-overlay state only. They are gated because they change delivery posture, not because they write to the backend — a distinction worth recording, because a reviewer looking for backend writes would otherwise read their presence as a mistake. **4. Seven CLI surfaces never reach the gate** and must not appear in the package: `plan`, `apply --dry-run`, `route`, `audit`, `catalog`, `decision inspect`, and `evidence`. `apply --dry-run` is the one flex-auth cannot enforce. `apply` is an action and `apply --dry-run` is not, but both would arrive as `apply` — the distinction is invisible to the PDP. What keeps them apart is that the PEP does not call the gate for a dry run. flex-auth records the limit rather than implying a control it does not have. ## Dual control on `destroy` `destroy` requires an **approval-claim** on `context.approval`, issued by `approval-engine`, whose `valid_now` is `true`. That is the whole check. 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. 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. ### Closed: `pdp_digest` is the mapping, and there will be no table `approval-engine` declined to publish an action/target mapping (their commit `6d0dfc8`) and was right to. A PIP asserting that `secrets.kv.destroy` **means** `destroy` would author policy semantics over two vocabularies it does not own — the same layer boundary flex-auth invoked against its own composed object — and the failure is asymmetric: a wrong mapping silently accepts a claim approved for a *different* action, which is worse than no mapping. ```text claim.binding.pdp_digest == decision.binding.request_digest ``` is the mapping, and it is stronger than a name table because it does not translate: it compares the PDP's own digest to the PDP's own digest, in one vocabulary, with nobody asserting equivalence. `binding.pdp_digest` is now **always present and may be null**, so its absence is a stated fact rather than a missing key. Null means the approval was not issued against a PDP decision — it proves an approval exists, not that it was issued against the request being decided. **The `destroy` gate is therefore: `pdp_digest` non-null and equal**, enforced by the PEP, before `SECRETS-WP-0007-T04` makes `destroy` reachable.