# NousResearch/hermes-agent · Gateway preflight systemd user socket detection

## Open now

- 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+.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #17427. Verify it addresses your case.
- **Recommended:** read PR #17427 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-17427`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #17243: \[Bug\]: \`systemctl --user\` is functional but "✗ User systemd not reachable" on \`hermes gateway restart\`

## Prior & competing PRs

- **#17427: fix(gateway): accept user systemd private socket during preflight (#17243)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/gateway.py`, `scripts/release.py`, `tests/hermes_cli/test_gateway_service.py`
  - fix hunks:
  - `hermes_cli/gateway.py`

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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 user@.service can take a second or two to spawn the socket
    -    after ``loginctl enable-linger`` runs.  Returns True once the socket exists.
    +    Linger-enabled user@.service 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 user@.service to spawn
           the socket.
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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"
    ```

    ```diff
    @@ -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()
     
    ```

    ```diff
    @@ -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, ""),
             )
    ```

    ```diff
    @@ -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"),
    ```

    ```diff
    @@ -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, ""),
             )
    ```

    ```diff
    @@ -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, ""),
             )
    ```
- **#17253: fix(gateway): check for systemd private socket in preflight (fixes #17243)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17427`, `#17363`, `#17243`
- **#17260: fix(gateway): relax systemd preflight to handle missing D-Bus socket**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#17427`
- **#17358: fix: detect user systemd via private socket on Debian 13+**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17253`
- **#17363: fix: accept user systemd private socket during gateway preflight**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `37d107e03`, `#17427`

## Fix status

- Merged to main in PR #17427 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-29`.
