NK-WP-0048-T05: fold ops-warden's review into the type; build memo r3
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:
parent
2bf4fa0b9a
commit
33ebda59d4
12 changed files with 277 additions and 65 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue