diff --git a/.claude/rules/credential-routing.md b/.claude/rules/credential-routing.md index 4ef5546..06e6b43 100644 --- a/.claude/rules/credential-routing.md +++ b/.claude/rules/credential-routing.md @@ -4,9 +4,15 @@ for inference. Run this check **before** requesting secrets, API keys, SSH access, login tokens, or database passwords — in any repo, not only `ops-warden`. -ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every -other credential need belongs to another subsystem. **Do not** message -`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key. +ops-warden **issues SSH certificates** (`warden sign`, `cert_command`) **and is the +operator access front door** for every other credential need. For `exec_capable` lanes +(OpenBao reads, key-cape login) `warden access --fetch/--exec` **proxies the fetch +as you** — it runs the owner's tool with your identity and streams the value to you; +ops-warden holds, caches, and logs nothing. For non-exec lanes it points you at the owner. + +**Do not** `POST /messages/` to `ops-warden` expecting a secret *value* — a State Hub +reply is always a pointer. The **value comes from the CLI front door** (`warden access`), +run with **your** identity, never from the inbox. ### Lookup (do this first) @@ -31,14 +37,14 @@ Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run wa ### Quick routing table -| I need… | Owner | ops-warden executes? | +| I need… | Owner | ops-warden role | | --- | --- | --- | -| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` | -| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only | -| Login / OIDC / MFA | key-cape / Keycloak | No — route only | -| Authorization decision | flex-auth | No — route only | -| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` | -| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only | +| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Issue** — `warden sign` | +| API key, DB password, provider token | OpenBao (`railiance-platform`) | **Assist** — `warden access --fetch/--exec` proxies as you; OpenBao keeps custody | +| Login / OIDC / MFA | key-cape / Keycloak | **Assist** — `warden access --fetch` runs the login as you | +| Authorization decision | flex-auth | Route only | +| activity-core → issue-core emission | activity-core + issue-core | Route — `warden route show activity-core-issue-sink` | +| SSH tunnel | ops-bridge (+ `cert_command` from warden) | Route only | ### Anti-patterns (do not do these) diff --git a/SCOPE.md b/SCOPE.md index 68a3845..5c7eacc 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -8,10 +8,12 @@ ## One-liner -Operational access steward for the NetKingdom security model — issues short-lived -SSH certificates for `adm`/`agt`/`atm` actors, documents how to obtain other -credential types from the right platform subsystems, stewards workload security -posture conformance, and keeps ops access guidance aligned with NetKingdom canon. +Operational access steward and **front door** for the NetKingdom security model — issues +short-lived SSH certificates for `adm`/`agt`/`atm` actors, and for every other credential +need is the operator front door (`warden access`): routes to the owning subsystem and, for +`exec_capable` lanes (OpenBao reads, key-cape login), **proxies the fetch as the caller** +without taking custody. Also stewards workload security posture conformance and keeps ops +access guidance aligned with NetKingdom canon. --- @@ -297,6 +299,19 @@ description: Issues short-lived CA-signed SSH certificates for adm/agt/atm actor keywords: [ssh, certificate, ca, credential, warden, ops-warden, pki, openbao, vault, netkingdom] ``` +```capability +type: security +title: Operator access front door (caller-identity fetch proxy) +description: warden access is the operator front door for any NetKingdom credential need. + It renders the owner, auth method, path, and policy status, and for exec_capable lanes + (OpenBao secret reads, key-cape OIDC login) proxies the fetch as the caller — running + the owner's tool with the caller's identity and streaming the value to them. ops-warden + takes no custody: it holds, caches, and logs no secret value (transparent conduit, not a + broker). Use this to obtain an API key, DB credential, npm token, or login — not a State + Hub message. +keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, login, proxy, fetch, exec, warden-access, front-door, routing] +``` + --- ## Getting Oriented diff --git a/src/warden/cli.py b/src/warden/cli.py index 1eec9ed..b5af4d5 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -545,6 +545,14 @@ def _entry_summary(entry) -> dict: "owner_repo": entry.owner_repo, "subsystem": entry.subsystem, "warden_executes": entry.warden_executes, + # warden_role tells an agent at a glance whether ops-warden runs this lane + # itself (issue), proxies the fetch as the caller (assist), or only points (route). + "warden_role": ( + "issue" if entry.warden_executes + else "assist" if entry.exec_capable + else "route" + ), + "exec_capable": entry.exec_capable, "wiki_ref": entry.wiki_ref, "canon_ref": entry.canon_ref, "reviewed": entry.reviewed, @@ -567,7 +575,12 @@ def _print_entry_table( from warden.routing.catalog import days_since_review for e in entries: - executes = "[green]issue[/green]" if e.warden_executes else "route" + if e.warden_executes: + executes = "[green]issue[/green]" + elif e.exec_capable: + executes = "[cyan]assist[/cyan]" # warden access --fetch/--exec proxies it + else: + executes = "route" status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]" if show_reviewed: days = days_since_review(e.reviewed) @@ -661,6 +674,12 @@ def route_show( if entry.warden_executes: summary["steps"] = entry.steps summary["cert_command"] = entry.cert_command + elif entry.exec_capable: + summary["next_action"] = ( + f"ops-warden can proxy this as the caller: `warden access --fetch`" + f" (or `--exec -- `); runs {entry.owner_repo}'s tool with your " + f"identity. See `{entry.wiki_ref}`." + ) else: summary["next_action"] = ( f"next action on `{entry.owner_repo}` — see `{entry.wiki_ref}`" @@ -734,6 +753,14 @@ def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict: if entry.warden_executes: payload["next_action"] = "ops-warden issues this directly — see cert_command" payload["cert_command"] = entry.cert_command + elif expanded.exec_capable: + verb = "fetch" if entry.lane != "login" else "login" + payload["next_action"] = ( + f"ops-warden can proxy this {verb} as the caller: " + f"`warden access --fetch`" + + ("" if entry.lane == "login" else " (or `--exec -- `)") + + f". Runs {entry.owner_repo}'s tool with your identity; ops-warden holds no value." + ) else: payload["next_action"] = ( f"obtain from {entry.owner_repo} ({entry.subsystem}); " @@ -979,11 +1006,21 @@ def access( " note : remaining <…> placeholders are owner-confirmed names " f"(coordinate with {entry.owner_repo})." ) - console.print( - f"\n[yellow]ops-warden does not hold this secret.[/yellow] " - f"Obtain it from [bold]{entry.owner_repo}[/bold] as shown — " - "warden advises, the owner vends." - ) + if expanded.exec_capable: + verb = "fetch this for you" if entry.lane != "login" else "run this login for you" + console.print( + f"\n[green]ops-warden can {verb}[/green] as the caller — " + f"[bold]{proxy} --fetch[/bold]" + + ("" if entry.lane == "login" else f" (or [bold]{proxy} --exec -- [/bold])") + + f". It runs {entry.owner_repo}'s tool with [bold]your[/bold] identity; the " + "value streams to you and ops-warden never holds, caches, or logs it." + ) + else: + console.print( + f"\n[yellow]ops-warden does not hold this secret.[/yellow] " + f"Obtain it from [bold]{entry.owner_repo}[/bold] as shown — " + "warden advises, the owner vends." + ) # --------------------------------------------------------------------------- diff --git a/tests/test_access.py b/tests/test_access.py index 394fddb..b9d7c21 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -72,7 +72,17 @@ def test_access_advisory_output(monkeypatch): assert r.exit_code == 0 assert "railiance-platform" in r.stdout assert "platform/workloads/coulomb_social/" in r.stdout - assert "does not hold this secret" in r.stdout + # npm is an exec_capable lane → the front door leads with the proxy, not "owner vends". + assert "can fetch this for you" in r.stdout + assert "never holds" in r.stdout + + +def test_access_route_only_lane_says_owner_vends(monkeypatch): + """A non-exec lane (host principal deploy) keeps the advise-only framing.""" + monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(_repo_catalog())) + r = runner.invoke(app, ["access", "host principal deploy"]) + assert r.exit_code == 0 + assert "warden advises, the owner vends" in r.stdout def test_access_json_shape_is_secret_free(monkeypatch): diff --git a/tests/test_routing.py b/tests/test_routing.py index 05f099d..13624bb 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -248,6 +248,7 @@ def test_cli_show_ssh_json_includes_cert_pattern(repo_catalog_env): assert result.exit_code == 0 data = json.loads(result.stdout) assert data["warden_executes"] is True + assert data["warden_role"] == "issue" assert "warden sign" in data["cert_command"] assert data["steps"] @@ -256,8 +257,12 @@ def test_cli_show_routed_has_next_action_not_steps(repo_catalog_env): result = runner.invoke(app, ["route", "show", "openbao-api-key", "--json"]) data = json.loads(result.stdout) assert data["warden_executes"] is False + # exec_capable lane surfaces as an "assist" role so agents see it is proxyable. + assert data["warden_role"] == "assist" + assert data["exec_capable"] is True assert "steps" not in data assert "next_action" in data + assert "proxy" in data["next_action"] def test_cli_show_unknown_exits_one(repo_catalog_env): diff --git a/workplans/WARDEN-WP-0017-access-front-door-discoverability.md b/workplans/WARDEN-WP-0017-access-front-door-discoverability.md new file mode 100644 index 0000000..fcf85c2 --- /dev/null +++ b/workplans/WARDEN-WP-0017-access-front-door-discoverability.md @@ -0,0 +1,116 @@ +--- +id: WARDEN-WP-0017 +type: workplan +title: "Access front-door discoverability — stop reading as SSH-only" +domain: infotech +repo: ops-warden +status: finished +owner: claude +topic_slug: custodian +planning_priority: high +planning_order: 17 +created: "2026-06-27" +updated: "2026-06-27" +--- + +# WARDEN-WP-0017 — Access front-door discoverability + +**Problem:** WP-0014 made ops-warden the operator **access front door** — for +`exec_capable` lanes (OpenBao reads, key-cape login) `warden access --fetch/--exec` +proxies the fetch **as the caller** and streams the value to them (ops-warden holds +nothing). But every *discovery* surface still tells the pre-WP-0014 story, so agents +(e.g. whynot-design needing `NPM_AUTH_TOKEN`) conclude "ops-warden only issues SSH certs +and replies with a pointer, not a token" and never find the proxy. + +**Fix:** propagate the WP-0014 conduit charter to the surfaces agents actually read. This +is a *messaging/discoverability* change — **no** change to the security model: the conduit +stays a conduit (no custody, no standing broker; the responsibility-map boundary holds). + +**Out of scope:** ops-warden holding/brokering token values (that would override the +WP-0014 charter); shipping the concrete OpenBao npm KV path (railiance-platform infra — +tracked separately); any new fetch capability (the proxy already exists). + +**Depends on:** WP-0014 (the proxy lane being described). + +--- + +## Surfaces that mislead today + +| Surface | Says now | Should say | +| --- | --- | --- | +| `warden route` table `warden` column | binary `issue` / `route` | `issue` / **`assist`** (exec_capable) / `route` | +| `warden route` `--json` | no proxyability field | add `warden_role` + `exec_capable` | +| `warden access` closing line | "warden advises, the owner vends" | for exec_capable: "ops-warden can fetch this for you as the caller…" | +| `.claude/rules/credential-routing.md` | "issues SSH certs **only**… reply is a pointer, not a key" | issues SSH certs **and** is the access front door; exec_capable lanes proxy as you | +| Federated capability registry | only "SSH certificate issuance" | also "Operator access front door / caller-identity fetch proxy" | +| SCOPE one-liner + capability block | SSH + routing + posture | add the access-assist/proxy front door | + +--- + +## Tasks + +### T1 — CLI discoverability: route role + access framing + +```task +id: WARDEN-WP-0017-T01 +status: done +priority: high +``` + +- [x] `warden route` table: three-valued `warden` column — `issue` / `assist` + (exec_capable) / `route`. `_entry_summary` JSON gains `warden_role` + `exec_capable`; + `route show` JSON `next_action` surfaces the proxy for exec_capable lanes. +- [x] `warden access` closing line: for `exec_capable` lanes leads with "ops-warden can + fetch this for you as the caller (`--fetch`/`--exec`); runs the owner's tool with + your identity, value never held/cached/logged." Non-exec lanes keep "advises, owner + vends." `_access_json` `next_action` mirrors it. +- [x] Tests in `tests/test_routing.py` (warden_role issue/assist) and `tests/test_access.py` + (front-door framing for exec lane, owner-vends for route-only lane). 210 pass. + +### T2 — Agent rule + SCOPE reframe + +```task +id: WARDEN-WP-0017-T02 +status: done +priority: high +``` + +- [x] `.claude/rules/credential-routing.md`: reframed the lead ("issues SSH certs **and** + is the operator access front door…") and the quick routing table (`ops-warden role` + column: Issue / Assist / Route). Kept the true anti-pattern: don't POST a State Hub + message for a secret *value* — it comes from the CLI front door run as you. +- [x] SCOPE one-liner reframed to "steward **and front door**"; added a second `capability` + block "Operator access front door (caller-identity fetch proxy)". + +### T3 — Federated capability registration + +```task +id: WARDEN-WP-0017-T03 +status: done +priority: medium +``` + +- [x] Registered the State Hub capability "Operator access front door (caller-identity + fetch proxy)" (id `708e46f6`, repo ops-warden) — the hub had **no** ops-warden + security capability before, so the front door was undiscoverable cross-domain. +- [x] Sent whynot-design (msg `83a3bb2e`) the corrected path: `warden access "npm auth + token" --fetch/--exec`, the CLI refresh, the OpenBao-auth prereq, and the + railiance-platform path caveat. + +--- + +## Acceptance + +- An agent doing the first-line `warden route find` / `--json` lookup can see ops-warden + *assists* (proxies) the OpenBao lane, not merely points. +- The credential-routing rule and federated capability registry describe the access + front door; none of them say "SSH certificates only". +- The conduit boundary is unchanged and explicit: ops-warden fetches *as the caller* and + holds nothing — no custody, no broker. + +--- + +## See also + +- `WARDEN-WP-0014` (the proxy lane), `wiki/OperatorAccessAssist.md` +- `.claude/rules/credential-routing.md`, `registry/routing/catalog.yaml`