mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
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:
parent
e3505c7f73
commit
38c56a1e86
2 changed files with 97 additions and 41 deletions
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue