docs: narrow qonto blocker, record key-cape lane ownership

key-cape corrected two blockers that had stopped being true after our
2026-08-28 source-read:

- rapp-qonto-keycape-client: `keycape service-token` (2026-09-05) is the
  native exchange the blocker recorded as absent, and `keycape verify-client`
  (2026-09-08) is rotation step 3 as one command. Narrowed to steps 1-2 --
  successor generation and the CAS write -- rather than cleared, as they
  asked. rotation.automatable -> false so a future executable driver is not
  told a lane with no admitted custody transport is drivable; the per-step
  truth moves into the steps.
- key-cape-oidc-login: ownership ACCEPTED by key-cape, so verified moves from
  asked-and-waiting to owner-confirmed. Lane stays interim -- acceptance
  covers the identity half, while the fetch_command yields an OpenBao token
  whose mount, role mapping and enforcement are not key-cape's.

Two tests pinned `key-cape-oidc-login` to sitting `asked-and-waiting`, so
answering the question broke them -- they failed on good news. Both now
assert the property instead: an unverified blocker is stale regardless of
date, over whatever lanes are in that state.

Verifying the routing answer against our own front door turned up a defect:
`warden plan` returns `autonomous` for a custody *write* and answers it with
read transports, because it has no read-versus-mutate intent. Recorded as
WARDEN-WP-0038 (proposed) -- the WP-0033-T06 shape, as a class this time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EPuTc18FjU5WFqoSEKH3C

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1276224@bnt-lap001
Assistant-Session: 426ec497-e1c4-4dd3-b417-dfce1ca1dbc3
This commit is contained in:
tegwick 2026-09-08 14:55:02 +02:00
parent 96dbb22aab
commit c1330049d9
5 changed files with 290 additions and 24 deletions

View file

@ -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

View file

@ -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=<domain> 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

View file

@ -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"

View file

@ -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=<domain>` 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.

View file

@ -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 <lane> --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.