diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index a53a76d..3ff6ae9 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -67,6 +67,29 @@ entries: policy_ref: "flex-auth check secret.read:" exec_capable: true + - id: whynot-design-npm-publish + title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry) + need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package] + owner_repo: railiance-platform + subsystem: OpenBao + warden_executes: false + wiki_ref: wiki/playbooks/whynot-design-npm-publish.md#worker-checklist + canon_ref: net-kingdom/docs/platform-identity-security-architecture.md + reviewed: "2026-06-29" + status: active + # Concrete, owner-confirmed lane — railiance-platform CCR-2026-0001 (commit 8f617fc): + # status=active, access_frontdoor.readiness=ready, resolvable=true; positive fetch + # passed and negative (non-whynot) login denied. Zero-placeholder fetch: an automated + # caller can `warden access whynot-design-npm-publish --exec -- npm publish` directly. + # The path was corrected to the `coulomb` tenant — the whynot-design/whynot-design/… + # form is superseded; do not reintroduce it. + auth_method: "bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read" + path_template: "platform/workloads/coulomb/whynot-design/npm-publish" + fetch_command: "bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish" + policy_ref: "flex-auth check secret.read:whynot-design" + exec_capable: true + lane: secret + - id: flex-auth-policy-check title: Authorization decision — may this actor perform this action need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision] diff --git a/src/warden/cli.py b/src/warden/cli.py index b5af4d5..95191e0 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -553,6 +553,9 @@ def _entry_summary(entry) -> dict: else "route" ), "exec_capable": entry.exec_capable, + # resolvable: can `warden access --fetch` run this now with no <…> to fill? + # Lets an automated caller gate on readiness before attempting a fetch. + "resolvable": entry.resolvable, "wiki_ref": entry.wiki_ref, "canon_ref": entry.canon_ref, "reviewed": entry.reviewed, diff --git a/src/warden/routing/catalog.py b/src/warden/routing/catalog.py index c332227..d53446b 100644 --- a/src/warden/routing/catalog.py +++ b/src/warden/routing/catalog.py @@ -126,7 +126,15 @@ class Catalog: return [e for e in self.entries if e.is_active] def find(self, query: str, include_draft: bool = False, limit: int = 5) -> List[RouteEntry]: - """Rank entries by keyword overlap with the query. Highest first.""" + """Rank entries by keyword overlap with the query. Highest first. + + An exact catalog-id match wins outright — this is what makes a stable keyed + command (`warden access whynot-design-npm-publish`) resolve deterministically + regardless of keyword collisions with other lanes. + """ + exact = self.get(query.strip()) + if exact is not None and (include_draft or exact.is_active): + return [exact] tokens = [t for t in query.lower().replace("-", " ").split() if t] pool = self.listed(include_draft=include_draft) scored = [(e.match_score(tokens), e) for e in pool] diff --git a/src/warden/routing/models.py b/src/warden/routing/models.py index 06a8c9a..7a8c21b 100644 --- a/src/warden/routing/models.py +++ b/src/warden/routing/models.py @@ -53,6 +53,21 @@ class RouteEntry: """True when structured assist fields are present (advisory richness).""" return any((self.auth_method, self.path_template, self.fetch_command)) + @property + def resolvable(self) -> bool: + """True when `warden access --fetch` can run this lane with no further input. + + A resolvable lane is active, exec_capable, and its fetch command (with the path + inlined) carries no unresolved ``<...>`` placeholder. Template lanes — like the + generic ``openbao-api-key`` or the ````-parameterized login — are *not* + resolvable until an owner ships concrete names. Lets an automated caller know + whether ``--fetch`` will work *before* attempting it (whynot-design request). + """ + if not (self.is_active and self.exec_capable and self.fetch_command): + return False + blob = f"{self.fetch_command} {self.path_template or ''}" + return "<" not in blob and ">" not in blob + def match_score(self, tokens: List[str]) -> int: """Keyword-overlap score against need_keywords, title, and id. diff --git a/tests/test_access.py b/tests/test_access.py index b9d7c21..e85d70b 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -107,5 +107,5 @@ def test_access_ssh_lane_points_to_sign(monkeypatch): def test_access_no_match_exits_nonzero(monkeypatch): monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(_repo_catalog())) - r = runner.invoke(app, ["access", "zzzz-no-such-need-xyzzy"]) + r = runner.invoke(app, ["access", "zzzz qqqq xyzzy"]) assert r.exit_code == 1 diff --git a/tests/test_routing.py b/tests/test_routing.py index 13624bb..91f1dda 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -76,6 +76,29 @@ def test_real_catalog_has_one_executed_lane(): assert [e.id for e in executed] == ["ssh-cert-host-access"] +def test_whynot_design_npm_lane_is_concrete_and_resolvable(): + """The provisioned npm publish lane has no placeholders and reports resolvable.""" + catalog = load_catalog(_repo_catalog()) + e = catalog.get("whynot-design-npm-publish") + assert e is not None and e.is_active and e.exec_capable + assert e.resolvable is True + assert "<" not in e.fetch_command and ">" not in e.fetch_command + assert "platform/workloads/coulomb/whynot-design/npm-publish" in e.fetch_command + + +def test_generic_and_template_lanes_not_resolvable(): + catalog = load_catalog(_repo_catalog()) + # generic openbao lane has /; login lane has . + assert catalog.get("openbao-api-key").resolvable is False + assert catalog.get("key-cape-oidc-login").resolvable is False + + +def test_find_exact_id_wins_over_keyword_collision(): + catalog = load_catalog(_repo_catalog()) + # "npm" alone collides with openbao-api-key; the exact id must resolve uniquely. + assert catalog.find("whynot-design-npm-publish", limit=1)[0].id == "whynot-design-npm-publish" + + def test_no_double_source_rule_rejects_routed_steps(tmp_path): bad = dict(ROUTED_ENTRY) bad["steps"] = ["do a thing on OpenBao"] # non-SSH entry must not carry steps diff --git a/wiki/playbooks/whynot-design-npm-publish.md b/wiki/playbooks/whynot-design-npm-publish.md new file mode 100644 index 0000000..7f116e2 --- /dev/null +++ b/wiki/playbooks/whynot-design-npm-publish.md @@ -0,0 +1,75 @@ +# whynot-design npm publish token + +Date: 2026-06-29 +Catalog: `whynot-design-npm-publish` (status `active`, `resolvable: true`) +Owner: `railiance-platform` (OpenBao) · provisioning CCR-2026-0001 (commit 8f617fc) + +The `NPM_AUTH_TOKEN` that publishes `@whynot/design` to the coulomb Gitea npm registry +(`https://gitea.coulomb.social/api/packages/coulomb/npm/`). ops-warden **does not hold +this token** — it is the access front door: `warden access` proxies the read from OpenBao +**as the caller** and never persists, caches, or logs the value. + +--- + +## Owner-confirmed lane (no placeholders) + +| Field | Value | +| --- | --- | +| OpenBao path | `platform/workloads/coulomb/whynot-design/npm-publish` | +| Field | `NPM_AUTH_TOKEN` | +| KV mount | `platform` | +| Read policy | `workload-kv-read-whynot-design-npm-publish` | +| OIDC login | `bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read` | +| Bound group | `whynot-design` | +| flex-auth ref | `secret.read:whynot-design` (if tenant policy requires pre-approval) | +| Runbook (owner) | `railiance-platform/docs/workload-kv-access-lanes.md` | + +> The `platform/workloads/whynot-design/whynot-design/npm-publish` path from early in the +> provisioning thread is **superseded** — the live path is under the `coulomb` tenant. + +--- + +## Worker checklist + +1. **Authenticate as yourself** (you need your own identity; ops-warden adds none): + ```bash + bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read + ``` + Your token must carry the `whynot-design` group bound claim; a non-whynot identity is + denied by policy (verified negative case). + +2. **Fetch or run via the front door** — keyed by the stable catalog id, zero placeholders: + ```bash + warden access whynot-design-npm-publish --fetch # stream the token to you + warden access whynot-design-npm-publish --exec -- npm publish # inject into the child only + ``` + The value transits to you (or the child env) and never enters ops-warden's memory, disk, + or audit log (metadata-only audit). + +3. **Readiness gate (for automated callers).** Before attempting `--fetch`, check the flag: + ```bash + warden route show whynot-design-npm-publish --json | jq .resolvable # true + ``` + `resolvable: true` means the lane is concrete and `--fetch` will run; a template lane + reports `false`. + +4. **Publish is outward-facing and immutable.** `npm publish` is irreversible and public. + Even once the token resolves, hold for an explicit operator "yes, publish" — do not + auto-run it from an agent. + +--- + +## Scopes + +This lane is the **publish** token only. A separate **read/install** token (for consumers +of `@whynot/design`) is a distinct need and would be its own catalog id +(`whynot-design-npm-read`) once railiance-platform provisions it — do not conflate them. + +--- + +## See also + +- `wiki/OperatorAccessAssist.md` — the `warden access` front door + guardrails +- `wiki/CredentialRouting.md` — routing model +- `railiance-platform/docs/workload-kv-access-lanes.md`, + `workplans/RAILIANCE-WP-0006-workload-kv-access-lanes.md` diff --git a/workplans/WARDEN-WP-0017-access-front-door-discoverability.md b/workplans/WARDEN-WP-0017-access-front-door-discoverability.md index fcf85c2..fe9c5b1 100644 --- a/workplans/WARDEN-WP-0017-access-front-door-discoverability.md +++ b/workplans/WARDEN-WP-0017-access-front-door-discoverability.md @@ -11,6 +11,7 @@ planning_priority: high planning_order: 17 created: "2026-06-27" updated: "2026-06-27" +state_hub_workstream_id: "cf8b392e-7624-4585-8935-a85e29202935" --- # WARDEN-WP-0017 — Access front-door discoverability @@ -55,6 +56,7 @@ tracked separately); any new fetch capability (the proxy already exists). id: WARDEN-WP-0017-T01 status: done priority: high +state_hub_task_id: "6e98df42-b5b4-49f8-a444-3c6346c8abd7" ``` - [x] `warden route` table: three-valued `warden` column — `issue` / `assist` @@ -73,6 +75,7 @@ priority: high id: WARDEN-WP-0017-T02 status: done priority: high +state_hub_task_id: "6e2a7067-1afc-4f38-8d99-4d5c36a4661c" ``` - [x] `.claude/rules/credential-routing.md`: reframed the lead ("issues SSH certs **and** @@ -88,6 +91,7 @@ priority: high id: WARDEN-WP-0017-T03 status: done priority: medium +state_hub_task_id: "7199625b-e78e-4495-8ca0-076100ae9f08" ``` - [x] Registered the State Hub capability "Operator access front door (caller-identity diff --git a/workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md b/workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md new file mode 100644 index 0000000..8679b68 --- /dev/null +++ b/workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md @@ -0,0 +1,99 @@ +--- +id: WARDEN-WP-0018 +type: workplan +title: "Activate whynot-design npm publish lane + resolvable readiness flag" +domain: infotech +repo: ops-warden +status: finished +owner: claude +topic_slug: custodian +planning_priority: high +planning_order: 18 +created: "2026-06-29" +updated: "2026-06-29" +--- + +# WARDEN-WP-0018 — whynot-design npm lane activation + `resolvable` flag + +**Trigger:** railiance-platform completed provisioning the whynot-design npm publish lane +(CCR-2026-0001, commit 8f617fc): `status=active`, `access_frontdoor.readiness=ready`, +`resolvable=true`, positive fetch passed + negative (non-whynot) login denied. They asked +ops-warden to activate the dedicated catalog selector and notify whynot-design. This is the +first concrete `warden access --fetch`-resolvable non-SSH lane — the end-to-end proof of the +WP-0014 conduit + WP-0017 discoverability work. + +**whynot-design's spec** (msg 2687dc31) drove the shape: zero-placeholder command keyed by a +stable id, owner-confirmed concrete path/field/role, a machine-readable readiness flag, and a +publish-vs-read scope split. + +**Boundary unchanged:** ops-warden holds no token; the lane proxies the read as the caller. + +--- + +## Tasks + +### T1 — Concrete catalog entry + playbook + +```task +id: WARDEN-WP-0018-T01 +status: done +priority: high +``` + +- [x] Added `whynot-design-npm-publish` to `registry/routing/catalog.yaml` (`status: active`, + `exec_capable`, `lane: secret`) with the **owner-confirmed, zero-placeholder** handoff: + path `platform/workloads/coulomb/whynot-design/npm-publish` (the superseded + `whynot-design/whynot-design/…` form is **not** used), field `NPM_AUTH_TOKEN`, OIDC + `bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read`, policy + `workload-kv-read-whynot-design-npm-publish`, flex-auth `secret.read:whynot-design`. +- [x] `wiki/playbooks/whynot-design-npm-publish.md` — worker checklist, scopes, operator + go-ahead note (publish is immutable + outward-facing). Catalog `wiki_ref` points to it. +- [x] Passes the `_assert_no_secret_material` guard (templates/identifiers only, no value). + +### T2 — `resolvable` readiness flag + stable-id resolution + +```task +id: WARDEN-WP-0018-T02 +status: done +priority: high +``` + +- [x] `RouteEntry.resolvable` — true when a lane is active, exec_capable, and its fetch + command/path carry **no** unresolved `<…>` placeholder. Surfaced in the route/access + `--json` (`_entry_summary`). Generic `openbao-api-key` and the `` login lane + report `false`; `whynot-design-npm-publish` reports `true`. +- [x] `Catalog.find` now resolves an **exact catalog-id** match first, so + `warden access whynot-design-npm-publish …` is deterministic regardless of keyword + collisions (whynot-design's "stable keyed command"). +- [x] Tests: `tests/test_routing.py` (concrete+resolvable lane, template lanes not + resolvable, exact-id wins); fixed a `test_access` no-match query that incidentally + substring-collided (`no` ⊂ `whynot`). 213 pass, lint clean. + +### T3 — Close the loop + +```task +id: WARDEN-WP-0018-T03 +status: done +priority: medium +``` + +- [x] Notified whynot-design (reply 744977ae) with the zero-placeholder command + `warden access whynot-design-npm-publish --exec -- npm publish`, the `resolvable` gate, + the coulomb-tenant path correction, and the operator-go-ahead reminder. +- [x] Confirmed activation to railiance-platform (reply f76d3a9e). Sibling lanes + (`issue-core-ingestion-api-key`, `openrouter-llm-connect`) stay `draft` per their + deferral, pending CCR-2026-0002/0003 provisioning. + +--- + +## Acceptance + +- `warden access whynot-design-npm-publish` resolves to a concrete, owner-confirmed, + zero-placeholder lane; `--json` reports `resolvable: true`. +- Template/generic lanes report `resolvable: false`; exact-id lookup is deterministic. +- No secret value in catalog, playbook, tests, or logs; ops-warden holds nothing. + +## See also + +- `WARDEN-WP-0014` (proxy lane), `WARDEN-WP-0017` (discoverability) +- railiance-platform CCR-2026-0001, `docs/workload-kv-access-lanes.md`