generated from coulomb/repo-seed
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>
This commit is contained in:
17
src/warden/routing/__init__.py
Normal file
17
src/warden/routing/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Routing lookup — read-only pointer layer over registry/routing/catalog.yaml.
|
||||
|
||||
This package never calls OpenBao, flex-auth, key-cape, ops-bridge, or any other
|
||||
subsystem. It loads the machine-readable routing catalog and answers "who owns
|
||||
this need and where is the authoritative doc". The one lane ops-warden executes
|
||||
(SSH certificate issuance) is the only entry that carries authored steps.
|
||||
"""
|
||||
from warden.routing.catalog import Catalog, CatalogError, find_catalog_path, load_catalog
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
__all__ = [
|
||||
"Catalog",
|
||||
"CatalogError",
|
||||
"RouteEntry",
|
||||
"find_catalog_path",
|
||||
"load_catalog",
|
||||
]
|
||||
171
src/warden/routing/catalog.py
Normal file
171
src/warden/routing/catalog.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Load and validate the routing pointer catalog.
|
||||
|
||||
The catalog lives at ``registry/routing/catalog.yaml`` in the repo root. Resolution
|
||||
order:
|
||||
|
||||
1. ``WARDEN_ROUTING_CATALOG`` env var, if set (used by tests / overrides).
|
||||
2. Walk upward from this module looking for ``registry/routing/catalog.yaml``.
|
||||
|
||||
Validation enforces the **no-double-source rule**: only ``warden_executes: true``
|
||||
entries may carry an authored ``steps`` block or a ``cert_command``. Any non-SSH
|
||||
entry that does so is a validation error — ops-warden points at the owner's doc, it
|
||||
never restates another subsystem's procedure.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
_REQUIRED_FIELDS = (
|
||||
"id",
|
||||
"title",
|
||||
"need_keywords",
|
||||
"owner_repo",
|
||||
"subsystem",
|
||||
"warden_executes",
|
||||
"wiki_ref",
|
||||
"canon_ref",
|
||||
"reviewed",
|
||||
"status",
|
||||
)
|
||||
_VALID_STATUS = ("active", "draft")
|
||||
|
||||
|
||||
class CatalogError(Exception):
|
||||
"""Raised when the routing catalog is missing or invalid."""
|
||||
|
||||
|
||||
def find_catalog_path(start: Optional[Path] = None) -> Path:
|
||||
"""Locate registry/routing/catalog.yaml.
|
||||
|
||||
Honors WARDEN_ROUTING_CATALOG first; otherwise walks up from `start`
|
||||
(default: this module) until a repo root containing the catalog is found.
|
||||
"""
|
||||
override = os.environ.get("WARDEN_ROUTING_CATALOG")
|
||||
if override:
|
||||
return Path(os.path.expanduser(override))
|
||||
|
||||
rel = Path("registry") / "routing" / "catalog.yaml"
|
||||
here = (start or Path(__file__)).resolve()
|
||||
for parent in [here, *here.parents]:
|
||||
candidate = parent / rel
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
raise CatalogError(
|
||||
f"Routing catalog not found ({rel}). Set WARDEN_ROUTING_CATALOG to override."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Catalog:
|
||||
path: Path
|
||||
entries: List[RouteEntry]
|
||||
|
||||
# --- lookup helpers ---------------------------------------------------
|
||||
|
||||
def get(self, entry_id: str) -> Optional[RouteEntry]:
|
||||
for e in self.entries:
|
||||
if e.id == entry_id:
|
||||
return e
|
||||
return None
|
||||
|
||||
def listed(self, include_draft: bool = False) -> List[RouteEntry]:
|
||||
if include_draft:
|
||||
return list(self.entries)
|
||||
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."""
|
||||
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]
|
||||
scored = [(s, e) for s, e in scored if s > 0]
|
||||
scored.sort(key=lambda pair: (-pair[0], pair[1].id))
|
||||
return [e for _, e in scored[:limit]]
|
||||
|
||||
|
||||
def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(f"entry #{index} is not a mapping")
|
||||
|
||||
missing = [f for f in _REQUIRED_FIELDS if f not in raw]
|
||||
if missing:
|
||||
ident = raw.get("id", f"#{index}")
|
||||
raise CatalogError(f"entry {ident!r} missing required field(s): {', '.join(missing)}")
|
||||
|
||||
warden_executes = bool(raw["warden_executes"])
|
||||
steps = raw.get("steps") or []
|
||||
cert_command = raw.get("cert_command")
|
||||
status = str(raw["status"])
|
||||
|
||||
if status not in _VALID_STATUS:
|
||||
raise CatalogError(
|
||||
f"entry {raw['id']!r} has invalid status {status!r} (expected one of {_VALID_STATUS})"
|
||||
)
|
||||
|
||||
# No-double-source rule: authored procedure only on the SSH lane.
|
||||
if not warden_executes and steps:
|
||||
raise CatalogError(
|
||||
f"entry {raw['id']!r} is not warden_executes but carries a `steps` block "
|
||||
"— routed needs point at the owner's doc; they must not restate procedure "
|
||||
"(no-double-source rule)."
|
||||
)
|
||||
if not warden_executes and cert_command:
|
||||
raise CatalogError(
|
||||
f"entry {raw['id']!r} is not warden_executes but carries a `cert_command`."
|
||||
)
|
||||
|
||||
if not isinstance(raw["need_keywords"], list):
|
||||
raise CatalogError(f"entry {raw['id']!r} need_keywords must be a list")
|
||||
|
||||
return RouteEntry(
|
||||
id=str(raw["id"]),
|
||||
title=str(raw["title"]),
|
||||
need_keywords=[str(k) for k in raw["need_keywords"]],
|
||||
owner_repo=str(raw["owner_repo"]),
|
||||
subsystem=str(raw["subsystem"]),
|
||||
warden_executes=warden_executes,
|
||||
wiki_ref=str(raw["wiki_ref"]),
|
||||
canon_ref=str(raw["canon_ref"]),
|
||||
reviewed=str(raw["reviewed"]),
|
||||
status=status,
|
||||
steps=[str(s) for s in steps],
|
||||
cert_command=str(cert_command) if cert_command else None,
|
||||
)
|
||||
|
||||
|
||||
def load_catalog(path: Optional[Path] = None) -> Catalog:
|
||||
"""Load, parse, and validate the routing catalog."""
|
||||
catalog_path = path or find_catalog_path()
|
||||
if not catalog_path.exists():
|
||||
raise CatalogError(f"Routing catalog not found: {catalog_path}")
|
||||
|
||||
try:
|
||||
with catalog_path.open() as f:
|
||||
raw = yaml.safe_load(f)
|
||||
except yaml.YAMLError as e:
|
||||
raise CatalogError(f"Invalid YAML in {catalog_path}: {e}") from e
|
||||
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError("Catalog must be a YAML mapping")
|
||||
|
||||
raw_entries = raw.get("entries")
|
||||
if not isinstance(raw_entries, list) or not raw_entries:
|
||||
raise CatalogError("Catalog has no `entries` list")
|
||||
|
||||
entries: List[RouteEntry] = []
|
||||
seen: set[str] = set()
|
||||
for i, raw_entry in enumerate(raw_entries):
|
||||
entry = _parse_entry(raw_entry, i)
|
||||
if entry.id in seen:
|
||||
raise CatalogError(f"duplicate entry id: {entry.id!r}")
|
||||
seen.add(entry.id)
|
||||
entries.append(entry)
|
||||
|
||||
return Catalog(path=catalog_path, entries=entries)
|
||||
49
src/warden/routing/models.py
Normal file
49
src/warden/routing/models.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""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
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return self.status == "active"
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user