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

@@ -171,7 +171,91 @@ def test_cli_worker_dry_run(monkeypatch):
assert "nothing executed" in r.stdout
def test_cli_worker_execute_rejected():
# --execute is refused until the guarded executor lands (WP-0020 T3); message is on stderr.
def test_cli_worker_execute_runs(monkeypatch):
# --execute now runs the guarded executor; empty inbox → clean exit.
monkeypatch.setattr("warden.worker.HubClient.unread", lambda self, to_agent="ops-warden": [])
r = runner.invoke(app, ["worker", "run", "--execute"])
assert r.exit_code == 2
assert r.exit_code == 0
# --- executor (T3) -----------------------------------------------------------
class _FakeHub:
def __init__(self):
self.calls = []
def mark_read(self, message_id):
self.calls.append(("mark_read", message_id))
def send_reply(self, *, to_agent, subject, body, thread_id=None, from_agent="ops-warden"):
self.calls.append(("reply", to_agent, subject, body, thread_id))
def add_progress(self, *, summary, topic_id, event_type="note", author="ops-warden"):
self.calls.append(("progress", summary))
def _plan(actions, **over):
base = dict(message_id="m1", from_agent="alice", subject="where?", actions=actions,
raw={"thread_id": "t1"})
base.update(over)
return WorkerPlan(**base)
def test_executor_route_answer_replies_and_marks_read():
from warden.worker import execute_plan
hub = _FakeHub()
a = PlannedAction(kind="route_answer", summary="ans", payload={"answer": "the answer"})
execute_plan(_plan([a]), hub)
kinds = [c[0] for c in hub.calls]
assert "reply" in kinds and "mark_read" in kinds
reply = next(c for c in hub.calls if c[0] == "reply")
assert reply[3] == "the answer" and reply[2].lower().startswith("re:")
def test_executor_reply_with_body():
from warden.worker import execute_plan
hub = _FakeHub()
a = PlannedAction(kind="reply", summary="ack", payload={"body": "acknowledged"})
execute_plan(_plan([a]), hub)
assert any(c[0] == "reply" and c[3] == "acknowledged" for c in hub.calls)
def test_executor_reply_without_body_left_for_human():
from warden.worker import execute_plan
hub = _FakeHub()
out = execute_plan(_plan([PlannedAction(kind="reply", summary="ack")]), hub)
assert not any(c[0] == "reply" for c in hub.calls)
assert any("left for human" in r for r in out)
def test_executor_skips_escalated_plan():
from warden.worker import execute_plan
hub = _FakeHub()
a = PlannedAction(kind="reply", summary="x", risk="escalate", reason="secret")
out = execute_plan(_plan([a]), hub)
assert hub.calls == []
assert any("escalate" in r for r in out)
def test_executor_leaves_catalog_diff_for_human():
from warden.worker import execute_plan
hub = _FakeHub()
out = execute_plan(_plan([PlannedAction(kind="propose_catalog_diff", summary="change X")]), hub)
assert hub.calls == []
assert any("left for human: propose_catalog_diff" in r for r in out)
def test_executor_progress_note():
from warden.worker import execute_plan
hub = _FakeHub()
execute_plan(_plan([PlannedAction(kind="progress_note", summary="did X")]), hub, topic_id="t")
assert any(c[0] == "progress" for c in hub.calls)
def test_executor_reports_failure_without_crashing():
from warden.worker import execute_plan
class Boom(_FakeHub):
def mark_read(self, message_id):
raise RuntimeError("hub down")
out = execute_plan(_plan([PlannedAction(kind="mark_read", summary="x")]), Boom())
assert any("FAILED" in r for r in out)