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:
@@ -23,6 +23,11 @@ app = typer.Typer(
|
||||
)
|
||||
inventory_app = typer.Typer(help="Manage principals inventory", no_args_is_help=True)
|
||||
app.add_typer(inventory_app, name="inventory")
|
||||
route_app = typer.Typer(
|
||||
help="Look up which subsystem owns a credential need (read-only pointer layer)",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
app.add_typer(route_app, name="route")
|
||||
|
||||
console = Console()
|
||||
err = Console(stderr=True)
|
||||
@@ -512,3 +517,143 @@ def log(
|
||||
e.get("backend", ""),
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# warden route — read-only routing lookup over the pointer catalog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_catalog():
|
||||
from warden.routing import CatalogError, load_catalog
|
||||
try:
|
||||
return load_catalog()
|
||||
except CatalogError as e:
|
||||
err.print(f"[red]Routing catalog error:[/red] {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _entry_summary(entry) -> dict:
|
||||
"""Pointer-only summary. Never includes secret material."""
|
||||
return {
|
||||
"id": entry.id,
|
||||
"title": entry.title,
|
||||
"owner_repo": entry.owner_repo,
|
||||
"subsystem": entry.subsystem,
|
||||
"warden_executes": entry.warden_executes,
|
||||
"wiki_ref": entry.wiki_ref,
|
||||
"canon_ref": entry.canon_ref,
|
||||
"reviewed": entry.reviewed,
|
||||
"status": entry.status,
|
||||
}
|
||||
|
||||
|
||||
def _print_entry_table(entries, title: str) -> None:
|
||||
table = Table(title=title)
|
||||
table.add_column("ID")
|
||||
table.add_column("Need")
|
||||
table.add_column("Owner")
|
||||
table.add_column("warden")
|
||||
table.add_column("Status")
|
||||
for e in entries:
|
||||
executes = "[green]issue[/green]" if e.warden_executes else "route"
|
||||
status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]"
|
||||
table.add_row(e.id, e.title, e.owner_repo, executes, status_styled)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@route_app.command("list")
|
||||
def route_list(
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
all_entries: Annotated[bool, typer.Option("--all", help="Include draft entries")] = False,
|
||||
tag: Annotated[Optional[str], typer.Option("--tag", help="Filter by need keyword")] = None,
|
||||
) -> None:
|
||||
"""List routing scenarios. Active-only unless --all."""
|
||||
catalog = _load_catalog()
|
||||
entries = catalog.listed(include_draft=all_entries)
|
||||
if tag:
|
||||
t = tag.lower()
|
||||
entries = [e for e in entries if t in [k.lower() for k in e.need_keywords]]
|
||||
|
||||
if output_json:
|
||||
print(json.dumps([_entry_summary(e) for e in entries], indent=2))
|
||||
return
|
||||
|
||||
if not entries:
|
||||
console.print("No matching routing entries.")
|
||||
return
|
||||
_print_entry_table(entries, "Routing scenarios")
|
||||
|
||||
|
||||
@route_app.command("show")
|
||||
def route_show(
|
||||
entry_id: Annotated[str, typer.Argument(help="Catalog entry id (see `warden route list`)")],
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Show owner, pointers, and (SSH only) the authored steps for one scenario."""
|
||||
catalog = _load_catalog()
|
||||
entry = catalog.get(entry_id)
|
||||
if entry is None:
|
||||
err.print(
|
||||
f"[red]Unknown routing id {entry_id!r}.[/red] "
|
||||
f"Try: warden route find {entry_id!r}"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output_json:
|
||||
summary = _entry_summary(entry)
|
||||
summary["need_keywords"] = entry.need_keywords
|
||||
if entry.warden_executes:
|
||||
summary["steps"] = entry.steps
|
||||
summary["cert_command"] = entry.cert_command
|
||||
else:
|
||||
summary["next_action"] = (
|
||||
f"next action on `{entry.owner_repo}` — see `{entry.wiki_ref}`"
|
||||
)
|
||||
print(json.dumps(summary, indent=2))
|
||||
return
|
||||
|
||||
console.print(f"[bold]{entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
|
||||
console.print(f" owner : {entry.owner_repo} ({entry.subsystem})")
|
||||
console.print(f" wiki : {entry.wiki_ref}")
|
||||
console.print(f" canon : {entry.canon_ref}")
|
||||
console.print(f" reviewed : {entry.reviewed} status: {entry.status}")
|
||||
|
||||
if entry.warden_executes:
|
||||
console.print("\n[green]ops-warden issues this directly.[/green]")
|
||||
console.print(f" cert_command: [bold]{entry.cert_command}[/bold]")
|
||||
if entry.steps:
|
||||
console.print(" steps:")
|
||||
for i, step in enumerate(entry.steps, 1):
|
||||
console.print(f" {i}. {step}")
|
||||
console.print(
|
||||
" precondition: actor in inventory? backend configured? run `warden status`."
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"\n[yellow]ops-warden does not issue this.[/yellow] "
|
||||
f"Next action on [bold]{entry.owner_repo}[/bold] — see {entry.wiki_ref}."
|
||||
)
|
||||
|
||||
|
||||
@route_app.command("find")
|
||||
def route_find(
|
||||
query: Annotated[str, typer.Argument(help="Free-text need, e.g. 'issue core api key'")],
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
all_entries: Annotated[bool, typer.Option("--all", help="Include draft entries")] = False,
|
||||
limit: Annotated[int, typer.Option("--limit", help="Max matches")] = 5,
|
||||
) -> None:
|
||||
"""Rank routing scenarios by keyword overlap with the query."""
|
||||
catalog = _load_catalog()
|
||||
matches = catalog.find(query, include_draft=all_entries, limit=limit)
|
||||
|
||||
if output_json:
|
||||
print(json.dumps([_entry_summary(e) for e in matches], indent=2))
|
||||
return
|
||||
|
||||
if not matches:
|
||||
console.print(
|
||||
f"No routing match for {query!r}. "
|
||||
"Try `warden route list --all` to browse all scenarios."
|
||||
)
|
||||
return
|
||||
_print_entry_table(matches, f"Matches for {query!r}")
|
||||
|
||||
Reference in New Issue
Block a user