ops-warden/src/warden/cli.py
tegwick 5149946a4c
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
WARDEN-WP-0029: implement plan front door, org posture, desk, freshness
Ship posture-aware access planning: organization_posture=build (axis C),
catalog freshness warnings, warden plan verdicts, localhost founder desk,
and playbook/agent guidance that retire /tmp file-drop patterns.

Compose route catalog + handoff rather than a second routing layer.
2026-07-18 16:59:37 +02:00

1995 lines
73 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""OpsWarden CLI."""
from __future__ import annotations
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Annotated, List, Optional
import typer
from rich.console import Console
from rich.table import Table
from warden.ca import CAError, LocalCA, parse_cert_metadata
from warden.config import ConfigError, WardenConfig, load_config
from warden.policy import check_sign_policy
from warden.inventory import ActorEntry, InventoryError, PrincipalsInventory, load_inventory, save_inventory
from warden.models import ActorType, CertSpec, DEFAULT_TTL_HOURS, validate_actor_name
from warden.scorecard import run_scorecard
app = typer.Typer(
help="OpsWarden — SSH CA and certificate lifecycle manager",
no_args_is_help=True,
)
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")
policy_app = typer.Typer(
help="Look up Workload Security Posture descriptors (read-only; env posture + maturity)",
no_args_is_help=True,
)
app.add_typer(policy_app, name="policy")
worker_app = typer.Typer(
help="Autonomous coordination worker (WP-0020; dry-run only until executor lands)",
no_args_is_help=True,
)
app.add_typer(worker_app, name="worker")
activity_app = typer.Typer(
help="Unified metadata-only audit view (WARDEN-WP-0022)",
no_args_is_help=True,
)
app.add_typer(activity_app, name="activity")
memory_app = typer.Typer(
help="Cross-runtime experiential memory via phase-memory (WARDEN-WP-0024)",
no_args_is_help=True,
)
app.add_typer(memory_app, name="memory")
console = Console()
err = Console(stderr=True)
@app.callback()
def _bootstrap_memory(ctx: typer.Context) -> None:
"""Implicitly load phase-memory for every warden command (opt-out: WARDEN_MEMORY=0)."""
if ctx.invoked_subcommand is None:
return
try:
from warden import memory as warden_memory
warden_memory.ensure_memory_context(implicit=True)
except Exception: # noqa: BLE001 — memory must never block warden commands
return
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _record_memory_episode(
*,
command: str,
outcome: str,
need: str = "",
route_id: str = "",
) -> None:
try:
from warden import memory as warden_memory
except ImportError:
return
if not warden_memory.enabled() or not warden_memory.memory_available():
return
try:
warden_memory.record_command_episode(
command=command,
outcome=outcome,
need=need,
route_id=route_id,
)
except RuntimeError:
return
def _load_cfg() -> WardenConfig:
try:
return load_config()
except ConfigError as e:
err.print(f"[red]Config error:[/red] {e}")
raise typer.Exit(1)
def _load_inventory(cfg: WardenConfig) -> PrincipalsInventory:
try:
return load_inventory(cfg.inventory_path)
except InventoryError as e:
err.print(f"[red]Inventory error:[/red] {e}")
raise typer.Exit(1)
def _get_ca(cfg: WardenConfig):
if cfg.backend == "vault":
from warden.vault import VaultCA
return VaultCA(cfg.vault, cfg.state_dir)
return LocalCA(cfg.ca_key, cfg.state_dir)
def _apply_policy_gate(cfg: WardenConfig, spec: CertSpec) -> None:
"""Run flex-auth check when policy.enabled; sets spec.policy_decision_id."""
decision_id = check_sign_policy(cfg.policy, spec)
if decision_id:
spec.policy_decision_id = decision_id
# ---------------------------------------------------------------------------
# warden sign
# ---------------------------------------------------------------------------
@app.command()
def sign(
actor_name: Annotated[str, typer.Argument(help="Actor name (e.g. agt-state-hub-bridge)")],
pubkey: Annotated[Path, typer.Option("--pubkey", help="Path to actor's public key file")],
ttl: Annotated[Optional[int], typer.Option("--ttl", help="Override TTL in hours")] = None,
) -> None:
"""Sign a public key for the given actor. Writes cert text to stdout.
This is the cert_command interface: ops-bridge calls this and uses stdout
as the certificate passed to SSH alongside the private key.
"""
cfg = _load_cfg()
inventory = _load_inventory(cfg)
entry = inventory.actors.get(actor_name)
if entry is None:
err.print(
f"[red]Actor {actor_name!r} not found in inventory.[/red] "
f"Add it with: warden inventory add"
)
raise typer.Exit(1)
spec = CertSpec(
actor_name=actor_name,
actor_type=entry.actor_type,
pubkey_path=pubkey,
ttl_hours=ttl or entry.ttl_hours,
principals=entry.principals,
identity=actor_name,
)
ca = _get_ca(cfg)
try:
_apply_policy_gate(cfg, spec)
record = ca.sign(spec)
except CAError as e:
err.print(f"[red]Signing failed:[/red] {e}")
raise typer.Exit(1)
# cert_command interface: write cert text to stdout only
print(record.cert_path.read_text().strip())
_record_memory_episode(
command="sign",
outcome="resolved",
need=f"ssh cert {actor_name}",
route_id="ops-warden-ssh-cert",
)
# ---------------------------------------------------------------------------
# warden issue
# ---------------------------------------------------------------------------
@app.command()
def issue(
actor_name: Annotated[str, typer.Argument(help="Actor name")],
ttl: Annotated[Optional[int], typer.Option("--ttl", help="Override TTL in hours")] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Generate a new keypair and sign it for the given actor.
Only supported with the local backend. Outputs keypair + cert paths and metadata.
"""
cfg = _load_cfg()
if cfg.backend != "local":
err.print("[red]warden issue is only supported with the local backend.[/red]")
raise typer.Exit(1)
inventory = _load_inventory(cfg)
entry = inventory.actors.get(actor_name)
if entry is None:
err.print(f"[red]Actor {actor_name!r} not found in inventory.[/red]")
raise typer.Exit(1)
ca = LocalCA(cfg.ca_key, cfg.state_dir)
try:
privkey_path, pubkey_path = ca.generate_keypair(actor_name)
except CAError as e:
err.print(f"[red]Key generation failed:[/red] {e}")
raise typer.Exit(1)
spec = CertSpec(
actor_name=actor_name,
actor_type=entry.actor_type,
pubkey_path=pubkey_path,
ttl_hours=ttl or entry.ttl_hours,
principals=entry.principals,
identity=actor_name,
)
try:
_apply_policy_gate(cfg, spec)
record = ca.sign(spec)
except CAError as e:
err.print(f"[red]Signing failed:[/red] {e}")
raise typer.Exit(1)
result = {
"actor": actor_name,
"privkey": str(privkey_path),
"cert": str(record.cert_path),
"identity": record.identity,
"principals": record.principals,
"valid_before": record.valid_before.isoformat(),
"signed_at": record.signed_at.isoformat(),
}
if output_json:
print(json.dumps(result, indent=2))
else:
console.print(f"[green]Issued credentials for {actor_name}[/green]")
for k, v in result.items():
console.print(f" {k}: {v}")
# ---------------------------------------------------------------------------
# warden status
# ---------------------------------------------------------------------------
@app.command()
def status(
actor_name: Annotated[Optional[str], typer.Argument(help="Actor name (omit for all)")] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
state_dir_override: Annotated[Optional[Path], typer.Option("--state-dir", help="State dir path (bypasses config)")] = None,
) -> None:
"""Show certificate status. Exits 1 if any cert is expired."""
now = datetime.now(timezone.utc)
if state_dir_override is not None:
state_dir = state_dir_override
else:
cfg = _load_cfg()
state_dir = cfg.state_dir
if actor_name:
cert_path = state_dir / f"{actor_name}-cert.pub"
paths = [cert_path] if cert_path.exists() else []
else:
paths = sorted(state_dir.glob("*-cert.pub")) if state_dir.exists() else []
if not paths:
msg = (
f"No certificate found for {actor_name!r} (static key / no cert)"
if actor_name
else "No certificates in state dir."
)
console.print(msg)
return
rows = []
for cert_path in paths:
name = cert_path.stem.replace("-cert", "")
try:
meta = parse_cert_metadata(cert_path)
valid_before = meta["valid_before"]
remaining = valid_before - now
secs = remaining.total_seconds()
if secs > 0:
h, rem = divmod(int(secs), 3600)
m = rem // 60
remaining_str = f"{h}h {m}m"
expired = False
else:
remaining_str = "EXPIRED"
expired = True
rows.append({
"actor": name,
"identity": meta["identity"],
"principals": ", ".join(meta["principals"]),
"valid_before": valid_before.isoformat(),
"remaining": remaining_str,
"expired": expired,
})
except Exception as e:
rows.append({"actor": name, "error": str(e), "expired": False})
if output_json:
print(json.dumps(rows, indent=2))
else:
table = Table(title="Certificate Status")
table.add_column("Actor")
table.add_column("Identity")
table.add_column("Principals")
table.add_column("Valid Before (UTC)")
table.add_column("Remaining")
for row in rows:
if "error" in row:
table.add_row(row["actor"], "[red]parse error[/red]", "", "", row["error"])
else:
rem_styled = (
f"[red]{row['remaining']}[/red]" if row["expired"] else row["remaining"]
)
table.add_row(
row["actor"],
row["identity"],
row["principals"],
row["valid_before"],
rem_styled,
)
console.print(table)
if any(r.get("expired") for r in rows):
raise typer.Exit(1)
# ---------------------------------------------------------------------------
# warden scorecard
# ---------------------------------------------------------------------------
@app.command()
def scorecard(
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Run compliance scorecard checks (AccessManagementDirective §5, cert-side)."""
cfg = _load_cfg()
inventory = _load_inventory(cfg)
results = run_scorecard(cfg.state_dir, inventory)
passed = sum(1 for r in results if r.passed)
total = len(results)
if output_json:
print(json.dumps(
[{"check": r.name, "passed": r.passed, "detail": r.detail} for r in results],
indent=2,
))
else:
table = Table(title=f"OpsWarden Scorecard ({passed}/{total})")
table.add_column("Check")
table.add_column("Status")
table.add_column("Detail")
for r in results:
status_str = "[green]PASS[/green]" if r.passed else "[red]FAIL[/red]"
table.add_row(r.name, status_str, r.detail)
console.print(table)
# Always surface org posture in human scorecard (WP-0029 T02)
try:
from warden.posture import load_posture
org = load_posture().organization_posture
console.print(
f"\n[dim]organization_posture:[/dim] [bold]{org.id}[/bold] — {org.summary[:160]}"
)
except Exception: # noqa: BLE001
pass
console.print(
f"\nScore: {passed}/{total} "
+ ("[green]Operational[/green]" if passed == total else "[yellow]Needs attention[/yellow]")
)
if passed < total:
raise typer.Exit(1)
# ---------------------------------------------------------------------------
# warden inventory
# ---------------------------------------------------------------------------
@inventory_app.command("list")
def inventory_list(
output_json: Annotated[bool, typer.Option("--json")] = False,
) -> None:
"""List all actors in the principals inventory."""
cfg = _load_cfg()
inventory = _load_inventory(cfg)
if not inventory.actors:
console.print("No actors in inventory.")
return
if output_json:
print(json.dumps({
name: {
"type": e.actor_type.value,
"principals": e.principals,
"ttl_hours": e.ttl_hours,
"description": e.description,
}
for name, e in inventory.actors.items()
}, indent=2))
return
table = Table(title=f"Principals Inventory ({cfg.inventory_path})")
table.add_column("Actor")
table.add_column("Type")
table.add_column("Principals")
table.add_column("TTL (h)")
table.add_column("Description")
for name, e in inventory.actors.items():
table.add_row(
name,
e.actor_type.value,
", ".join(e.principals),
str(e.ttl_hours),
e.description,
)
console.print(table)
@inventory_app.command("add")
def inventory_add(
actor_name: Annotated[str, typer.Argument(help="Actor name (e.g. agt-state-hub-bridge)")],
actor_type: Annotated[ActorType, typer.Option("--type", "-t", help="adm | agt | atm")],
principals: Annotated[
Optional[List[str]],
typer.Option("--principal", "-p", help="Principal (repeat for multiple)"),
] = None,
ttl: Annotated[Optional[int], typer.Option("--ttl", help="TTL in hours")] = None,
description: Annotated[str, typer.Option("--description", "-d")] = "",
) -> None:
"""Add an actor to the principals inventory."""
cfg = _load_cfg()
try:
validate_actor_name(actor_name, actor_type)
except ValueError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(1)
resolved_principals: List[str] = principals or [actor_name]
inventory = _load_inventory(cfg)
inventory.actors[actor_name] = ActorEntry(
name=actor_name,
actor_type=actor_type,
principals=resolved_principals,
ttl_hours=ttl or DEFAULT_TTL_HOURS[actor_type],
description=description,
)
try:
save_inventory(inventory, cfg.inventory_path)
except Exception as e:
err.print(f"[red]Failed to save inventory:[/red] {e}")
raise typer.Exit(1)
console.print(
f"[green]Added[/green] {actor_name} "
f"(type={actor_type.value}, principals={resolved_principals}, ttl={ttl or DEFAULT_TTL_HOURS[actor_type]}h)"
)
@inventory_app.command("remove")
def inventory_remove(
actor_name: Annotated[str, typer.Argument(help="Actor name to remove")],
) -> None:
"""Remove an actor from the principals inventory."""
cfg = _load_cfg()
inventory = _load_inventory(cfg)
if actor_name not in inventory.actors:
err.print(f"[red]Actor {actor_name!r} not in inventory.[/red]")
raise typer.Exit(1)
del inventory.actors[actor_name]
try:
save_inventory(inventory, cfg.inventory_path)
except Exception as e:
err.print(f"[red]Failed to save inventory:[/red] {e}")
raise typer.Exit(1)
console.print(f"[green]Removed[/green] {actor_name}")
# ---------------------------------------------------------------------------
# warden cleanup
# ---------------------------------------------------------------------------
@app.command()
def cleanup(
actor_name: Annotated[Optional[str], typer.Argument(help="Actor name (omit for all)")] = None,
dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview without deleting")] = False,
) -> None:
"""Remove stale (expired > 5 min) certificates from state dir."""
cfg = _load_cfg()
cutoff = datetime.now(timezone.utc) - timedelta(minutes=5)
if actor_name:
cert_path = cfg.state_dir / f"{actor_name}-cert.pub"
paths = [cert_path] if cert_path.exists() else []
else:
paths = sorted(cfg.state_dir.glob("*-cert.pub")) if cfg.state_dir.exists() else []
removed = []
for cert_path in paths:
try:
meta = parse_cert_metadata(cert_path)
except Exception:
continue
if meta["valid_before"] < cutoff:
if dry_run:
console.print(f"would remove: {cert_path.name}")
else:
cert_path.unlink()
console.print(f"removed: {cert_path.name}")
removed.append(cert_path.name)
if not removed:
console.print("No stale certificates found.")
# ---------------------------------------------------------------------------
# warden log
# ---------------------------------------------------------------------------
@app.command()
def log(
actor_name: Annotated[Optional[str], typer.Argument(help="Filter by actor name")] = None,
last: Annotated[int, typer.Option("--last", help="Show last N entries")] = 20,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Show outgoing certificate signing history."""
cfg = _load_cfg()
log_path = cfg.state_dir / "signatures.log"
if not log_path.exists():
if output_json:
print("[]")
else:
console.print("No signatures log found.")
return
entries = []
for line in log_path.read_text().splitlines():
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if actor_name and entry.get("actor") != actor_name:
continue
entries.append(entry)
entries = entries[-last:]
if output_json:
print(json.dumps(entries, indent=2))
return
if not entries:
console.print("No matching log entries.")
return
table = Table(title="Signatures Log")
table.add_column("Timestamp")
table.add_column("Actor")
table.add_column("Type")
table.add_column("Identity")
table.add_column("TTL (h)")
table.add_column("Valid Before (UTC)")
table.add_column("Backend")
for e in entries:
table.add_row(
e.get("timestamp", "")[:19],
e.get("actor", ""),
e.get("actor_type", ""),
e.get("identity", ""),
str(e.get("ttl_hours", "")),
e.get("valid_before", "")[:19],
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,
# warden_role tells an agent at a glance whether ops-warden runs this lane
# itself (issue), proxies the fetch as the caller (assist), or only points (route).
"warden_role": (
"issue" if entry.warden_executes
else "assist" if entry.exec_capable
else "route"
),
"exec_capable": entry.exec_capable,
# resolvable: can `warden access --fetch` run this now with no <…> to fill?
# Lets an automated caller gate on readiness before attempting a fetch.
"resolvable": entry.resolvable,
# Owner-native exec front door (WP-0019): when present, this subsystem's exec is
# the PRIMARY path; ops-warden's proxy is the transparent fallback.
**(
{
"exec_owner": entry.exec_owner,
"exec_command": entry.exec_command,
"pointer_command": entry.pointer_command,
}
if entry.has_native_exec
else {}
),
"wiki_ref": entry.wiki_ref,
"canon_ref": entry.canon_ref,
"reviewed": entry.reviewed,
"status": entry.status,
# Agent read-boundary (WP-0026 T04) — high-risk lanes deny raw agent data reads.
"risk": entry.risk,
"high_risk": entry.is_high_risk,
# 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 {}
),
}
def _print_entry_table(
entries, title: str, *, show_reviewed: bool = False, stale_threshold_days: int = 90
) -> None:
table = Table(title=title)
table.add_column("ID")
table.add_column("Need")
table.add_column("Owner")
table.add_column("warden")
if show_reviewed:
table.add_column("Reviewed")
table.add_column("Days")
table.add_column("Status")
from warden.routing.catalog import days_since_review
for e in entries:
if e.warden_executes:
executes = "[green]issue[/green]"
elif e.exec_capable:
executes = "[cyan]assist[/cyan]" # warden access --fetch/--exec proxies it
else:
executes = "route"
status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]"
if show_reviewed:
days = days_since_review(e.reviewed)
reviewed_styled = (
f"[yellow]{e.reviewed}[/yellow]"
if days > stale_threshold_days
else e.reviewed
)
table.add_row(
e.id, e.title, e.owner_repo, executes, reviewed_styled, str(days), status_styled
)
else:
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,
stale_only: Annotated[
bool, typer.Option("--stale", help="Show entries past review cadence (see --stale-days)")
] = False,
stale_days: Annotated[
int,
typer.Option(
"--stale-days",
help="Days since reviewed before an entry is stale (default 90)",
min=1,
),
] = 90,
) -> None:
"""List routing scenarios. Active-only unless --all."""
from warden.routing.catalog import days_since_review
catalog = _load_catalog()
if stale_only:
entries = catalog.stale(include_draft=all_entries, threshold_days=stale_days)
else:
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]]
freshness = catalog.freshness(stale_threshold_days=stale_days)
if output_json:
# Stable array of entries for agents. Freshness lives on human output +
# `warden plan --json` (`catalog` field); avoid breaking list parsers.
payload = []
for e in entries:
row = _entry_summary(e)
if stale_only:
row["days_since_review"] = days_since_review(e.reviewed)
row["stale_threshold_days"] = stale_days
payload.append(row)
print(json.dumps(payload, indent=2))
return
# Human path: always show catalog freshness (WP-0029 T05)
console.print(
f"[dim]catalog[/dim] source={freshness.source} "
f"hash={freshness.content_hash} "
f"reviewed={freshness.newest_reviewed or ''} "
f"entries={freshness.active_count}/{freshness.entry_count}"
)
for w in freshness.warnings:
console.print(f"[yellow]catalog warning:[/yellow] {w}")
if not entries:
if stale_only:
console.print(f"No stale routing entries (threshold: {stale_days} days since reviewed).")
else:
console.print("No matching routing entries.")
return
title = (
f"Stale routing scenarios (>{stale_days}d since reviewed)"
if stale_only
else "Routing scenarios"
)
_print_entry_table(
entries, title, show_reviewed=stale_only, stale_threshold_days=stale_days
)
@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
elif entry.has_native_exec:
summary["next_action"] = (
f"primary: run via {entry.exec_owner} — `{entry.exec_command}`; ops-warden "
f"routes to the owner (fallback: `warden access <need> --exec`). See `{entry.wiki_ref}`."
)
elif entry.exec_capable:
summary["next_action"] = (
f"ops-warden can proxy this as the caller: `warden access <need> --fetch`"
f" (or `--exec -- <cmd>`); runs {entry.owner_repo}'s tool with your "
f"identity. See `{entry.wiki_ref}`."
)
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}."
)
@app.command("taint")
def taint_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:
"""Report whether a lane's OpenBao secret is marked EXPOSED (WP-0026 T05).
Reads KV v2 *metadata only* (custom_metadata: exposed_at, exposed_version, …).
Never reads secret data. Advisory — does not rotate or clear taint.
"""
from warden.taint import TaintError, fetch_taint_status
catalog = _load_catalog()
entry = catalog.get(entry_id)
if entry is None:
# Drafts are findable by exact id via get even when not listed.
err.print(
f"[red]Unknown routing id {entry_id!r}.[/red] Try: warden route find {entry_id!r} --all"
)
raise typer.Exit(1)
try:
status = fetch_taint_status(entry)
except TaintError as e:
err.print(f"[red]taint status unavailable:[/red] {e}")
raise typer.Exit(2)
if output_json:
print(json.dumps(status.to_dict(), indent=2))
return
console.print(f"[bold]Taint status — {entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
console.print(f" path : {status.path}")
if status.error:
console.print(f" [yellow]query error[/yellow] : {status.error}")
console.print(
" [dim]Need caller OpenBao auth with metadata-read on the path "
"(agent-high-risk-boundary allows metadata; workload-kv-read allows both).[/dim]"
)
raise typer.Exit(3)
if status.tainted:
console.print(" tainted : [red]yes (EXPOSED)[/red]")
console.print(f" exposed_at : {status.exposed_at}")
console.print(f" exposed_version : {status.exposed_version}")
console.print(f" exposed_reason : {status.exposed_reason}")
console.print(f" exposed_ref : {status.exposed_ref}")
console.print(f" current_version : {status.current_version}")
console.print(
"\n[yellow]Advisory:[/yellow] rotate/re-establish per "
f"`warden rotate-guide {entry.id}` then clear custom_metadata keys "
"(exposed_at, exposed_version, …). No auto-rotation (Strand B)."
)
else:
console.print(" tainted : [green]no[/green]")
console.print(f" current_version : {status.current_version}")
@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'")],
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."""
try:
from warden import memory as warden_memory
warden_memory.ensure_memory_context(need=query, implicit=True)
except Exception: # noqa: BLE001
pass
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))
if matches:
_record_memory_episode(
command="route find",
outcome="resolved",
need=query,
route_id=matches[0].id,
)
else:
_record_memory_episode(command="route find", outcome="skipped", need=query)
return
if not matches:
_record_memory_episode(command="route find", outcome="skipped", need=query)
console.print(
f"No routing match for {query!r}. "
"Try `warden route list --all` to browse all scenarios."
)
return
_record_memory_episode(
command="route find",
outcome="resolved",
need=query,
route_id=matches[0].id,
)
_print_entry_table(matches, f"Matches for {query!r}")
# ---------------------------------------------------------------------------
# warden access — operator front door (advisory; proxy lands in T3)
# ---------------------------------------------------------------------------
def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict:
"""Stable, secret-free JSON shape for agentic operators. WP-0014 T2."""
payload = _entry_summary(entry)
payload["domain"] = domain
payload["policy_gate"] = gate
payload["handoff"] = {
"auth_method": expanded.auth_method,
"path_template": expanded.path_template,
"fetch_command": expanded.fetch_command,
"policy_ref": expanded.policy_ref,
"exec_capable": expanded.exec_capable,
}
if entry.warden_executes:
payload["next_action"] = "ops-warden issues this directly — see cert_command"
payload["cert_command"] = entry.cert_command
elif entry.has_native_exec:
payload["next_action"] = (
f"primary: run via {entry.exec_owner} — `{entry.exec_command}`; "
"ops-warden routes to the owner (fallback: `warden access <need> --exec`). "
"ops-warden holds no token."
)
elif expanded.exec_capable:
verb = "fetch" if entry.lane != "login" else "login"
payload["next_action"] = (
f"ops-warden can proxy this {verb} as the caller: "
f"`warden access <need> --fetch`"
+ ("" if entry.lane == "login" else " (or `--exec -- <cmd>`)")
+ f". Runs {entry.owner_repo}'s tool with your identity; ops-warden holds no value."
)
else:
payload["next_action"] = (
f"obtain from {entry.owner_repo} ({entry.subsystem}); "
"ops-warden holds no value"
)
return payload
def _access_proxy(
entry,
*,
domain: Optional[str],
field: Optional[str],
path: Optional[str],
do_exec: bool,
child_argv: list,
no_policy: bool,
out_path: Optional[str] = None,
wrap: bool = False,
wrap_ttl: str = "5m",
unsafe_stdout: bool = False,
fingerprint: bool = False,
) -> None:
"""Proxy a non-SSH credential fetch as the caller (WP-0014 T3).
Enforces the three guardrails: caller identity (no warden token), policy gate
before fetch, and transit-only (no value persisted or logged). All warden chatter
goes to stderr so --fetch stdout carries only the secret.
"""
from warden.proxy import (
ProxyError,
build_wrapped_fetch,
caller_auth_present,
proxy_exec,
proxy_fetch,
proxy_fetch_fingerprint,
proxy_fetch_to_file,
proxy_fetch_wrapped,
resolve_fetch_command,
write_audit,
)
from warden.policy import check_fetch_policy
if not entry.exec_capable:
err.print(
f"[red]{entry.id!r} is not exec_capable.[/red] "
"Use `warden access` (advisory) and obtain it from the owner directly."
)
raise typer.Exit(2)
# Proxy is privileged — require a real config for policy posture + audit sink.
try:
cfg = load_config()
except ConfigError as e:
err.print(
f"[red]Proxy requires warden.yaml[/red] (policy gate + audit sink): {e}\n"
"Advisory mode works without it: drop --fetch/--exec."
)
raise typer.Exit(2)
is_login = entry.lane == "login"
decision_id = None
if is_login:
# Login lane: interactive auth bootstrap. No caller-auth precheck (you have no
# token yet — that's the point) and no secret-read gate (it needs an identity
# this flow establishes). --exec is meaningless here.
if do_exec:
err.print(
"[red]--exec is not valid for a login lane[/red] "
f"({entry.id!r} is interactive auth). Use --fetch."
)
raise typer.Exit(2)
err.print(
"[dim]login lane — interactive auth bootstrap; no secret-read gate, "
"token stays in the caller's own store.[/dim]"
)
else:
# G1 — caller identity. ops-warden adds no token of its own.
if not caller_auth_present():
err.print(
"[red]No caller credential found[/red] (VAULT_TOKEN/BAO_TOKEN or ~/.vault-token). "
f"Authenticate first: {entry.auth_method or 'see the owner auth path'}."
)
raise typer.Exit(3)
# G3 — policy gate before fetch.
if cfg.policy.enabled:
try:
decision_id = check_fetch_policy(
cfg.policy, need_id=entry.id, owner_repo=entry.owner_repo, domain=domain
)
except CAError as e:
err.print(f"[red]Policy gate denied the fetch:[/red] {e}")
raise typer.Exit(4)
err.print(f"[green]flex-auth allow[/green] (decision {decision_id}).")
elif not no_policy:
err.print(
"[yellow]flex-auth gate is not enforced[/yellow] (policy.enabled=false). "
"Re-run with [bold]--no-policy[/bold] to proxy ungated, or enable the gate."
)
raise typer.Exit(4)
else:
err.print("[yellow]Proxying ungated[/yellow] (--no-policy; gate not enforced).")
# Wrapping (WP-0026 T02) uses its own command shape; the value-bearing transports
# share the resolved fetch command.
if wrap and not is_login:
try:
resolved = build_wrapped_fetch(entry, path=path, ttl=wrap_ttl)
except ProxyError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(2)
else:
try:
resolved = resolve_fetch_command(entry, domain=domain, field=field, path=path)
except ProxyError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(2)
# T04 — agent identity on a high-risk lane: never stream raw secret data.
# Agents may use sanctioned transports (--out / --exec / --wrap / --fingerprint).
agent_id = os.environ.get("WARDEN_AGENT_ID", "").strip()
raw_value_stream = (
not is_login and not do_exec and not wrap and not out_path and not fingerprint
)
if raw_value_stream and entry.is_high_risk and agent_id:
err.print(
f"[red]Agent read-boundary:[/red] {entry.id!r} is risk=high; "
f"agent identity {agent_id!r} must not stream raw secret data.\n"
"Use a sanctioned transport (value stays off the session transcript):\n"
" --out FILE write to a mode-0600 file\n"
" --exec -- CMD inject into a child process env only\n"
" --wrap single-use OpenBao wrapping token (unwrap out-of-band)\n"
" --fingerprint masked presence/length/hash only\n"
"OpenBao policy `agent-high-risk-boundary` also denies data-read for agents."
)
raise typer.Exit(7)
# T02 — the sanctioned fetch transports (file / env / wrapping token) never put a
# secret value on stdout. Streaming a value to stdout is the documented anti-pattern:
# allowed only to an interactive terminal, and only with an explicit acknowledgment
# when stdout is captured/piped (the logged-context disclosure risk).
if raw_value_stream:
import sys as _sys
if not _sys.stdout.isatty() and not unsafe_stdout:
err.print(
"[red]Refusing to stream a secret value to a non-terminal stdout[/red] "
"(captured/piped output is a disclosure risk). Use a sanctioned transport:\n"
" --out FILE write the value to a mode-0600 file\n"
" --exec -- CMD inject it into a child process env\n"
" --wrap return a single-use OpenBao wrapping token to unwrap yourself\n"
"Override only for an interactive human session: --unsafe-stdout."
)
raise typer.Exit(6)
action = "login" if is_login else ("exec" if do_exec else "fetch")
err.print(
f"[dim]proxy {action}: {entry.id}{entry.owner_repo} "
f"(caller identity; value not persisted)[/dim]"
)
try:
if do_exec:
if not child_argv:
err.print("[red]--exec needs a command after `--`[/red], e.g. `-- npm publish`.")
raise typer.Exit(2)
rc = proxy_exec(resolved, env_var=field or "", child_argv=child_argv)
elif wrap:
token = proxy_fetch_wrapped(resolved)
# The wrapping token is not the secret value — safe to hand back on stdout.
print(token)
err.print(
f"[dim]wrapping token (single-use, ttl {wrap_ttl}) — unwrap in your own "
f"context: [bold]bao unwrap {'<token>'}[/bold][/dim]"
)
rc = 0
elif out_path:
rc = proxy_fetch_to_file(resolved, Path(out_path))
err.print(f"[dim]value written to {out_path} (mode 0600); not shown[/dim]")
elif fingerprint:
fp = proxy_fetch_fingerprint(resolved)
# Masked fingerprint only — presence, length, short hash; never the value.
print(fp.render())
err.print(
"[dim]masked fingerprint (defense-in-depth; not the value). Compare "
"sha256 prefixes to confirm two parties hold the same secret.[/dim]"
)
rc = 0
else:
rc = proxy_fetch(resolved)
except ProxyError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(5)
finally:
try:
write_audit(
cfg.state_dir,
need_id=entry.id,
owner_repo=entry.owner_repo,
domain=domain,
action=action,
decision_id=decision_id,
)
except OSError as e:
err.print(f"[yellow]audit write failed:[/yellow] {e}")
raise typer.Exit(rc)
@app.command(
"access",
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def access(
ctx: typer.Context,
need: Annotated[str, typer.Argument(help="Free-text need, e.g. 'npm token', 'db password'")],
domain: Annotated[
Optional[str],
typer.Option("--domain", help="Substitute <domain> in path/auth templates, e.g. coulomb_social"),
] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON (stable, secret-free)")] = False,
all_entries: Annotated[bool, typer.Option("--all", help="Include draft entries")] = False,
do_fetch: Annotated[
bool, typer.Option("--fetch", help="Proxy the fetch as the caller (pair with --out/--wrap; raw stdout is guarded)")
] = False,
do_exec: Annotated[
bool,
typer.Option("--exec", help="Run the trailing command (after --) with the secret in its env"),
] = False,
field: Annotated[
Optional[str], typer.Option("--field", help="Secret field / env-var name, e.g. NPM_AUTH_TOKEN")
] = None,
path: Annotated[
Optional[str], typer.Option("--path", help="Override the owner-side path template")
] = None,
out_path: Annotated[
Optional[str],
typer.Option("--out", help="Sanctioned transport: write the value to this mode-0600 file, not stdout"),
] = None,
wrap: Annotated[
bool,
typer.Option("--wrap", help="Sanctioned transport: return a single-use OpenBao wrapping token (bao unwrap)"),
] = False,
wrap_ttl: Annotated[
str, typer.Option("--wrap-ttl", help="TTL for the --wrap response-wrapping token")
] = "5m",
unsafe_stdout: Annotated[
bool,
typer.Option("--unsafe-stdout", help="Acknowledge streaming a value to a captured/piped stdout (anti-pattern)"),
] = False,
fingerprint: Annotated[
bool,
typer.Option("--fingerprint", help="Show a masked fingerprint (presence, length, short hash) — never the value"),
] = False,
no_policy: Annotated[
bool,
typer.Option("--no-policy", help="Acknowledge proxying when the flex-auth gate is not enforced"),
] = False,
) -> None:
"""Operator front door: how to obtain any credential, gated and audited.
Advisory by default — renders the owner, auth method, path template, command
skeleton, and policy gate status for the best-matching need. ops-warden issues
the SSH lane directly and **routes every other need to its owner** — it never
holds or vends the secret value.
With --fetch / --exec it proxies the fetch *as the caller* for exec_capable lanes:
the flex-auth gate runs first, ops-warden adds no credential of its own, the value
is never persisted or logged, and only metadata is audited.
"""
from warden.access import expand_handoff, policy_gate_status
try:
from warden import memory as warden_memory
warden_memory.ensure_memory_context(need=need, implicit=True)
except Exception: # noqa: BLE001
pass
catalog = _load_catalog()
matches = catalog.find(need, include_draft=all_entries, limit=1)
if not matches:
err.print(
f"[red]No access match for {need!r}.[/red] "
"Try `warden route list --all` to browse, or rephrase the need."
)
raise typer.Exit(1)
entry = matches[0]
if do_fetch or do_exec or out_path or wrap or fingerprint:
_access_proxy(
entry,
domain=domain,
field=field,
path=path,
do_exec=do_exec,
child_argv=list(ctx.args),
no_policy=no_policy,
out_path=out_path,
wrap=wrap,
wrap_ttl=wrap_ttl,
unsafe_stdout=unsafe_stdout,
fingerprint=fingerprint,
)
return
expanded = expand_handoff(entry, domain)
gate = policy_gate_status()
if output_json:
print(json.dumps(_access_json(entry, expanded, gate, domain), indent=2))
_record_memory_episode(
command="access",
outcome="resolved",
need=need,
route_id=entry.id,
)
return
_record_memory_episode(
command="access",
outcome="resolved",
need=need,
route_id=entry.id,
)
console.print(f"[bold]{entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
console.print(f" owner : {entry.owner_repo} ({entry.subsystem})")
if entry.warden_executes:
console.print("\n[green]ops-warden issues this directly.[/green]")
console.print(f" run : [bold]{entry.cert_command}[/bold]")
if entry.steps:
for i, step in enumerate(entry.steps, 1):
console.print(f" {i}. {step}")
return
if expanded.auth_method:
console.print(f" auth : {expanded.auth_method}")
if expanded.path_template:
console.print(f" path : {expanded.path_template}")
if expanded.fetch_command:
console.print(f" fetch : {expanded.fetch_command}")
if expanded.policy_ref:
console.print(f" policy : {expanded.policy_ref} [dim]({gate})[/dim]")
console.print(f" wiki : {entry.wiki_ref}")
console.print(f" canon : {entry.canon_ref}")
proxy = f"warden access {need!r}"
if domain:
proxy += f" --domain {domain}"
if entry.has_native_exec:
console.print(
f" exec : [bold]{entry.exec_command}[/bold] "
f"[cyan](via {entry.exec_owner} — primary)[/cyan]"
)
if entry.pointer_command:
console.print(f" pointer : [dim]{entry.pointer_command}[/dim]")
if expanded.exec_capable:
label = "fallback" if entry.has_native_exec else "proxy"
hint = (
"transparent conduit — fetches as you"
if entry.lane != "login"
else "runs the interactive login as you"
)
console.print(f" {label:<8} : [dim]{proxy} --fetch[/dim] [yellow]({hint})[/yellow]")
if expanded.path_template and "<" in expanded.path_template:
console.print(
" note : remaining <…> placeholders are owner-confirmed names "
f"(coordinate with {entry.owner_repo})."
)
if entry.has_native_exec:
console.print(
f"\n[green]Primary:[/green] run it via [bold]{entry.exec_owner}[/bold] — "
f"[bold]{entry.exec_command}[/bold]. ops-warden routes to the owner and holds no token.\n"
f"[dim]Fallback:[/dim] [bold]{proxy} --exec -- <cmd>[/bold] — ops-warden's transparent "
"conduit (runs the fetch as you, holds nothing)."
)
elif expanded.exec_capable:
verb = "fetch this for you" if entry.lane != "login" else "run this login for you"
console.print(
f"\n[green]ops-warden can {verb}[/green] as the caller — "
f"[bold]{proxy} --fetch[/bold]"
+ ("" if entry.lane == "login" else f" (or [bold]{proxy} --exec -- <cmd>[/bold])")
+ f". It runs {entry.owner_repo}'s tool with [bold]your[/bold] identity; the "
"value streams to you and ops-warden never holds, caches, or logs it."
)
else:
console.print(
f"\n[yellow]ops-warden does not hold this secret.[/yellow] "
f"Obtain it from [bold]{entry.owner_repo}[/bold] as shown — "
"warden advises, the owner vends."
)
# ---------------------------------------------------------------------------
# warden policy — read-only Workload Security Posture lookup (WP-0015 T2)
# ---------------------------------------------------------------------------
def _load_posture():
from warden.posture import PostureError, load_posture
try:
return load_posture()
except PostureError as e:
err.print(f"[red]Posture descriptor error:[/red] {e}")
raise typer.Exit(1)
@policy_app.command("list")
def policy_list(
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""List posture axes: env, maturity, and organization lifecycle (WP-0029)."""
cat = _load_posture()
org = cat.organization_posture
if output_json:
print(json.dumps({
"env_postures": [vars(e) for e in cat.env_postures],
"maturity_levels": [vars(m) for m in cat.maturity_levels],
"dataclass_floor": cat.dataclass_floor,
"requires_env_posture": cat.requires_env_posture,
"organization_posture": vars(org),
}, indent=2))
return
env_table = Table(title="Axis A — environment posture")
for col in ("ID", "rank", "backend", "real values", "user data", "audit"):
env_table.add_column(col)
for e in sorted(cat.env_postures, key=lambda x: x.rank):
env_table.add_row(e.id, str(e.rank), e.backend, e.real_values, e.real_user_data, e.audit)
console.print(env_table)
mat_table = Table(title="Axis B — workload maturity")
for col in ("ID", "rank", "phase", "max dataclass", "promotion gate"):
mat_table.add_column(col)
for m in sorted(cat.maturity_levels, key=lambda x: x.rank):
mat_table.add_row(m.id, str(m.rank), m.phase, m.max_dataclass, ", ".join(m.promotion_gate) or "")
console.print(mat_table)
org_table = Table(title="Axis C — organization lifecycle posture (WP-0029)")
org_table.add_column("ID")
org_table.add_column("Summary")
org_table.add_column("Relaxations")
org_table.add_column("Graduation triggers")
org_table.add_row(
org.id,
org.summary[:80] + ("" if len(org.summary) > 80 else ""),
", ".join(org.relaxations) or "",
", ".join(org.graduation_triggers) or "",
)
console.print(org_table)
console.print(
f"\n[dim]lattice: deliver iff env=={cat.requires_env_posture} and "
"workload.maturity >= secret.required_maturity (and the dataclass floor).[/dim]"
)
@policy_app.command("show")
def policy_show(
descriptor_id: Annotated[
str,
typer.Argument(
help="Env posture (dev/test/prod), maturity (M0M3), or organization posture id (build)"
),
],
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Show one environment posture, maturity level, or organization posture."""
cat = _load_posture()
env = cat.env(descriptor_id)
mat = cat.maturity(descriptor_id)
org = cat.organization_posture if cat.organization_posture.id == descriptor_id else None
# Alias: `organization` always shows axis C
if descriptor_id in ("organization", "organization_posture", "org"):
org = cat.organization_posture
if env is None and mat is None and org is None:
err.print(
f"[red]Unknown descriptor {descriptor_id!r}.[/red] "
"Try `warden policy list`."
)
raise typer.Exit(1)
if org is not None and env is None and mat is None:
if output_json:
print(json.dumps({"axis": "organization_posture", **vars(org)}, indent=2))
return
console.print(f"[bold]{org.id}[/bold] ([cyan]organization lifecycle posture[/cyan])")
console.print(f" {'summary':14}: {org.summary}")
console.print(f" {'relaxations':14}: {', '.join(org.relaxations) or ''}")
console.print(
f" {'graduation':14}: {', '.join(org.graduation_triggers) or ''}"
)
return
obj = env or mat
if output_json:
print(json.dumps({"axis": "env_posture" if env else "maturity_level", **vars(obj)}, indent=2))
return
axis = "environment posture" if env else "workload maturity level"
console.print(f"[bold]{obj.id}[/bold] ([cyan]{axis}[/cyan])")
for k, v in vars(obj).items():
if k == "id":
continue
console.print(f" {k:14}: {', '.join(v) if isinstance(v, list) else v}")
if mat:
floor = [dc for dc, lvl in cat.dataclass_floor.items() if lvl == mat.id]
if floor:
console.print(f" {'dataclass floor':14}: {', '.join(floor)} require this level")
# ---------------------------------------------------------------------------
# warden plan — policy decision front door (WARDEN-WP-0029 T01)
# ---------------------------------------------------------------------------
@app.command("plan")
def plan_cmd(
need: Annotated[str, typer.Argument(help="Free-text credential / access need")],
actor: Annotated[
Optional[str],
typer.Option("--actor", help="Optional actor id for audit context (e.g. agt-...)"),
] = None,
domain: Annotated[
Optional[str],
typer.Option("--domain", help="Optional domain substitution for path templates"),
] = None,
output_json: Annotated[bool, typer.Option("--json", help="Machine-readable plan")] = False,
include_draft: Annotated[
bool, typer.Option("--all", help="Include draft catalog lanes in matching")
] = False,
) -> None:
"""Policy decision front door: autonomous / founder_required / unroutable.
Composes the routing catalog + access handoff + organization posture. Never
holds secret values. Agents must call this before drafting founder credential steps.
"""
from warden.plan import build_plan
access_plan = build_plan(
need, actor=actor, domain=domain, include_draft=include_draft
)
# Metadata-only audit
try:
cfg = _load_cfg()
from warden.audit import record_event
record_event(
cfg.state_dir,
kind="plan",
action="plan",
subject=actor or "",
target=access_plan.lane_id or "",
outcome=access_plan.verdict,
need=need[:200],
organization_posture=access_plan.organization_posture,
lane_id=access_plan.lane_id or "",
)
except Exception: # noqa: BLE001 — audit must never block plan
pass
_record_memory_episode(
command="plan",
outcome=access_plan.verdict,
need=need,
route_id=access_plan.lane_id or "",
)
if output_json:
print(json.dumps(access_plan.to_dict(), indent=2))
return
verdict_style = {
"autonomous": "[green]autonomous[/green]",
"founder_required": "[yellow]founder_required[/yellow]",
"unroutable": "[red]unroutable[/red]",
}.get(access_plan.verdict, access_plan.verdict)
console.print(f"[bold]verdict[/bold]: {verdict_style}")
console.print(f"[bold]need[/bold]: {access_plan.need}")
console.print(
f"[bold]posture[/bold]: [cyan]{access_plan.organization_posture}[/cyan] "
f"policy_gate={access_plan.policy_gate}"
)
if access_plan.lane_id:
console.print(
f"[bold]lane[/bold]: {access_plan.lane_id}{access_plan.lane_title or ''}"
)
for reason in access_plan.reasons:
console.print(f"[dim]reason:[/dim] {reason}")
if access_plan.commands:
console.print("\n[bold]commands[/bold]")
for c in access_plan.commands:
console.print(f" {c}")
if access_plan.founder_act:
fa = access_plan.founder_act
console.print("\n[bold]founder act[/bold]")
console.print(f" kind: {fa.kind}")
console.print(f" summary: {fa.summary}")
for k, v in fa.details.items():
console.print(f" {k}: {v}")
console.print(
"\n[dim]Open the act surface:[/dim] "
f"warden desk --act {fa.kind}"
+ (f" --lane {access_plan.lane_id}" if access_plan.lane_id else "")
)
if access_plan.ccr_stub:
console.print("\n[bold]CCR stub[/bold] (unroutable — propose a lane)")
console.print(f" {access_plan.ccr_stub.get('title')}")
for step in access_plan.ccr_stub.get("steps") or []:
console.print(f" - {step}")
cat = access_plan.catalog
if cat:
console.print(
f"\n[dim]catalog source={cat.get('source')} hash={cat.get('content_hash')} "
f"bundled={cat.get('using_bundled')}[/dim]"
)
for w in cat.get("warnings") or []:
console.print(f"[yellow]catalog warning:[/yellow] {w}")
# ---------------------------------------------------------------------------
# warden desk — founder interaction surface (WARDEN-WP-0029 T03)
# ---------------------------------------------------------------------------
@app.command("desk")
def desk_cmd(
act: Annotated[
Optional[str],
typer.Option("--act", help="Founder act: approve | oidc_login | paste_once_provision"),
] = None,
summary: Annotated[
Optional[str],
typer.Option("--summary", help="Plain-language description of the act"),
] = None,
lane: Annotated[
Optional[str],
typer.Option("--lane", help="Catalog lane id for context"),
] = None,
path: Annotated[
Optional[str],
typer.Option("--path", help="Concrete OpenBao path (paste_once_provision)"),
] = None,
field: Annotated[
str,
typer.Option("--field", help="KV field name for paste_once_provision"),
] = "value",
oidc_command: Annotated[
Optional[str],
typer.Option("--oidc-command", help="Login command to display (oidc_login)"),
] = None,
plan_json: Annotated[
Optional[Path],
typer.Option("--plan-json", help="Path to warden plan --json output"),
] = None,
port: Annotated[
int,
typer.Option("--port", help="Port (0 = ephemeral)"),
] = 0,
no_browser: Annotated[
bool,
typer.Option("--no-browser", help="Do not open a browser"),
] = False,
dry_run: Annotated[
bool,
typer.Option("--dry-run", help="Do not call bao; simulate paste-once write"),
] = False,
) -> None:
"""Localhost founder interaction surface (build-phase: OS session trust).
Serves a short-lived page on 127.0.0.1 for approve / OIDC confirm / paste-once
provision into OpenBao. Secret values never appear in audit or CLI history.
"""
from warden.desk import (
DeskError,
load_plan_json,
new_session,
run_desk,
session_from_plan_dict,
)
try:
if plan_json is not None:
session = session_from_plan_dict(load_plan_json(plan_json))
# Allow CLI overrides on top of plan
if path:
session.path = path
if field:
session.kv_field = field
else:
if not act:
err.print(
"[red]desk requires --act or --plan-json.[/red] "
"Example: warden desk --act approve --summary 'enable policy'"
)
raise typer.Exit(2)
session = new_session(
act=act,
summary=summary or act,
lane_id=lane or "",
path=path or "",
kv_field=field,
oidc_command=oidc_command or "",
)
except DeskError as e:
err.print(f"[red]desk error:[/red] {e}")
raise typer.Exit(1)
try:
cfg = _load_cfg()
from warden.audit import record_event
record_event(
cfg.state_dir,
kind="desk",
action="desk_open",
subject="",
target=session.lane_id or session.act,
outcome="open",
act=session.act,
lane_id=session.lane_id,
)
except Exception: # noqa: BLE001
cfg = None
try:
finished = run_desk(
session,
port=port,
open_browser=not no_browser,
dry_run=dry_run,
)
except DeskError as e:
err.print(f"[red]desk error:[/red] {e}")
raise typer.Exit(1)
if cfg is not None:
try:
from warden.audit import record_event
record_event(
cfg.state_dir,
kind="desk",
action="desk_close",
subject="",
target=finished.lane_id or finished.act,
outcome=finished.result,
act=finished.act,
lane_id=finished.lane_id,
# message is metadata-only by construction for approve/login;
# for provision it must not include the secret (desk never puts it there)
detail=finished.message[:200] if finished.message else "",
)
except Exception: # noqa: BLE001
pass
console.print(
f"[bold]desk result:[/bold] {finished.result}"
+ (f"{finished.message}" if finished.message else "")
)
if finished.result in ("denied", "error"):
raise typer.Exit(1)
# ---------------------------------------------------------------------------
# warden worker — autonomous coordination worker (WP-0020 T1: dry-run scaffold)
# ---------------------------------------------------------------------------
@worker_app.command("run")
def worker_run(
once: Annotated[bool, typer.Option("--once", help="Process the inbox once and exit")] = True,
dry_run: Annotated[
bool,
typer.Option("--dry-run/--execute", help="Plan only (default); --execute lands in WP-0020 T3"),
] = True,
brain: Annotated[
str,
typer.Option("--brain", help="Planner: 'rule' (deterministic, default) or 'llm' (llm-connect)"),
] = "rule",
full_auto: Annotated[
bool,
typer.Option("--full-auto", help="With --execute: auto-send replies + mark-read (default is conservative: triage + drafts only)"),
] = False,
) -> None:
"""Read ops-warden's unread coordination requests and act on them, guardrailed.
Default `--dry-run` previews. `--execute` runs the **conservative** tier: triage new
messages into a reviewed digest with drafted replies, post one progress note, and send
NOTHING to other agents (safe to schedule). `--execute --full-auto` auto-sends the safe
allowlisted actions. The allowlist + no-secret guardrails hold in every mode.
"""
from warden.worker import (
HubClient, LlmConnectBrain, RuleBrain, build_plans, execute_plans, render_plans,
run_conservative,
)
if brain not in ("rule", "llm"):
err.print(f"[red]Unknown --brain {brain!r}[/red] (expected 'rule' or 'llm').")
raise typer.Exit(2)
hub = HubClient()
try:
messages = hub.unread()
except Exception as e: # noqa: BLE001 — surface any transport error as a clean message
err.print(f"[red]Could not read the State Hub inbox:[/red] {e}")
raise typer.Exit(1)
chosen = LlmConnectBrain() if brain == "llm" else RuleBrain()
plans = build_plans(messages, chosen)
auto = sum(1 for p in plans if not p.escalated)
if dry_run:
console.print(render_plans(plans))
console.print(
f"\n[dim]{len(plans)} request(s): {auto} auto-actionable, "
f"{len(plans) - auto} need a human. (dry-run — nothing executed)[/dim]"
)
return
# --execute. Topic for audit progress events.
topic_id = "cee7bedf-2b48-46ef-8601-006474f2ad7a"
if full_auto:
console.print("[yellow]Executing FULL-AUTO (in-scope only; escalations left for a human)…[/yellow]")
console.print(execute_plans(plans, hub, topic_id=topic_id))
else:
console.print("[green]Conservative triage[/green] — drafting; nothing sent to other agents.")
console.print(run_conservative(plans, hub, topic_id=topic_id))
@worker_app.command("drafts")
def worker_drafts() -> None:
"""List the worker's pending drafted replies (from the conservative tier)."""
from warden.worker import list_drafts
console.print(list_drafts())
@worker_app.command("approve")
def worker_approve(
message_id: Annotated[str, typer.Argument(help="Message id to send the drafted reply for")],
body: Annotated[
Optional[str], typer.Option("--body", help="Override the drafted reply text before sending")
] = None,
) -> None:
"""Send a reviewed draft as the reply and mark the message read."""
from warden.worker import HubClient, approve_draft
try:
console.print(approve_draft(message_id, HubClient(), body_override=body))
except Exception as e: # noqa: BLE001 — surface transport errors cleanly
err.print(f"[red]Approve failed:[/red] {e}")
raise typer.Exit(1)
@activity_app.callback(invoke_without_command=True)
def activity_show(
days: Annotated[int, typer.Option("--days", help="Look back N days")] = 7,
kind: Annotated[
Optional[str],
typer.Option("--kind", help="Filter: sign, access, worker, hub"),
] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
include_hub: Annotated[
bool, typer.Option("--hub", help="Include State Hub progress notes")
] = False,
) -> None:
"""Show what ops-warden did recently (metadata only — no secret values)."""
from warden.audit import collect_activity, fetch_hub_notes
cfg = _load_cfg()
kinds = {kind} if kind else None
events = collect_activity(cfg.state_dir, days=days, kinds=kinds)
if include_hub and (kinds is None or "hub" in kinds):
events.extend(fetch_hub_notes(days=days))
events.sort(key=lambda e: str(e.get("ts", "")))
if output_json:
print(json.dumps(events, indent=2))
return
if not events:
console.print(f"No activity in the last {days} day(s).")
return
table = Table(title=f"ops-warden activity (last {days} days)")
table.add_column("When", style="dim")
table.add_column("Kind")
table.add_column("Action")
table.add_column("Subject")
table.add_column("Target")
table.add_column("Outcome")
for event in events:
table.add_row(
str(event.get("ts", ""))[:19],
str(event.get("kind", "")),
str(event.get("action", "")),
str(event.get("subject", ""))[:24],
str(event.get("target", ""))[:28],
str(event.get("outcome", "")),
)
console.print(table)
@worker_app.command("status")
def worker_status_cmd() -> None:
"""Show worker state: pending drafts, triage count, last digest, timer status."""
import subprocess
from warden.worker import worker_status
console.print(worker_status())
try:
st = subprocess.run(
["systemctl", "--user", "is-active", "ops-warden-worker.timer"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
console.print(f"timer : {st or 'unknown'}")
except Exception: # noqa: BLE001 — systemd may be absent (cron/other host)
console.print("timer : (systemd not available)")
# ---------------------------------------------------------------------------
# warden memory — cross-runtime experiential memory (WARDEN-WP-0024)
# ---------------------------------------------------------------------------
@memory_app.command("status")
def memory_status(
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Show canonical phase-memory store status (metadata only)."""
from warden import memory as warden_memory
if not warden_memory.memory_available():
err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]")
raise typer.Exit(2)
try:
payload = warden_memory.status()
except RuntimeError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(2)
if output_json:
print(json.dumps(payload, indent=2))
return
console.print(f"store_path : {payload.get('store_path', '')}")
console.print(f"profile_id : {payload.get('profile_id', '')}")
console.print(f"episode_count : {payload.get('episode_count', 0)}")
console.print(f"session_kinds : {payload.get('episode_counts_by_session_kind', {})}")
console.print(f"last_activation: {payload.get('last_activation_at') or ''}")
@memory_app.command("activate")
def memory_activate(
need: Annotated[str, typer.Option("--need", help="Optional routing need fingerprint source")] = "",
agent: Annotated[
Optional[str],
typer.Option("--agent", help="Agent id for session_kind warden.agent.<id> (claude, codex, grok, …)"),
] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Inspect or refresh coordination memory (optional — memory loads by default)."""
from warden import memory as warden_memory
if not warden_memory.memory_available():
err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]")
raise typer.Exit(2)
try:
payload = warden_memory.activate(need=need, agent=agent, implicit=False)
except RuntimeError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(2)
if output_json:
print(json.dumps(payload, indent=2))
return
console.print(warden_memory.format_activation_summary(payload))