diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index a88f714010..7bf1506270 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -822,8 +822,17 @@ def find_windows_gateway_services( # Ownership before state: an OS service above the gateway (Task Scheduler's svchost for a # task-launched gateway, BITS mid-transition) is never its supervisor, so neither its # PID nor its status may steer the pause. Only Hermes-owned services reach the guards below. - service_binpath = str(_scm_service_field(service, "binpath") or "") - if not hermes_owns_windows_service(service_name, service_binpath, hermes_roots): + # The name alone settles Hermes-named services; binpath (QueryServiceConfig) is asked only + # for the rest, and a service that refuses even that to this user is one this user could + # not `sc stop` either — never Hermes's, never a reason to abort the enumeration. + owned = hermes_owns_windows_service(service_name, "", hermes_roots) + if not owned: + try: + service_binpath = str(_scm_service_field(service, "binpath") or "") + except psutil_module.AccessDenied: + continue + owned = hermes_owns_windows_service(service_name, service_binpath, hermes_roots) + if not owned: continue service_status = _scm_service_field(service, "status") service_pid = int(_scm_service_field(service, "pid") or 0) diff --git a/tests/hermes_cli/test_gateway.py b/tests/hermes_cli/test_gateway.py index 326c48c5cf..b5b7cf185b 100644 --- a/tests/hermes_cli/test_gateway.py +++ b/tests/hermes_cli/test_gateway.py @@ -1327,6 +1327,7 @@ def test_find_windows_gateway_services_ignores_task_scheduler_ancestor(monkeypat """gateway <- cmd.exe <- svchost.exe(Schedule) <- services.exe: the Task Scheduler host is not the gateway's supervisor, so a task-launched gateway is a plain process (#97208); the same tree under a Hermes-owned service (by binary path) stays SCM-supervised.""" + import psutil import hermes_cli.gateway_windows as gateway_windows monkeypatch.setattr(gateway_windows, "hermes_service_roots", lambda: (r"C:\hermes\hermes-agent",)) @@ -1355,7 +1356,8 @@ def test_find_windows_gateway_services_ignores_task_scheduler_ancestor(monkeypat def run(service): return gateway.find_windows_gateway_services( - psutil_module=SimpleNamespace(win_service_iter=lambda: [service], Process=FakeProcess), + psutil_module=SimpleNamespace( + win_service_iter=lambda: [service], Process=FakeProcess, AccessDenied=psutil.AccessDenied), profile_processes=[profile], ) @@ -1363,6 +1365,16 @@ def test_find_windows_gateway_services_ignores_task_scheduler_ancestor(monkeypat owned = run(FakeService("gw", r'"C:\hermes\hermes-agent\venv\Scripts\hermes.exe" gateway run')) assert [(s.name, s.service_pid, s.gateway_pid) for s in owned] == [("gw", 2360, 18480)] + # QueryServiceConfig denied to this user (hardened third-party service): not Hermes's, and never a + # reason to abort the whole enumeration; a Hermes-NAMED service is settled without asking binpath. + class DeniedConfigService(FakeService): + def binpath(self): + raise psutil.AccessDenied(2360, self._name) + + assert run(DeniedConfigService("Hardened", "")) == [] + named = run(DeniedConfigService("HermesGateway", "")) + assert [(s.name, s.service_pid, s.gateway_pid) for s in named] == [("HermesGateway", 2360, 18480)] + def test_find_windows_gateway_services_rejects_shared_service_host_pid(monkeypatch): """A shared host PID cannot prove which service owns the gateway subtree."""