merge origin/main (2 commits) into ethie/pm-clean

This commit is contained in:
ethernet
2026-09-21 07:32:26 -04:00
7 changed files with 100 additions and 7 deletions

View File

@@ -63,7 +63,7 @@ def _cu_doctor(args) -> None:
def _cu_perms_status(args) -> None:
import json as _json
from tools.computer_use.permissions import computer_use_status
from tools.computer_use.permissions import TCC_FIELDS, computer_use_status, stale_tcc_grant_hint
st = computer_use_status()
if bool(getattr(args, "json", False)):
print(_json.dumps(st, indent=2, sort_keys=True))
@@ -81,6 +81,8 @@ def _cu_perms_status(args) -> None:
print(f" {glyph(st['screen_recording'])} Screen Recording")
if not st["ready"]:
print(" Grant: hermes computer-use permissions grant")
if hint := stale_tcc_grant_hint(*(f for f in TCC_FIELDS if st[f] is False)):
print(f" {hint}")
else: # no TCC model — readiness is driver health
print(f" {glyph(st['ready'])} driver health (no permission toggles on {st['platform']})")
for c in st["checks"]:

View File

@@ -450,3 +450,28 @@ class TestDoctorVersionIdentity:
payload = json.loads(out.getvalue())
assert payload["hermes_identity"]["version_mismatch"] is False
def test_failed_tcc_row_from_health_report_names_the_stale_row_reset_for_that_service():
"""A failed ``tcc_*`` row from the driver's own health_report (0.22+, not only the 0.10 fallback probes) must
carry the stale-grant recovery for its own TCC service, because System Settings can show the toggle ON while
the daemon is denied (trycua/cua#3170)."""
from tools.computer_use import doctor
report = _ok_report()
report["checks"] = [{"name": "tcc_screen_recording", "status": "fail", "message": "Screen Recording is not granted.",
"hint": "Grant it in System Settings."},
{"name": "tcc_accessibility", "status": "pass", "message": "granted"}]
proc = _fake_proc_with_responses(
{"jsonrpc": "2.0", "id": 1, "result": {}},
{"jsonrpc": "2.0", "id": 2, "result": {"structuredContent": report}},
)
with patch("shutil.which", return_value="/fake/cua-driver"), patch("subprocess.Popen", return_value=proc), \
patch("sys.stdout", new_callable=StringIO) as out:
doctor.run_doctor(json_output=True)
checks = {c["name"]: c for c in json.loads(out.getvalue())["checks"]}
assert checks["tcc_screen_recording"]["hint"].startswith("Grant it in System Settings.")
assert "tccutil reset ScreenCapture com.trycua.driver" in checks["tcc_screen_recording"]["hint"]
assert "reset Accessibility" not in checks["tcc_screen_recording"]["hint"]
assert "tccutil" not in checks["tcc_accessibility"].get("hint", "")

View File

