Gateway preflight systemd user socket detection
NousResearch/hermes-agent · cluster 17243
Data as of
Nothing open on GitHub. Fix merged in #17427. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #17427. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #17427. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A bug report and several fix attempts addressing false 'User systemd not reachable' errors during gateway preflight checks, specifically around detecting user systemd via private D-Bus sockets on systems like Debian 13+.
Open issues0
Open PRs0
Fixes on record
-
#17427 fix(gateway): accept user systemd private socket during preflight (#17243)
files:
hermes_cli/gateway.py,scripts/release.py,tests/hermes_cli/test_gateway_service.pyShow the diff
-
hermes_cli/gateway.py@@ -830,6 +830,22 @@ def _user_dbus_socket_path() -> Path: return Path(xdg) / "bus" +def _user_systemd_private_socket_path() -> Path: + """Return the per-user systemd private socket path (regardless of existence).""" + xdg = os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}" + return Path(xdg) / "systemd" / "private" + + +def _user_systemd_socket_ready() -> bool: + """Return True when user-scope systemd has a reachable control socket. + + Some distros expose only the per-user systemd private socket even when the + D-Bus session bus socket is absent. ``systemctl --user`` can still work in + that configuration, so preflight checks must treat either socket as valid. + """ + return _user_dbus_socket_path().exists() or _user_systemd_private_socket_path().exists() + + def _ensure_user_systemd_env() -> None: """Ensure DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are set for systemctl --user. @@ -853,28 +869,29 @@ def _ensure_user_systemd_env() -> None: def _wait_for_user_dbus_socket(timeout: float = 3.0) -> bool: - """Poll for the user D-Bus socket to appear, up to ``timeout`` seconds. + """Poll for the user systemd runtime socket(s), up to ``timeout`` seconds. - Linger-enabled [email protected] can take a second or two to spawn the socket - after ``loginctl enable-linger`` runs. Returns True once the socket exists. + Linger-enabled [email protected] can take a second or two to spawn its control + socket(s) after ``loginctl enable-linger`` runs. Returns True once either + the user D-Bus socket or the per-user systemd private socket exists. """ import time deadline = time.monotonic() + timeout while time.monotonic() < deadline: - if _user_dbus_socket_path().exists(): + if _user_systemd_socket_ready(): _ensure_user_systemd_env() return True time.sleep(0.2) - return _user_dbus_socket_path().exists() + return _user_systemd_socket_ready() def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None: - """Ensure ``systemctl --user`` will reach the user D-Bus session bus. + """Ensure ``systemctl --user`` will reach the user-scope systemd instance. - No-op when the bus socket is already there (the common case on desktops - and linger-enabled servers). On fresh SSH sessions where the socket is - missing: + No-op when the user D-Bus socket or per-user systemd private socket is + already there (the common case on desktops and linger-enabled servers). On + fresh SSH sessions where both are missing: * If linger is already enabled, wait briefly for [email protected] to spawn the socket. @@ -888,8 +905,7 @@ def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None: systemd operations and surface the message to the user. """ _ensure_user_systemd_env() - bus_path = _user_dbus_socket_path() - if bus_path.exists(): + if _user_systemd_socket_ready(): return import getpass @@ -903,7 +919,7 @@ def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None: # Linger is on but socket still missing — unusual; fall through to error. _raise_user_systemd_unavailable( username, - reason="User D-Bus socket is missing even though linger is enabled.", + reason="User systemd control sockets are missing even though linger is enabled.", fix_hint=( f" systemctl start user@{os.getuid()}.service\n" " (may require sudo; try again after the command succeeds)" -
scripts/release.py@@ -45,6 +45,7 @@ "[redacted:email]": "teknium1", "[redacted:email]": "teknium1", "[redacted:email]": "exiao", + "[redacted:email]": "rylena", "[redacted:email]": "revaraver", # Matrix parity salvage batch (April 2026) "sr@samirusani": "samrusani", -
tests/hermes_cli/test_gateway_service.py@@ -14,6 +14,26 @@ ) +class TestUserSystemdPrivateSocketPreflight: + def test_preflight_accepts_private_socket_without_dbus_bus(self, monkeypatch): + monkeypatch.setattr(gateway_cli, "_ensure_user_systemd_env", lambda: None) + monkeypatch.setattr(gateway_cli, "_user_dbus_socket_path", lambda: Path("/tmp/missing-bus")) + monkeypatch.setattr(gateway_cli, "_user_systemd_private_socket_path", lambda: Path("/tmp/private-socket")) + monkeypatch.setattr(Path, "exists", lambda self: str(self) == "/tmp/private-socket") + + gateway_cli._preflight_user_systemd(auto_enable_linger=False) + + def test_wait_for_user_dbus_socket_accepts_private_socket(self, monkeypatch): + calls = [] + monkeypatch.setattr(gateway_cli, "_ensure_user_systemd_env", lambda: calls.append("env")) + monkeypatch.setattr(gateway_cli, "_user_dbus_socket_path", lambda: Path("/tmp/missing-bus")) + monkeypatch.setattr(gateway_cli, "_user_systemd_private_socket_path", lambda: Path("/tmp/private-socket")) + monkeypatch.setattr(Path, "exists", lambda self: str(self) == "/tmp/private-socket") + + assert gateway_cli._wait_for_user_dbus_socket(timeout=0.1) is True + assert calls == ["env"] + + class TestSystemdServiceRefresh: def test_systemd_install_repairs_outdated_unit_without_force(self, tmp_path, monkeypatch): unit_path = tmp_path / "hermes-gateway.service" @@ -1105,6 +1125,10 @@ def test_noop_when_bus_socket_exists(self, monkeypatch): gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: True})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) # Should not raise, no subprocess calls needed. gateway_cli._preflight_user_systemd() @@ -1114,6 +1138,10 @@ def test_raises_when_linger_disabled_and_loginctl_denied(self, monkeypatch): gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (False, ""), ) @@ -1142,6 +1170,10 @@ def test_raises_when_loginctl_missing(self, monkeypatch): gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (None, "loginctl not found"), @@ -1159,6 +1191,10 @@ def test_linger_enabled_but_socket_still_missing(self, monkeypatch): gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (True, ""), ) @@ -1177,6 +1213,10 @@ def test_enable_linger_succeeds_and_socket_appears(self, monkeypatch, capsys): gateway_cli, "_user_dbus_socket_path", lambda: type("P", (), {"exists": lambda self: False})(), ) + monkeypatch.setattr( + gateway_cli, "_user_systemd_private_socket_path", + lambda: type("P", (), {"exists": lambda self: False})(), + ) monkeypatch.setattr( gateway_cli, "get_systemd_linger_status", lambda: (False, ""), )
-
4 PRs closed without merging
From the close comments: superseded by another PR 2 · done elsewhere 1 · merged via another change 1
-
#17253 fix(gateway): check for systemd private socket in preflight (fixes #17243) · superseded by another PR →
#17427,#17363,#17243 -
#17260 fix(gateway): relax systemd preflight to handle missing D-Bus socket · done elsewhere →
#17427 -
#17358 fix: detect user systemd via private socket on Debian 13+ · superseded by another PR →
#17253 -
#17363 fix: accept user systemd private socket during gateway preflight · merged via another change →
37d107e,#17427
Closed issues · 1
- Closed#17243[Bug]: `systemctl --user` is functional but "✗ User systemd not reachable" on `hermes gateway restart`
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #17427
- Closed issues
- 1
- Attempts
- 4 PRs closed without merging
- Fix link checked
- 29 Apr 2026