From b2e24b986ffa8369250ae73a175a5374ea821fa9 Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Sun, 30 Aug 2026 01:46:02 +0800 Subject: [PATCH] fix(computer-use): stop launching retired browser-grant runtimes --- hermes_cli/config_defaults.py | 13 +-- .../test_computer_use_cua_0_10_permissions.py | 74 +++++++++---- tools/computer_use/cua_backend.py | 103 +----------------- 3 files changed, 53 insertions(+), 137 deletions(-) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 76b38121ff..49b11d168f 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -3750,10 +3750,7 @@ DEFAULT_CONFIG = { # False = always enable the overlay "no_overlay": None, # cua-driver permission mode for each Hermes computer-use runtime. - # standard (default) — cua-driver's own approval boundary. Protected - # operations (e.g. attaching to an existing signed-in browser - # profile) fail closed unless grant_existing_profile is enabled - # below. + # standard (default) — cua-driver's own approval boundary. # bounded — repeatable automation under a user-reviewed session # capability manifest (set capability_manifest below). No runtime # prompts; anything outside the manifest fails closed inside @@ -3774,14 +3771,6 @@ DEFAULT_CONFIG = { # cua-driver identity (com.trycua.driver / an official signing team). # Enable only when developing the driver locally from source. "allow_unsigned_driver": False, - # Pre-authorize existing-profile browser attachment in standard mode - # (cua-driver's trusted-launcher `--grant existing-profile`). When - # true, the agent can attach to your already-running, signed-in - # Chrome/Edge window — exposing that profile's live pages, cookies, - # and storage to the browser protocol — without a per-use prompt. - # Leave false to keep existing-profile attachment failing closed; - # isolated driver-owned profiles work either way. - "grant_existing_profile": False, }, # ========================================================================= diff --git a/tests/tools/test_computer_use_cua_0_10_permissions.py b/tests/tools/test_computer_use_cua_0_10_permissions.py index a6e8f05b94..dd3e6c11a0 100644 --- a/tests/tools/test_computer_use_cua_0_10_permissions.py +++ b/tests/tools/test_computer_use_cua_0_10_permissions.py @@ -2,8 +2,9 @@ from __future__ import annotations +import asyncio from types import SimpleNamespace -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -213,35 +214,62 @@ def test_standard_backend_does_not_spawn_an_embedded_daemon(): assert unrestricted._embedded_daemon is not None -def test_standard_existing_profile_grant_owns_private_macos_runtime(): - from tools.computer_use.cua_backend import _standard_runtime_launch_args +def test_retired_browser_grant_cannot_change_standard_runtime(tmp_path, monkeypatch): + from tools.computer_use.cua_backend import _AsyncBridge, _CuaDriverSession - args, socket_path = _standard_runtime_launch_args( - ["mcp"], - grant_existing_profile=True, - platform="darwin", - socket_path="/tmp/hermes-cua-test.sock", + (tmp_path / "config.yaml").write_text( + "computer_use:\n grant_existing_profile: true\n", + encoding="utf-8", ) + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + session = _CuaDriverSession(_AsyncBridge()) + captured = {} - assert args == [ - "mcp", - "--grant", - "existing-profile", - "--socket", - "/tmp/hermes-cua-test.sock", - ] - assert socket_path == "/tmp/hermes-cua-test.sock" + async def drive_lifecycle(): + def capture_params(**kwargs): + captured.update(kwargs) + return MagicMock() + with patch( + "tools.computer_use.cua_backend.resolve_cua_driver_cmd", + return_value="/opt/cua-driver", + ), patch( + "tools.computer_use.cua_backend._resolve_mcp_invocation", + return_value=("/opt/cua-driver", ["mcp"]), + ), patch( + "mcp.StdioServerParameters", side_effect=capture_params + ), patch( + "mcp.client.stdio.stdio_client" + ) as stdio_client, patch( + "mcp.ClientSession" + ) as client_session: + stdio_client.return_value.__aenter__ = AsyncMock( + return_value=(MagicMock(), MagicMock()) + ) + stdio_client.return_value.__aexit__ = AsyncMock(return_value=None) + live_session = MagicMock() + live_session.initialize = AsyncMock() + live_session.list_tools = AsyncMock(return_value=MagicMock(tools=[])) + client_session.return_value.__aenter__ = AsyncMock( + return_value=live_session + ) + client_session.return_value.__aexit__ = AsyncMock(return_value=None) -def test_standard_existing_profile_grant_stays_in_process_off_macos(): - from tools.computer_use.cua_backend import _standard_runtime_launch_args + async def stop_when_ready(): + while session._shutdown_event is None: + await asyncio.sleep(0) + session._shutdown_event.set() - args, socket_path = _standard_runtime_launch_args( - ["mcp"], grant_existing_profile=True, platform="linux" - ) + stop_task = asyncio.create_task(stop_when_ready()) + try: + await session._lifecycle_coro() + finally: + await stop_task - assert args == ["mcp", "--grant", "existing-profile"] - assert socket_path is None + asyncio.run(drive_lifecycle()) + + assert captured["command"] == "/opt/cua-driver" + assert captured["args"] == ["mcp"] def test_transport_reset_invalidates_native_capabilities(): diff --git a/tools/computer_use/cua_backend.py b/tools/computer_use/cua_backend.py index 4982426eef..2d3937787d 100644 --- a/tools/computer_use/cua_backend.py +++ b/tools/computer_use/cua_backend.py @@ -309,26 +309,6 @@ def _cua_capability_manifest() -> Optional[str]: return raw.strip() -def _cua_grant_existing_profile() -> bool: - """True when the user pre-authorized existing-profile browser attachment. - - Reads ``computer_use.grant_existing_profile`` (default False). This is - cua-driver's trusted-launcher grant. Hermes passes - ``--grant existing-profile`` when it launches the standard-mode runtime. - On macOS it also selects a private socket so the newly configured - CuaDriver.app runtime cannot collide with an already-running default - daemon. The setting never applies to bounded mode, where the reviewed - capability manifest owns authorization. - - It DOES apply to unrestricted mode. An approval bypass (``--yolo``, - ``-z``) is consent to skip prompts, not consent to read an existing - browser profile's live pages, cookies, and storage, so the host-side - grant floor enforces this key even when the private unrestricted daemon - would answer the launch. - """ - return bool(_computer_use_cfg().get("grant_existing_profile", False)) - - def _manifest_is_mode_independent(path: str) -> bool: """True when this capability manifest may accompany any permission mode. @@ -360,36 +340,6 @@ def _manifest_is_mode_independent(path: str) -> bool: return isinstance(version, int) and not isinstance(version, bool) and version >= 3 -def _standard_runtime_launch_args( - args: List[str], - *, - grant_existing_profile: bool, - platform: str, - socket_path: Optional[str] = None, -) -> Tuple[List[str], Optional[str]]: - """Return MCP args and any private runtime socket owned by this transport. - - Windows and Linux run the standard runtime in the MCP process, so the - launch grant can be passed directly. macOS proxies through CuaDriver.app; - a grant must therefore launch a fresh app daemon on a private socket - instead of trying to reconfigure the default daemon. - - ``platform`` is explicit so this policy can be tested as a pure function - on every CI host. - """ - result = list(args) - if not grant_existing_profile: - return result, None - result.extend(["--grant", "existing-profile"]) - if platform != "darwin": - return result, None - private_socket = socket_path or os.path.join( - tempfile.gettempdir(), f"hermes-cua-standard-{uuid.uuid4().hex[:12]}.sock" - ) - result.extend(["--socket", private_socket]) - return result, private_socket - - def _computer_use_max_image_dimension() -> Optional[int]: """Longest-edge cap for cua-driver screenshots, or None to leave unset. @@ -1717,10 +1667,6 @@ class _CuaDriverSession: # Used to revive a logical ended-session rejection without # recursive call_tool re-entry or backend-owned state (#71166). self._declared_session_id: Optional[str] = None - # A macOS standard-mode launch grant belongs to the app daemon that - # receives it. Select and own a private endpoint so an existing - # default daemon cannot reject or silently miss the requested grant. - self._owned_standard_runtime_socket: Optional[str] = None self._transport_generation = 0 self._transport_reset_callback: Optional[Any] = None @@ -1764,13 +1710,6 @@ class _CuaDriverSession: child_env = self._embedded_daemon.child_env() else: command, args = _resolve_mcp_invocation(driver_cmd) - args, owned_socket = _standard_runtime_launch_args( - args, - grant_existing_profile=_cua_grant_existing_profile(), - platform=sys.platform, - socket_path=self._owned_standard_runtime_socket, - ) - self._owned_standard_runtime_socket = owned_socket child_env = cua_driver_child_env() _t_manifest = _time.monotonic() params = StdioServerParameters( @@ -1879,17 +1818,8 @@ class _CuaDriverSession: with self._lock: if self._started: return - # A previous transport may have died without taking down its - # private app daemon. Stop that exact endpoint before relaunching - # with --grant; grants cannot modify an already-running runtime. - if self._owned_standard_runtime_socket is not None: - self._stop_owned_standard_runtime_locked() self._bridge.start() - try: - self._start_lifecycle_locked() - except Exception: - self._stop_owned_standard_runtime_locked() - raise + self._start_lifecycle_locked() self._started = True def _start_lifecycle_locked(self) -> None: @@ -1935,11 +1865,9 @@ class _CuaDriverSession: def stop(self) -> None: with self._lock: if not self._started: - self._stop_owned_standard_runtime_locked() return self._started = False self._stop_lifecycle_locked() - self._stop_owned_standard_runtime_locked() def set_transport_reset_callback(self, callback: Any) -> None: """Register a synchronous cache invalidation hook for transport swaps.""" @@ -1954,34 +1882,6 @@ class _CuaDriverSession: except Exception as exc: logger.debug("cua-driver transport reset callback failed: %s", exc) - def _stop_owned_standard_runtime_locked(self) -> None: - """Stop the exact private macOS app daemon launched for a grant.""" - socket_path = getattr(self, "_owned_standard_runtime_socket", None) - if not socket_path: - return - self._owned_standard_runtime_socket = None - driver_command = resolve_cua_driver_cmd() - if driver_command: - from tools.environments.local import _sanitize_subprocess_env - - try: - subprocess.run( - [driver_command, "stop", "--socket", socket_path], - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - timeout=3.0, - creationflags=windows_hide_flags(), - env=_sanitize_subprocess_env(cua_driver_child_env()), - ) - except (OSError, subprocess.SubprocessError): - pass - if os.path.exists(socket_path): - try: - os.remove(socket_path) - except OSError: - pass - def _stop_lifecycle_locked(self) -> None: """Signal shutdown + wait for the lifecycle coroutine to unwind. Caller must hold self._lock.""" @@ -2194,7 +2094,6 @@ class _CuaDriverSession: except Exception as e: logger.debug("cua-driver session cleanup before reconnect failed: %s", e) self._started = False - self._stop_owned_standard_runtime_locked() # Clear stale capability state; the next start populates from scratch. self._capabilities = {} self._tool_schemas = {}