WARDEN-WP-0026 T06: rotation guidance registry + warden rotate-guide
- 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:
parent
ac09f21ad3
commit
c3eb59ea04
9 changed files with 385 additions and 12 deletions
|
|
@ -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'")],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue