fix(computer-use): flag macOS zero-display capture in doctor + discovery reason
A headless Mac or asleep built-in panel leaves ScreenCaptureKit with 0 shareable displays while TCC grants pass — health_report stays ok and every capture silently returns 0x0 (#67165). Guard at the report seam (_apply_display_count_guard, both real and fallback paths): flips the screen_capture_capability check to fail with recovery actions (wake display / HDMI dummy / virtual display) and downgrades ok -> degraded. The empty-discovery reason ladder gains the matching darwin rung. Composed from #52949 (sujeet111) and #67259 (webtecnica); both PRs predate the doctor rewrite and the envelope normalization on main, so this reimplements their shared intent at the current seams. Co-authored-by: Sujeet <64351924+sujeet111@users.noreply.github.com> Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
This commit is contained in:
@@ -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"
|
||||
@@ -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)"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user