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.
270 lines
12 KiB
Python
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.") |