fix(computer_use): probe cua-driver-rs release tag, not monorepo releases/latest

The install pre-flight asset probe queried trycua/cua's `releases/latest`,
which floats across the monorepo's components (agent-*, computer-*, lume-*,
train-*) — most ship zero binary assets. So the probe false-negatived and
hard-blocked `install_cua_driver` (line 770: `if not probe: return False`)
BEFORE the upstream installer ran, on Linux, Windows, and Intel macOS — even
though the installer it gates resolves the right tag and would have succeeded.

Net effect: the normal enable path (`hermes tools` → Computer Use post-setup,
and `hermes computer-use install`) refused to install on every platform this
PR claims to support.

Fix: list `/releases?per_page=100`, pick the newest `cua-driver-rs-v*` tag,
and match its assets on OS-token + arch — mirroring what the upstream
`install.sh` already does. Fail open if no driver release surfaces (installer
remains the source of truth). Adds an OS-token gate so a darwin asset can't
satisfy a Linux probe.

Tests: updated the install-probe fixtures to the list-of-releases shape with
`cua-driver-rs-v*` tags + OS-token asset names; added a regression guard
(`test_releases_latest_tag_ignored_picks_driver_rs_tag`) for the monorepo
floating-latest case. 25/25 install + 192 computer_use tests green.

Verified live: probe returns True for all six platform/arch combos against
the real GitHub releases API.
This commit is contained in:
teknium1 2026-06-22 06:30:16 -07:00 committed by Teknium
parent e3505c7f73
commit 38c56a1e86
2 changed files with 97 additions and 41 deletions

View file

