fix(computer-use): windowless, non-interactive Windows autostart repair

The shared cua-driver install/refresh path reached _repair_cua_driver_autostart_windows, which spawned powershell.exe without CREATE_NO_WINDOW and without -NonInteractive. Under a windowless parent (Desktop backend, detached gateway, logon task) that child allocated its OWN console: a blank PowerShell window parked on the desktop for as long as the elevated -Verb RunAs -Wait child lived, with no interactive prompt it could unwind from. Measured live on Windows 11 (pythonw parent): production kwargs open a visible console/Terminal window, the same spawn with CREATE_NO_WINDOW opens none.

A failed repair also failed the whole install, so a compatible, working Computer Use toolset read as broken and the install was re-attempted on the next run. It now degrades instead: warn plus the elevated 'cua-driver autostart enable' hint.

Fixes #115017
This commit is contained in:
finn763
2026-09-18 21:04:05 +08:00
committed by Teknium
parent efa31fff8c
commit 6f29d89767
2 changed files with 92 additions and 4 deletions

View File

@@ -303,7 +303,13 @@ def install_cua_driver(upgrade: bool = False, require_confirmed_update: bool = F
suffix = "" if version is None else f": {version or 'unknown version'}"
_print_success(f" {driver_cmd} already installed{suffix}.")
if is_windows and not _repair_cua_driver_autostart_windows(binary, verbose=False):
return _fail(" cua-driver is compatible, but Windows autostart repair failed.")
# Degrade, do not fail (#115017): the driver is installed and compatible — only its
# logon task is missing (declined or unavailable UAC consent, no interactive desktop).
# Returning False here made the whole install report failure, so a working Computer Use
# toolset read as broken and the install was re-attempted on the next run. The reachable
# next step is the manual elevated command.
_print_warning(" cua-driver is compatible; its logon auto-start was not registered.")
_print_info(" From an elevated shell, run: cua-driver autostart enable")
_print_cua_platform_notes(is_windows, is_linux, fresh_install=False)
return True
if repair_existing:
@@ -512,7 +518,16 @@ def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> b
"""Best-effort repair for Windows installer autostart quoting failures.
Older install.ps1 builds interpolated the binary path into a PowerShell command string, which
split at the first space. If the scheduled task is missing, retry via Start-Process's
structured ``-FilePath`` / ``-ArgumentList`` parameters instead."""
structured ``-FilePath`` / ``-ArgumentList`` parameters instead.
The wrapper itself must stay invisible and unattended (#115017): this runs from the shared
install/refresh path, which a windowless parent drives (Desktop backend, detached gateway, a
logon task). A ``powershell.exe`` spawned without ``CREATE_NO_WINDOW`` allocates its OWN console
there — a blank PowerShell window parked on the user's desktop for as long as the elevated
``-Verb RunAs -Wait`` child lives — and without ``-NonInteractive`` that shell can sit on an
interactive prompt instead of unwinding. ``CREATE_NO_WINDOW`` (stdlib ``windows_hide_flags``,
the same seam every other spawn in this module uses) keeps stdio as pipes so the failure text
is still reported; the OS-owned UAC consent UI is unaffected."""
if sys.platform != "win32" or _cua_driver_autostart_registered_windows():
return True
binary = shutil.which(driver_cmd)
@@ -525,8 +540,10 @@ def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> b
_print_info(" Registering cua-driver auto-start..." if verbose
else " Repairing cua-driver auto-start registration...")
try:
result = _run_text([ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd],
timeout=300, env=_cua_driver_env())
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:

View File

@@ -1628,6 +1628,77 @@ class TestWindowsAutostartRepair:
assert f"$exe = '{driver}'" in ps_command
assert f"& {driver}" not in ps_command
@pytest.mark.windows_only
def test_repair_spawns_no_console_window_and_failure_degrades(self):
"""``windows_only``: issue #115017 — the auto-start repair must not park a blank PowerShell
window on the desktop, and a failed repair must not fail the install.
This repair is reached from the shared install/refresh path, which a windowless parent
drives (Desktop backend, detached gateway, a logon task). A ``powershell.exe`` spawned
there WITHOUT ``CREATE_NO_WINDOW`` allocates its own console: a blank PowerShell window that
stays on screen for as long as the elevated ``-Verb RunAs -Wait`` child lives (verified live
on Windows 11: the window appears, and with ``CREATE_NO_WINDOW`` it never does). Without
``-NonInteractive`` that shell can also sit on an interactive prompt instead of unwinding.
"""
from hermes_cli import tools_config_cua as tools_config
create_no_window = 0x08000000
calls = []
driver = (
r"C:\Users\Ha Trung\AppData\Local\Programs\Cua"
r"\cua-driver\bin\cua-driver.exe"
)
def fake_which(name: str):
if name == "cua-driver":
return driver
if name == "powershell":
return r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
return None
def fake_run(cmd, **kwargs):
calls.append((cmd, kwargs))
if cmd[0] == "schtasks.exe": # task absent -> the repair branch runs
return SimpleNamespace(returncode=1, stdout="", stderr="")
return SimpleNamespace(returncode=1, stdout="", stderr="denied") # repair itself fails
with patch.object(tools_config.shutil, "which", side_effect=fake_which), \
patch("subprocess.run", side_effect=fake_run), \
patch.object(tools_config, "_post_setup_no_window_flags",
side_effect=lambda **kw: create_no_window), \
patch.object(tools_config, "_print_warning") as warn, \
patch.object(tools_config, "_print_info"):
assert tools_config._repair_cua_driver_autostart_windows(
"cua-driver", verbose=False
) is False
ps_cmd, ps_kwargs = next(
(cmd, kwargs) for cmd, kwargs in calls
if str(cmd[0]).lower().endswith("powershell.exe")
)
assert ps_kwargs.get("creationflags") == create_no_window, (
"windowless spawn is the fix: without CREATE_NO_WINDOW the child allocates its own "
"visible console window on the user's desktop"
)
assert "-NonInteractive" in ps_cmd, "no interactive prompt may be presented"
assert ps_cmd[-2:] == ["-Command", ps_cmd[-1]], "script stays the -Command argument"
assert ps_kwargs.get("timeout"), "the repair must stay bounded, never block the caller"
# Degrade, do not fail: the driver is installed and compatible, only its logon task is
# missing — install_cua_driver must not report the whole toolset as broken.
with patch.object(tools_config, "_resolved_cua_driver_cmd", return_value=driver), \
patch.object(tools_config, "_cua_driver_contract_status",
return_value={"ready": True, "version": "0.20.0", "reason": ""}), \
patch.object(tools_config, "_cua_driver_version", return_value="0.20.0"), \
patch.object(tools_config, "_repair_cua_driver_autostart_windows",
return_value=False), \
patch.object(tools_config, "_print_success"):
assert tools_config.install_cua_driver(
upgrade=False, show_installer_progress=False
) is True
assert any("auto-start" in str(call) for call in warn.call_args_list)
class TestCuaVersionSummary:
"""`hermes computer-use status` prints one line, whatever the binary says.