feat(WP-0011): warden route lookup CLI over the pointer catalog
Add a read-only `warden route` command group (list/show/find) that reads
registry/routing/catalog.yaml and tells a worker which subsystem owns a need
and which wiki/canon doc to follow. ops-warden still executes exactly one lane
(SSH); routed entries return a pointer and never call any subsystem.
- src/warden/routing/: models.py + catalog.py loader; enforces the
no-double-source rule (non-SSH entries with steps/cert_command fail validation),
dup-id and schema checks.
- route list (active-only unless --all, --tag), route show (SSH appends steps +
cert pattern; routed ends with "next action on <owner> — see <wiki_ref>"),
route find (keyword ranking, --json).
- tests/test_routing.py: load/validation, find ranking, CLI JSON shapes, plus a
drift guard (every wiki_ref anchor resolves; every entry has a reviewed date).
- Docs: wiki/AccessRouting.md CLI section, README quick reference, SCOPE A3 -> A4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:03:24 +02:00
|
|
|
"""Data model for routing catalog entries.
|
|
|
|
|
|
|
|
|
|
A `RouteEntry` is a pointer: it names the owner and the authoritative doc for a
|
|
|
|
|
credential need. Only the SSH lane (`warden_executes: true`) may carry an authored
|
|
|
|
|
`steps` block and a `cert_command` pattern — every other entry is identifiers and
|
|
|
|
|
pointers only (the no-double-source rule, enforced in `catalog.py`).
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 14:40:30 +02:00
|
|
|
@dataclass
|
|
|
|
|
class RotationGuide:
|
|
|
|
|
"""Structured-but-advisory renewal guidance for a lane (WARDEN-WP-0026 T06).
|
|
|
|
|
|
|
|
|
|
Held in the ops-warden registry, never in OpenBao. ``steps`` are authored
|
|
|
|
|
advisory prose (screened for secret material like every catalog string) — they
|
|
|
|
|
tell an operator *how* to renew, they are not executed here. ``method`` is
|
|
|
|
|
``rotate`` (provider re-mints the same kind of credential) or ``re-establish``
|
|
|
|
|
(regenerate from source, e.g. a new age keypair + re-encrypt). ``automatable``
|
|
|
|
|
is a hint for a future Strand-B executable driver (WARDEN-WP-0027).
|
|
|
|
|
"""
|
|
|
|
|
method: str # "rotate" | "re-establish"
|
|
|
|
|
steps: List[str]
|
|
|
|
|
owner: str
|
|
|
|
|
automatable: bool = False
|
|
|
|
|
|
|
|
|
|
|
feat(WP-0011): warden route lookup CLI over the pointer catalog
Add a read-only `warden route` command group (list/show/find) that reads
registry/routing/catalog.yaml and tells a worker which subsystem owns a need
and which wiki/canon doc to follow. ops-warden still executes exactly one lane
(SSH); routed entries return a pointer and never call any subsystem.
- src/warden/routing/: models.py + catalog.py loader; enforces the
no-double-source rule (non-SSH entries with steps/cert_command fail validation),
dup-id and schema checks.
- route list (active-only unless --all, --tag), route show (SSH appends steps +
cert pattern; routed ends with "next action on <owner> — see <wiki_ref>"),
route find (keyword ranking, --json).
- tests/test_routing.py: load/validation, find ranking, CLI JSON shapes, plus a
drift guard (every wiki_ref anchor resolves; every entry has a reviewed date).
- Docs: wiki/AccessRouting.md CLI section, README quick reference, SCOPE A3 -> A4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:03:24 +02:00
|
|
|
@dataclass
|
|
|
|
|
class RouteEntry:
|
|
|
|
|
id: str
|
|
|
|
|
title: str
|
|
|
|
|
need_keywords: List[str]
|
|
|
|
|
owner_repo: str
|
|
|
|
|
subsystem: str
|
|
|
|
|
warden_executes: bool
|
|
|
|
|
wiki_ref: str
|
|
|
|
|
canon_ref: str
|
|
|
|
|
reviewed: str
|
|
|
|
|
status: str # "active" | "draft"
|
|
|
|
|
# SSH lane only — None/empty for routed (non-executed) needs.
|
|
|
|
|
steps: List[str] = field(default_factory=list)
|
|
|
|
|
cert_command: Optional[str] = None
|
2026-06-27 16:00:56 +02:00
|
|
|
# Structured handoff (WP-0014) — optional, allowed on any lane. These are
|
|
|
|
|
# *templates and pointers* the `warden access` assist layer renders (and, for
|
|
|
|
|
# exec_capable lanes, proxies). They are NOT authored procedure prose and they
|
|
|
|
|
# never carry a secret value — only placeholders (`<...>`) and field names.
|
|
|
|
|
# Validation in catalog.py enforces the no-secret-material rule on every one.
|
|
|
|
|
auth_method: Optional[str] = None # how the caller authenticates to the owner
|
|
|
|
|
path_template: Optional[str] = None # owner-side path with `<...>` placeholders
|
|
|
|
|
fetch_command: Optional[str] = None # command skeleton run *as the caller*
|
|
|
|
|
exec_capable: bool = False # may `warden access --fetch/--exec` proxy it
|
|
|
|
|
policy_ref: Optional[str] = None # flex-auth check the fetch path runs first
|
2026-06-27 17:31:55 +02:00
|
|
|
# Proxy lane semantics (WP-0014 T4):
|
|
|
|
|
# "secret" — read a value (gated by flex-auth secret-read; caller must already
|
|
|
|
|
# be authenticated; value transits via inherit-stdout or child env).
|
|
|
|
|
# "login" — interactive auth bootstrap (OIDC/MFA). No secret-read gate (you have
|
|
|
|
|
# no identity yet), no caller-auth precheck (the point is to get one),
|
|
|
|
|
# run interactively as the caller; warden never captures the token.
|
|
|
|
|
lane: str = "secret"
|
2026-06-29 17:41:49 +02:00
|
|
|
# Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem
|
|
|
|
|
# (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the
|
|
|
|
|
# catalog routes to it and keeps ops-warden's own --fetch/--exec proxy as a transparent
|
|
|
|
|
# fallback (route-primary, proxy-fallback). Pointers/templates only — never a value.
|
|
|
|
|
exec_owner: Optional[str] = None # subsystem owning the native exec (e.g. secrets-engine)
|
|
|
|
|
exec_command: Optional[str] = None # e.g. "secrets-engine exec --catalog <id> -- <cmd>"
|
|
|
|
|
pointer_command: Optional[str] = None # e.g. "secrets-engine route <id> --json"
|
2026-07-16 14:40:30 +02:00
|
|
|
# Rotation / re-establishment guidance (WP-0026 T06) — advisory, no secret values.
|
|
|
|
|
rotation: Optional[RotationGuide] = None
|
feat(WP-0011): warden route lookup CLI over the pointer catalog
Add a read-only `warden route` command group (list/show/find) that reads
registry/routing/catalog.yaml and tells a worker which subsystem owns a need
and which wiki/canon doc to follow. ops-warden still executes exactly one lane
(SSH); routed entries return a pointer and never call any subsystem.
- src/warden/routing/: models.py + catalog.py loader; enforces the
no-double-source rule (non-SSH entries with steps/cert_command fail validation),
dup-id and schema checks.
- route list (active-only unless --all, --tag), route show (SSH appends steps +
cert pattern; routed ends with "next action on <owner> — see <wiki_ref>"),
route find (keyword ranking, --json).
- tests/test_routing.py: load/validation, find ranking, CLI JSON shapes, plus a
drift guard (every wiki_ref anchor resolves; every entry has a reviewed date).
- Docs: wiki/AccessRouting.md CLI section, README quick reference, SCOPE A3 -> A4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:03:24 +02:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_active(self) -> bool:
|
|
|
|
|
return self.status == "active"
|
|
|
|
|
|
2026-07-16 14:40:30 +02:00
|
|
|
@property
|
|
|
|
|
def has_rotation(self) -> bool:
|
|
|
|
|
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
|
|
|
|
return self.rotation is not None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def vends_secret(self) -> bool:
|
|
|
|
|
"""True when this lane hands back a rotatable static secret value.
|
|
|
|
|
|
|
|
|
|
Rotation guidance (WP-0026 T06) applies to these. It excludes the SSH lane
|
|
|
|
|
(short-lived certs — renewal is re-issuance), ``login`` lanes (re-auth, no
|
|
|
|
|
stored value), and pure routing pointers with no secret path (tunnel,
|
|
|
|
|
principals, emission sinks, policy checks).
|
|
|
|
|
"""
|
|
|
|
|
if self.warden_executes or self.lane != "secret":
|
|
|
|
|
return False
|
|
|
|
|
return bool(self.path_template or self.fetch_command or self.exec_owner)
|
|
|
|
|
|
2026-06-29 17:41:49 +02:00
|
|
|
@property
|
|
|
|
|
def has_native_exec(self) -> bool:
|
|
|
|
|
"""True when an owner-native exec front door is the primary path for this lane."""
|
|
|
|
|
return bool(self.exec_owner and self.exec_command)
|
|
|
|
|
|
2026-06-27 16:00:56 +02:00
|
|
|
@property
|
|
|
|
|
def has_handoff(self) -> bool:
|
|
|
|
|
"""True when structured assist fields are present (advisory richness)."""
|
|
|
|
|
return any((self.auth_method, self.path_template, self.fetch_command))
|
|
|
|
|
|
feat(WARDEN-WP-0018): activate whynot-design npm publish lane + resolvable flag
railiance-platform finished provisioning the whynot-design npm publish lane
(CCR-2026-0001, commit 8f617fc: active, readiness=ready, resolvable=true, positive
fetch + negative denial verified). First concrete warden access --fetch-resolvable
non-SSH lane — end-to-end proof of the WP-0014 conduit + WP-0017 discoverability.
T1 — catalog entry whynot-design-npm-publish (active, exec_capable) 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 role whynot-design-workload-kv-read, policy + flex-auth ref. Added
wiki/playbooks/whynot-design-npm-publish.md.
T2 — RouteEntry.resolvable (active + exec_capable + no <…> placeholder), surfaced in
route/access --json; Catalog.find resolves an exact catalog-id first so
`warden access whynot-design-npm-publish` is deterministic. Tests added; fixed a
no-match test query that substring-collided (no ⊂ whynot). 213 pass, lint clean.
T3 — notified whynot-design (zero-placeholder command + resolvable gate + path
correction) and confirmed activation to railiance-platform. Sibling lanes stay draft
per their deferral.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 00:32:00 +02:00
|
|
|
@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 ``<domain>``-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
|
|
|
|
|
|
feat(WP-0011): warden route lookup CLI over the pointer catalog
Add a read-only `warden route` command group (list/show/find) that reads
registry/routing/catalog.yaml and tells a worker which subsystem owns a need
and which wiki/canon doc to follow. ops-warden still executes exactly one lane
(SSH); routed entries return a pointer and never call any subsystem.
- src/warden/routing/: models.py + catalog.py loader; enforces the
no-double-source rule (non-SSH entries with steps/cert_command fail validation),
dup-id and schema checks.
- route list (active-only unless --all, --tag), route show (SSH appends steps +
cert pattern; routed ends with "next action on <owner> — see <wiki_ref>"),
route find (keyword ranking, --json).
- tests/test_routing.py: load/validation, find ranking, CLI JSON shapes, plus a
drift guard (every wiki_ref anchor resolves; every entry has a reviewed date).
- Docs: wiki/AccessRouting.md CLI section, README quick reference, SCOPE A3 -> A4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:03:24 +02:00
|
|
|
def match_score(self, tokens: List[str]) -> int:
|
|
|
|
|
"""Keyword-overlap score against need_keywords, title, and id.
|
|
|
|
|
|
|
|
|
|
Pure ranking helper — no I/O, no external calls.
|
|
|
|
|
"""
|
|
|
|
|
haystack = set(k.lower() for k in self.need_keywords)
|
|
|
|
|
haystack.update(self.id.lower().replace("-", " ").split())
|
|
|
|
|
haystack.update(self.title.lower().replace("-", " ").split())
|
|
|
|
|
score = 0
|
|
|
|
|
for tok in tokens:
|
|
|
|
|
t = tok.lower()
|
|
|
|
|
if t in haystack:
|
|
|
|
|
score += 2
|
|
|
|
|
elif any(t in h or h in t for h in haystack):
|
|
|
|
|
score += 1
|
|
|
|
|
return score
|