@ -689,24 +689,52 @@ def _check_cua_driver_asset_for_arch() -> bool:
# Unknown arch — fail open and let the installer surface the error.
return True
# Probe the latest release for an OS+arch asset before falling through to
# the upstream installer.
# Probe the cua-driver release for an OS+arch asset before falling through
# to the upstream installer.
#
# The cua-driver-rs binaries are published to the trycua/cua monorepo under
# tag prefix ``cua-driver-rs-v*``. The repo's ``releases/latest`` is NOT
# that — it floats across the monorepo's other components (agent-*,
# computer-*, lume-*, train-*), most of which ship zero binary assets. So
# we list releases and pick the newest ``cua-driver-rs-v*`` tag, matching
# what the upstream install.sh does. Failing to find one => fail open and
# let the installer (which resolves the tag itself) be the source of truth.
driver_tag_prefix = "cua-driver-rs-v"
api_url = (
"https://api.github.com/repos/trycua/cua/releases/latest"
"https://api.github.com/repos/trycua/cua/releases?per_page=100"
)
try:
req = urllib.request.Request(api_url, headers={"Accept": "application/vnd.github+json"})
with urllib.request.urlopen(req, timeout=10) as resp:
release = _json.loads(resp.read().decode())
tag = release.get("tag_name", "")
assets = release.get("assets", [])
releases = _json.loads(resp.read().decode())
if not isinstance(releases, list):
return True
# GitHub returns releases newest-first; take the first cua-driver-rs tag.
driver_release = next(
(
r for r in releases
if str(r.get("tag_name", "")).startswith(driver_tag_prefix)
),
None,
)
if driver_release is None:
# No cua-driver-rs release surfaced (API hiccup / unexpected shape).
# Fail open — the installer resolves the tag on its own.
return True
tag = driver_release.get("tag_name", "")
assets = driver_release.get("assets", [])
# OS token gates the asset alongside arch so a darwin asset can't
# satisfy a Linux probe (every cua-driver-rs release ships all three
# OSes, so the arch token alone would always match).
os_token = {"Darwin": "darwin", "Windows": "windows", "Linux": "linux"}.get(system, "")
has_asset = any(
any(a in a_info.get("name", "").lower() for a in arch_names)
os_token in (name := a_info.get("name", "").lower())
and any(a in name for a in arch_names)
for a_info in assets
)
if not has_asset:
_print_warning(
f" Latest CUA release ({tag}) has no {system} {arch_label} asset."
f" Latest cua-driver release ({tag}) has no {system} {arch_label} asset."
)
_print_info(
" CUA Driver may not yet ship a build for this platform."

View file

@ -108,38 +108,40 @@ class TestCheckCuaDriverAssetForArch:
def test_x86_64_with_asset_returns_true(self):
from hermes_cli import tools_config
release = {
"tag_name": "cua-driver-v0.1.6",
releases = [{
"tag_name": "cua-driver-rs-v0.1.6",
"assets": [
{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"},
{"name": "cua-driver-0.1.6-darwin-x86_64.tar.gz"},
{"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"},
{"name": "cua-driver-rs-0.1.6-darwin-x86_64.tar.gz"},
],
}
}]
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(release).encode()
mock_resp.read.return_value = json.dumps(releases).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
with patch("platform.machine", return_value="x86_64"), \
with patch("platform.system", return_value="Darwin"), \
patch("platform.machine", return_value="x86_64"), \
patch("urllib.request.urlopen", return_value=mock_resp):
assert tools_config._check_cua_driver_asset_for_arch() is True
def test_x86_64_without_asset_returns_false(self):
from hermes_cli import tools_config
release = {
"tag_name": "cua-driver-v0.1.6",
releases = [{
"tag_name": "cua-driver-rs-v0.1.6",
"assets": [
{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"},
{"name": "cua-driver.tar.gz"},
{"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"},
{"name": "cua-driver-rs.tar.gz"},
],
}
}]
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(release).encode()
mock_resp.read.return_value = json.dumps(releases).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
with patch("platform.machine", return_value="x86_64"), \
with patch("platform.system", return_value="Darwin"), \
patch("platform.machine", return_value="x86_64"), \
patch("urllib.request.urlopen", return_value=mock_resp), \
patch.object(tools_config, "_print_warning") as warn, \
patch.object(tools_config, "_print_info"):
@ -159,12 +161,12 @@ class TestCheckCuaDriverAssetForArch:
"""When the latest release has no Intel asset, skip the installer."""
from hermes_cli import tools_config
release = {
"tag_name": "cua-driver-v0.1.6",
"assets": [{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"}],
}
releases = [{
"tag_name": "cua-driver-rs-v0.1.6",
"assets": [{"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"}],
}]
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(release).encode()
mock_resp.read.return_value = json.dumps(releases).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
@ -183,12 +185,12 @@ class TestCheckCuaDriverAssetForArch:
"""On upgrade with no Intel asset, return whether binary existed."""
from hermes_cli import tools_config
release = {
"tag_name": "cua-driver-v0.1.6",
"assets": [{"name": "cua-driver-0.1.6-darwin-arm64.tar.gz"}],
}
releases = [{
"tag_name": "cua-driver-rs-v0.1.6",
"assets": [{"name": "cua-driver-rs-0.1.6-darwin-arm64.tar.gz"}],
}]
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(release).encode()
mock_resp.read.return_value = json.dumps(releases).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
@ -346,10 +348,12 @@ class TestCheckCuaDriverAssetCrossPlatform:
@staticmethod
def _mock_release(asset_names):
release = {"tag_name": "cua-driver-v0.5.0",
"assets": [{"name": n} for n in asset_names]}
# The probe lists /releases and picks the newest cua-driver-rs-v* tag,
# so the mock returns a LIST of releases with that tag prefix.
releases = [{"tag_name": "cua-driver-rs-v0.5.0",
"assets": [{"name": n} for n in asset_names]}]
resp = MagicMock()
resp.read.return_value = json.dumps(release).encode()
resp.read.return_value = json.dumps(releases).encode()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
@ -358,8 +362,8 @@ class TestCheckCuaDriverAssetCrossPlatform:
from hermes_cli import tools_config
resp = self._mock_release([
"cua-driver-0.5.0-windows-amd64.zip",
"cua-driver-0.5.0-darwin-arm64.tar.gz",
"cua-driver-rs-0.5.0-windows-x86_64.zip",
"cua-driver-rs-0.5.0-darwin-arm64.tar.gz",
])
with patch("platform.system", return_value="Windows"), \
patch("platform.machine", return_value="AMD64"), \
@ -370,7 +374,7 @@ class TestCheckCuaDriverAssetCrossPlatform:
from hermes_cli import tools_config
resp = self._mock_release([
"cua-driver-0.5.0-windows-amd64.zip",
"cua-driver-rs-0.5.0-windows-x86_64.zip",
])
with patch("platform.system", return_value="Windows"), \
patch("platform.machine", return_value="ARM64"), \
@ -385,7 +389,7 @@ class TestCheckCuaDriverAssetCrossPlatform:
from hermes_cli import tools_config
resp = self._mock_release([
"cua-driver-0.5.0-linux-x86_64.tar.gz",
"cua-driver-rs-0.5.0-linux-x86_64.tar.gz",
])
with patch("platform.system", return_value="Linux"), \
patch("platform.machine", return_value="x86_64"), \
@ -396,7 +400,7 @@ class TestCheckCuaDriverAssetCrossPlatform:
from hermes_cli import tools_config
resp = self._mock_release([
"cua-driver-0.5.0-linux-aarch64.tar.gz",
"cua-driver-rs-0.5.0-linux-arm64.tar.gz",
])
with patch("platform.system", return_value="Linux"), \
patch("platform.machine", return_value="aarch64"), \
@ -407,7 +411,7 @@ class TestCheckCuaDriverAssetCrossPlatform:
from hermes_cli import tools_config
resp = self._mock_release([
"cua-driver-0.5.0-linux-x86_64.tar.gz",
"cua-driver-rs-0.5.0-linux-x86_64.tar.gz",
])
with patch("platform.system", return_value="Linux"), \
patch("platform.machine", return_value="aarch64"), \
@ -416,3 +420,27 @@ class TestCheckCuaDriverAssetCrossPlatform:
patch.object(tools_config, "_print_info"):
assert tools_config._check_cua_driver_asset_for_arch() is False
warn.assert_called_once()
def test_releases_latest_tag_ignored_picks_driver_rs_tag(self):
"""A non-driver tag at the head of the list must not gate the probe.
Regression guard: the monorepo's newest release is often a Python
component (agent-*, computer-*) with zero binary assets. The probe
must skip past it to the newest cua-driver-rs-v* release.
"""
from hermes_cli import tools_config
releases = [
{"tag_name": "agent-v0.8.3", "assets": []},
{"tag_name": "computer-v0.5.19", "assets": []},
{"tag_name": "cua-driver-rs-v0.6.0",
"assets": [{"name": "cua-driver-rs-0.6.0-linux-x86_64-binary.tar.gz"}]},
]
resp = MagicMock()
resp.read.return_value = json.dumps(releases).encode()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
with patch("platform.system", return_value="Linux"), \
patch("platform.machine", return_value="x86_64"), \
patch("urllib.request.urlopen", return_value=resp):
assert tools_config._check_cua_driver_asset_for_arch() is True