NK-WP-0048-T05: fold ops-warden's review into the type; build memo r3
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Canon section 3 defines warden_executes narrowly and adds per-step lane,
value_handling and ready_check; catalog entries need a templated body. The
type gets lanes, generated-in-custody minting, a Ready gate before bind-env,
memberships only from the accepted case, mfa-pinned and secret-delivered
checks, and the R4-prime catalog body. 110 tests pass. Memo r3 supersedes r2
and validates on both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5457@bnt-lap001
Assistant-Session: 792bf84a-3783-4f5d-a811-cded61dd2012
This commit is contained in:
tegwick 2026-10-09 00:22:04 +02:00
parent 2bf4fa0b9a
commit 33ebda59d4
12 changed files with 277 additions and 65 deletions

View file

@ -317,10 +317,11 @@
"type": "object",
"additionalProperties": false,
"required": [
"auth",
"entry_point",
"id",
"owner",
"entry_point",
"auth",
"value_handling",
"warden_executes"
],
"properties": {
@ -341,7 +342,8 @@
"minLength": 1
},
"warden_executes": {
"type": "boolean"
"type": "boolean",
"description": "ops-warden invokes the owner's declared entry point through the caller proxy, under the caller's identity; ops-warden holds no credential for it (ops-warden ADR-0002)."
},
"attended": {
"type": "boolean"
@ -355,6 +357,22 @@
"when": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]* (==|!=) [A-Za-z0-9_.:/-]+$"
},
"lane": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$",
"description": "ops-warden catalog lane that resolves this step; required when warden_executes or attended"
},
"value_handling": {
"enum": [
"none",
"generated_in_custody"
]
},
"ready_check": {
"type": "string",
"minLength": 1,
"description": "status-only gate checked before the next step; never reads a value"
}
}
},

View file

@ -79,10 +79,28 @@ A type is a directory `security-case-types/<type-id>/` in net-kingdom containing
initial trust, owner attribution)
- `materialization`: the OpenBao structure ops-mason creates per case, as
templates over the parameters: policy, role, KV path, catalog entry;
a policy or role templates its rules (`body`), not only its name
a policy, role or catalog entry templates its body (`body`), not only its
name, so that cases add values and never design
- `follow_through`: the ops-warden route for each step, `warden_executes`,
the auth method and the entry point; which steps need an attended human
login, if any, and why
login, if any, and why. Each step also names:
- `lane`: the reviewed ops-warden catalog lane that resolves it. This is
required for every step ops-warden executes or groups into an attended
session, and ops-warden refuses to plan a step whose lane is missing or
unreviewed.
- `value_handling`: `none`, or `generated_in_custody` for a secret that is
generated inside the broker and custody. A generated value never passes
through the run, its argv, stdout or receipts.
- optionally `ready_check`: a status-only gate that must hold before the
next step (for example, a delivery reporting Ready). It never reads a
value.
`warden_executes: true` means exactly this: ops-warden invokes the owner's
declared entry point through its existing caller proxy, under the caller's
identity and the caller's own lane. It never means that ops-warden holds a
standing credential for the step; that would make it a broker (ops-warden
ADR-0002). *(Clarified 2026-10-09 from ops-warden's review,
WARDEN-WP-0044-T01 R1–R3.)*
- `verification`: the checks that prove a case is live (positive and
negative), runnable without reading a secret value
- `rollback`: the exact inverse of each step

View file

@ -79,6 +79,11 @@ spec:
- id: catalog-entry
kind: catalog_entry
template: "local-identity-{operator_username}"
body: |
id: local-identity-{operator_username}
owner_repo: net-kingdom
warden_executes: false
path_template: {signing_key_ref}
# What ops-warden carries out, step by step, on whose entry point.
follow_through:
@ -86,8 +91,10 @@ spec:
owner: net-kingdom
entry_point: "uv run --project local-identity local-identity init"
auth: "local operator shell on the target host"
lane: local-identity-operator
warden_executes: false
attended: true
value_handling: none
attended_justification: >-
The bootstrap identity exists before any managed identity, so no
unattended principal can run it.

View file

