feat(WARDEN-WP-0020): T3 — guarded executor (worker now acts, not just plans)

HubClient gains writes (mark_read, send_reply, add_progress). execute_plan/execute_plans
run the safe, allowlisted actions autonomously: route_answer (reply with the computed
answer + auto mark-read), reply (LLM-drafted body), progress_note, mark_read. Escalated
plans and non-auto-executable kinds are left for a human; every action is metadata-only
(no secret value read/sent/logged).

Deliberate guardrail: propose_catalog_diff and any code/routing change is NOT auto-executed
even under full-auto — a bad catalog commit could misroute credentials, so it goes to human
review (recoverability over convenience). AUTO_EXECUTABLE is the messaging/hub tier only.

`warden worker run --execute` runs the executor (dry-run still default). 7 executor tests
(reply+mark, with/without body, escalated skip, catalog-diff-left-for-human, progress,
failure-without-crash); 243 pass, lint clean. First live --execute shakedown is the
operator's (staged rollout); T4 schedules it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 23:19:13 +02:00
parent d36867f381
commit f8ac55367c
4 changed files with 226 additions and 25 deletions

View File

@@ -1171,30 +1171,34 @@ def worker_run(
Either way the allowlist + no-secret guardrails are enforced on every action. --execute
is rejected until the guarded executor (T3) ships; dry-run is the default.
"""
from warden.worker import HubClient, LlmConnectBrain, RuleBrain, build_plans, render_plans
if not dry_run:
err.print(
"[red]--execute is not available yet[/red] (WP-0020 T3). "
"The worker runs dry-run only until the guarded executor lands."
)
raise typer.Exit(2)
from warden.worker import (
HubClient, LlmConnectBrain, RuleBrain, build_plans, execute_plans, render_plans,
)
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 = HubClient().unread()
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)
console.print(render_plans(plans))
auto = sum(1 for p in plans if not p.escalated)
console.print(
f"\n[dim]{len(plans)} request(s): {auto} auto-actionable, "
f"{len(plans) - auto} need a human. (dry-run — nothing executed)[/dim]"
)
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: run the guarded executor. Topic for audit progress events.
topic_id = "cee7bedf-2b48-46ef-8601-006474f2ad7a"
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))