generated from coulomb/repo-seed
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user