@ -84,6 +84,18 @@ session (WARDEN-WP-0044). The way to remove this
step is a KeyCape change: load clients from a non-secret registry and reference
confidential secrets from custody. That would be a 1.1 or 2.0 of this type.
## Lanes, value handling and gates
Every step ops-warden executes or groups names its catalog lane
(`identity-provisioner-groups` and `keycape-client-secret-generate` are new and
must exist before the first case runs; the attended steps use the existing
`key-cape-oidc-login`). "ops-warden executes" means ops-warden invokes the owner's
entry point through its caller proxy, under the caller's identity: it holds no
credential (canon section 3). `mint-secret` generates the value inside the
broker and custody (`value_handling: generated_in_custody`), so no value passes
through the run. `deliver-secret` gates `bind-env` with a status-only Ready check.
`ensure-groups` adds exactly the memberships recorded in the accepted case.
## Known limits
- No loopback or plain-http redirects (CLI and local development clients).

View file

@ -13,9 +13,10 @@ from pathlib import Path
HERE = Path(__file__).resolve().parent
HUB_DECISION = "e418263e-a71f-4f01-a970-c87c9e187c09"
# r2 supersedes the first memo (delivered before ops-mason's review). A new id is
# needed: the dispatcher refuses new content under a delivered idempotency key.
MEMO_ID = "nk-wp-0048-t05-oidc-browser-relying-party-r2"
# r3 supersedes r2 (built before ops-warden's review) and the first memo (built
# before ops-mason's review). Each revision needs a new id: the dispatcher
# refuses new content under a delivered idempotency key.
MEMO_ID = "nk-wp-0048-t05-oidc-browser-relying-party-r3"
ITEM = f"{MEMO_ID}-packet"
packet = (HERE / "packet.md").read_text()

File diff suppressed because one or more lines are too long

View file

