NousResearch/hermes-agent

cua-driver refresh behavior on restricted macOS

Cluster 47865 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #58754. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #58754. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #58754. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs concern how cua-driver refresh is handled when /Applications is not writable, typically on non-admin macOS systems. They also touch a related session startup timeout adjustment and phase timing logging.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #58754 fix(update): skip cua-driver refresh when /Applications is unwritable (salvage #47884)

    merged to main

    files: hermes_cli/config.py, hermes_cli/main.py, hermes_cli/tools_config.py, hermes_cli/web_server.py, tests/hermes_cli/test_install_cua_driver.py

    Show the diff
    • hermes_cli/config.py
      @@ -2952,6 +2952,11 @@ def _ensure_hermes_home_managed(home: Path):
               #               ignored paths — node_modules, venv, build outputs —
               #               are never touched.
               "non_interactive_local_changes": "stash",
      +        # Refresh an already-installed cua-driver during `hermes update`.
      +        # The refresh is best-effort and macOS-only. Turn this off if the
      +        # upstream installer is not appropriate for the machine, for example
      +        # on non-admin accounts where `/Applications` is not writable.
      +        "refresh_cua_driver": True,
           },
       
           # Language Server Protocol — semantic diagnostics from real
    • hermes_cli/main.py
      @@ -10243,7 +10243,23 @@ def _print_items(items, label, key, fallback_key=None):
               # predictable cadence (matches when they pull new agent code) without
               # adding startup latency or a per-launch GitHub API call.
               try:
      -            if sys.platform in ("darwin", "win32", "linux") and shutil.which("cua-driver"):
      +            refresh_cua_driver = True
      +            try:
      +                from hermes_cli.config import load_config
      +
      +                _update_cfg = (load_config() or {}).get("updates", {})
      +                if isinstance(_update_cfg, dict):
      +                    refresh_cua_driver = bool(
      +                        _update_cfg.get("refresh_cua_driver", True)
      +                    )
      +            except Exception as cfg_exc:
      +                logger.debug("Could not read updates.refresh_cua_driver: %s", cfg_exc)
      +
      +            if (
      +                refresh_cua_driver
      +                and sys.platform in ("darwin", "win32", "linux")
      +                and shutil.which("cua-driver")
      +            ):
                       from hermes_cli.tools_config import install_cua_driver
       
                       print()
    • hermes_cli/tools_config.py
      @@ -705,6 +705,19 @@ def _pip_install(
       # no Python-side duplication.
       
       
      +def _cua_install_target_writable() -> bool:
      +    """Return whether the upstream installer can write its app bundle target."""
      +    if sys.platform != "darwin":
      +        return True
      +    applications_dir = "/Applications"
      +    try:
      +        if not os.path.isdir(applications_dir):
      +            return True
      +        return os.access(applications_dir, os.W_OK)
      +    except Exception:
      +        return True
      +
      +
       def install_cua_driver(upgrade: bool = False) -> bool:
           """Install or refresh the cua-driver binary used by Computer Use.
       
      @@ -747,6 +760,14 @@ def install_cua_driver(upgrade: bool = False) -> bool:
       
           # Not installed → fresh install path (only when caller asked for it).
           if not binary and not upgrade:
      +        if not _cua_install_target_writable():
      +            _print_info(
      +                "    /Applications is not writable; skipping cua-driver install."
      +            )
      +            _print_info(
      +                "    Run from an admin account or install cua-driver manually."
      +            )
      +            return False
               if not shutil.which(fetch_tool):
                   _print_warning(f"    {fetch_tool} not found — install manually:")
                   _print_info("      https://github.com/trycua/cua/blob/main/libs/cua-driver/README.md")
      @@ -778,6 +799,15 @@ def install_cua_driver(upgrade: bool = False) -> bool:
               return True
       
           # upgrade=True path — refresh to the latest upstream release.
      +    if not _cua_install_target_writable():
      +        _print_info(
      +            "    /Applications is not writable; skipping cua-driver refresh."
      +        )
      +        _print_info(
      +            "    Run `hermes computer-use install --upgrade` from an admin account to update it."
      +        )
      +        return bool(binary)
      +
           if not shutil.which(fetch_tool):
               _print_warning(f"    {fetch_tool} not found — cannot refresh cua-driver.")
               return bool(binary)
    • hermes_cli/web_server.py
      @@ -708,6 +708,14 @@ async def _token_auth_seam(request: Request, call_next):
               ),
               "options": ["stash", "discard"],
           },
      +    "updates.refresh_cua_driver": {
      +        "type": "bool",
      +        "description": (
      +            "Refresh an already-installed cua-driver during hermes update. "
      +            "Disable this on non-admin macOS accounts where /Applications is "
      +            "not writable."
      +        ),
      +    },
       }
       
       # Categories with fewer fields get merged into "general" to avoid tab sprawl.
    • tests/hermes_cli/test_install_cua_driver.py
      @@ -4,12 +4,12 @@
       re-running it is the canonical upgrade path. ``install_cua_driver(upgrade=True)``
       must:
       
      -* Be macOS-only — no-op silently on Linux/Windows so ``hermes update`` can
      -  call it unconditionally without warning every non-macOS user.
      +* Be supported-platform-only — no-op silently elsewhere so ``hermes update``
      +  can call it unconditionally without warning unsupported-platform users.
       * Re-run the installer even when the binary is already on PATH (this is the
         fix for the "we only pulled cua-driver once on enable" complaint).
       * Preserve original ``upgrade=False`` behaviour for the toolset-enable flow:
      -  skip if installed, install otherwise, warn on non-macOS.
      +  skip if installed, install otherwise, warn on unsupported platforms.
       
       The pre-install arch probe that used to live alongside this function was
       deleted (see top-of-file comment in tools_config.py) — the upstream
      @@ -67,6 +67,41 @@ def test_upgrade_on_macos_without_binary_runs_installer(self):
                   assert tools_config.install_cua_driver(upgrade=True) is True
                   runner.assert_called_once()
       
      +    def test_upgrade_on_macos_non_writable_applications_skips_refresh(self):
      +        from hermes_cli import tools_config
      +
      +        with patch("platform.system", return_value="Darwin"), \
      +             patch.object(tools_config.shutil, "which",
      +                          side_effect=lambda n: "/usr/local/bin/" + n
      +                                                 if n in {"cua-driver", "curl"} else None), \
      +             patch.object(tools_config, "_cua_install_target_writable",
      +                          return_value=False), \
      +             patch.object(tools_config, "_run_cua_driver_installer") as runner, \
      +             patch.object(tools_config, "_print_info") as info:
      +            assert tools_config.install_cua_driver(upgrade=True) is True
      +            runner.assert_not_called()
      +            assert any(
      +                "/Applications is not writable" in call.args[0]
      +                for call in info.call_args_list
      +            )
      +
      +    def test_fresh_install_on_macos_non_writable_applications_skips_install(self):
      +        from hermes_cli import tools_config
      +
      +        with patch("platform.system", return_value="Darwin"), \
      +             patch.object(tools_config.shutil, "which",
      +                          side_effect=lambda n: "/usr/bin/curl" if n == "curl" else None), \
      +             patch.object(tools_config, "_cua_install_target_writable",
      +                          return_value=False), \
      +             patch.object(tools_config, "_run_cua_driver_installer") as runner, \
      +             patch.object(tools_config, "_print_info") as info:
      +            assert tools_config.install_cua_driver(upgrade=False) is False
      +            runner.assert_not_called()
      +            assert any(
      +                "/Applications is not writable" in call.args[0]
      +                for call in info.call_args_list
      +            )
      +
           def test_non_upgrade_on_macos_with_binary_skips_install(self):
               from hermes_cli import tools_config
       
  • #58760 fix(computer-use): raise cua-driver session startup timeout to 30s + phase timing log (salvage #57034)

    merged to main

    files: tools/computer_use/cua_backend.py

    Show the diff
    • tools/computer_use/cua_backend.py
      @@ -567,6 +567,7 @@ async def _lifecycle_coro(self) -> None:
               different task than it was entered in" warning emitted by the
               previous _aenter/_aexit split.
               """
      +        import time as _time
               from mcp import ClientSession, StdioServerParameters
               from mcp.client.stdio import stdio_client
               from tools.environments.local import _sanitize_subprocess_env
      @@ -574,6 +575,7 @@ async def _lifecycle_coro(self) -> None:
               # Build the shutdown event on the loop's thread so the asyncio
               # primitive belongs to the correct loop.
               self._shutdown_event = asyncio.Event()
      +        _t0 = _time.monotonic()
       
               try:
                   if not cua_driver_binary_available():
      @@ -583,6 +585,7 @@ async def _lifecycle_coro(self) -> None:
                   # the MCP server, instead of hardcoding ["mcp"]. Falls back
                   # transparently for older drivers / any discovery failure.
                   command, args = _resolve_mcp_invocation(_CUA_DRIVER_CMD)
      +            _t_manifest = _time.monotonic()
                   params = StdioServerParameters(
                       command=command,
                       args=args,
      @@ -594,12 +597,20 @@ async def _lifecycle_coro(self) -> None:
                   async with stdio_client(params) as (read, write):
                       async with ClientSession(read, write) as session:
                           await session.initialize()
      +                    _t_init = _time.monotonic()
                           # Populate capabilities + capability_version BEFORE
                           # exposing the session to callers, so the first
                           # tool call already sees them.
                           await self._populate_capabilities(session)
                           self._session = session
                           self._ready_event.set()
      +                    logger.info(
      +                        "cua-driver session ready in %.1fs "
      +                        "(manifest=%.1fs, mcp_init=%.1fs)",
      +                        _time.monotonic() - _t0,
      +                        _t_manifest - _t0,
      +                        _t_init - _t_manifest,
      +                    )
                           # Hold the contexts open until stop() / restart asks
                           # us to wind down. Tool calls run as their own tasks
                           # on the same loop and touch self._session directly.
      @@ -677,10 +688,10 @@ def _start_lifecycle_locked(self) -> None:
               self._lifecycle_future = asyncio.run_coroutine_threadsafe(
                   self._lifecycle_coro(), loop
               )
      -        if not self._ready_event.wait(timeout=15.0):
      +        if not self._ready_event.wait(timeout=30.0):
                   # Best-effort: signal shutdown if the future is still alive.
                   self._signal_shutdown_locked()
      -            raise RuntimeError("cua-driver session never reached ready (timeout 15s)")
      +            raise RuntimeError("cua-driver session never reached ready (timeout 30s)")
               # If setup failed, the lifecycle coroutine set _setup_error
               # before setting _ready_event. Re-raise it on the caller's thread.
               if self._setup_error is not None:

2 PRs closed without merging

From the close comments: merged via another change 2

  • #47884 fix(update): skip cua-driver refresh when Applications is unwritable · merged via another change → c118b03, #58754
  • #47899 fix(cli): skip cua-driver refresh when /Applications is not writable · merged via another change → #47884, #58754, #57034, #58760
Closed issues · 1
  • Closed#47865Nonfatal: `hermes update` cua-driver refresh fails noisily on non-admin macOS (`/Applications` not writable)
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#58754, #58760
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
5 Jul 2026

For your agent

The same facts, typed.