From e295a2215acd55f2ee930fc7a4cd2df1c5464234 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:47:40 -0700 Subject: [PATCH] fix(gateway): include user-local bin paths in systemd unit PATH (#3527) Add ~/.local/bin, ~/.cargo/bin, ~/go/bin, ~/.npm-global/bin to the systemd unit PATH so tools installed via uv/pipx/cargo/go are discoverable by MCP servers and terminal commands. Uses a _build_user_local_paths() helper that checks exists() before adding, and correctly resolves home dir for both user and system service types. Co-authored-by: Kal Sze --- hermes_cli/gateway.py | 21 +++++++++++++++++++-- tests/hermes_cli/test_gateway_service.py | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index ba13cb4e8..b8a1faa0c 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -420,6 +420,17 @@ def get_hermes_cli_path() -> str: # Systemd (Linux) # ============================================================================= +def _build_user_local_paths(home: Path, path_entries: list[str]) -> list[str]: + """Return user-local bin dirs that exist and aren't already in *path_entries*.""" + candidates = [ + str(home / ".local" / "bin"), # uv, uvx, pip-installed CLIs + str(home / ".cargo" / "bin"), # Rust/cargo tools + str(home / "go" / "bin"), # Go tools + str(home / ".npm-global" / "bin"), # npm global packages + ] + return [p for p in candidates if p not in path_entries and Path(p).exists()] + + def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) -> str: python_path = get_python_path() working_dir = str(PROJECT_ROOT) @@ -434,13 +445,16 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) resolved_node_dir = str(Path(resolved_node).resolve().parent) if resolved_node_dir not in path_entries: path_entries.append(resolved_node_dir) - path_entries.extend(["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]) - sane_path = ":".join(path_entries) hermes_home = str(get_hermes_home().resolve()) + common_bin_paths = ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"] + if system: username, group_name, home_dir = _system_service_identity(run_as_user) + path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries)) + path_entries.extend(common_bin_paths) + sane_path = ":".join(path_entries) return f"""[Unit] Description={SERVICE_DESCRIPTION} After=network-online.target @@ -472,6 +486,9 @@ StandardError=journal WantedBy=multi-user.target """ + path_entries.extend(_build_user_local_paths(Path.home(), path_entries)) + path_entries.extend(common_bin_paths) + sane_path = ":".join(path_entries) return f"""[Unit] Description={SERVICE_DESCRIPTION} After=network.target diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index 0189f036b..12bae0f31 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -1,6 +1,7 @@ """Tests for gateway service management helpers.""" import os +from pathlib import Path from types import SimpleNamespace import hermes_cli.gateway as gateway_cli @@ -354,6 +355,20 @@ class TestGeneratedUnitUsesDetectedVenv: assert "/venv/" not in unit or "/.venv/" in unit +class TestGeneratedUnitIncludesLocalBin: + """~/.local/bin must be in PATH so uvx/pipx tools are discoverable.""" + + def test_user_unit_includes_local_bin_in_path(self): + unit = gateway_cli.generate_systemd_unit(system=False) + home = str(Path.home()) + assert f"{home}/.local/bin" in unit + + def test_system_unit_includes_local_bin_in_path(self): + unit = gateway_cli.generate_systemd_unit(system=True) + # System unit uses the resolved home dir from _system_service_identity + assert "/.local/bin" in unit + + class TestEnsureUserSystemdEnv: """Tests for _ensure_user_systemd_env() D-Bus session bus auto-detection."""