@ -4,10 +4,11 @@ Workplan: NK-WP-0048 (net-kingdom), task NK-WP-0048-T05
Hub decision: e418263e-a71f-4f01-a970-c87c9e187c09
Canon: canon/standards/security-case-types_v0.1.md (accepted 2026-10-08), section 5
Type: security-case-types/oidc-browser-relying-party/ (type.yaml, README.md, tests)
Memo: nk-wp-0048-t05-oidc-browser-relying-party-r2. It supersedes memo
nk-wp-0048-t05-oidc-browser-relying-party, delivered earlier on 2026-10-08,
which described the type before ops-mason's review. Please answer this one;
both serve the same hub decision, and the first answer resolves it.
Memo: nk-wp-0048-t05-oidc-browser-relying-party-r3. It supersedes r2
(nk-wp-0048-t05-oidc-browser-relying-party-r2, which lacked ops-warden's review)
and the first memo (already returned). Please return r2 with a comment that r3
supersedes it, then answer this one. All three serve the same hub decision, and
the first accept or decline resolves it.
## Question
@ -32,6 +33,8 @@ ops-mason materializes it and ops-warden follows through. The type becomes
the request's proposed `platform/workloads/forgejo/netkingdom-oidc-client`.
- It does not adopt the existing hand-built clients. Adoption, when it happens,
does not change their live registrations.
- It does not create the two catalog lanes listed below. They are needed before
the first case runs (T06), each through its own owner.
## The shape being acknowledged
@ -42,30 +45,41 @@ and its owner. A case cannot choose where a confidential client's secret lives:
the type derives `platform/workloads/keycape/<client_id>`, as the live
secrets-engine-openbao client already uses. Secrets never appear in a case.
Steps, in order, each on its owner's entry point:
Steps, in order, each on its owner's entry point and its catalog lane:
| Step | Owner | Runs | Confidential only |
| --- | --- | --- | --- |
| ensure-groups | net-kingdom (identity-provisioner) | ops-warden | no |
| mint-secret | railiance-platform (credential broker) | ops-warden | yes |
| deliver-secret | railiance-platform (delivery into the sso namespace) | owner | yes |
| bind-env | net-kingdom (KeyCape Deployment) | attended operator | yes |
| register-client | net-kingdom (keycape-client.py) | attended operator | no |
| hand-over | the consuming service's owner | owner | no |
| Step | Owner | Runs | Lane | Values | Confidential only |
| --- | --- | --- | --- | --- | --- |
| ensure-groups | net-kingdom (identity-provisioner) | ops-warden | identity-provisioner-groups (new) | none | no |
| mint-secret | railiance-platform (credential broker) | ops-warden | keycape-client-secret-generate (new) | generated in custody | yes |
| deliver-secret | railiance-platform (delivery into the sso namespace) | owner; gate: Ready, status only | — | none | yes |
| bind-env | net-kingdom (KeyCape Deployment) | attended operator | key-cape-oidc-login | none | yes |
| register-client | net-kingdom (keycape-client.py) | attended operator | key-cape-oidc-login | none | no |
| hand-over | the consuming service's owner | owner | — | none | no |
ops-mason builds, for confidential clients, the custody path; two read
policies in the existing family, `workload-kv-read-keycape-<client>-eso` and
`workload-kv-read-keycape-<client>-<consuming service>`, each granting `read`
on that one data path and nothing else; and the Kubernetes-auth role
"ops-warden runs" means exactly this, now written into the canon: ops-warden
invokes the owner's entry point through its caller proxy, under the caller's
identity and lane. It never holds a standing credential for the step. A secret
is generated inside the broker and custody and never passes through the run.
`ensure-groups` adds exactly the memberships recorded in the accepted case and
nothing else.
ops-mason builds, for confidential clients, the custody path; two read policies
in the existing family, `workload-kv-read-keycape-<client>-eso` and
`workload-kv-read-keycape-<client>-<consuming service>`, each granting `read` on
that one data path and nothing else; and the Kubernetes-auth role
`external-secrets-keycape-<client>` that ESO uses to deliver the secret (its
service account only, 15m, no default policy). For every case it adds the
catalog entry `<consuming service>-keycape-client`. A case adds no new path
layout or policy family.
catalog entry `<consuming service>-keycape-client`, whose body the type templates
(owner key-cape, risk high, the consumer's workload reference, path and field,
native delegation, rotation not automatable until a run is proven). A case adds
no new path layout, policy family or route design.
Verification never reads a value. The authorization endpoint accepts each
registered redirect and refuses another; the groups exist; for confidential
clients, a default identity is denied the custody path and the consumer's policy
is allowed it. Rollback is the exact inverse, in reverse order.
registered redirect and refuses another; the groups exist with exactly the
accepted memberships; for confidential clients, the delivery reports Ready, a
default identity is denied the custody path and the consumer's policy is allowed
it; when MFA is pinned, the registered client shows the pin. Rollback is the
exact inverse, in reverse order.
## Why this shape (alternatives rejected)
@ -77,14 +91,20 @@ is allowed it. Rollback is the exact inverse, in reverse order.
- Secrets in case files for convenience: cases carry no secrets.
- A custody path chosen per case: every case would add its own layout and
policy family to review (rejected in ops-mason's review).
- One type-level catalog route instead of one entry per case: it would drop the
consumer's workload reference that zone selection needs (ops-warden withdrew
this proposal and agreed with ops-mason).
- ops-warden holding a standing service token: it would become a broker, which
ops-warden's own design rules out.
## Attended steps
`register-client` (and, for confidential clients, `bind-env`) need one attended
operator session. KeyCape's live client registry is a Kubernetes Secret, and
agents do not read or write Secrets. Removing this needs a KeyCape change
(clients from a non-secret registry, secrets referenced from custody). That
would be a new version of this type, with its own decision.
operator session, grouped by ops-warden on the existing `key-cape-oidc-login`
lane. KeyCape's live client registry is a Kubernetes Secret, and agents do not
read or write Secrets. Removing this needs a KeyCape change (clients from a
non-secret registry, secrets referenced from custody). That would be a new
version of this type, with its own decision.
## Known limits of 1.0
@ -96,22 +116,26 @@ user-engine portal and vergabe fit 1.0 exactly.
## Evidence
- Validator: tools/security-case-types/, 19 tests (a policy or role must
template its rules, not only its name)
- This type: 12 tests, including the rendered Forgejo materialization (path,
both policy bodies, ESO role, catalog entry), the Forgejo case checked field by field against
the accepted request, and a regression showing that the vergabe case reproduces
its hand-built registration exactly
- Validator: tools/security-case-types/, 22 tests (bodies for policies, roles
and catalog entries; lanes for executed or attended steps; value handling on
every step; no generated value in an attended step)
- This type: 14 tests, including the rendered Forgejo case (custody path, both
policy bodies, ESO role, catalog entry body, lanes, value handling, the
delivery gate, the MFA and delivery checks), the Forgejo case checked field by
field against the accepted request, and a regression showing that the vergabe
case reproduces its hand-built registration exactly
- Entry points: identity-provisioner groups ensure/revoke (39 tests),
keycape-client.py (15 tests; it refuses a case-set custody path)
keycape-client.py (15 tests)
- Declaration net-kingdom.keycape-client-registration passes the Playbook
Capability Contract validator
## Reviews before this decision
ops-mason reviewed the materialization section (MASON-WP-0010-T01) and asked
for a derived custody path, reuse of the existing policy family, templated
policy rules and the ESO role; all are in this version, and ops-mason finds the
section acceptable. ops-warden's review of the follow-through section
(WARDEN-WP-0044-T01) is still in progress. Answering now acknowledges a
follow-through its executor has not yet confirmed.
Both executors have reviewed their sections, and everything they required is in
this version. ops-mason (MASON-WP-0010-T01) asked for a derived custody path,
reuse of the existing policy family, templated policy rules, the ESO role and a
per-case catalog entry. ops-warden (WARDEN-WP-0044-T01) asked for a catalog lane
per step, a narrow canon meaning of "ops-warden runs", per-step value handling,
a Ready gate before bind-env, memberships only from the accepted case, MFA and
delivery checks, and a templated catalog entry body. ops-warden and ops-mason
report no remaining conflict.

View file

@ -133,3 +133,28 @@ def test_confidential_case_renders_custody_delivery_and_one_attended_session():
plan = r["composition"]
assert plan["execution"]["permitted"] is False
assert r["execution"] == {"permitted": False}
def test_ops_warden_review_is_folded_in():
issues, r = v.render_case(FORGEJO, as_implemented(), DECLS)
assert msgs(issues) == []
steps = {s["id"]: s for s in r["follow_through"]}
lanes = {k: s.get("lane") for k, s in steps.items()}
assert lanes["ensure-groups"] == "identity-provisioner-groups"
assert lanes["mint-secret"] == "keycape-client-secret-generate"
assert lanes["bind-env"] == lanes["register-client"] == "key-cape-oidc-login"
assert steps["mint-secret"]["value_handling"] == "generated_in_custody"
assert all(s["value_handling"] == "none" for k, s in steps.items() if k != "mint-secret")
assert "Ready=True" in steps["deliver-secret"]["ready_check"]
assert "accepted case" in steps["ensure-groups"]["entry_point"]
checks = {c["id"] for c in r["verification"]}
assert {"secret-delivered", "mfa-pinned"} <= checks
entry = next(m for m in r["materialization"] if m["kind"] == "catalog_entry")
assert "workload_ref: applicable, name forgejo" in entry["body"]
assert "path_template: platform/workloads/keycape/forgejo" in entry["body"]
def test_public_client_without_mfa_skips_delivery_and_mfa_checks():
_, r = v.render_case(PUBLIC, as_implemented(), DECLS)
checks = {c["id"] for c in r["verification"]}
assert not {"secret-delivered", "mfa-pinned"} & checks

View file

@ -139,20 +139,38 @@ spec:
- id: catalog-entry
kind: catalog_entry
template: "{consuming_service}-keycape-client"
body: |
id: {consuming_service}-keycape-client
owner_repo: key-cape
warden_executes: false
risk: high
status: active once verification passes
workload_ref: applicable, name {consuming_service}, declaration_ref of the consumer's published declaration or the explicit unknown form, never inferred
path_template: platform/workloads/keycape/{client_id}
field: CLIENT_SECRET
delegation: native, intended_owner key-cape
rotation: mint-secret then verification; automatable false until a run has been proven
description: >-
One entry per case (ops-mason item 5, ops-warden R4'). It carries the
consumer's workload reference, which ADR-0009 needs for zone selection.
follow_through:
- id: ensure-groups
owner: net-kingdom
entry_point: "identity-provisioner POST /v1/groups/ensure (groups {access_groups}; members {initial_members})"
auth: "identity-provisioner service token (bearer), via the ops-warden proxy; the provisioner holds the LLDAP admin credential"
entry_point: "identity-provisioner POST /v1/groups/ensure (groups {access_groups}; members exactly {initial_members} as recorded in the accepted case, nothing else)"
auth: "the caller's own identity-provisioner credential through its lane; ops-warden invokes it via the caller proxy and holds no token"
lane: identity-provisioner-groups
warden_executes: true
attended: false
value_handling: none
- id: mint-secret
owner: railiance-platform
when: "client_type == confidential"
entry_point: "railiance-platform credential broker: generate a client secret into bao:platform/workloads/keycape/{client_id}"
auth: "credential broker issuer, scoped to the case's KV path"
entry_point: "railiance-platform credential broker: generate a client secret inside custody at bao:platform/workloads/keycape/{client_id}"
auth: "the caller's broker grant for the case's KV path, through its lane; the value is generated in custody and never returned"
lane: keycape-client-secret-generate
warden_executes: true
attended: false
value_handling: generated_in_custody
- id: deliver-secret
owner: railiance-platform
when: "client_type == confidential"
@ -160,13 +178,17 @@ spec:
auth: "railiance-platform ESO delivery through role external-secrets-keycape-{client_id}; no value leaves custody"
warden_executes: false
attended: false
value_handling: none
ready_check: "the delivery for sso/keycape-{client_id}-client reports Ready=True (status only, no value read)"
- id: bind-env
owner: net-kingdom
when: "client_type == confidential"
entry_point: "patch Deployment sso/keycape: env KEYCAPE_<CLIENT_ID>_CLIENT_SECRET from sso/keycape-{client_id}-client"
auth: "operator kube credentials for namespace sso"
lane: key-cape-oidc-login
warden_executes: false
attended: true
value_handling: none
attended_justification: >-
A change to the live identity provider's Deployment. It runs in the same
attended operator session as register-client, before it, because
@ -176,8 +198,10 @@ spec:
owner: net-kingdom
entry_point: "sso-mfa/k8s/keycape/keycape-client.py apply --client-file <client-from-case> --case-acceptance <record> (net-kingdom.keycape-client-registration)"
auth: "operator write access to the live KeyCape client registry in namespace sso"
lane: key-cape-oidc-login
warden_executes: false
attended: true
value_handling: none
attended_justification: >-
The live KeyCape client registry is a Kubernetes Secret, and agents do
not read or write Secrets. Until KeyCape loads clients from a
@ -190,6 +214,7 @@ spec:
auth: "the consumer owner's own deployment path"
warden_executes: false
attended: false
value_handling: none
verification:
- id: authorize-accepts-redirects
@ -211,7 +236,17 @@ spec:
expect: allow
reads_secret_value: false
- id: groups-present
check: "LLDAP groups {access_groups} exist"
check: "LLDAP groups {access_groups} exist, with exactly the memberships of the accepted case added"
expect: present
reads_secret_value: false
- id: secret-delivered
when: "client_type == confidential"
check: "the delivery for sso/keycape-{client_id}-client reports Ready=True (status only)"
expect: present
reads_secret_value: false
- id: mfa-pinned
when: "mfa_required == true"
check: "keycape-client.py inspect for {client_id} shows the MFA pin on the registered client (non-secret client view)"
expect: present
reads_secret_value: false

View file

@ -46,6 +46,8 @@ CASE_STATUSES = ["proposed", "accepted", "materialized", "live", "retired"]
RISK_CLASSES = {"low", "medium", "high"}
ACCEPTERS = {"owner", "founder"}
MATERIALIZATION_KINDS = {"openbao_policy", "openbao_role", "kv_path", "catalog_entry"}
VALUE_HANDLING = {"none", "generated_in_custody"}
LANE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
TYPE_SECTIONS = [
"id", "version", "status", "title", "purpose", "parameters", "composition",
"materialization", "follow_through", "verification", "rollback", "acceptance",
@ -256,8 +258,8 @@ def _validate_materialization(items: Any, params: dict[str, dict], issues: list[
seen.add(item.get("id"))
if not item.get("template"):
issues.append(err(f"{path}[{i}].template", "required"))
if item["kind"] in {"openbao_policy", "openbao_role"} and not item.get("body"):
issues.append(err(f"{path}[{i}].body", "a policy or role needs its rules templated, not only its name"))
if item["kind"] in {"openbao_policy", "openbao_role", "catalog_entry"} and not item.get("body"):
issues.append(err(f"{path}[{i}].body", "a policy, role or catalog entry needs its body templated, not only its name"))
_validate_templates(items, path, params, issues)
@ -282,6 +284,21 @@ def _validate_follow_through(steps: Any, issues: list[Issue]) -> set[str]:
issues.append(err(f"{p}.warden_executes", "must be true or false"))
if s.get("attended") is True and not s.get("attended_justification"):
issues.append(err(f"{p}.attended_justification", "an attended step must say why it cannot be unattended"))
# R3 (ops-warden review): every step says what happens to values; a
# generated secret is created inside custody and never passes the run.
if s.get("value_handling") not in VALUE_HANDLING:
issues.append(err(f"{p}.value_handling", f"must be one of {sorted(VALUE_HANDLING)}"))
if s.get("value_handling") == "generated_in_custody" and s.get("attended") is True:
issues.append(err(f"{p}.value_handling", "a value generated in custody is never handled in an attended session"))
# R1: ops-warden resolves every step it executes or groups into an
# attended session through a reviewed catalog lane.
lane = s.get("lane")
if (s.get("warden_executes") is True or s.get("attended") is True) and not lane:
issues.append(err(f"{p}.lane", "a step ops-warden executes or groups needs its catalog lane"))
if lane is not None and not (isinstance(lane, str) and LANE.fullmatch(lane)):
issues.append(err(f"{p}.lane", "must be a catalog id (lowercase letters, digits, hyphens)"))
if "ready_check" in s and not (isinstance(s["ready_check"], str) and s["ready_check"].strip()):
issues.append(err(f"{p}.ready_check", "must describe a status-only check"))
return ids

View file

@ -183,3 +183,26 @@ def test_bad_condition_is_refused():
found = msgs(v.validate_type(t))
assert any("unknown parameter 'nonexistent'" in m for m in found)
assert any("must be '<parameter> == <value>'" in m for m in found)
def test_follow_through_needs_value_handling_and_lanes():
t = copy.deepcopy(TYPE)
step = t["spec"]["follow_through"][0]
del step["value_handling"]
del step["lane"]
found = msgs(v.validate_type(t))
assert any("value_handling" in m for m in found)
assert any("catalog lane" in m for m in found)
def test_generated_value_never_in_attended_step():
t = copy.deepcopy(TYPE)
t["spec"]["follow_through"][0]["value_handling"] = "generated_in_custody"
assert any("never handled in an attended session" in m for m in msgs(v.validate_type(t)))
def test_catalog_entry_needs_a_body():
t = copy.deepcopy(TYPE)
item = next(m for m in t["spec"]["materialization"] if m["kind"] == "catalog_entry")
del item["body"]
assert any("catalog entry needs its body" in m for m in msgs(v.validate_type(t)))

View file

@ -259,6 +259,38 @@ hold r2, fold them in and build r3.
key-cape and railiance-apps were told about Forgejo's new custody
path.
2026-10-09: r2 was not answered (hub decision `e418263e` open; dispatcher 9
delivered), so r2 is held and r3 replaces it. ops-warden withdrew R4 for R4′
(message a66a9204): one catalog entry per case with its body templated in the
type. Folded in:
- Canon section 3: a narrow meaning of `warden_executes: true` (caller proxy,
caller identity, no standing credential; ops-warden ADR-0002), plus per-step
`lane`, `value_handling` and an optional `ready_check`. A catalog entry body is
now required, like policy and role bodies.
- Schema and validator enforce all of these.
- The type: lanes `identity-provisioner-groups` and
`keycape-client-secret-generate` (both new) and `key-cape-oidc-login`;
`mint-secret` is `generated_in_custody`; a Ready gate on `deliver-secret`;
`ensure-groups` adds exactly the accepted case's memberships; new checks
`secret-delivered` and `mfa-pinned`; the catalog-entry body follows R4′.
- 110 tests pass.
Memo r3 (`nk-wp-0048-t05-oidc-browser-relying-party-r3`, packet
`sha256:726dfc01…`) passes informed-decision's and the dispatcher's validation.
The founder queues it with the same command, returns r2 with a comment that r3
supersedes it, then answers r3.
Prerequisites for T06, not for T05:
- the two new lanes, each through its owner;
- a founder-approved rollout of an identity-provisioner image containing
630ebfd (the deployed one is main-6c4fcaf from 2026-09-24, without
`/v1/groups/ensure`).
ops-mason's request to create LLDAP group `fluid-telegram-operators` (message
259ec26d) was answered: not possible from here by that path; the founder creates
it in the LLDAP admin UI.
```task
id: NK-WP-0048-T06
status: wait