mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
fix: resolve cua-driver from user-local install paths
This commit is contained in:
parent
a8e6c0f853
commit
030822b68d
2 changed files with 98 additions and 8 deletions
|
|
@ -148,6 +148,41 @@ class TestRegistration:
|
|||
patch("tools.computer_use.cua_backend.cua_driver_binary_available", return_value=False):
|
||||
assert cu_tool.check_computer_use_requirements() is False
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX user-local path regression")
|
||||
def test_check_fn_finds_user_local_cua_driver_when_path_omits_it(self, tmp_path, monkeypatch):
|
||||
"""Desktop/TUI launched from Finder/Dock can omit ~/.local/bin from PATH.
|
||||
|
||||
The cua-driver installer commonly places the binary there, so the
|
||||
registry check must still expose the computer_use tool schema.
|
||||
"""
|
||||
from tools.computer_use import tool as cu_tool
|
||||
|
||||
driver = tmp_path / ".local" / "bin" / "cua-driver"
|
||||
driver.parent.mkdir(parents=True)
|
||||
driver.write_text("#!/bin/sh\nexit 0\n")
|
||||
driver.chmod(0o755)
|
||||
|
||||
monkeypatch.delenv("HERMES_CUA_DRIVER_CMD", raising=False)
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
|
||||
|
||||
with patch("tools.computer_use.tool.sys.platform", "darwin"), \
|
||||
patch("tools.computer_use.cua_backend.sys.platform", "darwin"):
|
||||
assert cu_tool.check_computer_use_requirements() is True
|
||||
|
||||
def test_cua_driver_cmd_env_override_is_resolved_dynamically(self, tmp_path, monkeypatch):
|
||||
from tools.computer_use import cua_backend
|
||||
|
||||
driver = tmp_path / "custom-cua-driver"
|
||||
driver.write_text("#!/bin/sh\nexit 0\n")
|
||||
driver.chmod(0o755)
|
||||
|
||||
monkeypatch.setenv("HERMES_CUA_DRIVER_CMD", str(driver))
|
||||
monkeypatch.setenv("PATH", "/usr/bin:/bin")
|
||||
|
||||
assert cua_backend.resolve_cua_driver_cmd() == str(driver)
|
||||
assert cua_backend.cua_driver_binary_available() is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch & action routing
|
||||
|
|
@ -2331,8 +2366,8 @@ class TestCuaEnvironmentScrubbing:
|
|||
return MagicMock()
|
||||
|
||||
with patch.dict(os.environ, test_env, clear=True), \
|
||||
patch("tools.computer_use.cua_backend.cua_driver_binary_available",
|
||||
return_value=True), \
|
||||
patch("tools.computer_use.cua_backend.resolve_cua_driver_cmd",
|
||||
return_value="cua-driver"), \
|
||||
patch("tools.computer_use.cua_backend._resolve_mcp_invocation",
|
||||
return_value=("cua-driver", ["mcp"])), \
|
||||
patch("mcp.StdioServerParameters", side_effect=capture_env), \
|
||||
|
|
|
|||
|
|
@ -137,7 +137,8 @@ def _action_result_from(
|
|||
# only have *looked* like it pinned. For a reproducible version, point
|
||||
# `HERMES_CUA_DRIVER_CMD` at a specific binary instead.
|
||||
|
||||
_CUA_DRIVER_CMD = os.environ.get("HERMES_CUA_DRIVER_CMD", "cua-driver")
|
||||
_CUA_DRIVER_CMD_ENV = "HERMES_CUA_DRIVER_CMD"
|
||||
_CUA_DRIVER_DEFAULT_CMD = "cua-driver"
|
||||
_CUA_DRIVER_ARGS = ["mcp"] # stdio MCP transport (fallback when the
|
||||
# driver doesn't expose `manifest` — see
|
||||
# `_resolve_mcp_invocation` below)
|
||||
|
|
@ -375,9 +376,59 @@ def _is_macos() -> bool:
|
|||
return sys.platform == "darwin"
|
||||
|
||||
|
||||
def _has_path_separator(value: str) -> bool:
|
||||
return os.sep in value or (os.altsep is not None and os.altsep in value)
|
||||
|
||||
|
||||
def _candidate_cua_driver_commands() -> List[str]:
|
||||
"""Return candidate cua-driver commands in resolution order.
|
||||
|
||||
Desktop apps launched from Finder/Dock often inherit a narrow PATH that
|
||||
omits user-local install directories. The upstream cua-driver installer
|
||||
commonly places the binary under ``~/.local/bin`` on POSIX systems, so a
|
||||
Hermes Desktop/TUI session can otherwise filter out the `computer_use`
|
||||
tool even though `hermes computer-use doctor` succeeds from a login shell.
|
||||
"""
|
||||
configured = os.environ.get(_CUA_DRIVER_CMD_ENV)
|
||||
if configured:
|
||||
# An explicit override is authoritative: if it is wrong, report the
|
||||
# driver missing instead of silently picking a different binary.
|
||||
return [configured]
|
||||
|
||||
candidates = [_CUA_DRIVER_DEFAULT_CMD]
|
||||
home = os.path.expanduser("~")
|
||||
if sys.platform == "win32":
|
||||
candidates.extend([
|
||||
os.path.join(home, ".local", "bin", "cua-driver.exe"),
|
||||
os.path.join(home, ".local", "bin", "cua-driver"),
|
||||
])
|
||||
else:
|
||||
candidates.extend([
|
||||
os.path.join(home, ".local", "bin", "cua-driver"),
|
||||
os.path.join(home, ".cargo", "bin", "cua-driver"),
|
||||
"/opt/homebrew/bin/cua-driver",
|
||||
"/usr/local/bin/cua-driver",
|
||||
])
|
||||
return candidates
|
||||
|
||||
|
||||
def resolve_cua_driver_cmd() -> Optional[str]:
|
||||
"""Resolve the cua-driver executable for PATH-constrained surfaces."""
|
||||
for candidate in _candidate_cua_driver_commands():
|
||||
expanded = os.path.expanduser(candidate)
|
||||
if _has_path_separator(expanded):
|
||||
if shutil.which(expanded):
|
||||
return expanded
|
||||
else:
|
||||
resolved = shutil.which(expanded)
|
||||
if resolved:
|
||||
return resolved
|
||||
return None
|
||||
|
||||
|
||||
def cua_driver_binary_available() -> bool:
|
||||
"""True if `cua-driver` is on $PATH or HERMES_CUA_DRIVER_CMD resolves."""
|
||||
return bool(shutil.which(_CUA_DRIVER_CMD))
|
||||
"""True if `cua-driver` resolves via env, PATH, or known install paths."""
|
||||
return resolve_cua_driver_cmd() is not None
|
||||
|
||||
|
||||
def cua_driver_update_check(*, timeout: float = 8.0) -> Optional[Dict[str, Any]]:
|
||||
|
|
@ -392,10 +443,13 @@ def cua_driver_update_check(*, timeout: float = 8.0) -> Optional[Dict[str, Any]]
|
|||
``error`` field is set), or the output didn't parse. Best-effort; never
|
||||
raises.
|
||||
"""
|
||||
driver_cmd = resolve_cua_driver_cmd()
|
||||
if not driver_cmd:
|
||||
return None
|
||||
try:
|
||||
from tools.environments.local import _sanitize_subprocess_env
|
||||
proc = subprocess.run(
|
||||
[_CUA_DRIVER_CMD, "check-update", "--json"],
|
||||
[driver_cmd, "check-update", "--json"],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
# Some older drivers don't have the verb and fall through to a
|
||||
# stdin-reading mode rather than erroring — DEVNULL gives them EOF
|
||||
|
|
@ -749,14 +803,15 @@ class _CuaDriverSession:
|
|||
self._startup_phase = "binary-check"
|
||||
|
||||
try:
|
||||
if not cua_driver_binary_available():
|
||||
driver_cmd = resolve_cua_driver_cmd()
|
||||
if not driver_cmd:
|
||||
raise RuntimeError(cua_driver_install_hint())
|
||||
|
||||
# Surface 8: ask cua-driver itself which subcommand spawns
|
||||
# the MCP server, instead of hardcoding ["mcp"]. Falls back
|
||||
# transparently for older drivers / any discovery failure.
|
||||
self._startup_phase = "manifest-discovery"
|
||||
command, args = _resolve_mcp_invocation(_CUA_DRIVER_CMD)
|
||||
command, args = _resolve_mcp_invocation(driver_cmd)
|
||||
_t_manifest = _time.monotonic()
|
||||
params = StdioServerParameters(
|
||||
command=command,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue