feat(computer-use): refresh cua-driver on hermes update + add install --upgrade (#24063)

cua-driver was only installed once on toolset enable: `_run_post_setup` early-returns when the binary is already on PATH, so upstream fixes (e.g. v0.1.6 Safari window-focus fix) never reached existing users without manual reinstall.

Two refresh points now:
- `hermes update` re-runs the upstream installer at the end of the update if cua-driver is on PATH (macOS-only, no-op otherwise). Ties driver freshness to the user-controlled update cadence — no startup latency, no per-launch GitHub API call.
- `hermes computer-use install --upgrade` for manual force-refresh.

The upstream `install.sh` always pulls the latest release, so re-running is the canonical upgrade path. No version-comparison logic needed.

`hermes computer-use status` now shows the installed version, and points at `--upgrade` for refreshing.
This commit is contained in:
Teknium 2026-05-11 17:10:58 -07:00 committed by GitHub
parent 97a0e69df0
commit ced1990c1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 308 additions and 50 deletions

View file

@ -7801,6 +7801,22 @@ def _cmd_update_impl(args, gateway_mode: bool):
except Exception as e:
logger.debug("FHS PATH guard check failed: %s", e)
# Refresh the cua-driver binary used by the Computer Use toolset.
# The upstream installer is gated on macOS and on the binary already
# being on PATH, so this is a no-op for users who don't have it.
# Tying the refresh to ``hermes update`` gives users a predictable
# cadence (matches when they pull new agent code) without adding
# startup latency or a per-launch GitHub API call.
try:
if sys.platform == "darwin" and shutil.which("cua-driver"):
from hermes_cli.tools_config import install_cua_driver
print()
print("→ Refreshing cua-driver (Computer Use)...")
install_cua_driver(upgrade=True)
except Exception as e:
logger.debug("cua-driver refresh failed: %s", e)
# Write exit code *before* the gateway restart attempt.
# When running as ``hermes update --gateway`` (spawned by the gateway's
# /update command), this process lives inside the gateway's systemd
@ -10801,10 +10817,19 @@ Examples:
)
computer_use_sub = computer_use_parser.add_subparsers(dest="computer_use_action")
computer_use_sub.add_parser(
computer_use_install = computer_use_sub.add_parser(
"install",
help="Install or repair the cua-driver binary (macOS)",
)
computer_use_install.add_argument(
"--upgrade",
action="store_true",
help=(
"Re-run the upstream installer even if cua-driver is already on "
"PATH. The upstream install.sh always pulls the latest release, "
"so this performs an in-place upgrade."
),
)
computer_use_sub.add_parser(
"status",
help="Print whether cua-driver is installed and on PATH",
@ -10813,14 +10838,27 @@ Examples:
def cmd_computer_use(args):
action = getattr(args, "computer_use_action", None)
if action == "install":
from hermes_cli.tools_config import _run_post_setup
_run_post_setup("cua_driver")
from hermes_cli.tools_config import install_cua_driver
install_cua_driver(upgrade=bool(getattr(args, "upgrade", False)))
return
if action == "status":
import shutil
import subprocess
path = shutil.which("cua-driver")
if path:
print(f"cua-driver: installed at {path}")
version = ""
try:
version = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
except Exception:
pass
if version:
print(f"cua-driver: installed at {path} ({version})")
else:
print(f"cua-driver: installed at {path}")
print(" Refresh to latest: hermes computer-use install --upgrade")
return
print("cua-driver: not installed")
print(" Run: hermes computer-use install")

View file

@ -591,6 +591,132 @@ def _pip_install(
)
def install_cua_driver(upgrade: bool = False) -> bool:
"""Install or refresh the cua-driver binary used by Computer Use.
The upstream installer always pulls the latest release tag, so re-running
it is the canonical way to upgrade. We expose two modes:
* ``upgrade=False`` original post-setup behaviour: skip if already
installed, install otherwise. Used by the toolset enable flow where
we don't want to surprise the user with a network fetch.
* ``upgrade=True`` always re-run the installer (or call ``cua-driver
update`` if the binary supports it). Used by ``hermes update`` and
by ``hermes computer-use install --upgrade``.
Returns True iff cua-driver is installed (or successfully refreshed)
when the function returns. macOS-only silently returns False on
other platforms.
"""
import platform as _plat
import shutil
import subprocess
if _plat.system() != "Darwin":
if upgrade:
# Silent on non-macOS — `hermes update` calls this for every
# user; only macOS users with cua-driver care.
return False
_print_warning(" Computer Use (cua-driver) is macOS-only; skipping.")
return False
binary = shutil.which("cua-driver")
# Not installed → fresh install path (only when caller asked for it).
if not binary and not upgrade:
if not shutil.which("curl"):
_print_warning(" curl not found — install manually:")
_print_info(" https://github.com/trycua/cua/blob/main/libs/cua-driver/README.md")
return False
return _run_cua_driver_installer(label="Installing")
# Already installed and caller didn't ask to upgrade → just confirm.
if binary and not upgrade:
try:
version = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
_print_success(f" cua-driver already installed: {version or 'unknown version'}")
except Exception:
_print_success(" cua-driver already installed.")
_print_info(" Grant macOS permissions if not done yet:")
_print_info(" System Settings > Privacy & Security > Accessibility")
_print_info(" System Settings > Privacy & Security > Screen Recording")
return True
# upgrade=True path — refresh to the latest upstream release.
if not shutil.which("curl"):
_print_warning(" curl not found — cannot refresh cua-driver.")
return bool(binary)
if binary:
# Show before/after version when we have a baseline. Best-effort.
try:
before = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
except Exception:
before = ""
else:
before = ""
ok = _run_cua_driver_installer(label="Refreshing", verbose=False)
if ok and before:
try:
after = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
if after and after != before:
_print_success(f" cua-driver upgraded: {before}{after}")
elif after:
_print_info(f" cua-driver up to date: {after}")
except Exception:
pass
return ok
def _run_cua_driver_installer(label: str = "Installing", verbose: bool = True) -> bool:
"""Run the upstream cua-driver install.sh. Returns True on success.
The script is idempotent: it always downloads the latest release, so
re-running it on an already-installed system performs an upgrade.
"""
import shutil
import subprocess
install_cmd = (
"/bin/bash -c \"$(curl -fsSL "
"https://raw.githubusercontent.com/trycua/cua/main/"
"libs/cua-driver/scripts/install.sh)\""
)
if verbose:
_print_info(f" {label} cua-driver (macOS background computer-use)...")
else:
_print_info(f" {label} cua-driver...")
try:
result = subprocess.run(install_cmd, shell=True, timeout=300)
if result.returncode == 0 and shutil.which("cua-driver"):
if verbose:
_print_success(" cua-driver installed.")
_print_info(" IMPORTANT — grant macOS permissions now:")
_print_info(" System Settings > Privacy & Security > Accessibility")
_print_info(" System Settings > Privacy & Security > Screen Recording")
_print_info(" Both must allow the terminal / Hermes process.")
return True
_print_warning(f" cua-driver {label.lower()} did not complete. Re-run manually:")
_print_info(f" {install_cmd}")
return False
except subprocess.TimeoutExpired:
_print_warning(f" cua-driver {label.lower()} timed out. Re-run manually.")
return False
except Exception as e:
_print_warning(f" cua-driver {label.lower()} failed: {e}")
return False
def _run_post_setup(post_setup_key: str):
"""Run post-setup hooks for tools that need extra installation steps."""
import shutil
@ -729,51 +855,7 @@ def _run_post_setup(post_setup_key: str):
_print_info(" docker run -p 9377:9377 -e CAMOFOX_PORT=9377 jo-inc/camofox-browser")
elif post_setup_key == "cua_driver":
# cua-driver provides macOS background computer-use (SkyLight SPIs).
# Install via upstream curl script if the binary isn't on $PATH yet.
import platform as _plat
import subprocess
if _plat.system() != "Darwin":
_print_warning(" Computer Use (cua-driver) is macOS-only; skipping.")
return
if shutil.which("cua-driver"):
try:
version = subprocess.run(
["cua-driver", "--version"],
capture_output=True, text=True, timeout=5,
).stdout.strip()
_print_success(f" cua-driver already installed: {version or 'unknown version'}")
except Exception:
_print_success(" cua-driver already installed.")
_print_info(" Grant macOS permissions if not done yet:")
_print_info(" System Settings > Privacy & Security > Accessibility")
_print_info(" System Settings > Privacy & Security > Screen Recording")
return
if not shutil.which("curl"):
_print_warning(" curl not found — install manually:")
_print_info(" https://github.com/trycua/cua/blob/main/libs/cua-driver/README.md")
return
_print_info(" Installing cua-driver (macOS background computer-use)...")
try:
install_cmd = (
"/bin/bash -c \"$(curl -fsSL "
"https://raw.githubusercontent.com/trycua/cua/main/"
"libs/cua-driver/scripts/install.sh)\""
)
result = subprocess.run(install_cmd, shell=True, timeout=300)
if result.returncode == 0 and shutil.which("cua-driver"):
_print_success(" cua-driver installed.")
_print_info(" IMPORTANT — grant macOS permissions now:")
_print_info(" System Settings > Privacy & Security > Accessibility")
_print_info(" System Settings > Privacy & Security > Screen Recording")
_print_info(" Both must allow the terminal / Hermes process.")
else:
_print_warning(" cua-driver install did not complete. Re-run manually:")
_print_info(f" {install_cmd}")
except subprocess.TimeoutExpired:
_print_warning(" cua-driver install timed out. Re-run manually.")
except Exception as e:
_print_warning(f" cua-driver install failed: {e}")
install_cua_driver(upgrade=False)
elif post_setup_key == "kittentts":
try: