Skip to content

fix(control-channel): allow web:setup over the ZMQ control channel - #17

Open
rakhimovv wants to merge 1 commit into
Noietch:mainfrom
rakhimovv:fix/control-channel-setup-verb
Open

fix(control-channel): allow web:setup over the ZMQ control channel#17
rakhimovv wants to merge 1 commit into
Noietch:mainfrom
rakhimovv:fix/control-channel-setup-verb

Conversation

@rakhimovv

@rakhimovv rakhimovv commented Sep 1, 2026

Copy link
Copy Markdown

Thanks for putting this out publicly — being able to run the whole stack against the fake nodes with no robot attached is what made it possible to find this.

The question behind the PR: is setup deliberately left out of _COMMANDS, or is it an omission? I could not find a reason for it in the code, so I have assumed the latter. Happy to close this if the exclusion is intentional.

What I ran into

run.py dispatches a setup verb, and the HTTP console reaches it:

  • src/core/app/run.py:729if verb == "setup":
  • src/core/app/console/server.py:3081"/api/setup": "web:setup"

But setup is not in _COMMANDS, so it is not in WEB_COMMAND_VERBS, which the ZMQ control channel uses as its allow-list (control_channel.py:29, checked at :73-74). The browser is unaffected — _enqueue at server.py:1525 puts the command on the queue directly, without consulting the allow-list — so this only shows up on the ZMQ path.

On main at 0e0e5f8, using your own test helpers:

import tests.app.test_control_channel as t
from core.app.control_channel import _handle_message

rt = t._runtime()
print(_handle_message(rt, {"cmd": "web:setup"}), rt.command_queue.qsize())
print(_handle_message(t._runtime(), {"cmd": "web:warmup"}))
{'ok': False, 'error': "command not allowed: 'setup'"} 0
{'ok': True, 'cmd': 'web:warmup'}

warmup is there as a control, so the channel itself works. setup specifically is refused, and nothing is queued.

The practical effect: a non-eval (deploy) config cannot be driven to a running state over the control channel. web:run is accepted, but _dispatch_run returns early at run.py:1117 on not session.is_setup_done. There is no log line, so it reads as a silent no-op. The sequence in the eva_ctl.py module docstring (select_modesetuprun) stops at the second step for this reason. Eval configs are unaffected, since bootstrap and warmup are both in the catalog.

Where it seems to have come from

control_channel.py originally carried its own inline _ALLOWED_VERBS, and that set did include "setup" (git show 8a4fc87:src/core/app/control_channel.py, line 52). When the list was centralised into command_catalog.py in 77fd2dc, the new _COMMANDS did not carry setup across. git log -p --follow -- src/core/app/command_catalog.py | grep -c '"setup"' returns 0.

The change

One catalog entry, plus two tests.

The existing consistency test at tests/app/test_control_channel.py:12-19 asserts only WEB_COMMAND_VERBS <= branches | {"select_collect_task"}. A verb that exists as a run.py branch but is missing from _COMMANDS satisfies that subset, so nothing flagged it. This PR adds the other direction, plus a channel-level test for web:setup. Both fail before the catalog entry:

FAILED tests/app/test_control_channel.py::test_every_dispatch_branch_is_reachable_over_the_control_channel
FAILED tests/app/test_control_channel.py::test_channel_accepts_setup_and_queues_it
2 failed, 4 passed

and pass with it. ruff check . and ruff format --check . are both clean.

The control ids on the new entry are the two bound to /api/setup in console/static/js/main.js:404,407; run.js:370 also fires it automatically with no button involved.

End-to-end check with the fix applied

To confirm the catalog entry is sufficient rather than just necessary, I drove a full offline stack — examples/fake_policy plus examples/hardware/agibot_g2/run_fake_node.sh plus headless eva on a deploy config with control_channel.enabled=True — entirely over ZMQ via eva_ctl:

setup: {"ok": true, "cmd": "web:setup"}
before_run={'session_status': 'ready',   'policy_connected': True, 'step_index': 0}
run:    {"ok": true, "cmd": "web:run"}
sampling: ('ready', 0) -> ('running', 15) -> ... -> ('running', 345)
final={'session_status': 'ready', 'step_index': 360}

Before the change the same script stops at command not allowed: 'setup', session_status stays unset, and step_index never leaves 0. So this one entry is what unblocks the whole headless deploy path, not just the single rejected command.

Possibly relevant to #14

Flagging this only in case it's useful for #14. Its MCP policy_run sends, in order, web:select_mode:real, web:switch_task:…, web:setup, web:run through EvaControlClient.require_ok, which raises RuntimeError on ok: false — so against a live EVA the third of those would hit this. As far as I can tell the stub client in tests/tools/test_mcp.py returns {"ok": True, ...} for any cmd, so I don't think that branch's own tests would surface it. I have not run that branch, so the runtime behaviour is inferred rather than observed. What I did check is its head (d6f40c0): _COMMANDS there still holds 47 verbs with warmup present and no setup entry, so the rejection path is unchanged on that branch.

Tested on Ubuntu 22.04, Python 3.11, fully offline (fake node plus examples/fake_policy, no hardware).

`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant