Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 changed files with 3442369 additions and 0 deletions
+471
View File
@@ -0,0 +1,471 @@
"""Opt-in macOS smoke test for the installed cua-driver live MCP contract.
This script never installs, updates, or grants an existing browser profile. Start
an isolated daemon separately, then point this script at its socket:
cua-driver serve --embedded --socket /tmp/hermes-cua-0-9-live.sock \
--no-permissions-gate --no-overlay
CUA_DRIVER_LIVE_SOCKET=/tmp/hermes-cua-0-9-live.sock \
.venv/bin/python tests/computer_use/live_cua_0_9_smoke.py
The output deliberately excludes process IDs, window IDs, socket paths, and
driver payloads. Each cell is classified as pass, structured_refusal,
environment_unavailable, or unproven.
"""
import asyncio
import json
import os
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path
from typing import Any
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
def structured(result: Any) -> dict[str, Any]:
value = getattr(result, "structuredContent", None)
if isinstance(value, dict):
return value
dumped = result.model_dump(by_alias=True) if hasattr(result, "model_dump") else {}
for key in ("structuredContent", "structured_content"):
value = dumped.get(key)
if isinstance(value, dict):
return value
for block in getattr(result, "content", []) or []:
text = getattr(block, "text", None)
if not isinstance(text, str):
continue
try:
value = json.loads(text)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
return value
return {}
def refusal_code(payload: dict[str, Any]) -> str | None:
refusal = payload.get("refusal")
return payload.get("code") or (
refusal.get("code") if isinstance(refusal, dict) else None
)
def textedit_process_contains(pid: int, marker: str) -> bool:
"""Read the exact throwaway process through the native AX script bridge."""
script = """
on run argv
set targetPid to item 1 of argv as integer
set markerText to item 2 of argv
tell application "System Events"
tell first application process whose unix id is targetPid
set documentText to value of text area 1 of scroll area 1 of window 1
end tell
end tell
return (documentText contains markerText) as text
end run
"""
try:
result = subprocess.run(
["osascript", "-e", script, "--", str(pid), marker],
capture_output=True,
text=True,
timeout=5,
check=False,
)
except (OSError, subprocess.SubprocessError):
return False
return result.returncode == 0 and result.stdout.strip().lower() == "true"
async def run_smoke(socket_path: str) -> dict[str, dict[str, Any]]:
session_id = f"hermes-cua-live-{uuid.uuid4().hex[:8]}"
params = StdioServerParameters(
command="cua-driver",
args=["mcp", "--embedded", "--socket", socket_path],
)
report: dict[str, dict[str, Any]] = {
"foreground": {"classification": "unproven"},
"typed_browser": {"classification": "unproven"},
}
launched_pid: int | None = None
isolated_browser_pid: int | None = None
browser_pid: int | None = None
prior_foreground_pids: set[int] = set()
file_descriptor, temporary_name = tempfile.mkstemp(
prefix="hermes-cua-live-", suffix=".txt"
)
os.close(file_descriptor)
smoke_path = Path(temporary_name)
try:
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as client:
await client.initialize()
await client.call_tool("start_session", {"session": session_id})
try:
before_windows = structured(
await client.call_tool(
"list_windows",
{"on_screen_only": True, "session": session_id},
)
)
prior_foreground_pids = {
pid
for row in before_windows.get("windows") or []
if "textedit" in str(row.get("app_name") or "").lower()
and isinstance((pid := row.get("pid")), int)
}
launched = structured(
await client.call_tool(
"launch_app",
{
"name": "TextEdit",
"urls": [smoke_path.as_uri()],
"creates_new_application_instance": True,
"session": session_id,
},
)
)
launched_pid = launched.get("pid")
windows = launched.get("windows") or []
if isinstance(launched_pid, int) and not windows:
await client.call_tool(
"wait", {"seconds": 1, "session": session_id}
)
refreshed = structured(
await client.call_tool(
"list_windows",
{"on_screen_only": True, "session": session_id},
)
)
windows = [
row
for row in refreshed.get("windows") or []
if row.get("pid") == launched_pid
]
window_id = windows[0].get("window_id") if windows else None
if (
not isinstance(launched_pid, int)
or launched_pid in prior_foreground_pids
or not isinstance(window_id, int)
):
report["foreground"] = {
"classification": "environment_unavailable",
"stage": "throwaway_target",
}
else:
focus = await client.call_tool(
"bring_to_front",
{"pid": launched_pid, "window_id": window_id},
)
before = structured(
await client.call_tool(
"get_window_state",
{
"pid": launched_pid,
"window_id": window_id,
"session": session_id,
},
)
)
editor = next(
(
element
for element in before.get("elements") or []
if str(element.get("role") or "").lower()
in {"axtextarea", "axtextfield"}
),
None,
)
if not isinstance(editor, dict):
report["foreground"] = {
"classification": "unproven",
"stage": "editor_discovery",
}
else:
marker = "hermes foreground smoke"
type_args = {
"pid": launched_pid,
"window_id": window_id,
"element_index": editor.get("index"),
"text": marker,
"delivery_mode": "foreground",
"session": session_id,
}
token = editor.get("element_token")
if isinstance(token, str) and token:
type_args["element_token"] = token
typed = structured(
await client.call_tool("type_text", type_args)
)
saved = structured(
await client.call_tool(
"hotkey",
{
"pid": launched_pid,
"window_id": window_id,
"keys": ["cmd", "s"],
"delivery_mode": "foreground",
"session": session_id,
},
)
)
await client.call_tool(
"wait", {"seconds": 0.5, "session": session_id}
)
after = structured(
await client.call_tool(
"get_window_state",
{
"pid": launched_pid,
"window_id": window_id,
"session": session_id,
},
)
)
fresh_contains_marker = marker in json.dumps(
after.get("elements") or []
)
native_document_confirmed = textedit_process_contains(
launched_pid, marker
)
file_contains_marker = marker in smoke_path.read_text(
encoding="utf-8"
)
report["foreground"] = {
"classification": (
"pass"
if not focus.isError
and not refusal_code(typed)
and not refusal_code(saved)
and (
typed.get("verified") is True
or fresh_contains_marker
or native_document_confirmed
or file_contains_marker
)
else "unproven"
),
"focus_transport_ok": not focus.isError,
"effect": typed.get("effect"),
"verified": typed.get("verified"),
"fresh_state": bool(after.get("elements")),
"fresh_state_confirmed": fresh_contains_marker,
"native_document_confirmed": (
native_document_confirmed
),
"saved_file_confirmed": file_contains_marker,
"action_schema_omitted_bring_to_front": (
"bring_to_front" not in type_args
),
}
# Use only a driver-owned isolated profile. Never request,
# mint, print, or persist an existing-profile grant token.
listed = structured(
await client.call_tool(
"list_windows",
{"on_screen_only": True, "session": session_id},
)
)
browser_row = next(
(
row
for row in listed.get("windows") or []
if "chrome" in str(row.get("app_name") or "").lower()
),
None,
)
browser_pid = browser_row.get("pid") if browser_row else None
browser_window = (
browser_row.get("window_id") if browser_row else None
)
if not isinstance(browser_pid, int) or not isinstance(
browser_window, int
):
report["typed_browser"] = {
"classification": "environment_unavailable",
"stage": "browser_target",
}
else:
prepared = structured(
await client.call_tool(
"browser_prepare",
{
"pid": browser_pid,
"window_id": browser_window,
"allow_launch": True,
"profile": {"mode": "isolated_new"},
"session": session_id,
},
)
)
isolated_browser_pid = prepared.get("prepared_pid")
code = refusal_code(prepared)
if prepared.get("status") == "refused" or code:
report["typed_browser"] = {
"classification": "structured_refusal",
"code": code,
}
else:
prepared_pid = prepared.get("prepared_pid") or browser_pid
await client.call_tool(
"wait", {"seconds": 1, "session": session_id}
)
prepared_windows = structured(
await client.call_tool(
"list_windows",
{
"on_screen_only": True,
"session": session_id,
},
)
)
prepared_row = next(
(
row
for row in prepared_windows.get("windows") or []
if row.get("pid") == prepared_pid
),
None,
)
prepared_window = (
prepared_row.get("window_id")
if prepared_row
else browser_window
)
bound = structured(
await client.call_tool(
"get_browser_state",
{
"pid": prepared_pid,
"window_id": prepared_window,
"session": session_id,
},
)
)
tabs = bound.get("tabs") or []
tab_id = tabs[0].get("tab_id") if tabs else None
target_id = bound.get("target_id")
if (
bound.get("status") == "ok"
and bound.get("binding_quality") == "exact"
and bound.get("mutation_allowed") is True
and isinstance(tab_id, str)
and isinstance(target_id, str)
):
snapshot = structured(
await client.call_tool(
"get_browser_state",
{
"target_id": target_id,
"tab_id": tab_id,
"snapshot_format": "semantic_v2",
"session": session_id,
},
)
)
navigated = structured(
await client.call_tool(
"browser_navigate",
{
"target_id": target_id,
"tab_id": tab_id,
"url": "about:blank",
"session": session_id,
},
)
)
fresh = structured(
await client.call_tool(
"get_browser_state",
{
"target_id": target_id,
"tab_id": tab_id,
"snapshot_format": "semantic_v2",
"session": session_id,
},
)
)
report["typed_browser"] = {
"classification": "pass",
"exact_binding": True,
"mutation_allowed": True,
"initial_snapshot": snapshot.get("status")
in (None, "ok"),
"mutation_transport": navigated.get("status")
in (None, "ok"),
"fresh_verification": fresh.get("status")
in (None, "ok"),
}
else:
report["typed_browser"] = {
"classification": "unproven",
"stage": "exact_binding",
"code": refusal_code(bound),
}
finally:
if (
isinstance(launched_pid, int)
and launched_pid not in prior_foreground_pids
):
await client.call_tool(
"kill_app", {"pid": launched_pid, "session": session_id}
)
if (
isinstance(isolated_browser_pid, int)
and isolated_browser_pid != browser_pid
):
await client.call_tool(
"kill_app",
{"pid": isolated_browser_pid, "session": session_id},
)
await client.call_tool("end_session", {"session": session_id})
finally:
smoke_path.unlink(missing_ok=True)
return report
def main() -> int:
report: dict[str, dict[str, Any]] = {
"foreground": {"classification": "environment_unavailable"},
"typed_browser": {"classification": "environment_unavailable"},
}
if sys.platform != "darwin":
for cell in report.values():
cell["stage"] = "macos_host_required"
else:
socket_path = os.environ.get(
"CUA_DRIVER_LIVE_SOCKET", "/tmp/hermes-cua-0-9-live.sock"
)
if not Path(socket_path).is_socket():
for cell in report.values():
cell["stage"] = "isolated_daemon_required"
else:
try:
report = asyncio.run(run_smoke(socket_path))
except Exception as exc: # pragma: no cover - host/driver boundary
report = {
"foreground": {
"classification": "environment_unavailable",
"stage": "driver_connection",
"error_type": type(exc).__name__,
},
"typed_browser": {
"classification": "environment_unavailable",
"stage": "driver_connection",
"error_type": type(exc).__name__,
},
}
print(json.dumps(report, indent=2, sort_keys=True))
return int(any(cell.get("classification") != "pass" for cell in report.values()))
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,65 @@
"""Tests for cua-driver subprocess teardown on interpreter exit.
``CuaDriverBackend`` spawns a long-lived ``cua-driver`` child process and
caches it for the life of the Hermes process. Nothing ever tore it down, so
the driver outlived the session that started it (#28152 item 3 — "Hermes does
not keep the driver alive after tool completion"). #69903 fixed the *overlay*
redraw loop that made the lingering process burn CPU, but not the lingering
process itself.
``tools/computer_use/tool.py`` registers an ``atexit`` hook that stops the
cached backend, mirroring ``browser_tool``'s
``atexit.register(_emergency_cleanup_all_sessions)``.
These assert the behavior contract — the hook is registered, it stops a live
backend, it is a no-op when nothing was ever started, and it never raises out
of ``atexit`` — not a snapshot of the module's source.
"""
import atexit
from unittest.mock import MagicMock, patch
from tools.computer_use import tool as cu_tool
class TestAtexitTeardown:
def test_shutdown_stops_a_live_backend(self):
"""A cached backend is stopped when the interpreter exits."""
fake = MagicMock()
with patch.object(cu_tool, "_backend", fake):
cu_tool._shutdown_backend_atexit()
fake.stop.assert_called_once()
def test_shutdown_stops_every_session_backend(self):
"""Session-scoped caches are all drained, not only the legacy slot."""
first = MagicMock()
second = MagicMock()
with patch.object(cu_tool, "_backend", None), \
patch.object(cu_tool, "_backends", {"one": first, "two": second}), \
patch.object(cu_tool, "_backend_call_locks", {}):
cu_tool._shutdown_backend_atexit()
first.stop.assert_called_once()
second.stop.assert_called_once()
assert cu_tool._backends == {}
def test_hook_is_registered_with_atexit(self):
"""Importing the tool module registers the teardown hook.
Verified by unregistering and re-registering: atexit.unregister only
removes a function that was actually registered, so a successful
round-trip proves the import-time registration happened.
"""
atexit.unregister(cu_tool._shutdown_backend_atexit)
try:
with patch.object(cu_tool, "_backend", MagicMock()) as fake:
# Re-register and fire the full atexit chain the way the
# interpreter would, then confirm our hook ran.
atexit.register(cu_tool._shutdown_backend_atexit)
atexit._run_exitfuncs()
fake.stop.assert_called_once()
finally:
atexit.unregister(cu_tool._shutdown_backend_atexit)
atexit.register(cu_tool._shutdown_backend_atexit)
@@ -0,0 +1,60 @@
"""Regression test: the cua-driver CLI-fallback transport must sanitize the
subprocess environment like every other cua-driver spawn site.
``_CuaDriverSession._call_tool_via_cli()`` (the EAGAIN/silent-empty MCP
fallback) invoked ``subprocess.run`` with no ``env=`` at all, so the
third-party ``cua-driver`` binary inherited the full, unsanitized parent
environment — including provider API keys and other Hermes-managed
secrets that ``_lifecycle_coro``'s primary MCP spawn already strips via
``_sanitize_subprocess_env(cua_driver_child_env())``.
"""
import json
from unittest.mock import MagicMock
from tools.computer_use.cua_backend import _CuaDriverSession
def _make_session() -> _CuaDriverSession:
# _call_tool_via_cli() doesn't touch any instance state (bridge/session/
# capabilities); bypass __init__ so the test doesn't need a real
# _AsyncBridge.
return object.__new__(_CuaDriverSession)
def _fake_completed_process(stdout: str) -> MagicMock:
proc = MagicMock()
proc.stdout = stdout
proc.stderr = ""
proc.returncode = 0
return proc
def test_cli_fallback_strips_provider_secret_from_subprocess_env(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", "«redacted:sk-…»")
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.setattr(
"tools.computer_use.cua_backend.resolve_cua_driver_cmd",
lambda: "/resolved/cua-driver",
)
captured = {}
def fake_run(cmd, **kwargs):
captured["env"] = kwargs.get("env")
return _fake_completed_process(json.dumps({"tree_markdown": "root"}))
monkeypatch.setattr("subprocess.run", fake_run)
session = _make_session()
result = session._call_tool_via_cli("list_windows", {}, timeout=5.0)
assert result["isError"] is False
assert captured["env"] is not None, "subprocess.run must receive an explicit env="
assert "ANTHROPIC_API_KEY" not in captured["env"]
# Sanitization filters secrets, not everything — an ordinary var survives.
# Original PATH entries are preserved; the hermes console-script dir may
# be prepended (see _sanitize_subprocess_env, issue #92998).
assert captured["env"].get("PATH", "").endswith("/usr/bin:/bin")
+252
View File
@@ -0,0 +1,252 @@
"""Tests for the cua-driver --no-overlay policy.
cua-driver's cursor overlay rendering loop can consume CPU indefinitely when
idle (#28152, #47032), and on Linux/X11 its fullscreen always-on-top overlay
window can wedge the desktop when a session ends uncleanly. Hermes passes
``--no-overlay`` to suppress it when the ``computer_use.no_overlay`` config is
enabled (or auto-detected on macOS, headless Linux / WSL2, and Linux X11).
These assert the behavior contract (auto-detect, explicit override, version
probe), not specific config snapshots.
"""
import os
from unittest.mock import MagicMock, mock_open, patch
import pytest
from tools.computer_use import cua_backend
class TestNoOverlayFlag:
def test_explicit_true_overrides(self):
with patch("hermes_cli.config.load_config",
return_value={"computer_use": {"no_overlay": True}}):
assert cua_backend._cua_no_overlay() is True
@pytest.mark.macos_only
def test_config_load_failure_falls_through_to_auto_detect_macos(self):
"""Unreadable config => auto-detect (macOS defaults to overlay off).
macOS-only: the auto-detect verdict IS ``sys.platform == "darwin"``,
so a patched platform would only re-assert the patch.
"""
with patch("hermes_cli.config.load_config",
side_effect=RuntimeError("boom")):
assert cua_backend._cua_no_overlay() is True
@pytest.mark.linux_only
def test_config_load_failure_falls_through_to_auto_detect_linux(self, monkeypatch):
"""Unreadable config must not raise; headless Linux auto-detects off.
Linux-only: the auto-detect branch here keys off ``DISPLAY`` and
``/proc/version``, neither of which exists to be probed elsewhere.
"""
monkeypatch.delenv("DISPLAY", raising=False)
with patch("hermes_cli.config.load_config",
side_effect=RuntimeError("boom")):
assert cua_backend._cua_no_overlay() is True
@pytest.mark.linux_only
def test_linux_x11_auto_detects_off(self, monkeypatch):
"""X11 desktop (DISPLAY set, no Wayland) defaults the overlay off.
The X11 overlay is a fullscreen always-on-top all-workspaces window
that can get stuck over every workspace after an unclean session end,
wedging desktop input until the app restarts. Config must not need to
opt out per-machine.
"""
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
monkeypatch.delenv("XDG_SESSION_TYPE", raising=False)
with patch("hermes_cli.config.load_config", return_value={}):
assert cua_backend._cua_no_overlay() is True
@pytest.mark.linux_only
def test_linux_x11_explicit_session_type_also_off(self, monkeypatch):
"""XDG_SESSION_TYPE=x11 without Wayland env is still X11."""
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.setenv("XDG_SESSION_TYPE", "x11")
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
with patch("hermes_cli.config.load_config", return_value={}):
assert cua_backend._cua_no_overlay() is True
@pytest.mark.linux_only
def test_linux_wayland_keeps_overlay(self, monkeypatch):
"""Wayland desktop keeps the overlay: the compositor owns the
overlay surface lifecycle, so it cannot get stuck above every
workspace the way an X11 window can."""
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.setenv("WAYLAND_DISPLAY", "wayland-0")
monkeypatch.setenv("XDG_SESSION_TYPE", "wayland")
with patch("hermes_cli.config.load_config", return_value={}):
assert cua_backend._cua_no_overlay() is False
@pytest.mark.linux_only
def test_linux_x11_explicit_false_overrides_auto_detect(self, monkeypatch):
"""An explicit ``no_overlay: false`` must restore the cursor even on
X11 — auto-detection is the default, never a hard lock."""
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
monkeypatch.delenv("XDG_SESSION_TYPE", raising=False)
with patch("hermes_cli.config.load_config",
return_value={"computer_use": {"no_overlay": False}}):
assert cua_backend._cua_no_overlay() is False
class TestDriverSupportsNoOverlay:
def test_returns_true_when_help_shows_flag(self):
fake_help = "Usage: cua-driver [OPTIONS] COMMAND\n --no-overlay Disable cursor overlay\n"
with patch("subprocess.run") as mock_run:
mock_run.return_value.stdout = fake_help
mock_run.return_value.stderr = ""
assert cua_backend._cua_driver_supports_no_overlay("cua-driver") is True
def test_help_probe_passes_sanitized_env(self):
"""The ``--help`` subprocess must not leak provider credentials
via the inherited parent environment (third-party binary; same
policy as the manifest probe and MCP spawn).
"""
from unittest.mock import MagicMock
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="--no-overlay in help", stderr="")
cua_backend._cua_driver_supports_no_overlay.cache_clear()
cua_backend._cua_driver_supports_no_overlay("cua-driver")
kwargs = mock_run.call_args.kwargs
assert "env" in kwargs, (
"subprocess.run was called without env= — cua-driver is a "
"third-party binary and must not receive inherited secrets"
)
# The sanitized env must come from the same helper the MCP
# spawn uses, so the policy is consistent across every
# cua-driver invocation in this file.
assert kwargs["env"] is not None
class TestMcpInvocationUsesResolvedCommand:
"""Surface 8 (NousResearch/hermes-agent#47072) + sweeper feedback
#4701565902: when the manifest surfaces a relocated executable for
``mcp_invocation.command``, the support probe must run against THAT
binary, not the system-resolved ``_CUA_DRIVER_CMD``. Otherwise a
wrapper/relocation with a different feature set either crashes on
the unknown flag (when the probe falsely reports support) or
silently keeps an unwanted overlay (when the probe falsely reports
no support).
"""
@staticmethod
def _fake_run(stdout: str = "", returncode: int = 0):
from unittest.mock import MagicMock
def _run(*args, **kwargs):
proc = MagicMock()
proc.stdout = stdout
proc.returncode = returncode
return proc
return _run
def test_manifest_command_drives_support_probe(self):
"""When the manifest returns a distinct command, the support
probe runs against the manifest command, not the input
``driver_cmd`` parameter.
"""
from unittest.mock import patch
from tools.computer_use.cua_backend import _resolve_mcp_invocation
manifest = (
'{"mcp_invocation":'
'{"command":"/opt/relocated/cua-driver","args":["mcp"]}}'
)
with patch("subprocess.run", new=self._fake_run(stdout=manifest)), \
patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
patch.object(
cua_backend, "_cua_driver_supports_no_overlay",
return_value=True,
) as mock_probe:
cua_backend._cua_driver_supports_no_overlay.cache_clear()
cmd, args = _resolve_mcp_invocation("/usr/bin/cua-driver")
assert cmd == "/opt/relocated/cua-driver"
# The support probe must be called with the manifest-resolved
# command, not the input driver_cmd argument.
mock_probe.assert_called_with("/opt/relocated/cua-driver")
def test_probe_distinguishes_support_between_binaries(self):
"""Different binaries must produce independent support verdicts.
The cache is keyed on ``driver_cmd``; the same cached result
must not leak between the system binary and a manifest-relocated
one.
"""
with patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
patch.object(
cua_backend, "_cua_driver_supports_no_overlay",
side_effect=lambda cmd: cmd == "/opt/relocated/cua-driver",
):
# System binary does NOT support, manifest binary DOES.
args = cua_backend._mcp_args_with_overlay_flag(
["mcp"], driver_cmd="/usr/bin/cua-driver",
)
assert "--no-overlay" not in args
args = cua_backend._mcp_args_with_overlay_flag(
["mcp"], driver_cmd="/opt/relocated/cua-driver",
)
assert "--no-overlay" in args
class TestMcpArgsOverlayFlag:
def test_appended_when_enabled_and_supported(self):
with patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
patch.object(cua_backend, "_cua_driver_supports_no_overlay", return_value=True):
result = cua_backend._mcp_args_with_overlay_flag(["mcp"])
assert result == ["mcp", "--no-overlay"]
def test_not_appended_when_disabled(self):
with patch.object(cua_backend, "_cua_no_overlay", return_value=False), \
patch.object(cua_backend, "_cua_driver_supports_no_overlay", return_value=True):
result = cua_backend._mcp_args_with_overlay_flag(["mcp"])
assert result == ["mcp"]
def test_does_not_mutate_original_list(self):
original = ["mcp"]
with patch.object(cua_backend, "_cua_no_overlay", return_value=True), \
patch.object(cua_backend, "_cua_driver_supports_no_overlay", return_value=True):
result = cua_backend._mcp_args_with_overlay_flag(original)
assert "--no-overlay" in result
assert "--no-overlay" not in original
class TestEmbeddedDaemonOverlayFlag:
def test_serve_process_disables_overlay_when_policy_requires_it(self):
daemon = cua_backend._EmbeddedCuaDaemon("/usr/bin/cua-driver", "unrestricted")
process = MagicMock()
process.poll.return_value = None
status = MagicMock(returncode=0)
with patch.object(
cua_backend,
"_resolve_mcp_invocation",
return_value=("/usr/bin/cua-driver", ["mcp"]),
), patch.object(
cua_backend, "_cua_no_overlay", return_value=True,
), patch.object(
cua_backend, "_cua_driver_supports_no_overlay", return_value=True,
), patch.object(
cua_backend.subprocess, "Popen", return_value=process,
) as popen, patch.object(
cua_backend.subprocess, "run", return_value=status,
), patch.object(cua_backend.threading, "Thread"):
daemon.start()
command = popen.call_args.args[0]
assert command[:2] == ["/usr/bin/cua-driver", "serve"]
assert "--no-overlay" in command
+48
View File
@@ -0,0 +1,48 @@
"""Behavior contracts for computer_use latency knobs."""
from unittest.mock import patch
from tools.computer_use import cua_backend
from tools.computer_use import tool as cu_tool
def test_max_image_dimension_default():
with patch("hermes_cli.config.load_config", return_value={}):
assert cua_backend._computer_use_max_image_dimension() == 1456
def test_capture_after_mode_default_som():
with patch("hermes_cli.config.load_config", return_value={}):
assert cu_tool._capture_after_mode() == "som"
def test_aux_vision_route_caches_per_provider_model(monkeypatch):
cu_tool._AUX_VISION_ROUTE_CACHE.clear()
calls = {"n": 0}
monkeypatch.setattr(
"agent.auxiliary_client._read_main_provider", lambda: "openai"
)
monkeypatch.setattr(
"agent.auxiliary_client._read_main_model", lambda: "gpt-test"
)
def fake_load():
calls["n"] += 1
return {"auxiliary": {"vision": {}}}
monkeypatch.setattr("hermes_cli.config.load_config", fake_load)
monkeypatch.setattr(
"tools.computer_use.vision_routing.should_route_capture_to_aux_vision",
lambda *a, **k: True,
)
assert cu_tool._should_route_through_aux_vision() is True
assert cu_tool._should_route_through_aux_vision() is True
assert calls["n"] == 1
@@ -0,0 +1,248 @@
"""Regression tests: every remaining cua-driver spawn site must sanitize the
subprocess environment.
PR #58889 fixed the CLI-fallback transport; review of that fix found four
sibling spawn sites still handing the third-party ``cua-driver`` binary the
full parent environment (provider API keys included):
- ``cua_backend._resolve_mcp_invocation`` (``cua-driver manifest``) — no
``env=`` at all
- ``cua_backend.cua_driver_update_check`` (``check-update --json``) —
telemetry env but no secret sanitization
- ``doctor._drive_health_report`` (``<binary> mcp``) — telemetry env only
- ``permissions._run`` (every permission probe) — telemetry env only
"""
import json
import os
from unittest.mock import MagicMock
SECRET = "sk-super-secret-should-not-leak"
CREATE_NO_WINDOW = 0x08000000
def _fake_completed_process(stdout: str) -> MagicMock:
proc = MagicMock()
proc.stdout = stdout
proc.stderr = ""
proc.returncode = 0
return proc
def _capture_run(captured, stdout=""):
def fake_run(cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs.get("env")
captured["creationflags"] = kwargs.get("creationflags")
return _fake_completed_process(stdout)
return fake_run
def _assert_sanitized(captured):
env = captured["env"]
assert env is not None, "subprocess must receive an explicit env="
assert "ANTHROPIC_API_KEY" not in env
# Sanitization filters secrets, not everything — ordinary vars survive.
_assert_path_preserved(env)
# Confirms the telemetry helper still ran (default: telemetry disabled).
assert env.get("CUA_DRIVER_RS_TELEMETRY_ENABLED") == "0"
def _assert_path_preserved(env):
"""Original PATH entries survive sanitization; the hermes console-script
dir may be prepended (see _sanitize_subprocess_env, issue #92998) so we
assert the contract, not byte equality."""
from tools.environments.local import _resolve_hermes_bin_dir
path_val = env.get("PATH", "")
assert path_val.endswith("/usr/bin:/bin"), path_val
hermes_bin = _resolve_hermes_bin_dir()
if hermes_bin and path_val != "/usr/bin:/bin":
assert path_val.startswith(hermes_bin + os.pathsep), path_val
def _patch_windows_hide_flags(monkeypatch, module):
"""Pin the ``windows_hide_flags()`` seam so the console-hiding assertion
is host-independent.
``windows_hide_flags`` is our own platform probe (CREATE_NO_WINDOW on
Windows, ``0`` elsewhere). Patching that seam — rather than lying to the
interpreter about ``sys.platform`` — keeps the real subject of these
tests (does the spawn site forward its result to ``creationflags=``?)
covered on every host.
"""
monkeypatch.setattr(
module, "windows_hide_flags", lambda: CREATE_NO_WINDOW, raising=False
)
def test_resolve_mcp_invocation_sanitizes_env(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
from tools.computer_use import cua_backend
captured = {}
_patch_windows_hide_flags(monkeypatch, cua_backend)
manifest = json.dumps({"mcp_invocation": {"command": "cua-driver", "args": ["mcp"]}})
monkeypatch.setattr(
cua_backend.subprocess, "run", _capture_run(captured, stdout=manifest)
)
cmd, args = cua_backend._resolve_mcp_invocation("cua-driver")
assert cmd == "cua-driver"
_assert_sanitized(captured)
assert captured["creationflags"] == CREATE_NO_WINDOW
def test_update_check_sanitizes_env(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
from tools.computer_use import cua_backend
captured = {}
_patch_windows_hide_flags(monkeypatch, cua_backend)
payload = json.dumps({
"current_version": "1.0.0",
"latest_version": "1.0.0",
"update_available": False,
})
# PATH is pinned to /usr/bin:/bin above, so the driver won't resolve;
# pin it so the check reaches the (sanitized) subprocess spawn.
monkeypatch.setattr(
cua_backend, "resolve_cua_driver_cmd", lambda *a, **k: "cua-driver"
)
monkeypatch.setattr(
cua_backend.subprocess, "run", _capture_run(captured, stdout=payload)
)
cua_backend.cua_driver_update_check(timeout=1.0)
_assert_sanitized(captured)
assert captured["creationflags"] == CREATE_NO_WINDOW
def test_cli_fallback_sanitizes_env_and_hides_console_on_windows(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
from tools.computer_use import cua_backend
captured = {}
_patch_windows_hide_flags(monkeypatch, cua_backend)
# Hermetic CI has no cua-driver binary; pin the resolver so the test
# exercises the spawn-env path instead of the install-hint early exit.
monkeypatch.setattr(
cua_backend, "resolve_cua_driver_cmd", lambda override=None: "cua-driver"
)
monkeypatch.setattr(
cua_backend.subprocess,
"run",
_capture_run(captured, stdout=json.dumps({"tree_markdown": "root"})),
)
session = object.__new__(cua_backend._CuaDriverSession)
result = session._call_tool_via_cli("list_windows", {}, timeout=5.0)
assert result["isError"] is False
_assert_sanitized(captured)
assert captured["creationflags"] == CREATE_NO_WINDOW
def test_permissions_run_sanitizes_env(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
from tools.computer_use import permissions
captured = {}
monkeypatch.setattr(
permissions.subprocess, "run", _capture_run(captured, stdout="{}")
)
permissions._run("cua-driver", "doctor", "--json", timeout=1.0)
_assert_sanitized(captured)
def test_doctor_spawn_sanitizes_env_and_hides_console_on_windows(monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
from tools.computer_use import doctor
captured = {}
_patch_windows_hide_flags(monkeypatch, doctor)
proc = MagicMock()
proc.stdout.readline.side_effect = [
json.dumps({"jsonrpc": "2.0", "id": 1, "result": {}}),
json.dumps({
"jsonrpc": "2.0",
"id": 2,
"result": {
"structuredContent": {
"schema_version": "1",
"overall": "ok",
"checks": [],
}
},
}),
]
def fake_popen(cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs.get("env")
captured["creationflags"] = kwargs.get("creationflags")
return proc
monkeypatch.setattr(doctor.subprocess, "Popen", fake_popen)
report = doctor._drive_health_report("cua-driver", timeout=1.0)
assert report["overall"] == "ok"
_assert_sanitized(captured)
assert captured["creationflags"] == CREATE_NO_WINDOW
def test_doctor_sanitized_env_helper(monkeypatch):
"""The doctor MCP spawn site must pass the sanitized env to Popen.
Behavioral check: intercept subprocess.Popen at the `_open_mcp` spawn
seam and assert the env it receives strips secrets and applies the
telemetry opt-out (no source-text inspection — that breaks on any
refactor with identical runtime behavior)."""
monkeypatch.setenv("ANTHROPIC_API_KEY", SECRET)
monkeypatch.setenv("PATH", "/usr/bin:/bin")
monkeypatch.delenv("HERMES_CUA_TELEMETRY", raising=False)
from tools.computer_use import doctor
env = doctor._sanitized_cua_env()
assert "ANTHROPIC_API_KEY" not in env
_assert_path_preserved(env)
assert env.get("CUA_DRIVER_RS_TELEMETRY_ENABLED") == "0"
# The Popen spawn site must actually use the sanitized helper.
captured = {}
class _FakeProc:
stdin = None
stdout = None
stderr = None
def _fake_popen(*args, **kwargs):
captured["env"] = kwargs.get("env")
return _FakeProc()
monkeypatch.setattr(doctor.subprocess, "Popen", _fake_popen)
doctor._open_mcp("cua-driver")
spawn_env = captured["env"]
assert spawn_env is not None, "_open_mcp must pass an explicit env"
assert "ANTHROPIC_API_KEY" not in spawn_env
assert spawn_env.get("CUA_DRIVER_RS_TELEMETRY_ENABLED") == "0"
+52
View File
@@ -0,0 +1,52 @@
"""Tests for the cua-driver telemetry opt-in policy.
cua-driver ships anonymous PostHog telemetry ENABLED by default upstream.
Hermes disables it unless the user opts in via
``computer_use.cua_telemetry: true``. The policy is applied by injecting
``CUA_DRIVER_RS_TELEMETRY_ENABLED=0`` into every cua-driver child env.
These assert the behavior contract (default disables, opt-in leaves the var
untouched, config failure fails safe toward disabled), not specific config
snapshots.
"""
from unittest.mock import patch
from tools.computer_use import cua_backend
_VAR = "CUA_DRIVER_RS_TELEMETRY_ENABLED"
class TestTelemetryDisabledFlag:
def test_explicit_false_disables(self):
with patch("hermes_cli.config.load_config",
return_value={"computer_use": {"cua_telemetry": False}}):
assert cua_backend._cua_telemetry_disabled() is True
def test_config_load_failure_fails_safe(self):
# Unreadable config => default to disabling telemetry (privacy-safe).
with patch("hermes_cli.config.load_config", side_effect=RuntimeError("boom")):
assert cua_backend._cua_telemetry_disabled() is True
class TestChildEnv:
def test_disabled_injects_var_zero(self):
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True):
env = cua_backend.cua_driver_child_env({"PATH": "/usr/bin"})
assert env[_VAR] == "0"
# base env is preserved
assert env["PATH"] == "/usr/bin"
def test_disabled_overrides_inherited_enabled(self):
# Even if the parent process had telemetry enabled, the default policy
# forces it off in the child.
with patch.object(cua_backend, "_cua_telemetry_disabled", return_value=True):
env = cua_backend.cua_driver_child_env({_VAR: "1"})
assert env[_VAR] == "0"
@@ -0,0 +1,22 @@
from unittest.mock import patch
from tools.computer_use import cua_backend
_VAR = "CUA_DRIVER_RS_ENABLE_WAYLAND"
def _child_env(base_env, native_wayland):
config = {"computer_use": {"native_wayland": native_wayland}}
with patch("hermes_cli.config.load_config", return_value=config), \
patch.object(cua_backend.sys, "platform", "linux"):
return cua_backend.cua_driver_child_env(base_env)
def test_configured_native_wayland_reaches_linux_wayland_child():
assert _child_env({"WAYLAND_DISPLAY": "wayland-1"}, True)[_VAR] == "1"
def test_native_wayland_not_injected_without_wayland_display_or_opt_in():
assert _VAR not in _child_env({"DISPLAY": ":0"}, True)
assert _VAR not in _child_env({"WAYLAND_DISPLAY": "wayland-1"}, False)
@@ -0,0 +1,36 @@
from __future__ import annotations
import json
from types import SimpleNamespace
from unittest.mock import patch
from tools.computer_use import cua_backend
def test_wsl_windows_manifest_path_translates_to_drvfs():
with patch("hermes_constants.is_wsl", return_value=True):
assert cua_backend._wsl_windows_path_to_posix(
r"C:\Users\Fernando\AppData\Local\cua-driver\cua-driver.exe"
) == "/mnt/c/Users/Fernando/AppData/Local/cua-driver/cua-driver.exe"
def test_resolve_mcp_invocation_normalizes_windows_manifest_command_in_wsl():
manifest = {
"mcp_invocation": {
"command": r"C:\Users\Fernando\AppData\Local\cua-driver\cua-driver.exe",
"args": ["mcp"],
}
}
proc = SimpleNamespace(returncode=0, stdout=json.dumps(manifest))
with (
patch.object(cua_backend.subprocess, "run", return_value=proc),
patch("hermes_constants.is_wsl", return_value=True),
):
command, args = cua_backend._resolve_mcp_invocation("cua-driver")
assert command == "/mnt/c/Users/Fernando/AppData/Local/cua-driver/cua-driver.exe"
assert args == ["mcp"]
+431
View File
@@ -0,0 +1,431 @@
"""Tests for ``tools.computer_use.doctor``.
The doctor module drives cua-driver's stable ``health_report`` MCP tool over
stdio JSON-RPC and renders the structured response. Most of the surface is
about parsing what cua-driver hands back, plus the exit-code contract
downstream consumers (CI / `hermes update`) rely on:
* Exit 0 when overall == "ok"
* Exit 1 when overall in ("degraded", "failed") — at least one check
failed but the tool itself ran successfully
* Exit 2 when the cua-driver binary is missing or the protocol breaks
We do NOT spin up a real cua-driver — that lives in the cua-driver
integration test suite (libs/cua-driver/rust/tests/integration/
test_health_report_mcp.py). Here we mock the subprocess and assert the
Hermes-side adapter behaves correctly against the documented response
shape.
"""
from __future__ import annotations
import json
import sys
from io import StringIO
from unittest.mock import MagicMock, patch
import pytest
# ── helpers ────────────────────────────────────────────────────────────────
def _fake_proc_with_responses(*responses: dict) -> MagicMock:
"""Build a MagicMock subprocess.Popen handle that yields one JSON-RPC
response per `readline()` call, then returns "" (EOF)."""
lines = [json.dumps(r) + "\n" for r in responses] + [""]
proc = MagicMock()
proc.stdin = MagicMock()
proc.stdout = MagicMock()
proc.stdout.readline = MagicMock(side_effect=lines)
proc.stderr = MagicMock()
proc.stderr.read = MagicMock(return_value="")
proc.wait = MagicMock(return_value=0)
proc.kill = MagicMock()
return proc
def _ok_report() -> dict:
"""Minimal well-formed health_report response."""
return {
"schema_version": "1",
"platform": "darwin",
"driver_version": "0.5.8",
"overall": "ok",
"checks": [
{"name": "binary_version", "status": "pass", "message": "cua-driver 0.5.8"},
{"name": "tcc_accessibility", "status": "pass", "message": "Accessibility is granted."},
],
}
def _degraded_report() -> dict:
"""Report with one failing check — overall=degraded."""
return {
"schema_version": "1",
"platform": "darwin",
"driver_version": "0.5.8",
"overall": "degraded",
"checks": [
{"name": "binary_version", "status": "pass", "message": "cua-driver 0.5.8"},
{
"name": "bundle_identity",
"status": "fail",
"message": "Process has no CFBundleIdentifier.",
"hint": "Run inside CuaDriver.app",
"data": {"executable_path": "/tmp/cua-driver"},
},
],
}
@pytest.fixture(autouse=True)
def _default_cli_version_matches_report(monkeypatch):
"""Existing tests mock only the MCP Popen handshake. ``subprocess.run``
(used for ``--version``) goes through Popen too, so without this the
mock breaks version probing. Default to a CLI version that matches
``_ok_report`` / ``_degraded_report`` (0.5.8); identity tests override.
"""
from tools.computer_use import doctor
monkeypatch.setattr(
doctor,
"_read_cli_version",
lambda binary, timeout=5.0: "cua-driver 0.5.8",
)
# ── exit codes ─────────────────────────────────────────────────────────────
class TestDoctorExitCodes:
def test_ok_exits_0(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO):
code = doctor.run_doctor()
assert code == 0
def test_degraded_exits_1(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _degraded_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO):
code = doctor.run_doctor()
assert code == 1
def test_failed_overall_exits_1(self):
"""`failed` overall (every check failed) is also exit 1, not 2 —
the tool ran successfully; the diagnosis was bad."""
from tools.computer_use import doctor
report = _degraded_report()
report["overall"] = "failed"
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": report}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO):
code = doctor.run_doctor()
assert code == 1
def test_protocol_error_exits_2(self, capsys):
"""An empty stdout response (driver crashed during handshake) is a
protocol failure → exit 2."""
from tools.computer_use import doctor
proc = MagicMock()
proc.stdin = MagicMock()
proc.stdout = MagicMock()
proc.stdout.readline = MagicMock(return_value="") # EOF on initialize
proc.stderr = MagicMock()
proc.stderr.read = MagicMock(return_value="boom\n")
proc.wait = MagicMock(return_value=0)
proc.kill = MagicMock()
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc):
code = doctor.run_doctor()
assert code == 2
# stderr should mention the failure
captured = capsys.readouterr()
assert "cua-driver" in captured.err.lower() or "health_report" in captured.err.lower()
# ── response-shape parsing ─────────────────────────────────────────────────
class TestResponseShapeParsing:
def test_prefers_structuredContent(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO) as out:
doctor.run_doctor()
# Header line includes driver version + platform + overall.
text = out.getvalue()
assert "darwin" in text
assert "ok" in text
def test_jsonrpc_error_response_exits_2(self, capsys):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "error": {"code": -32601, "message": "method not found"}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc):
code = doctor.run_doctor()
assert code == 2
assert "method not found" in capsys.readouterr().err
# ── args / arg passthrough ─────────────────────────────────────────────────
class TestArgPassthrough:
def test_include_passed_through_to_tools_call(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO):
doctor.run_doctor(include=["binary_version", "tcc_accessibility"])
# Inspect the second write to stdin — the tools/call payload.
writes = [call.args[0] for call in proc.stdin.write.call_args_list]
call_payload = next(json.loads(w) for w in writes if "tools/call" in w)
assert call_payload["params"]["arguments"]["include"] == [
"binary_version", "tcc_accessibility",
]
def test_skip_passed_through(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO):
doctor.run_doctor(skip=["bundle_identity"])
writes = [call.args[0] for call in proc.stdin.write.call_args_list]
call_payload = next(json.loads(w) for w in writes if "tools/call" in w)
assert call_payload["params"]["arguments"]["skip"] == ["bundle_identity"]
# ── json output ────────────────────────────────────────────────────────────
class TestJsonOutput:
def test_json_output_is_parseable_round_trip(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO) as out:
doctor.run_doctor(json_output=True)
# Verify the captured text round-trips through json.loads. Upstream
# health_report keys are preserved; Hermes adds hermes_identity.
parsed = json.loads(out.getvalue())
report = _ok_report()
for key, value in report.items():
assert parsed[key] == value
assert "hermes_identity" in parsed
assert parsed["hermes_identity"]["resolved_binary"]
# ── HERMES_CUA_DRIVER_CMD resolution ───────────────────────────────────────
class TestDriverCmdResolution:
def test_explicit_driver_cmd_arg_wins(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/explicit-binary") as which_mock, \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO):
doctor.run_doctor(driver_cmd="/custom/path/cua-driver")
# shutil.which should have been called with the explicit arg, not
# the env-var / default resolver.
which_mock.assert_called_with("/custom/path/cua-driver")
def test_env_var_used_when_no_arg_given(self, monkeypatch):
from tools.computer_use import doctor
monkeypatch.setenv("HERMES_CUA_DRIVER_CMD", "/env/path/cua-driver")
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/env/path/cua-driver") as which_mock, \
patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO), \
patch("hermes_cli.tools_config._cua_driver_cmd", side_effect=Exception("force env")):
# Force env-var resolution path inside run_doctor.
doctor.run_doctor()
which_mock.assert_called_with("/env/path/cua-driver")
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX user-local path regression")
def test_user_local_driver_is_found_when_path_omits_it(self, tmp_path, monkeypatch):
"""Doctor must inspect the same user-local driver as the runtime."""
from tools.computer_use import doctor
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.doctor._drive_health_report", return_value=_ok_report()) as health, \
patch("sys.stdout", new_callable=StringIO):
assert doctor.run_doctor() == 0
health.assert_called_once_with(str(driver), include=(), skip=(), timeout=12.0)
# ── cua-driver 0.10 unclassified health_report fallback ────────────────────
def _unclassified_health_result() -> dict:
"""MCP tools/call result shape from cua-driver 0.10.x denial."""
return {
"isError": True,
"content": [
{
"type": "text",
"text": (
"Permission denied: tool 'health_report' has no "
"reviewed risk classification"
),
}
],
"structuredContent": {"exit_code": 1},
}
def _perms_ok_result() -> dict:
return {
"isError": False,
"content": [{"type": "text", "text": "ok"}],
"structuredContent": {
"accessibility": True,
"screen_recording": True,
"screen_recording_capturable": True,
},
}
def _list_apps_ok_result() -> dict:
return {
"isError": False,
"content": [{"type": "text", "text": "Found 1 app"}],
"structuredContent": {
"apps": [{"name": "Finder", "pid": 1, "running": True}],
},
}
class TestHealthReportFallback:
"""cua-driver 0.10 marks health_report risk-unclassified → isError.
Doctor must NOT treat structuredContent={exit_code:1} as a real report
(that produced '• cua-driver ? on ? — ?'). It synthesizes schema_version=1
via check_permissions / list_apps / CLI --version instead.
"""
def test_extract_raises_health_report_unavailable_on_isError(self):
from tools.computer_use import doctor
with __import__("pytest").raises(doctor.HealthReportUnavailable) as ei:
doctor._extract_health_report_from_result(_unclassified_health_result())
assert "Permission denied" in str(ei.value) or "unclassified" in str(ei.value).lower() or "risk" in str(ei.value).lower()
# ── binary identity (CLI --version vs health_report) ───────────────────────
class TestDoctorVersionIdentity:
def test_header_prefers_cli_version_on_mismatch(self):
"""Windows has been observed reporting 0.8.3 via health_report while
the resolved binary is 0.12.6 — doctor must surface the real version."""
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
# _ok_report claims 0.5.8; CLI says 0.12.6
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch.object(doctor, "_read_cli_version", return_value="cua-driver 0.12.6"), \
patch("sys.stdout", new_callable=StringIO) as out:
code = doctor.run_doctor()
assert code == 0
text = out.getvalue()
assert "0.12.6" in text
assert "version mismatch" in text.lower()
assert "0.5.8" in text # health_report value still shown
def test_matching_versions_no_mismatch_flag(self):
from tools.computer_use import doctor
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": _ok_report()}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), \
patch("subprocess.Popen", return_value=proc), \
patch.object(doctor, "_read_cli_version", return_value="cua-driver 0.5.8"), \
patch("sys.stdout", new_callable=StringIO) as out:
code = doctor.run_doctor(json_output=True)
assert code == 0
payload = json.loads(out.getvalue())
assert payload["hermes_identity"]["version_mismatch"] is False
@@ -0,0 +1,32 @@
"""Regression tests for Computer Use readiness under a thin GUI PATH."""
from __future__ import annotations
import sys
from unittest.mock import MagicMock, patch
import pytest
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX user-local path regression")
def test_status_finds_user_local_driver_when_path_omits_it(tmp_path, monkeypatch):
"""Desktop status must agree with the runtime resolver, not bare PATH."""
from tools.computer_use import permissions
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")
# No platform faking: ``~/.local/bin/cua-driver`` is a POSIX resolution
# candidate on Linux exactly as on macOS, so the regression reproduces on
# the host we actually run on.
with patch.object(permissions, "_run", return_value=MagicMock(stdout="0.0.0")), \
patch.object(permissions, "_doctor", return_value={"ok": True, "checks": []}):
status = permissions.computer_use_status()
assert status["installed"] is True