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.
This commit is contained in:
parent
5c6b71b83b
commit
5149946a4c
18 changed files with 1690 additions and 102 deletions
|
|
@ -365,6 +365,16 @@ def scorecard(
|
|||
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]")
|
||||
|
|
@ -718,7 +728,11 @@ def route_list(
|
|||
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)
|
||||
|
|
@ -729,6 +743,16 @@ def route_list(
|
|||
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).")
|
||||
|
|
@ -1414,14 +1438,16 @@ def _load_posture():
|
|||
def policy_list(
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""List both posture axes: environment postures and workload maturity levels."""
|
||||
"""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
|
||||
|
||||
|
|
@ -1438,6 +1464,19 @@ def policy_list(
|
|||
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]"
|
||||
|
|
@ -1446,19 +1485,39 @@ def policy_list(
|
|||
|
||||
@policy_app.command("show")
|
||||
def policy_show(
|
||||
descriptor_id: Annotated[str, typer.Argument(help="An env posture (dev/test/prod) or maturity level (M0–M3)")],
|
||||
descriptor_id: Annotated[
|
||||
str,
|
||||
typer.Argument(
|
||||
help="Env posture (dev/test/prod), maturity (M0–M3), or organization posture id (build)"
|
||||
),
|
||||
],
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Show one environment posture or maturity level."""
|
||||
"""Show one environment posture, maturity level, or organization posture."""
|
||||
cat = _load_posture()
|
||||
env = cat.env(descriptor_id)
|
||||
mat = cat.maturity(descriptor_id)
|
||||
if env is None and mat is None:
|
||||
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))
|
||||
|
|
@ -1475,6 +1534,259 @@ def policy_show(
|
|||
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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue