Files
hermes-agent/hermes_cli/tools_config_cua.py
Brooklyn Nicholson 959c7649fd fix(updates/win): click-session poll, Intel-Mac installer docs, cua-driver opt-in autostart
click-session flake (#97982): a bare scrollIntoView() smooth scroll could be dropped under load, and the script
slept a fixed 3000ms before reading state. Extract click-session-helpers.mjs:
instant centered scroll, a bounded poll-for-composer loop instead of the
fixed sleep, and correct nested CDP envelope unwrapping (the old read logged
undefined).

Intel-Mac installer docs (#99033): the Hermes-Setup.dmg bootstrap installer
is built for Apple Silicon only, so Intel Macs hit "not supported on this
Mac". The desktop release pipeline already builds a native darwin-x64
bundle, so the docs now scope the arm64 limit to the bootstrap installer and
name the darwin-x64 bundle (or the CLI plus `hermes desktop`) as the Intel
path, in the desktop README and the platform-support build-targets section.

cua-driver autostart opt-in (#97389): Windows installs registered the
cua-driver-serve scheduled task on every install, with no opt-out, and
treated the task as an install-readiness requirement. Gate the
install-ready check and _repair_cua_driver_autostart_windows on the new
computer_use.autostart config key (default false = on-demand, fails
closed), extract the registration PowerShell into a testable helper, and
document the opt-in (EN + zh-Hans). Windows-only code path: unit tests
cover the registration args and the config gate; live Windows
verification pending.
2026-09-26 17:09:16 -05:00

270 lines
12 KiB
Python

"""PM-backed CUA setup and the host integration not supplied by its binary archive."""
from __future__ import annotations
import contextlib
import os
import shutil
import subprocess
import sys
import time
from typing import Optional
from hermes_cli.cli_output import (
print_info as _print_info, print_success as _print_success, print_warning as _print_warning)
def _run_text(cmd: list, *, timeout, capture_output: bool = True,
**kwargs) -> subprocess.CompletedProcess:
"""Run a text subprocess with consistent decoding."""
return subprocess.run(cmd, capture_output=capture_output, text=True, encoding="utf-8",
errors="replace", timeout=timeout, **kwargs)
def _fail(message: str, *hints: str) -> bool:
_print_warning(message)
for hint in hints:
_print_info(hint)
return False
def _print_output_tail(result: subprocess.CompletedProcess, printer=None) -> None:
for line in (result.stderr or result.stdout or "").strip().splitlines()[-3:]:
(printer or _print_info)(f" {line[:200]}")
def _post_setup_no_window_flags(*, streams_to_console: bool = False) -> int:
"""Hide Windows children unless their output is going to a real console."""
from hermes_cli._subprocess_compat import windows_hide_flags
flags = windows_hide_flags()
try:
if flags and streams_to_console and sys.stdout is not None and sys.stdout.isatty():
return 0
except Exception:
pass
return flags or 0
def _cua_driver_cmd() -> str:
return os.environ.get("HERMES_CUA_DRIVER_CMD", "").strip() or "cua-driver"
def _cua_version_summary(raw: str, *, limit: int = 120) -> str:
"""Bound an external binary's potentially multiline version banner."""
return next((line.strip()[:limit] for line in (raw or "").splitlines() if line.strip()), "")
def _resolved_cua_driver_cmd() -> Optional[str]:
from tools.computer_use.cua_backend_driver import resolve_cua_driver_cmd
return resolve_cua_driver_cmd()
def _cua_driver_env() -> dict:
from tools.computer_use.cua_backend import sanitized_cua_driver_env
return sanitized_cua_driver_env()
_CUA_DRIVER_CONTRACT_CACHE: dict = {}
def _cua_driver_contract_status(binary: Optional[str] = None) -> dict:
"""Cache the runtime manifest check by binary identity for UI polling."""
from tools.computer_use.cua_backend_driver import cua_driver_runtime_contract_status
resolved = binary or _resolved_cua_driver_cmd()
if not resolved:
return cua_driver_runtime_contract_status(None)
try:
stat = os.stat(resolved)
fingerprint = (resolved, stat.st_mtime_ns, stat.st_size)
except OSError:
return cua_driver_runtime_contract_status(resolved)
now = time.monotonic()
cache = _CUA_DRIVER_CONTRACT_CACHE
if cache.get("fingerprint") == fingerprint and now - cache.get("checked_at", 0.0) < 30.0:
return dict(cache["state"])
state = cua_driver_runtime_contract_status(resolved)
cache.update(fingerprint=fingerprint, checked_at=now, state=dict(state))
return state
def _cua_driver_install_ready() -> bool:
state = _cua_driver_contract_status()
if not state.get("ready"):
return False
if sys.platform == "darwin":
from tools.computer_use.cua_backend_daemon import _resolve_cua_driver_app_path
return bool(_resolve_cua_driver_app_path(state["binary"]))
# On-demand default (#97389): with computer_use.autostart off, a missing
# cua-driver-serve logon task is intentional, not a repair condition —
# requiring it here biased every enable flow toward registering it.
return (sys.platform != "win32"
or _cua_driver_autostart_registered_windows()
or not _cua_autostart_opt_in())
def install_cua_driver(upgrade: bool = False, show_installer_progress: bool = True) -> bool:
"""Prepare the PM pin and host setup for an explicit install/upgrade command.
Both CLI modes reconcile the same pin; neither discovers a vendor release.
A configured override is validated, never replaced or acquired by PM.
Unattended callers should use PM ensure directly, without interactive host setup.
"""
from pm import ensure
override = os.environ.get("HERMES_CUA_DRIVER_CMD", "").strip()
binary = _resolved_cua_driver_cmd()
fresh_install = binary is None
if override:
if not binary:
return _fail(f" HERMES_CUA_DRIVER_CMD does not resolve to an executable: {override}",
" Fix or unset the override before running computer-use install.")
else:
if show_installer_progress:
_print_info(" Preparing the pinned cua-driver with Hermes PM...")
try:
ensure("cua-driver", explicit=True)
except Exception as exc:
return _fail(f" cua-driver preparation failed: {exc}")
binary = _resolved_cua_driver_cmd()
if not binary:
return _fail(" PM did not select a usable cua-driver executable.")
_CUA_DRIVER_CONTRACT_CACHE.clear()
contract = _cua_driver_contract_status(binary)
if not contract.get("ready"):
hint = (" Update the binary selected by HERMES_CUA_DRIVER_CMD, or unset the override."
if override else " Run: hermes computer-use doctor")
return _fail(" cua-driver runtime contract is unusable: "
f"{contract.get('reason') or 'unknown error'}.", hint)
if sys.platform == "win32" and not _repair_cua_driver_autostart_windows(
binary, verbose=show_installer_progress):
return _fail(" cua-driver is compatible, but Windows autostart setup failed.")
if sys.platform == "darwin":
from tools.computer_use.cua_backend_daemon import (
_resolve_cua_driver_app_path, _validate_cua_driver_app_signature)
app = _resolve_cua_driver_app_path(binary)
if not app:
return _fail(" macOS computer use requires the signed CuaDriver.app, not a bare binary.",
" The PM cua-driver package must include its signed macOS app bundle.")
try:
_validate_cua_driver_app_signature(app)
result = _run_text([
"/System/Library/Frameworks/CoreServices.framework/Frameworks/"
"LaunchServices.framework/Support/lsregister", "-f", app], timeout=15)
except (RuntimeError, OSError, subprocess.SubprocessError) as exc:
return _fail(f" cua-driver macOS app registration failed: {exc}")
if result.returncode:
_print_output_tail(result)
return _fail(" cua-driver macOS app registration failed.")
if show_installer_progress:
_print_success(f" cua-driver ready: {contract.get('version') or 'unknown version'}.")
_print_cua_platform_notes(sys.platform == "win32", sys.platform == "linux",
fresh_install=fresh_install)
return True
def _ps_single_quote(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def _cua_driver_autostart_registered_windows(binary: Optional[str] = None) -> bool:
"""A task targeting a previous PM version is not a ready registration."""
if sys.platform != "win32":
return False
from xml.etree import ElementTree
binary = binary or _resolved_cua_driver_cmd()
if not binary:
return False
try:
result = subprocess.run(
["schtasks.exe", "/Query", "/TN", "cua-driver-serve", "/XML"],
capture_output=True, timeout=10, creationflags=_post_setup_no_window_flags())
if result.returncode:
return False
# Parse bytes: schtasks' XML declaration carries the output encoding.
task = ElementTree.fromstring(result.stdout)
commands = task.findall(".//{*}Exec/{*}Command")
return any(os.path.normcase((node.text or "").strip().strip('"')) == os.path.normcase(binary)
for node in commands)
except (OSError, subprocess.SubprocessError, ElementTree.ParseError):
return False
def _cua_autostart_opt_in() -> bool:
"""``computer_use.autostart`` — opt IN to the Windows per-boot ``cua-driver-serve`` logon
task. The default (absent/False) is on-demand (#97389): Computer Use starts the driver per
session exactly as macOS and Linux do, install/enable flows register no scheduled task, and
a missing task is not a repair condition. True registers (or repairs) the logon task at
install/enable time — needed when driving Windows over SSH (Session 0 has no interactive
desktop). Unreadable config fails closed to the on-demand default."""
with contextlib.suppress(Exception):
from hermes_cli.config import load_config
return bool(((load_config() or {}).get("computer_use") or {}).get("autostart", False))
return False
def _cua_autostart_registration_ps_command(binary: str) -> str:
"""PowerShell body for the elevated registration: Start-Process with a STRUCTURED
``-FilePath`` / ``-ArgumentList`` (older install.ps1 builds interpolated the binary path
into a command string, which split at the first space) and ``-Verb RunAs -Wait -PassThru``
so the exit code propagates."""
return (f"$exe = {_ps_single_quote(binary)}; "
"$proc = Start-Process -FilePath $exe -ArgumentList @('autostart','enable') "
"-Verb RunAs -Wait -PassThru -ErrorAction Stop; exit $proc.ExitCode")
def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> bool:
"""Register autostart using structured arguments, including paths with spaces.
Only registers when the user opted into the per-boot task via
``computer_use.autostart`` (#97389): the on-demand default registers
nothing, silently — returning True (nothing to repair) keeps callers from
printing "auto-start was not registered" warnings for an intentional
absence."""
if sys.platform != "win32":
return True
if not _cua_autostart_opt_in():
# On-demand default (#97389): the per-boot cua-driver-serve task is opt-in
# via computer_use.autostart. Registering here would recreate, on every
# enable/install, exactly the silent boot-time task the issue reports.
return True
binary = shutil.which(driver_cmd)
if not binary:
return False
if _cua_driver_autostart_registered_windows(binary):
return True
ps = shutil.which("powershell") or shutil.which("powershell.exe") or "powershell"
ps_cmd = _cua_autostart_registration_ps_command(binary)
_print_info(" Registering cua-driver auto-start..." if verbose
else " Repairing cua-driver auto-start registration...")
try:
result = _run_text([ps, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-Command", ps_cmd],
timeout=300, env=_cua_driver_env(),
creationflags=_post_setup_no_window_flags())
except subprocess.TimeoutExpired:
return _fail(" cua-driver autostart registration timed out.")
except Exception as exc:
return _fail(f" cua-driver autostart registration failed: {exc}")
if result.returncode == 0:
return _cua_driver_autostart_registered_windows(binary)
_print_warning(" cua-driver autostart registration failed.")
_print_output_tail(result)
_print_info(f" From an elevated shell, run: & {_ps_single_quote(binary)} autostart enable")
return False
def _print_cua_platform_notes(is_windows: bool, is_linux: bool, *, fresh_install: bool) -> None:
if is_windows:
_print_info(" cua-driver may spawn a UIAccess worker (cua-driver-uia.exe);")
_print_info(" Windows/SmartScreen may prompt the first time it runs.")
elif is_linux:
_print_warning(" Linux support is alpha.")
else:
_print_info(" IMPORTANT — grant macOS permissions now:" if fresh_install
else " Grant macOS permissions if not done yet:")
_print_info(" System Settings > Privacy & Security > Accessibility")
_print_info(" System Settings > Privacy & Security > Screen Recording")
_print_info(" Allow CuaDriver.app; run `hermes computer-use permissions grant` for guidance.")