feat(WARDEN-WP-0020): T2 — llm-connect brain (autonomous worker now thinks)

llm-connect is operational (operator set OPENROUTER_API_KEY). Contract discovered from
the running service: POST /execute {"prompt":...} -> {"content":...}.

LlmConnectBrain embeds the fixed charter + the inbox message as untrusted data, calls
/execute, and parses a JSON action plan (_extract_json tolerates fences/prose), escalating
defensively on malformed/empty/transport errors. The build_plans guardrail still enforces
the allowlist + no-secret invariant on whatever the model returns — the LLM cannot widen
ops-warden's authority. `warden worker run --brain rule|llm` selects the planner.

Live-verified on the real inbox: the LLM brain planned a sensible reply+mark_read for a
secrets-engine coordination message and correctly escalated a secret-custody request as
out-of-lane — better classification than the deterministic RuleBrain.

6 new tests, 236 pass, lint clean. T3 (guarded executor) and T4 (scheduling) remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 23:10:28 +02:00
parent 4287eccc80
commit 859beed07f
4 changed files with 169 additions and 9 deletions

View File

@@ -1160,14 +1160,18 @@ def worker_run(
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",
) -> None:
"""Read ops-warden's unread coordination requests and render a guardrailed plan.
T1 is dry-run only: it plans with the deterministic RuleBrain and applies the
allowlist + no-secret guardrails. The llm-connect brain (T2) and executor (T3) plug
into the same plan contract; --execute is rejected until T3 ships.
Plans with the deterministic RuleBrain (default) or the llm-connect brain (--brain llm).
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, RuleBrain, build_plans, render_plans
from warden.worker import HubClient, LlmConnectBrain, RuleBrain, build_plans, render_plans
if not dry_run:
err.print(
@@ -1176,13 +1180,18 @@ def worker_run(
)
raise typer.Exit(2)
if brain not in ("rule", "llm"):
err.print(f"[red]Unknown --brain {brain!r}[/red] (expected 'rule' or 'llm').")
raise typer.Exit(2)
try:
messages = HubClient().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)
plans = build_plans(messages, RuleBrain())
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(