"""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.")