diff --git a/tests/tools/test_computer_use_display_count_guard.py b/tests/tools/test_computer_use_display_count_guard.py new file mode 100644 index 0000000000..e45dd66387 --- /dev/null +++ b/tests/tools/test_computer_use_display_count_guard.py @@ -0,0 +1,60 @@ +"""macOS ScreenCaptureKit display_count=0 diagnosability. + +Composed from PR #52949 (sujeet111, doctor guard) and PR #67259 +(webtecnica, actionable-hint direction): a headless Mac or asleep panel +leaves SCK with zero shareable displays while TCC grants pass, so +health_report says ok and every capture returns 0x0 silently. The guard +turns that into a failed check + degraded overall + recovery hint. +""" + +from tools.computer_use.doctor import _apply_display_count_guard + + +def _report(status="pass", count=0, overall="ok", name="screen_capture_capability"): + return { + "overall": overall, + "checks": [ + { + "name": name, + "status": status, + "message": "ScreenCaptureKit reachable", + "data": {"display_count": count}, + } + ], + } + + +def test_zero_displays_downgrades_ok_to_degraded(): + out = _apply_display_count_guard(_report(count=0)) + assert out["overall"] == "degraded" + assert out["checks"][0]["status"] == "fail" + assert "0 shareable" in out["checks"][0]["message"] + assert "dongle" in out["checks"][0]["hint"] + + +def test_positive_display_count_untouched(): + out = _apply_display_count_guard(_report(count=1)) + assert out["overall"] == "ok" + assert out["checks"][0]["status"] == "pass" + + +def test_already_failed_check_not_rewritten(): + out = _apply_display_count_guard(_report(status="fail", count=0, overall="degraded")) + # Guard only flips pass->fail; an existing failure keeps its message. + assert out["checks"][0]["message"] == "ScreenCaptureKit reachable" + + +def test_missing_data_and_other_checks_ignored(): + out = _apply_display_count_guard( + {"overall": "ok", "checks": [{"name": "binary_version", "status": "pass"}]} + ) + assert out["overall"] == "ok" + out2 = _apply_display_count_guard({"overall": "ok", "checks": "bogus"}) + assert out2["overall"] == "ok" + + +def test_overall_failed_not_upgraded(): + out = _apply_display_count_guard(_report(count=0, overall="failed")) + # fail-worse states are never softened to degraded. + assert out["overall"] == "failed" + assert out["checks"][0]["status"] == "fail" diff --git a/tools/computer_use/cua_backend.py b/tools/computer_use/cua_backend.py index 4dec67ea14..2067684cfa 100644 --- a/tools/computer_use/cua_backend.py +++ b/tools/computer_use/cua_backend.py @@ -357,6 +357,15 @@ def _empty_discovery_reason() -> str: ) if sys.platform == "linux" and not os.environ.get("DISPLAY"): return "no DISPLAY is set — X11/XWayland is not reachable from this process" + if sys.platform == "darwin": + # Headless Mac / asleep panel: ScreenCaptureKit has 0 shareable + # displays while TCC grants look fine (#67165, #52925 lineage). + return ( + "window discovery returned no windows; on macOS this usually " + "means no shareable display (headless Mac or panel asleep) — " + "wake the display or attach a monitor/HDMI dummy, then run " + "`hermes computer-use doctor`" + ) return ( "window discovery returned no windows; run `hermes computer-use " "doctor` (display reachability, AX capability)" diff --git a/tools/computer_use/doctor.py b/tools/computer_use/doctor.py index 865a1df0e9..1379c8d95c 100644 --- a/tools/computer_use/doctor.py +++ b/tools/computer_use/doctor.py @@ -693,13 +693,53 @@ def _drive_health_report_or_fallback( ) -> Dict[str, Any]: """Prefer real health_report; on denial/non-schema, synthesize via probes.""" try: - return _drive_health_report( + report = _drive_health_report( binary, include=include, skip=skip, timeout=timeout, ) except HealthReportUnavailable as e: - return _compose_fallback_report( + report = _compose_fallback_report( binary, reason=str(e), timeout=timeout, ) + return _apply_display_count_guard(report) + + +def _apply_display_count_guard(report: Dict[str, Any]) -> Dict[str, Any]: + """Downgrade an 'ok' report whose screen capture has zero displays. + + macOS ScreenCaptureKit reports ``display_count=0`` on headless Macs and + when the built-in panel is asleep — TCC grants are fine, health_report + can still say pass/ok, but every capture will come back 0x0. Marking + the check failed (with the recovery actions) turns an undiagnosable + silent failure into an actionable one. Applied at the report seam so + the real health_report path and the composed fallback both get it. + + Composed from PR #52949 (sujeet111) and PR #67259 (webtecnica). + """ + checks = report.get("checks") + if not isinstance(checks, list): + return report + for check in checks: + if not isinstance(check, dict): + continue + if check.get("name") != "screen_capture_capability": + continue + data = check.get("data") + count = data.get("display_count") if isinstance(data, dict) else None + if count == 0 and check.get("status") == "pass": + check["status"] = "fail" + check["message"] = ( + "ScreenCaptureKit reachable but 0 shareable display(s) — " + "every capture will return 0x0." + ) + check["hint"] = ( + "Wake the built-in display, connect a monitor or HDMI dummy " + "dongle (e.g. Headless Ghost), or enable a virtual display " + "(Screen Sharing/VNC, BetterDisplay). Verify with " + "`system_profiler SPDisplaysDataType`." + ) + if report.get("overall") == "ok": + report["overall"] = "degraded" + return report def _print_text_report(