diff --git a/registry/generated/high-risk-data-paths.yaml b/registry/generated/high-risk-data-paths.yaml index 3b410dc..27bb658 100644 --- a/registry/generated/high-risk-data-paths.yaml +++ b/registry/generated/high-risk-data-paths.yaml @@ -10,11 +10,11 @@ # declares it, and is null where the field set has not been established -- # null means unknown, never 'one field'. -generated_at: "2026-09-04T23:20:26Z" +generated_at: "2026-09-08T12:52:59Z" source: ops-warden/registry/routing/catalog.yaml catalog_revision: "00145d705e88eeb9a880a85a3cc15a5205dde2d6" catalog_revision_date: "2026-09-05T01:19:48+02:00" -catalog_dirty: false +catalog_dirty: true high_risk_lane_count: 24 concrete_path_count: 15 diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 3be856d..fdd76e0 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -335,9 +335,9 @@ entries: delegation: mode: interim intended_owner: key-cape - blocked_on: "REFUSED by secrets-engine 2026-08-21: login, MFA and identity-token issuance are key-cape/Keycloak's; secrets-engine may consume OIDC for OpenBao auth but does not own the login capability. ops-warden agrees — intended_owner corrected from secrets-engine to key-cape. Asked of key-cape 2026-08-28; KEY-WP-0009 accepting issuance ownership for machine identities is the precedent that makes it answerable." - reviewed: "2026-08-28" - verified: asked-and-waiting + blocked_on: "ACCEPTED by key-cape 2026-09-08 (msg 12f1bdfa): key-cape owns interactive login, OIDC identity-token issuance and MFA for this lane, confirming the 2026-08-28 correction of intended_owner from secrets-engine (whose 2026-08-21 refusal was right). Lane stays interim because the accepted ownership covers only the identity half: the fetch_command yields an OPENBAO token, and the netkingdom auth mount, the role= mapping, the token store and its enforcement are OpenBao's, with role/policy definition railiance-platform's. keycape login is not a drop-in — it yields a key-cape issuer JWT written to a private file, a different credential type with a different verifier (KEY-WP-0014-T04). Retires when a key-cape-native front door yields the credential this route's consumers expect, or when the route is deliberately re-contracted to a JWT. Verified against key-cape docs/native-authentication.md, docs/openbao-service-auth-contract.md, config/service-clients.example.yaml, src/internal/server/oidc/discovery.go." + reviewed: "2026-09-08" + verified: owner-confirmed # Login lane (WP-0014 T4) — interactive auth bootstrap, not a secret read. No # secret-read gate (you have no identity yet) and no caller-auth precheck (the # point is to obtain one). warden runs it interactively as the caller and never @@ -764,9 +764,9 @@ entries: delegation: mode: interim intended_owner: key-cape - blocked_on: "client_secret_basic exchange is a key-cape protocol procedure, not a KV read; still no key-cape-native exchange/rotation command. Re-checked against key-cape source 2026-08-28: KEY-WP-0009 finished 2026-08-23 and did add bounded service-auth (per-client tokenLifetime, docs/openbao-service-auth-contract.md), but that is client_credentials JWT issuance for OpenBao machine login — it does not front this client_secret_basic exchange or its rotation. The server advertises client_secret_basic (src/internal/server/oidc/discovery.go) without exposing an owner command for it. Blocker stands." - reviewed: "2026-08-28" - verified: source-read + blocked_on: "Narrowed 2026-09-08 to rotation steps 1-2 only: successor generation and the CAS write to both custodians (platform/workloads/rapp-qonto/keycape-client field client_secret, and sso/keycape-rapp-qonto-client key client-secret) remain custody/deployment acts with no admitted execution and rollback contract, and no admitted ops-warden lane authorizes them. The key-cape-native exchange now exists (keycape service-token, 2026-09-05, KEY-WP-0014-T03) and step 3 verification exists as one command (keycape verify-client, 2026-09-08, including predecessor rejection and identical-secret detection); both are documented in key-cape/docs/native-authentication.md. The prior blocker recorded both as absent, which was accurate on 2026-08-28 and is not accurate now — corrected by key-cape (msg 08d42f47). Re-checked against key-cape source 2026-09-08." + reviewed: "2026-09-08" + verified: owner-confirmed risk: high workload_ref: applicability: applicable @@ -781,11 +781,15 @@ entries: rotation: method: rotate owner: key-cape - automatable: true + # Not drivable end to end: steps 1-2 are custody/deployment acts with no + # admitted execution transport, so an executable driver (WARDEN-WP-0027 + # Strand B) must not treat this lane as automatable. Step 3 alone is one + # owner command today — the precision lives in the steps, not the flag. + automatable: false steps: - - "Generate a fresh newline-free secret through an approved execution transport; never print it." - - "Write the same value to OpenBao and sso/keycape-rapp-qonto-client, then restart KeyCape." - - "Verify positive qonto:read exchange plus wrong-secret and excessive-scope denial without printing tokens." + - "Generate a fresh newline-free secret through an approved execution transport; never print it. NOT automatable — no admitted execution and rollback contract (see delegation.blocked_on)." + - "CAS-write the same value to platform/workloads/rapp-qonto/keycape-client and sso/keycape-rapp-qonto-client preserving siblings, then restart KeyCape in the agreed window. NOT automatable — same blocker; on failure reconcile both custodians to the same version before declaring rollback complete." + - "Verify positive qonto:read exchange plus predecessor-secret and excessive-scope denial without printing tokens: run the owner command keycape verify-client (key-cape/docs/native-authentication.md)." - id: net-kingdom-lldap-bind-credential title: NetKingdom LLDAP bind credential for identity and privacyIDEA resolver diff --git a/tests/test_routing.py b/tests/test_routing.py index cd3d6e8..58a3ce4 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -994,12 +994,31 @@ def test_asked_and_waiting_is_not_verification(): def test_stale_gaps_flags_unverified_even_when_the_date_is_today(): + """Property, not a named lane. + + Originally pinned to `key-cape-oidc-login` sitting `asked-and-waiting`; + key-cape answered on 2026-09-08, so the fixture lane moved and the test + failed on someone else's good news rather than on a defect. The claim being + made is about the verification method, so assert that instead: any lane + whose blocker was never re-established is stale on the day it was reviewed. + """ catalog = load_catalog(_repo_catalog()) - stale = {e.id for e in catalog.stale_gaps(include_draft=True, today=date(2026, 8, 21))} - # Asked of key-cape on 2026-08-21 and unanswered -- zero days old, still stale. - assert "key-cape-oidc-login" in stale - # Confirmed by the owner the same day -- fresh. - assert "issue-core-ingestion-api-key" not in stale + today = date(2026, 9, 8) + stale = {e.id for e in catalog.stale_gaps(include_draft=True, today=today)} + unverified = [ + e for e in catalog.gaps(include_draft=True) + if not e.effective_delegation.is_verified + ] + for entry in unverified: + assert entry.id in stale, f"{entry.id} is unverified but not stale" + # A lane the owner confirmed today is fresh, so staleness is not vacuous. + fresh = [ + e.id for e in catalog.gaps(include_draft=True) + if e.effective_delegation.is_verified + and e.effective_delegation.reviewed == today.isoformat() + ] + assert fresh, "expected at least one owner-confirmed-today lane" + assert not (set(fresh) & stale) def test_invalid_verification_method_rejected(tmp_path): @@ -1027,10 +1046,10 @@ def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env): assert result.exit_code == 3 rows = json.loads(result.stdout) assert any(r["stale"] for r in rows) - # An asked-and-waiting lane stays stale until it is verified, regardless of - # how many calendar days have elapsed since the request. - assert any( - r["stale"] - and r["verified"] == "asked-and-waiting" - for r in rows - ) + # A lane whose blocker was never re-established stays stale regardless of + # how many calendar days have elapsed since it was reviewed. Asserted over + # the verification method rather than one named lane -- see + # test_stale_gaps_flags_unverified_even_when_the_date_is_today. + for row in rows: + if row["verified"] in ("asked-and-waiting", "unverified"): + assert row["stale"], f"{row['id']} is unverified but not stale" diff --git a/workplans/ADHOC-2026-09-08.md b/workplans/ADHOC-2026-09-08.md new file mode 100644 index 0000000..faec8c8 --- /dev/null +++ b/workplans/ADHOC-2026-09-08.md @@ -0,0 +1,172 @@ +--- +id: WARDEN-WP-ADHOC-2026-09-08 +type: workplan +title: "Ad Hoc Tasks — 2026-09-08" +domain: infotech +repo: ops-warden +status: finished +owner: claude +topic_slug: netkingdom +created: "2026-09-08" +updated: "2026-09-08" +--- + +# Ad Hoc Tasks — 2026-09-08 + +Four key-cape messages, all follow-on from `WARDEN-WP-0033`. Two carry answers +ops-warden's catalog was waiting for; both make a recorded blocker less true than +it reads. This is the T05 mechanism paying out — and this time the owner ran it +against us rather than us running it against them. + +### T01 — Narrow the `rapp-qonto-keycape-client` blocker (msg `08d42f47`) + +```task +id: WARDEN-WP-ADHOC-2026-09-08-T01 +status: done +priority: high +``` + +The blocker recorded "still no key-cape-native exchange/rotation command", +`verified: source-read` against key-cape source on 2026-08-28. Accurate then; +two things landed after that read: + +- `keycape service-token` (2026-09-05, KEY-WP-0014-T03) — the client_secret_basic + exchange the blocker records as absent. +- `keycape verify-client` (2026-09-08) — rotation step 3 as one command, + including predecessor rejection and identical-secret detection. + +**Narrowed, not cleared**, as key-cape asked. What remains genuinely blocked is +steps 1-2: successor generation and the CAS write to both custodians. Those are +custody and deployment acts, key-cape deliberately ships no command for them, and +no admitted ops-warden lane authorizes them either (T03). + +`verified` moved to `owner-confirmed` — the correction came from the owner. + +`rotation.automatable` flipped `true` → `false`, which key-cape explicitly asked +us not to do "either way" without precision. Done with the precision they wanted, +in the only place the schema has room for it: the flag is a single hint consumed +by a future executable driver (`WARDEN-WP-0027` Strand B), and a driver told +`true` would attempt custody acts with no admitted transport — the dangerous +direction. So the flag fails safe and the *steps* now carry the per-step truth, +each naming whether it is automatable and why. Offered them a schema change if a +per-step field would serve better; not built on speculation. + +### T02 — Record key-cape's acceptance of `key-cape-oidc-login` (msg `12f1bdfa`) + +```task +id: WARDEN-WP-ADHOC-2026-09-08-T02 +status: done +priority: high +``` + +key-cape **accepted** ownership: interactive login, OIDC identity-token issuance +and MFA. This confirms the 2026-08-28 correction of `intended_owner` from +secrets-engine to key-cape, and confirms secrets-engine's 2026-08-21 refusal was +right. `verified: asked-and-waiting` → `owner-confirmed`. + +**The lane stays `interim`.** Acceptance covers the identity half only. The +`fetch_command` yields an *OpenBao* token — the `netkingdom` mount, the +`role=` mapping, the token store and its enforcement are OpenBao's, with +role and policy definition railiance-platform's. `keycape login` yields a +key-cape issuer JWT to a private file: different credential type, different +verifier, different enforcement owner. Accepting an owner is not the same as the +owner fronting the lane, and `ADR-0003` says the interim cover is retired when +the front door exists, not when ownership is agreed. Retirement condition is now +recorded on the entry. + +Their reading of the risk is right and we confirmed it: `warden_executes: false` +with a `wiki_ref` pointer, so per `ADR-0001` this is a pointer layer, not a code +path with programmatic consumers. `risk: standard` and +`workload_ref: not-applicable` are correct — a human identity act with no KV +path, consistent with how `ADR-0008` treats the other non-KV lanes. + +### T03 — Answer "who executes Qonto rotation steps 1-2" (msg `0a6c9d88`) + +```task +id: WARDEN-WP-ADHOC-2026-09-08-T03 +status: done +priority: high +``` + +Answer: **nobody, admittedly, today** — and ops-warden must not invent one. + +Steps 1-2 mutate custody on `platform/workloads/rapp-qonto/keycape-client` and +`sso/keycape-rapp-qonto-client`. Custody of platform workload paths is +railiance-platform's; the admitted execution front door for provisioned +secret-exec is `secrets-engine exec`. ops-warden owns neither and absorbing it +would be `ADR-0002` and `ADR-0005` violated in one act. Routed to +railiance-platform, with secrets-engine named as the transport owner — and +secrets-engine independently confirmed the same boundary three days earlier +(msg `15f0c0ca`): they will not rewrite a production lane's custody state from a +coordination message without owner confirmation. + +key-cape was right not to treat a generic `warden route` match as authorization. +See T04 for what our own tool did when asked. + +### T04 — `warden plan` returned `autonomous` for a custody *write* (defect) + +```task +id: WARDEN-WP-ADHOC-2026-09-08-T04 +status: done +priority: high +``` + +Verifying T03 against our own front door: + +``` +warden plan "generate a successor client secret ... and CAS-write it to + OpenBao platform/workloads/... and sso/..." --json + verdict: autonomous + lane_id: rapp-qonto-keycape-client + commands: warden access ... --exec / --out / --wrap +``` + +A need that says *generate* and *CAS-write* resolved to `autonomous` and was +answered with three **read** transports. This is the `WARDEN-WP-0033-T06` shape +recurring — a generic keyword match producing a confident wrong act — except T06 +fixed one route by adding an exact lane, and the defect is in the classifier. +`warden plan` has no notion of read versus mutate intent, so every write need +matches the lane that *reads* the same path and comes back `autonomous`. + +Not fixed here: a classifier is design work with an approval-semantics blast +radius, which the convention says is a workplan, not an adhoc. +Promoted to **`WARDEN-WP-0038`** (`proposed`). + +Worth stating plainly: key-cape distrusted this output on principle and was right +to. Had they trusted it, our tool would have told them to proceed autonomously on +an unadmitted custody mutation. + +### T05 — Acknowledge the KEY-WP-0009 handoff (msg `5bc58847`) + +```task +id: WARDEN-WP-ADHOC-2026-09-08-T05 +status: done +priority: low +``` + +Informational; no route retired or changed. Their routing note matches ours: +"login / OIDC / MFA" → key-cape means **issuance only**, and secret *values* +route to OpenBao custody. Nothing to change in `wiki/CredentialRouting.md`. + +Also records that the KEY-WP-0009 reply ops-warden reported never arriving indeed +never existed (KEY-WP-0014-T02, audited 2026-09-05). Our 2026-08-28 finding was +right, and the correction is theirs, recorded. + +### T06 — Unpin two tests from a transient catalog state + +```task +id: WARDEN-WP-ADHOC-2026-09-08-T06 +status: done +priority: medium +``` + +`test_stale_gaps_flags_unverified_even_when_the_date_is_today` and +`test_cli_route_gaps_fail_on_stale_exits_3` both asserted that +`key-cape-oidc-login` specifically sat `asked-and-waiting`. T02 answering that +question broke them — **the tests failed on good news**, not on a defect, and a +test that fails when a counterparty replies will get suppressed rather than read. + +Both now assert the property they were always about: any lane whose blocker was +never re-established is stale regardless of its date, over whatever lanes are in +that state. The named-lane version of the claim already exists as a unit test on +`Delegation.is_verified`, which is where a fixture belongs. diff --git a/workplans/WARDEN-WP-0038-plan-mutation-intent.md b/workplans/WARDEN-WP-0038-plan-mutation-intent.md new file mode 100644 index 0000000..a37d8ac --- /dev/null +++ b/workplans/WARDEN-WP-0038-plan-mutation-intent.md @@ -0,0 +1,71 @@ +--- +id: WARDEN-WP-0038 +type: workplan +title: "warden plan must distinguish reading a secret from mutating one" +domain: infotech +repo: ops-warden +status: proposed +owner: ops-warden +topic_slug: netkingdom +planning_priority: P1 +depends_on_workplans: + - WARDEN-WP-0029 +created: "2026-09-08" +updated: "2026-09-08" +--- + +# WARDEN-WP-0038 — Read versus mutate intent in the policy front door + +## The defect + +`warden plan` scores a need against catalog lanes by keyword overlap and returns +a verdict. It has no notion of what the caller wants to *do*. So a need that +says *generate a successor secret and CAS-write it to two custodians* matches the +lane that **reads** that path, scores well on the shared nouns, and returns: + +``` +verdict: autonomous +commands: warden access --exec / --out / --wrap +``` + +Three read transports offered as the answer to a write, under the verdict that +means "proceed without the founder". Reproduced 2026-09-08 against +`rapp-qonto-keycape-client` (`ADHOC-2026-09-08-T04`). + +## Why this is worse than a bad suggestion + +`warden plan` is the documented first step before drafting any founder credential +step (`.claude/rules/credential-routing.md`), and `autonomous` is consumed as an +authorization signal by agents that have no other check. `WARDEN-WP-0029` shipped +the verdict *because* instinct was not auditable; a verdict that is confidently +wrong in the permissive direction is worse than no verdict. + +key-cape hit this and refused to rely on it — "we are not treating a generic +match as authorization" — which is the only reason it surfaced as a design +question rather than as an unadmitted custody mutation. The control that caught +it was a counterparty's caution. That is not a control. + +`WARDEN-WP-0033-T06` is the same shape (rapp-postgres, generic +`openbao-api-key` match → `paste_once_provision` for an act that could not work) +and was fixed by adding one exact lane plus one regression. That fixed the +instance. This is the class. + +## Shape of the fix (to be designed, not assumed) + +- Classify need intent — read / mutate / establish — before scoring, and never + answer a mutate need with a read lane's transports. +- A mutate need against a lane ops-warden does not own is `unroutable` with a CCR + stub naming the custody owner, not `autonomous`. `ADR-0003`: cover gaps, never + silently own them; a permissive verdict on someone else's custody act is a + silent claim of ownership. +- Consider whether `rotation.steps` should be reachable from `plan` at all, given + they are advisory prose and several are explicitly not automatable. +- Regression from the reproduced case, in the manner of T06. + +## Open question for design + +Whether intent belongs in the matcher or in the lane. A lane could declare which +acts it admits (`read`, `rotate`, `provision`) and `plan` could refuse any act a +lane does not declare — stricter, and it fails closed on lanes that say nothing, +which is the majority today. That is a catalog schema change and needs its own +argument.