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:
parent
96dbb22aab
commit
c1330049d9
5 changed files with 290 additions and 24 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
172
workplans/ADHOC-2026-09-08.md
Normal file
172
workplans/ADHOC-2026-09-08.md
Normal 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.
|
||||
71
workplans/WARDEN-WP-0038-plan-mutation-intent.md
Normal file
71
workplans/WARDEN-WP-0038-plan-mutation-intent.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue