From 88216097608c85cbd731ae4bfa759b104c70220e Mon Sep 17 00:00:00 2001 From: Ruslan Rakhimov Date: Wed, 2 Sep 2026 00:41:46 +0300 Subject: [PATCH] fix(control-channel): allow web:setup over the ZMQ control channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `setup` is dispatched in run.py and routed by the console as `/api/setup`, but it was never listed in `_COMMANDS`, so it is absent from `WEB_COMMAND_VERBS` — the allow-list the ZMQ control channel checks. The browser is unaffected because `_enqueue` puts commands on the queue directly, so this only refused the ZMQ path, with `command not allowed: 'setup'`. Without it a non-eval (deploy) config cannot be driven to a running state over the channel: `web:run` is accepted but `_dispatch_run` returns early on `not session.is_setup_done`, with no log line. The sequence in the `eva_ctl.py` docstring fails at `web:setup` for this reason. The existing consistency test only asserts `WEB_COMMAND_VERBS <= branches`, which a verb missing from the catalog satisfies trivially, so add the other direction plus a channel-level test. Both fail before this change. --- src/core/app/command_catalog.py | 1 + tests/app/test_control_channel.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/core/app/command_catalog.py b/src/core/app/command_catalog.py index 1b1a2a5..05cfcf3 100644 --- a/src/core/app/command_catalog.py +++ b/src/core/app/command_catalog.py @@ -5,6 +5,7 @@ _COMMANDS = ( ("bootstrap", "web:bootstrap", ()), ("warmup", "web:warmup", ("be-setup",)), + ("setup", "web:setup", ("b-setup-retry", "b-replay-setup-retry")), ("switch_task", "web:switch_task:{task}", ("prompt-list",)), ("rl_select_task", "web:rl_select_task:{task}", ("rl-task-list",)), ("rl_select_policy", "web:rl_select_policy:{slot}", ("rl-policy-list",)), diff --git a/tests/app/test_control_channel.py b/tests/app/test_control_channel.py index 0771ed3..e530451 100644 --- a/tests/app/test_control_channel.py +++ b/tests/app/test_control_channel.py @@ -19,6 +19,19 @@ def test_catalog_matches_web_command_dispatch_branches() -> None: assert WEB_COMMAND_VERBS <= branches | {"select_collect_task"} +def test_every_dispatch_branch_is_reachable_over_the_control_channel() -> None: + """The catalog is the channel's allow-list, so a verb run.py dispatches but the + catalog omits is unreachable over ZMQ while still working in the browser.""" + source = inspect.getsource(run._handle_web_command) + branches = { + line.split('"', 2)[1] + for line in source.splitlines() + if line.lstrip().startswith('if verb == "') + } + assert branches, "no dispatch branches found; the parser above stopped matching" + assert branches <= WEB_COMMAND_VERBS + + def test_catalog_is_json_ready_and_exposes_rl_templates() -> None: entries = control_command_catalog() assert all(set(entry) == {"verb", "command", "controls"} for entry in entries) @@ -50,6 +63,13 @@ def test_channel_rejects_unknown_commands_without_queueing() -> None: assert runtime.command_queue.empty() +def test_channel_accepts_setup_and_queues_it() -> None: + runtime = _runtime() + reply = _handle_message(runtime, {"cmd": "web:setup"}) + assert reply == {"ok": True, "cmd": "web:setup"} + assert runtime.command_queue.get_nowait() == "web:setup" + + def test_channel_handles_rl_and_collect_commands() -> None: runtime = _runtime()