WARDEN-WP-0026 T06: rotation guidance registry + warden rotate-guide
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

- routing model: RotationGuide (method rotate|re-establish, steps, owner,
  automatable), RouteEntry.rotation + has_rotation + vends_secret.
- catalog parser: validate rotation block; secret-material screen gains a
  prose-safe mode (high-entropy detector only) so authored steps aren't tripped
  by substrings like "s."/"exists.".
- CLI: `warden rotate-guide <id>` (human + --json); route show --json now
  carries has_rotation + rotation.
- scorecard: catalog_rotation_coverage — every active secret-vending lane must
  carry a rotation block (SSH/login/pointer lanes exempt). Promotion checklist
  criterion 9.
- data: rotation blocks for all 7 active vending lanes + the draft
  railiance-backup lane (re-establish: age keypair regen + re-encrypt).
- fix pre-existing collision: bare `npm` keyword on forgejo-admin -> forgejo-npm
  so "npm token" routes to the generic lane (restores test_access expectations).
- tests: rotation parse/coverage/prose-screen/CLI in tests/test_routing.py;
  scorecard count 6 -> 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-16 14:40:30 +02:00
parent ac09f21ad3
commit c3eb59ea04
9 changed files with 385 additions and 12 deletions

View file

@ -630,6 +630,21 @@ def _entry_summary(entry) -> dict:
"canon_ref": entry.canon_ref,
"reviewed": entry.reviewed,
"status": entry.status,
# Renewal guidance (WP-0026 T06) — advisory, no secret values. `has_rotation`
# lets a caller gate before asking for the full block via `warden rotate-guide`.
"has_rotation": entry.has_rotation,
**(
{
"rotation": {
"method": entry.rotation.method,
"owner": entry.rotation.owner,
"automatable": entry.rotation.automatable,
"steps": entry.rotation.steps,
}
}
if entry.has_rotation
else {}
),
}
@ -788,6 +803,63 @@ def route_show(
)
@app.command("rotate-guide")
def rotate_guide(
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 how to rotate or re-establish a lane's credential (WP-0026 T06).
Advisory renewal guidance held in the ops-warden registry never a secret
value, and ops-warden does not execute it (that is Strand B, WARDEN-WP-0027).
"""
catalog = _load_catalog()
entry = catalog.get(entry_id)
if entry is None:
err.print(
f"[red]Unknown routing id {entry_id!r}.[/red] Try: warden route find {entry_id!r}"
)
raise typer.Exit(1)
if not entry.has_rotation:
if output_json:
print(json.dumps({"id": entry.id, "has_rotation": False}, indent=2))
else:
err.print(
f"[yellow]No rotation guidance for {entry.id!r}.[/yellow] "
+ (
"This is the SSH lane — renewal is re-issuance (`warden sign`)."
if entry.warden_executes
else "Add a `rotation:` block to the catalog entry (WP-0026 T06)."
)
)
raise typer.Exit(0 if entry.warden_executes else 1)
rot = entry.rotation
if output_json:
print(json.dumps(
{
"id": entry.id,
"method": rot.method,
"owner": rot.owner,
"automatable": rot.automatable,
"steps": rot.steps,
},
indent=2,
))
return
console.print(f"[bold]Rotation guidance — {entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
console.print(f" method : {rot.method} owner: {rot.owner} automatable: {rot.automatable}")
console.print(" steps:")
for i, step in enumerate(rot.steps, 1):
console.print(f" {i}. {step}")
console.print(
"\n[dim]Advisory only — ops-warden holds no value and does not execute this "
"(one-command rotation is Strand B, WARDEN-WP-0027).[/dim]"
)
@route_app.command("find")
def route_find(
query: Annotated[str, typer.Argument(help="Free-text need, e.g. 'issue core api key'")],