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

0open issues
0open PRs
6items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #17427 fix(gateway): accept user systemd private socket during preflight (#17243)

    merged to main

    files: hermes_cli/gateway.py, scripts/release.py, tests/hermes_cli/test_gateway_service.py

    Show 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.00 means 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 Lab

At a glance

Merged PRs
#17427
Closed issues
1
Attempts
4 PRs closed without merging
Fix link checked
29 Apr 2026

For your agent

The same facts, typed.