merge origin/main (2 commits) into ethie/pm-clean
This commit is contained in:
@@ -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"]:
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user