2026-10-08 22:17:03 +02:00
---
id: MASON-WP-0010
type: workplan
title: "Materialize SecurityCases from their type, with acceptance as the only gate"
domain: infotech
repo: ops-mason
2026-10-09 19:01:28 +02:00
status: blocked
2026-10-08 22:17:03 +02:00
flavor: implementation
owner: claude-code
topic_slug: netkingdom
created: "2026-10-08"
2026-10-09 19:01:28 +02:00
updated: "2026-10-09"
2026-10-08 22:17:03 +02:00
depends_on: []
related_repos:
- net-kingdom
- ops-warden
- railiance-platform
2026-10-08 22:19:56 +02:00
state_hub_workstream_id: "eabf2c39-94bd-5a11-8f2e-76d7a95d1583"
2026-10-08 22:17:03 +02:00
---
# 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
2026-10-08 23:21:03 +02:00
status: done
2026-10-08 22:17:03 +02:00
depends_on: [NK-WP-0048-T03]
priority: high
2026-10-08 22:19:56 +02:00
state_hub_task_id: "3382ea7b-65ca-59a9-97ae-273455af44c2"
2026-10-08 22:17:03 +02:00
```
### 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.
2026-10-08 23:21:03 +02:00
**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-<client>-<reader>` . 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 23:43:13 +02:00
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 14:59:09 +02:00
2026-10-09: the founder accepted memo r4; hub decision `e418263e` resolved and
the type is `acknowledged` (net-kingdom `0e86f95` ). T02 is unblocked.
2026-10-08 22:17:03 +02:00
```task
id: MASON-WP-0010-T02
2026-10-09 19:01:28 +02:00
status: done
2026-10-08 23:21:03 +02:00
depends_on: [MASON-WP-0010-T01, NK-WP-0048-T05]
2026-10-08 22:17:03 +02:00
priority: high
2026-10-08 22:19:56 +02:00
state_hub_task_id: "fd385537-7378-5539-8a6e-f45851296f42"
2026-10-08 22:17:03 +02:00
```
### T02 — Render a construction plan from a case
`mason plan --case <case.yaml>` 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 19:01:28 +02:00
2026-10-09 done: `ops-mason plan --case <case.yaml> [--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 19:47:45 +02:00
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…` .
2026-10-08 22:17:03 +02:00
```task
id: MASON-WP-0010-T03
2026-10-09 19:52:33 +02:00
status: progress
2026-10-08 22:17:03 +02:00
depends_on: [MASON-WP-0010-T02]
2026-10-09 19:52:33 +02:00
blocked_on: message-from:railiance-platform
blocking_reason: "Built 2026-10-09 (builds/case-forgejo-netkingdom-oidc). Acceptance waits on railiance-platform filing the CCR drafts and the founder applying them, plus mint-secret; then the Forgejo case is live."
2026-10-08 22:17:03 +02:00
priority: high
2026-10-08 22:19:56 +02:00
state_hub_task_id: "56cea6be-c613-586c-80cd-125340b374da"
2026-10-08 22:17:03 +02:00
```
### T03 — Build from an accepted case
`mason build --case <case.yaml>` 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 19:52:33 +02:00
2026-10-09: implemented. `ops-mason build --case < case > --plan < 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` ).
2026-10-08 22:17:03 +02:00
```task
id: MASON-WP-0010-T04
status: wait
depends_on: [MASON-WP-0010-T03]
2026-10-09 19:01:28 +02:00
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."
2026-10-08 22:17:03 +02:00
priority: medium
2026-10-08 22:19:56 +02:00
state_hub_task_id: "f6ad0d3c-8f47-5fdf-8115-6ca9d8a1fead"
2026-10-08 22:17:03 +02:00
```
### 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 12:41:20 +02:00
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 13:17:22 +02:00
2026-10-10 (T03 follow-up): `plan --case` now recognises objects listed in the
case's own committed bundle (`builds/case-<id>/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 14:13:02 +02:00
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 15:12:56 +02:00
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.