fix(tools): install cua-driver when Computer Use is enabled via 'hermes tools' (#22765)

Returning users who enabled '🖱️ Computer Use (macOS)' via 'hermes tools'
saw '✓ Saved configuration' but no install — cua-driver was never on
PATH and the toolset failed at first use. Two compounding causes:

1. _toolset_needs_configuration_prompt fell through to _toolset_has_keys,
   which returned True for any provider with empty env_vars. cua-driver
   has no env vars, so the gate skipped _configure_toolset entirely and
   _run_post_setup('cua_driver') never ran.

2. No stable CLI entry-point existed for re-running the install when
   the picker no-op'd it (e.g. when toggling the toolset off+on inside
   one picker session, where 'added' is empty).

Changes:

- hermes_cli/tools_config.py: add _POST_SETUP_INSTALLED registry
  mapping post_setup keys to installed-state predicates. The gate
  now returns True when any visible provider has a registered
  post_setup whose predicate fails. cua_driver is the only opt-in
  for now; other post_setup hooks keep their existing behaviour.
- hermes_cli/main.py: add 'hermes computer-use install' and
  'hermes computer-use status' as a stable docs target. install
  reuses the same _run_post_setup('cua_driver') path that the
  picker invokes; status reports whether cua-driver is on PATH.
- tools/computer_use/cua_backend.py: install hint now points users
  at 'hermes computer-use install' first.
- website/docs/user-guide/features/computer-use.md: document the
  new command as the primary install path.
- website/docs/reference/cli-commands.md: catalog 'hermes
  computer-use' alongside 'hermes tools'.
- tests/hermes_cli/test_post_setup_gating.py: regression coverage
  for the gate predicate (missing -> setup forced, installed ->
  setup skipped, broken predicate -> non-blocking, unregistered
  keys -> behaviour unchanged).

Fixes #22737. Reported by @f-trycua.
This commit is contained in:
Teknium 2026-05-09 13:02:25 -07:00 committed by GitHub
parent 6e5489c9f3
commit 8f711f79a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 205 additions and 4 deletions

View file

@ -12,6 +12,7 @@ the `platform_toolsets` key.
import json as _json
import logging
import os
import shutil
import sys
from pathlib import Path
from typing import Dict, List, Optional, Set
@ -1424,12 +1425,52 @@ def _visible_providers(cat: dict, config: dict) -> list[dict]:
return visible
_POST_SETUP_INSTALLED: dict = {
# post_setup_key -> predicate(): True when the install side-effect
# is already satisfied. Used by `_toolset_needs_configuration_prompt`
# to force the provider-setup flow when a no-key provider still needs
# a binary/dependency install (otherwise an already-configured user
# who toggles the toolset on via `hermes tools` gets a silent no-op
# because the gate sees "no env vars to ask about" and skips the
# provider-setup flow that would have run the post_setup hook).
#
# Only entries here are gated; other post_setup hooks (kittentts,
# piper, agent_browser, etc.) keep their existing behaviour. Add an
# entry when (a) the post_setup is the ONLY install side-effect for
# a no-key provider, and (b) an installed-state check is cheap and
# doesn't trigger a heavy import.
"cua_driver": lambda: bool(shutil.which("cua-driver")),
}
def _post_setup_already_installed(post_setup_key: str) -> bool:
"""Return True when the post_setup install side-effect is satisfied."""
predicate = _POST_SETUP_INSTALLED.get(post_setup_key)
if predicate is None:
# No install-state check registered → assume satisfied (don't
# change behaviour for hooks we haven't explicitly opted in).
return True
try:
return bool(predicate())
except Exception:
return True
def _toolset_needs_configuration_prompt(ts_key: str, config: dict) -> bool:
"""Return True when enabling this toolset should open provider setup."""
cat = TOOL_CATEGORIES.get(ts_key)
if not cat:
return not _toolset_has_keys(ts_key, config)
# If any visible provider has a registered post_setup install-state
# check that hasn't been satisfied (e.g. cua-driver binary not on
# PATH yet), force the configuration flow so `_configure_provider`
# invokes `_run_post_setup` and the install actually runs.
for provider in _visible_providers(cat, config):
post_setup = provider.get("post_setup")
if post_setup and not _post_setup_already_installed(post_setup):
return True
if ts_key == "tts":
tts_cfg = config.get("tts", {})
return not isinstance(tts_cfg, dict) or "provider" not in tts_cfg