{"repo":"NousResearch/hermes-agent","public_id":"17243","display_label":"Gateway preflight systemd user socket detection","summary":"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+.","subsystem":null,"counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":17427},"resolution":{"state":"resolved","by":"pr-17427"},"freshness":{"state":"verified","last_verified_at":"2026-04-29T11:57:02+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3333333333333333},"fix_edges":[{"issue_number":17243,"pr_number":17427,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":17243,"title":"[Bug]: `systemctl --user` is functional but \"✗ User systemd not reachable\" on `hermes gateway restart`","url":null,"state":"closed"},{"kind":"pr","number":17253,"title":"fix(gateway): check for systemd private socket in preflight (fixes #17243)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17427","number":17427},{"kind":"pr","value":"#17363","number":17363},{"kind":"pr","value":"#17243","number":17243}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":17260,"title":"fix(gateway): relax systemd preflight to handle missing D-Bus socket","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17427","number":17427}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":17358,"title":"fix: detect user systemd via private socket on Debian 13+","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17253","number":17253}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":17363,"title":"fix: accept user systemd private socket during gateway preflight","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"37d107e03","number":0},{"kind":"pr","value":"#17427","number":17427}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":17427,"title":"fix(gateway): accept user systemd private socket during preflight (#17243)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/gateway.py","scripts/release.py","tests/hermes_cli/test_gateway_service.py"],"fix_hunks":[{"file":"hermes_cli/gateway.py","hunks":["@@ -830,6 +830,22 @@ def _user_dbus_socket_path() -> Path:\n     return Path(xdg) / \"bus\"\n \n \n+def _user_systemd_private_socket_path() -> Path:\n+    \"\"\"Return the per-user systemd private socket path (regardless of existence).\"\"\"\n+    xdg = os.environ.get(\"XDG_RUNTIME_DIR\") or f\"/run/user/{os.getuid()}\"\n+    return Path(xdg) / \"systemd\" / \"private\"\n+\n+\n+def _user_systemd_socket_ready() -> bool:\n+    \"\"\"Return True when user-scope systemd has a reachable control socket.\n+\n+    Some distros expose only the per-user systemd private socket even when the\n+    D-Bus session bus socket is absent. ``systemctl --user`` can still work in\n+    that configuration, so preflight checks must treat either socket as valid.\n+    \"\"\"\n+    return _user_dbus_socket_path().exists() or _user_systemd_private_socket_path().exists()\n+\n+\n def _ensure_user_systemd_env() -> None:\n     \"\"\"Ensure DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are set for systemctl --user.\n ","@@ -853,28 +869,29 @@ def _ensure_user_systemd_env() -> None:\n \n \n def _wait_for_user_dbus_socket(timeout: float = 3.0) -> bool:\n-    \"\"\"Poll for the user D-Bus socket to appear, up to ``timeout`` seconds.\n+    \"\"\"Poll for the user systemd runtime socket(s), up to ``timeout`` seconds.\n \n-    Linger-enabled user@.service can take a second or two to spawn the socket\n-    after ``loginctl enable-linger`` runs.  Returns True once the socket exists.\n+    Linger-enabled user@.service can take a second or two to spawn its control\n+    socket(s) after ``loginctl enable-linger`` runs. Returns True once either\n+    the user D-Bus socket or the per-user systemd private socket exists.\n     \"\"\"\n     import time\n \n     deadline = time.monotonic() + timeout\n     while time.monotonic() < deadline:\n-        if _user_dbus_socket_path().exists():\n+        if _user_systemd_socket_ready():\n             _ensure_user_systemd_env()\n             return True\n         time.sleep(0.2)\n-    return _user_dbus_socket_path().exists()\n+    return _user_systemd_socket_ready()\n \n \n def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None:\n-    \"\"\"Ensure ``systemctl --user`` will reach the user D-Bus session bus.\n+    \"\"\"Ensure ``systemctl --user`` will reach the user-scope systemd instance.\n \n-    No-op when the bus socket is already there (the common case on desktops\n-    and linger-enabled servers).  On fresh SSH sessions where the socket is\n-    missing:\n+    No-op when the user D-Bus socket or per-user systemd private socket is\n+    already there (the common case on desktops and linger-enabled servers). On\n+    fresh SSH sessions where both are missing:\n \n     * If linger is already enabled, wait briefly for user@.service to spawn\n       the socket.","@@ -888,8 +905,7 @@ def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None:\n     systemd operations and surface the message to the user.\n     \"\"\"\n     _ensure_user_systemd_env()\n-    bus_path = _user_dbus_socket_path()\n-    if bus_path.exists():\n+    if _user_systemd_socket_ready():\n         return\n \n     import getpass","@@ -903,7 +919,7 @@ def _preflight_user_systemd(*, auto_enable_linger: bool = True) -> None:\n         # Linger is on but socket still missing — unusual; fall through to error.\n         _raise_user_systemd_unavailable(\n             username,\n-            reason=\"User D-Bus socket is missing even though linger is enabled.\",\n+            reason=\"User systemd control sockets are missing even though linger is enabled.\",\n             fix_hint=(\n                 f\"  systemctl start user@{os.getuid()}.service\\n\"\n                 \"  (may require sudo; try again after the command succeeds)\""]},{"file":"scripts/release.py","hunks":["@@ -45,6 +45,7 @@\n     \"[redacted:email]\": \"teknium1\",\n     \"[redacted:email]\": \"teknium1\",\n     \"[redacted:email]\": \"exiao\",\n+    \"[redacted:email]\": \"rylena\",\n     \"[redacted:email]\": \"revaraver\",\n     # Matrix parity salvage batch (April 2026)\n     \"sr@samirusani\": \"samrusani\","]},{"file":"tests/hermes_cli/test_gateway_service.py","hunks":["@@ -14,6 +14,26 @@\n )\n \n \n+class TestUserSystemdPrivateSocketPreflight:\n+    def test_preflight_accepts_private_socket_without_dbus_bus(self, monkeypatch):\n+        monkeypatch.setattr(gateway_cli, \"_ensure_user_systemd_env\", lambda: None)\n+        monkeypatch.setattr(gateway_cli, \"_user_dbus_socket_path\", lambda: Path(\"/tmp/missing-bus\"))\n+        monkeypatch.setattr(gateway_cli, \"_user_systemd_private_socket_path\", lambda: Path(\"/tmp/private-socket\"))\n+        monkeypatch.setattr(Path, \"exists\", lambda self: str(self) == \"/tmp/private-socket\")\n+\n+        gateway_cli._preflight_user_systemd(auto_enable_linger=False)\n+\n+    def test_wait_for_user_dbus_socket_accepts_private_socket(self, monkeypatch):\n+        calls = []\n+        monkeypatch.setattr(gateway_cli, \"_ensure_user_systemd_env\", lambda: calls.append(\"env\"))\n+        monkeypatch.setattr(gateway_cli, \"_user_dbus_socket_path\", lambda: Path(\"/tmp/missing-bus\"))\n+        monkeypatch.setattr(gateway_cli, \"_user_systemd_private_socket_path\", lambda: Path(\"/tmp/private-socket\"))\n+        monkeypatch.setattr(Path, \"exists\", lambda self: str(self) == \"/tmp/private-socket\")\n+\n+        assert gateway_cli._wait_for_user_dbus_socket(timeout=0.1) is True\n+        assert calls == [\"env\"]\n+\n+\n class TestSystemdServiceRefresh:\n     def test_systemd_install_repairs_outdated_unit_without_force(self, tmp_path, monkeypatch):\n         unit_path = tmp_path / \"hermes-gateway.service\"","@@ -1105,6 +1125,10 @@ def test_noop_when_bus_socket_exists(self, monkeypatch):\n             gateway_cli, \"_user_dbus_socket_path\",\n             lambda: type(\"P\", (), {\"exists\": lambda self: True})(),\n         )\n+        monkeypatch.setattr(\n+            gateway_cli, \"_user_systemd_private_socket_path\",\n+            lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n+        )\n         # Should not raise, no subprocess calls needed.\n         gateway_cli._preflight_user_systemd()\n ","@@ -1114,6 +1138,10 @@ def test_raises_when_linger_disabled_and_loginctl_denied(self, monkeypatch):\n             gateway_cli, \"_user_dbus_socket_path\",\n             lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n         )\n+        monkeypatch.setattr(\n+            gateway_cli, \"_user_systemd_private_socket_path\",\n+            lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n+        )\n         monkeypatch.setattr(\n             gateway_cli, \"get_systemd_linger_status\", lambda: (False, \"\"),\n         )","@@ -1142,6 +1170,10 @@ def test_raises_when_loginctl_missing(self, monkeypatch):\n             gateway_cli, \"_user_dbus_socket_path\",\n             lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n         )\n+        monkeypatch.setattr(\n+            gateway_cli, \"_user_systemd_private_socket_path\",\n+            lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n+        )\n         monkeypatch.setattr(\n             gateway_cli, \"get_systemd_linger_status\",\n             lambda: (None, \"loginctl not found\"),","@@ -1159,6 +1191,10 @@ def test_linger_enabled_but_socket_still_missing(self, monkeypatch):\n             gateway_cli, \"_user_dbus_socket_path\",\n             lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n         )\n+        monkeypatch.setattr(\n+            gateway_cli, \"_user_systemd_private_socket_path\",\n+            lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n+        )\n         monkeypatch.setattr(\n             gateway_cli, \"get_systemd_linger_status\", lambda: (True, \"\"),\n         )","@@ -1177,6 +1213,10 @@ def test_enable_linger_succeeds_and_socket_appears(self, monkeypatch, capsys):\n             gateway_cli, \"_user_dbus_socket_path\",\n             lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n         )\n+        monkeypatch.setattr(\n+            gateway_cli, \"_user_systemd_private_socket_path\",\n+            lambda: type(\"P\", (), {\"exists\": lambda self: False})(),\n+        )\n         monkeypatch.setattr(\n             gateway_cli, \"get_systemd_linger_status\", lambda: (False, \"\"),\n         )\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}