From b412ee9206e977526555129b185253ed35e2bc0b Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:11:41 -0700 Subject: [PATCH] fix(platform): an unreadable GPU classifies as unknown, so requires.gpu fails open as documented When every DRM card's vendor file or every Windows display adapter's MatchingDeviceId was unreadable, classify_gpu_vendors returned 'none' and availability refused with unsupported_gpu, contradicting the documented 'unreadable GPU passes' rule. Windows now records an unreadable numbered adapter as an empty id so the classifier sees it. Also drops the type-narrowing assert in availability() and makes _action's gpu_label required. --- hermes_platform/host/facts.py | 9 ++++++++- hermes_platform/resolver/availability.py | 3 +-- tests/hermes_cli/test_shared_metrics_install_v4.py | 2 +- tools/mcp_liveness.py | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/hermes_platform/host/facts.py b/hermes_platform/host/facts.py index 2c0eaf40c5..8696b259d1 100644 --- a/hermes_platform/host/facts.py +++ b/hermes_platform/host/facts.py @@ -233,7 +233,12 @@ _GPU_SCAN_LIMIT = 16 def classify_gpu_vendors(vendor_ids: list[str]) -> str: - """Return the highest-priority GPU class for PCI vendor ids, or ``none``.""" + """Return the highest-priority GPU class for PCI vendor ids, or ``none``. + + ``unknown`` when devices were seen but none of their vendor ids could be read. + """ + if vendor_ids and not any(v.strip() for v in vendor_ids): + return "unknown" found = {_GPU_PCI_VENDORS.get(v.strip().lower().removeprefix("0x")) for v in vendor_ids} return next((name for name in _GPU_PRIORITY if name in found), "none") @@ -263,6 +268,8 @@ def _windows_gpu_vendor_ids() -> list[str] | None: device_id = _winreg_str(f"{_DISPLAY_CLASS_KEY}\\{subkey}", "MatchingDeviceId").upper() if "VEN_" in device_id: vendors.append(device_id.split("VEN_", 1)[1][:4]) + elif subkey.isdigit(): # an adapter whose id could not be read + vendors.append("") except OSError: return None return vendors diff --git a/hermes_platform/resolver/availability.py b/hermes_platform/resolver/availability.py index 295dcf8dc4..09acd5ee73 100644 --- a/hermes_platform/resolver/availability.py +++ b/hermes_platform/resolver/availability.py @@ -89,9 +89,8 @@ def availability(entry: _HasRequirements, *, os_family: str | None = None) -> Av # the machines that have the GPU, and the MCP connection check still applies. if gpu is not None and facts.gpu_class() not in (gpu, "unknown"): return Availability("unsupported_gpu", min_version=entry.min_version) - if not entry.requires_app: + if definition is None: return Availability("no_requirements") - assert definition is not None resolver = AppResolver(definition) res = resolver.locate() looked_at = res.command[0] if res.command else (res.candidates[0].value if res.candidates else None) diff --git a/tests/hermes_cli/test_shared_metrics_install_v4.py b/tests/hermes_cli/test_shared_metrics_install_v4.py index 16c4dfc9bc..69bf8f97ca 100644 --- a/tests/hermes_cli/test_shared_metrics_install_v4.py +++ b/tests/hermes_cli/test_shared_metrics_install_v4.py @@ -220,7 +220,7 @@ def test_meminfo_total_is_parsed_in_bytes(): @pytest.mark.parametrize(("vendors", "gpu"), [ (["0x8086", "0x10de"], "nvidia"), (["0x8086", "0x1002"], "amd"), (["0x8086"], "intel"), - (["0x1af4"], "none"), ([], "none"), (["10DE"], "nvidia"), + (["0x1af4"], "none"), ([], "none"), (["10DE"], "nvidia"), ([""], "unknown"), (["", "0x10de"], "nvidia"), ]) def test_gpu_vendor_priority_prefers_the_discrete_card(vendors, gpu): assert facts.classify_gpu_vendors(vendors) == gpu diff --git a/tools/mcp_liveness.py b/tools/mcp_liveness.py index fc19021618..d8e4891f35 100644 --- a/tools/mcp_liveness.py +++ b/tools/mcp_liveness.py @@ -108,7 +108,7 @@ def liveness_for(server_name: str) -> Liveness: return Liveness("static") -def _action(state: LivenessState, app_name: str, gpu_label: str = "") -> tuple[str, Retry]: +def _action(state: LivenessState, app_name: str, gpu_label: str) -> tuple[str, Retry]: actions: dict[LivenessState, tuple[str, Retry]] = { "app_not_running": (f"Start {app_name}, then try again.", "after_user_action"), "hermes_not_connected": (f"Reconnect {app_name} in Hermes, then try again.", "after_user_action"),