--- id: MASON-WP-0010 type: workplan title: "Materialize SecurityCases from their type, with acceptance as the only gate" domain: infotech repo: ops-mason status: blocked flavor: implementation owner: claude-code topic_slug: netkingdom created: "2026-10-08" updated: "2026-10-11" depends_on: [] related_repos: - net-kingdom - ops-warden - railiance-platform state_hub_workstream_id: "eabf2c39-94bd-5a11-8f2e-76d7a95d1583" --- # MASON-WP-0010 — Materialize SecurityCases from their type, with acceptance as the only gate ## Why NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon `security-case-types_v0.1`, proposed). A type is designed once, with documentation, tests and an informed-decision. Each case of an implemented type is then light: it is accepted, ops-mason arranges it, and ops-warden follows through. ops-mason's four phases (construction plan, review, executive summary, build) stay exactly as they are for **new types and for structure that no type covers**. For a **KnownSecurityCase**, the construction plan is rendered from the type's materialization templates, and the executive-summary gate collapses to the case acceptance, because the shape was decided when the type was acknowledged. MASON-WP-0009 (a hand-built placement of a key-cape client secret) is the kind of work this replaces. It becomes a reference instance for the first type. ## Tasks ```task id: MASON-WP-0010-T01 status: done depends_on: [NK-WP-0048-T03] priority: high state_hub_task_id: "3382ea7b-65ca-59a9-97ae-273455af44c2" ``` ### T01 — Define the type's materialization section Propose the `materialization` part of the SecurityCaseType schema together with net-kingdom: policy, role, KV path structure and catalog-entry templates over case parameters, with naming rules. These must respect ops-mason's existing "respect, extend or compact before adding" review, so that a type cannot template sprawl. Write it for the first type, `oidc-browser-relying-party`, from MASON-WP-0009 and the existing key-cape client custody paths. **Review (2026-10-08) of net-kingdom `security-case-types/oidc-browser-relying-party/type.yaml` 1.0.** The structure (four items, all but the catalog entry conditional on `confidential`) is right. The names add three new families where existing ones cover the need, so they fail "respect, extend or compact before adding". Requested refinements, each from the live CCR-2026-0033 placement (`plans/keycape-secrets-engine-openbao-client-custody.md`): 1. **Fix the path in the type.** `secret_ref` should not be case-settable; a free pattern under `platform/workloads/` lets every case pick its own layout. Derive it: `platform/workloads/keycape/{client_id}` (field `CLIENT_SECRET`), as for `secrets-engine-openbao`. 2. **Reuse the policy family.** Existing readers are `workload-kv-read-keycape--`. Rename `keycape-client-{client_id}-read` to `workload-kv-read-keycape-{client_id}-eso` (KeyCape reads its client secret only through ESO), and `{consuming_service}-oidc-client-{client_id}-read` to `workload-kv-read-keycape-{client_id}-{consuming_service}`. Update verification `custody-allowed-to-consumer` and rollback to match. 3. **Template the policy body, not just its name.** An `openbao_policy` item needs its rules (`read` on `platform/data/workloads/keycape/{client_id}`, nothing else: no metadata, list, parent or sibling), or the review cannot check invariant (ii) and a rendered plan is not buildable. 4. **Add the missing ESO role.** `deliver-secret` needs a Kubernetes-auth role `external-secrets-keycape-{client_id}` (kind `openbao_role`, bound to the ESO service account, policy from item 2, 15m TTL, no default policy), as the reference placement has. 5. **Catalog naming.** The one existing entry is `rapp-qonto-keycape-client`; prefer `{consuming_service}-keycape-client` over `oidc-client-{client_id}`. 6. **Rollback order.** `mint-secret` "destroy" and `client-secret-path` "remove" act on the same KV path; collapse to one step (metadata delete after the ExternalSecret is gone) so rollback cannot fail on a missing path. With 1-4 applied, the section is acceptable to ops-mason for NK-WP-0048-T05. 2026-10-08: net-kingdom folded in all six (net-kingdom `074b3a0`, message `508e2a12`); its schema/validator now require a body on policy and role items. NK-WP-0048-T05 still waits on WARDEN-WP-0044-T01 and a founder answer. 2026-10-09: the founder accepted memo r4; hub decision `e418263e` resolved and the type is `acknowledged` (net-kingdom `0e86f95`). T02 is unblocked. ```task id: MASON-WP-0010-T02 status: done depends_on: [MASON-WP-0010-T01, NK-WP-0048-T05] priority: high state_hub_task_id: "fd385537-7378-5539-8a6e-f45851296f42" ``` ### T02 — Render a construction plan from a case `mason plan --case ` validates the case against its `implemented` type, renders the construction plan from the type's templates, and runs the existing consistency review against current OpenBao structure (read-only, no values). The output is the usual plan, marked `derived-from-type`, plus the case-acceptance reference in place of a fresh executive summary. Tests: render the reference case; refuse a case whose type is not `implemented`; refuse parameter values outside the type's validation; flag a collision with existing structure. 2026-10-09 done: `ops-mason plan --case [--out F] [--no-live]` (`src/ops_mason/case_plan.py`). Validation and rendering are net-kingdom's own `security_case_validator.py` (imported, not copied), with provider declarations loaded through its composer. ops-mason adds the review against existing structure: a read-only live survey (policy list, role lists, KV metadata, ops-warden catalog ids) that fails loudly instead of returning an empty structure, and flags each collision as a finding. Output is a construction plan with `derived_from_type` and `render_digest`, and section 5 is the case acceptance; status `reviewed` only when accepted and collision-free. Exit 0 clean, 1 collisions, 2 refused. `tests/test_case_plan.py` (8, on a copy of the type marked `implemented`); suite 167 passed. The live Forgejo case is refused today, correctly: the type is `acknowledged`, not yet `implemented`. 2026-10-09: net-kingdom accepted the Forgejo case (`net-kingdom/security-cases/forgejo-netkingdom-oidc.yaml`, message `d1bbcb44`). As the reference case it renders with `--allow-unimplemented`, mirroring the validator's flag: allowed only for an `acknowledged` type, marked `reference_case: true`; and `reviewed` now requires a live survey. Rendered live into `plans/case-forgejo-netkingdom-oidc.md`: status `reviewed`, no collision, digest `sha256:fb8b114d…`. ```task id: MASON-WP-0010-T03 status: progress depends_on: [MASON-WP-0010-T02] blocked_on: message-from:railiance-platform blocking_reason: "Typed exact structural evidence request is prepared. Native materialization waits on independently authenticated policy/role/layout/catalog observations and CCR42 consumer binding/application. Mint, delivery and first live verification are follow-through, not structural prerequisites." priority: high state_hub_task_id: "56cea6be-c613-586c-80cd-125340b374da" ``` ### T03 — Build from an accepted case `mason build --case ` builds a rendered plan when the case's acceptance record is present, with no separate executive-summary approval. Its audit trail names the case, the type version and the acceptance. It proposes the pointer-only ops-warden catalog entries the type defines, as today. Rollback removes exactly what this case created. Acceptance: the Forgejo reference case (NK-WP-0048-T06) materializes through this path. 2026-10-09: implemented. `ops-mason build --case --plan [--out] [--allow-unimplemented]` (`src/ops_mason/case_build.py`) refuses unless the case is accepted, the plan is `reviewed` and its render digest equals a fresh render. It writes a bundle and never touches OpenBao: one workload-kv-read CCR draft per policy (ESO policy + Kubernetes role, mirroring CCR-2026-0033; the consumer policy `in_flight` with `openbao.auth` missing until the consumer owner binds it), the policy HCL, an ops-warden catalog proposal, `rollback.yaml` (exactly what the case creates, reverse order) and a manifest with file hashes. Every file names the case, type version, render digest and acceptance record; the audit log gets a `case-build-prepared` record with the case acceptance as approver. The drafts are checked with railiance-platform's own `credential-change.py validate`. `tests/test_case_build.py` (6); suite 175 passed. Forgejo bundle built into `builds/case-forgejo-netkingdom-oidc/`, platform validation pass; handed to railiance-platform (message `2c409503`). ```task id: MASON-WP-0010-T04 status: wait depends_on: [MASON-WP-0010-T03] blocking_reason: "After T03, and only once a type fits: secrets-engine-openbao is a service (JWT) client, which oidc-browser-relying-party does not cover." priority: medium state_hub_task_id: "f6ad0d3c-8f47-5fdf-8115-6ca9d8a1fead" ``` ### T04 — Adopt MASON-WP-0009 as a case Once `oidc-browser-relying-party` (or the confidential service client type) is implemented, express MASON-WP-0009's placement as a case of that type without changing the live structure, and close the hand-built path. 2026-10-10 (T03): railiance-platform filed the drafts as CCR-2026-0041 (ESO reader) and CCR-2026-0042 (consumer reader, in_flight until railiance-apps binds it), messages `83465dcd`/`be3a762d`; the founder applied CCR-2026-0041 at 2026-10-09T18:09Z. Its review found a render defect: the CCRs declare `token_self_lifecycle: true` but the policies lacked the lookup-self/revoke-self stanzas, so the applier refused them. Fixed in `case_build.py` (test asserts the three stanzas; output now equals the platform-generated policy apart from comments) and in the committed bundle. Two follow-ups for the next render: net-kingdom changed the type to `KEYCAPE_{client_id:env}_CLIENT_SECRET` (ops-warden `23ba584`), so the render digest will change; and a live re-render of a partly applied case will report its own objects as collisions. The KV value waits on mint-secret (RPF-WP-0054). 2026-10-10 (T03 follow-up): `plan --case` now recognises objects listed in the case's own committed bundle (`builds/case-/rollback.yaml`, same case id) as "already present from this case's own build", not collisions; anything else existing still collides (2 tests, suite 177 passed). The committed Forgejo plan is stale: net-kingdom's `{client_id:env}` change moves the render digest from `fb8b114d…` to `5e97931e…`. Re-render it live under the next survey grant. 2026-10-10: Forgejo plan re-rendered live: `reviewed`, no collision, digest `af1f4f81…`; policy `workload-kv-read-keycape-forgejo-eso` shown as already present from this case's own bundle. The ESO role did not appear as present; check on the next grant. 2026-10-10: checked. `auth/kubernetes/role/external-secrets-keycape-forgejo` exists as applied (external-secrets/external-secrets, audience openbao, 15m, policy `workload-kv-read-keycape-forgejo-eso`, no default policy). Why the plan's "already present" list omitted it is not yet confirmed (the survey should list `auth/kubernetes/role`); reproduce under the next grant. The collision review is unaffected: it never flagged the role. 2026-10-10: type 1.1 (net-kingdom `e2174ce`, message `af66538c`) adds the consumer-enforces-groups verification and tightens redirect URIs; the case is re-pinned to @1.1. The live re-render above was already 1.1. Bundle rebuilt from it (`builds/case-forgejo-netkingdom-oidc/`, platform validation pass); the ESO policy is unchanged against the applied CCR-2026-0041, so 1.1 changes no OpenBao object. 2026-10-11 accepted-architecture source implementation: materialization is structure only under the already accepted canon; the stale mint-before- materialization blocker is removed, not replaced by a fake status label. `src/ops_mason/case_structure.py` and read-only `ops-mason structure-request` validate a separate current structural projection against the hashed bundle and prepare typed per-owner observations. Full execution drift remains visible: 35880bb adds --check-config to register-client, changing full render af1f4f81 → e64b56e4 while materialization/composition/verification/rollback remain equal. No prior execution approval is carried forward. Candidate receipt verification requires independent owner/manual provenance and derives undo only from verified creation receipts; planned rollback identities are not ownership. Thirty-three relevant tests and ruff pass. Status-only raw-hash binding previews accepted→materialized only after independent observations, preserving every parameter/acceptance field; it writes and signs nothing. Details and exact native owner seams: `docs/forgejo-structural-materialization-source-v1.md`. T03 remains progress; case remains accepted@1.1, not materialized/live. CCR42 consumer auth remains in_flight, and native owner evidence is not manufactured by source preparation. 2026-10-16 (T03 source compatibility): `plan`, `build` and `structure-request` now expose explicit `--types-root`, passing it to the existing exact-version NetKingdom API. Actual acknowledged 1.2 source renders the proposed Forgejo case into a six-object **draft**, preserving its absent acceptance. Six actual-source integration checks prove the root selection, dedicated consumer namespace/SA, ordinary unimplemented refusal, proposed build refusal, historical 1.1 bundle refusal and malformed namespace refusal. Existing 44 plan/build/ structure tests pass. No live survey, execution, case/type status, acceptance, native owner evidence or CCR was changed. T03 remains progress; actual accepted 1.2 case/reviewed source and authenticated native owner evidence remain required. 2026-10-17 (T03 source review): ops-mason source is complete for T03. `structure-request` prepares the typed request; `case_structure.verify` and `bind_status_transition` take an owner-supplied authenticating verifier by design, so no CLI that trusts observation JSON is added. Every remaining step is an owner observation or an attended native run. Attended checklist (in order; none may be simulated): 1. net-kingdom: accept the Forgejo case at type 1.2 (acceptance record in the case); ops-mason then re-renders `plan --case ... --types-root <1.2>` to `reviewed` and rebuilds the bundle (source-only, worker may run). 2. railiance-platform: supply independently authenticated metadata-only observations for ESO policy, `external-secrets-keycape-forgejo` role and KV layout, each marked created (with change receipt) or preexisting-matched, against the `structure-request` digest. No values. 3. railiance-apps + railiance-platform: bind CCR-2026-0042 consumer `openbao.auth` (namespace/SA), reconcile it from @1.0 to the current type, review and apply; supply its receipt. 4. ops-warden: authenticate the pointer-only catalog entry observation. 5. Owner (railiance-platform) supplies the authenticating verifier seam and, with founder go, runs `bind_status_transition` to preview accepted -> materialized; Warden accepts the new raw case digest. 6. Follow-through after materialization, not prerequisites: mint in custody, ESO delivery, first live verification (NK-WP-0048-T06).