diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index f29c42358a2e5..1852afbd8b8f5 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -2749,7 +2749,9 @@ def _systemd_watchdog_service_fields( return "notify", f"NotifyAccess=main\nWatchdogSec={seconds}s\n" -def _append_node_dir_for_service(path_entries: list[str]) -> None: +def _append_node_dir_for_service( + path_entries: list[str], hermes_root: Path | None = None +) -> None: """Add the Node directory a generated service unit should use to *path_entries*. The Hermes-managed Node under ``$HERMES_HOME/node`` goes first when it @@ -2757,16 +2759,27 @@ def _append_node_dir_for_service(path_entries: list[str]) -> None: a service unit is written once and then survives reboots, so resolving a system Node that happens to be ahead on the installing shell's PATH bakes the wrong interpreter in permanently — the exact failure the desktop - backend spawn was fixed for. Managed dirs are profile-scoped - (``get_hermes_home()``), so each profile's unit still names its own Node. + backend spawn was fixed for. Managed dirs are profile-scoped, so each + profile's unit still names its own Node. + + *hermes_root* is the Hermes home the unit will run against. System units + installed via sudo MUST pass the **target user's** home: probing the + default (the calling user's — root's — tree) would bake root's Node into + the target user's unit. The probe swallows OSError: an unreadable + candidate dir (hardened home) means "skip the rung", not "crash the + generator". PATH lookup remains the fallback rung for installs with no managed Node. """ from hermes_constants import iter_hermes_node_dirs - for directory in iter_hermes_node_dirs(): + for directory in iter_hermes_node_dirs(hermes_root): entry = str(directory) - if directory.is_dir() and entry not in path_entries: + try: + present = directory.is_dir() + except OSError: + present = False + if present and entry not in path_entries: path_entries.append(entry) resolved_node = shutil.which("node") @@ -2793,7 +2806,11 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) venv_dir = str(detected_venv) if detected_venv else str(PROJECT_ROOT / "venv") path_entries = _build_service_path_dirs() - _append_node_dir_for_service(path_entries) + if not system: + # System units append the managed Node dirs later, once the TARGET + # user's Hermes home is known — probing here would stat the calling + # (sudo → root's) tree and bake the wrong user's Node into the unit. + _append_node_dir_for_service(path_entries) common_bin_paths = [ "/usr/local/sbin", @@ -2827,6 +2844,17 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) working_dir = str(hermes_home) if hermes_home else _remap_path_for_user(working_dir, home_dir) venv_dir = _remap_path_for_user(venv_dir, home_dir) path_entries = [_remap_path_for_user(p, home_dir) for p in path_entries] + # Managed Node for the TARGET user's tree (see the skip above): probe + # the remapped hermes_home, not the calling user's. Prepend — the + # managed Node must outrank remapped shell-PATH entries, matching the + # user-unit ordering where it's appended before PATH capture. + _target_node_entries: list[str] = [] + _append_node_dir_for_service( + _target_node_entries, Path(hermes_home) if hermes_home else None + ) + path_entries = [ + e for e in _target_node_entries if e not in path_entries + ] + path_entries path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries)) path_entries.extend(_build_wsl_interop_paths(path_entries)) path_entries.extend(common_bin_paths)