@@ -93,4 +93,24 @@ def test_computer_use_install_propagates_setup_result(monkeypatch, ready, upgrad
monkeypatch.setattr(cua, "install_cua_driver", install)
args = ("install", "--upgrade") if upgrade else ("install",)
assert _invoke(monkeypatch, *args) == (0 if ready else 1)
install.assert_called_once_with(upgrade=upgrade)
install.assert_called_once_with(upgrade=upgrade)
def test_permissions_status_names_the_stale_tcc_row_for_the_missing_grant(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""A grant the daemon reports missing while System Settings shows it ON is a stale TCC row (trycua/cua#3170);
the status output must name the reset for exactly the missing service, never for one that is granted."""
from tools.computer_use import permissions
status = {"platform": "darwin", "platform_supported": True, "installed": True, "version": "cua-driver 0.28.2",
"ready": False, "can_grant": True, "checks": [], "source": None, "error": None,
"accessibility": False, "screen_recording": True, "screen_recording_capturable": True}
monkeypatch.setattr(permissions, "computer_use_status", lambda driver_cmd=None: status)
assert _invoke(monkeypatch, "permissions", "status") == 1
out = capsys.readouterr().out
assert "tccutil reset Accessibility com.trycua.driver" in out
assert "ScreenCapture" not in out
assert "hermes computer-use permissions grant" in out

View File

@@ -17,12 +17,13 @@ from collections import deque
from typing import Any, Dict, List, Optional, Tuple
from tools.computer_use import cua_backend_driver as _driver
from tools.computer_use.permissions import CUA_DRIVER_BUNDLE_ID
logger = logging.getLogger("tools.computer_use.cua_backend")
# The only bundle identity the private daemon may launch through, and the teams that sign official
# releases. Exact matches only: a suffixed identifier or other team is an impostor.
_CUA_DRIVER_BUNDLE_ID = "com.trycua.driver"
_CUA_DRIVER_BUNDLE_ID = CUA_DRIVER_BUNDLE_ID
_CUA_DRIVER_TEAM_IDS = ("4YEC26S9KF", "YCK386LBJ7")
_QUIET_ERRORS = (OSError, subprocess.SubprocessError)

View File

@@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tupl
from hermes_cli._subprocess_compat import windows_hide_flags
from tools.computer_use.permissions import _child_env as _sanitized_cua_env
from tools.computer_use.permissions import stale_tcc_grant_hint
# Match the ALLOWED_STATUS_VALUES + ALLOWED_OVERALL_VALUES the cua-driver integration test pins.
_STATUS_GLYPH = {"pass": "✅", "fail": "❌", "skip": "⏭️"}
@@ -272,6 +273,19 @@ def _compose_fallback_report(binary: str, *, reason: str = "", timeout: float =
"overall": _overall_from(checks), "checks": checks,
"fallback": True, "fallback_reason": reason or "health_report unavailable"}
_TCC_CHECK_FIELDS = {"tcc_accessibility": "accessibility", "tcc_screen_recording": "screen_recording"}
def _apply_stale_tcc_guard(report: Report) -> Report:
"""Append the stale-row recovery to every failed ``tcc_*`` check. Applied at the report seam so the driver's
own health_report rows (0.22+) get it too, not only the 0.10 fallback probes — the users hit by a stale row
are on current drivers (trycua/cua#3170)."""
checks = report.get("checks")
for check in (c for c in (checks if isinstance(checks, list) else ()) if isinstance(c, dict)):
field = _TCC_CHECK_FIELDS.get(check.get("name"))
if field and check.get("status") == "fail":
check["hint"] = f"{check.get('hint') or ''} {stale_tcc_grant_hint(field)}".strip()
return report
def _apply_display_count_guard(report: Report) -> Report:
"""Fail an 'ok' screen_capture_capability with ``display_count=0``: macOS ScreenCaptureKit reports 0 on headless
/ asleep panels — TCC fine, health_report ok, yet every capture is 0x0. Turns a silent failure actionable; applied
@@ -441,6 +455,7 @@ def run_doctor(driver_cmd: Optional[str] = None, *, include: Sequence[str] = (),
print(f"cua-driver health_report failed: {e}", file=sys.stderr)
return 2
report = _apply_display_count_guard(report)
report = _apply_stale_tcc_guard(report)
report = _apply_stale_unit_guard(report)
report = _apply_daemon_liveness_guard(report, binary)
identity = _build_identity(binary, report)

View File

@@ -18,6 +18,23 @@ from hermes_cli._subprocess_compat import windows_hide_flags
_RUNTIME_PLATFORMS = frozenset({"darwin", "win32", "linux"}) # mirrors the toolset platform_gate
_BOOLS = ("accessibility", "screen_recording", "screen_recording_capturable")
CUA_DRIVER_BUNDLE_ID = "com.trycua.driver" # the only identity TCC rows and the private daemon are keyed to
_TCC_SERVICES = {"accessibility": "Accessibility", "screen_recording": "ScreenCapture"} # status field -> tccutil service
TCC_FIELDS = tuple(_TCC_SERVICES)
def stale_tcc_grant_hint(*missing: str) -> str:
"""Recovery text for grants the daemon reports missing while System Settings shows the CuaDriver toggle ON.
TCC keys each row to the app's code-signing requirement, so a row written for an earlier CuaDriver build can
stop matching after a driver update: the toggle stays ON, the daemon is denied, and flipping the toggle does
not rewrite the requirement (trycua/cua#3170). Only a reset + re-grant recovers it, so the hint names the
exact rows for the *missing* status fields.
"""
resets = " && ".join(f"tccutil reset {_TCC_SERVICES[f]} {CUA_DRIVER_BUNDLE_ID}" for f in missing if f in _TCC_SERVICES)
if not resets:
return ""
return ("If System Settings already shows CuaDriver ON, the stored grant is stale (it no longer matches the installed "
f"driver's signature): run `{resets}`, then `hermes computer-use permissions grant`.")
def _child_env() -> Dict[str, str]:
"""cua-driver child env (telemetry policy + provider secrets stripped); ``os.environ`` on import error.
@@ -101,7 +118,7 @@ def request_permissions_grant(driver_cmd: Optional[str] = None) -> int:
print("cua-driver: not installed. Run: hermes computer-use install")
return 2
print("Requesting Accessibility + Screen Recording for CuaDriver.\n"
"macOS will show a dialog attributed to CuaDriver (com.trycua.driver) — approve it, then return here.")
f"macOS will show a dialog attributed to CuaDriver ({CUA_DRIVER_BUNDLE_ID}) — approve it, then return here.")
try:
return int(subprocess.run([binary, "permissions", "grant"], env=_child_env(), stdin=subprocess.DEVNULL).returncode)
except KeyboardInterrupt: # pragma: no cover - interactive

View File

@@ -84,7 +84,7 @@ platform-appropriate prereqs:
| Platform | Prereqs |
|---|---|
| **macOS** | System Settings → Privacy & Security → **Accessibility** + **Screen Recording**. Grant the identity named by `hermes computer-use doctor`. Standard mode uses CuaDriver.app; bounded and unrestricted modes use the Hermes host identity. |
| **macOS** | System Settings → Privacy & Security → **Accessibility** + **Screen Recording**. Grant the identity named by `hermes computer-use doctor` (CuaDriver, `com.trycua.driver`, in every permission mode — the driver daemon always launches through `CuaDriver.app`). |
| **Windows** | None at install time. If you're driving over SSH (not RDP / console), you need the autostart pattern — see [cua.ai/docs/how-to-guides/driver/windows-ssh](https://cua.ai/docs/how-to-guides/driver/windows-ssh) for the Session 0 ↔ Session 1+ proxy. |
| **Linux** | A reachable display server: `DISPLAY` set for X11, or `XDG_SESSION_TYPE=wayland`. Wayland sessions need an XWayland bridge for capture. AT-SPI must be on (default on GNOME/KDE/Xfce). |
@@ -154,8 +154,8 @@ resetting or closing the Hermes session, cancellation cleanup, or process exit
closes that transport session. Hermes also stops private runtimes that it
launched for bounded or unrestricted access. One Hermes
conversation cannot change another runtime's mode or grants. Bounded and
unrestricted modes use a private
embedded service under the Hermes host identity.
unrestricted modes use a private embedded daemon, launched through
`CuaDriver.app` on macOS (see above).
`smart` approval remains `standard`: an LLM classification cannot stand in for
a reviewed manifest.
@@ -610,6 +610,19 @@ run `hermes tools` and enable the Computer Use toolset.
didn't see may be blocking input. Dismiss it with `escape` or the close
button.
**macOS: System Settings shows CuaDriver ON, but `hermes computer-use
permissions status` / `doctor` report Accessibility or Screen Recording as
not granted** — the stored grant is stale. macOS keys each permission row to
the app's code-signing requirement; a row written for an earlier CuaDriver
build stops matching after a driver update, and flipping the toggle does not
rewrite it. Reset the affected rows and re-grant:
```
tccutil reset Accessibility com.trycua.driver
tccutil reset ScreenCapture com.trycua.driver
hermes computer-use permissions grant
```
**Element indices are stale** — SOM indices are only valid until the
next `capture`. Re-capture after any state-changing action. The
wrapper carries opaque `element_token`s for stale detection — you'll