From 5fdc9d2a9d4d1315a572b3104fe6e14f9ed94a73 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:02:19 -0700 Subject: [PATCH] fix: pin cua-driver refresh to the release check-update confirmed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream cua-driver installer scripts on trycua/cua@main carry a baked default version that Release Please bumps in the release PR *before* the release assets are published. During that window an unpinned installer run 404s on the asset download and the `hermes update` cua-driver refresh fails with: error: download failed: The remote server returned an error: (404) Not Found. ⚠ cua-driver refreshing did not complete. Re-run manually: ... Observed live 2026-07-29: baked version 0.14.0 vs latest published release 0.13.1 — every `hermes update` run with an out-of-date driver hit the warning until upstream publishes the assets. We already know the correct version: `cua-driver check-update --json` returns `latest_version` straight from the GitHub Releases API, whose entries by definition have published assets. When the check positively confirms an update, export that version as CUA_DRIVER_RS_VERSION into the installer child env (both install.sh and install.ps1 honour it over their baked default), so the refresh downloads the release that actually exists instead of racing the upstream release pipeline. Malformed / missing latest_version values fall back to the previous unpinned behaviour. The explicit `hermes computer-use install --upgrade` force path and fresh installs are unchanged. --- hermes_cli/tools_config.py | 41 ++++++- tests/hermes_cli/test_install_cua_driver.py | 113 ++++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index fdf6f343ed8df..56b5d9d8c6521 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -1002,6 +1002,7 @@ def install_cua_driver(upgrade: bool = False, require_confirmed_update: bool = F # multi-minute silent reinstall on every update. An explicit # `hermes computer-use install --upgrade` falls through and re-runs the # installer as before. + confirmed_version = None if binary: _state = None try: @@ -1025,6 +1026,19 @@ def install_cua_driver(upgrade: bool = False, require_confirmed_update: bool = F " Force a refresh with: hermes computer-use install --upgrade" ) return True + if _state is not None and _state.get("update_available"): + # Pin the installer to the release check-update just confirmed. + # `latest_version` comes from the GitHub Releases API, so its + # assets are published — unlike the installer script's baked + # version on `main`, which Release Please bumps in the release + # PR *before* the release assets exist. Installing unpinned in + # that window 404s (observed: baked 0.14.0 vs latest published + # 0.13.1). Malformed values are ignored → unpinned fallback. + import re as _re + + _latest = str(_state.get("latest_version") or "").strip().lstrip("vV") + if _re.fullmatch(r"\d+(\.\d+)*", _latest): + confirmed_version = _latest if binary: # Show before/after version when we have a baseline. Best-effort. @@ -1039,7 +1053,9 @@ def install_cua_driver(upgrade: bool = False, require_confirmed_update: bool = F else: before = "" - ok = _run_cua_driver_installer(label="Refreshing", verbose=False) + ok = _run_cua_driver_installer( + label="Refreshing", verbose=False, pin_version=confirmed_version + ) if ok and before: try: after = subprocess.run( @@ -1226,7 +1242,11 @@ def _clear_stale_cua_install_lock() -> None: logger.debug("stale cua install lock check failed: %s", e) -def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) -> bool: +def _run_cua_driver_installer( + label: str = "Installing", + verbose: bool = True, + pin_version: Optional[str] = None, +) -> bool: """Run the upstream cua-driver installer for this platform. The scripts are idempotent: they always download the latest release, so @@ -1235,6 +1255,13 @@ def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) - * macOS / Linux → ``curl -fsSL …/install.sh | /bin/bash``. * Windows → ``powershell -NoProfile -ExecutionPolicy Bypass -Command "irm …/install.ps1 | iex"``. + + ``pin_version`` (e.g. ``"0.13.1"``) is exported as + ``CUA_DRIVER_RS_VERSION`` so the installer downloads that exact release + instead of its baked-in default. The baked version on upstream ``main`` + is bumped by Release Please *before* the release assets are published, + so an unpinned run inside that window fails with a 404; pinning to the + version ``check-update`` confirmed sidesteps the race entirely. """ import platform as _plat import shutil @@ -1306,6 +1333,12 @@ def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) - _print_info(f" {label} cua-driver...") driver_cmd = _cua_driver_cmd() + installer_env = _cua_driver_env() + if pin_version: + # Both upstream installers (install.sh and install.ps1) honour + # CUA_DRIVER_RS_VERSION over their baked default. + installer_env["CUA_DRIVER_RS_VERSION"] = pin_version + # A previous timed-out install can leave the upstream installer's # concurrent-install lock behind; clear it when provably stale so the # refresh doesn't wedge waiting on a dead holder (issue #58762). @@ -1378,7 +1411,7 @@ def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) - # keep streaming live. if verbose: proc = subprocess.Popen( - install_cmd, shell=use_shell, env=_cua_driver_env(), + install_cmd, shell=use_shell, env=installer_env, creationflags=_post_setup_no_window_flags(streams_to_console=True), **popen_kwargs ) @@ -1393,7 +1426,7 @@ def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) - ) else: proc = subprocess.Popen( - install_cmd, shell=use_shell, env=_cua_driver_env(), + install_cmd, shell=use_shell, env=installer_env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", creationflags=_post_setup_no_window_flags(), diff --git a/tests/hermes_cli/test_install_cua_driver.py b/tests/hermes_cli/test_install_cua_driver.py index bd532ac7fb814..78cfbb25bd6da 100644 --- a/tests/hermes_cli/test_install_cua_driver.py +++ b/tests/hermes_cli/test_install_cua_driver.py @@ -712,3 +712,116 @@ class TestInstallerNoShell: assert "script" in captured assert not os.path.exists(captured["script"]) + + +class TestConfirmedVersionPinning: + """When check-update confirms a newer release, the installer run must be + pinned to that exact version via CUA_DRIVER_RS_VERSION. + + The upstream installer scripts on `main` carry a baked version that + Release Please bumps in the release PR *before* the release assets are + published. An unpinned install inside that window 404s (observed + 2026-07-29: baked 0.14.0 vs latest published release 0.13.1). Pinning to + check-update's `latest_version` — which comes from the Releases API and + therefore has published assets — sidesteps the race. + """ + + def _install(self, check_state): + from unittest.mock import MagicMock + + from hermes_cli import tools_config + + with patch("platform.system", return_value="Windows"), \ + patch.object(tools_config.shutil, "which", + side_effect=lambda n: "/x/" + n + if n in {"cua-driver", "curl", "powershell"} else None), \ + patch.object(tools_config, "_resolved_cua_driver_cmd", + return_value="/x/cua-driver.exe"), \ + patch.object(tools_config, "_cua_install_target_writable", + return_value=True), \ + patch("tools.computer_use.cua_backend.cua_driver_update_check", + return_value=check_state), \ + patch.object(tools_config, "_run_cua_driver_installer", + return_value=True) as runner, \ + patch("subprocess.run", + return_value=MagicMock(stdout="cua-driver 0.5.0", returncode=0)), \ + patch.object(tools_config, "_print_success"), \ + patch.object(tools_config, "_print_warning"), \ + patch.object(tools_config, "_print_info"): + ok = tools_config.install_cua_driver( + upgrade=True, require_confirmed_update=True + ) + return ok, runner + + def test_confirmed_update_pins_latest_version(self): + state = {"current_version": "0.12.6", "latest_version": "0.13.1", + "update_available": True} + ok, runner = self._install(state) + assert ok is True + assert runner.call_args.kwargs.get("pin_version") == "0.13.1" + + def test_v_prefixed_latest_version_is_normalized(self): + state = {"current_version": "0.12.6", "latest_version": "v0.13.1", + "update_available": True} + ok, runner = self._install(state) + assert ok is True + assert runner.call_args.kwargs.get("pin_version") == "0.13.1" + + def test_malformed_latest_version_falls_back_unpinned(self): + state = {"current_version": "0.12.6", "latest_version": "not a version", + "update_available": True} + ok, runner = self._install(state) + assert ok is True + assert runner.call_args.kwargs.get("pin_version") is None + + def test_missing_latest_version_falls_back_unpinned(self): + state = {"current_version": "0.12.6", "update_available": True} + ok, runner = self._install(state) + assert ok is True + assert runner.call_args.kwargs.get("pin_version") is None + + +class TestRunInstallerPinEnv: + """_run_cua_driver_installer(pin_version=...) exports CUA_DRIVER_RS_VERSION + into the installer child env; unpinned runs leave it untouched.""" + + def _run(self, pin_version): + import subprocess + from unittest.mock import MagicMock + + from hermes_cli import tools_config + + captured = {} + fake_proc = MagicMock() + fake_proc.pid = 1 + fake_proc.returncode = 1 + fake_proc.communicate.return_value = ("", None) + + def fake_popen(cmd, **kw): + captured["env"] = kw.get("env") + return fake_proc + + def fake_run(cmd, **kw): + m = MagicMock(); m.returncode = 0; m.stderr = "" + return m + + with patch("platform.system", return_value="Linux"), \ + patch("subprocess.run", side_effect=fake_run), \ + patch("subprocess.Popen", side_effect=fake_popen), \ + patch.object(tools_config, "_cua_driver_env", + return_value={"PATH": "/usr/bin"}), \ + patch.object(tools_config, "_clear_stale_cua_install_lock"), \ + patch.object(tools_config, "_print_warning"), \ + patch.object(tools_config, "_print_info"): + tools_config._run_cua_driver_installer( + label="Refreshing", verbose=False, pin_version=pin_version + ) + return captured.get("env") or {} + + def test_pin_version_exported_to_installer_env(self): + env = self._run("0.13.1") + assert env.get("CUA_DRIVER_RS_VERSION") == "0.13.1" + + def test_no_pin_leaves_env_untouched(self): + env = self._run(None) + assert "CUA_DRIVER_RS_VERSION" not in env