From 9138176dcda15b9f7ff6656835fcd125a0720afb Mon Sep 17 00:00:00 2001 From: Jack Earnest Date: Thu, 18 Jun 2026 22:42:42 +0000 Subject: [PATCH] fix(gateway): don't resolve node symlink into profile dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_systemd_unit() and generate_launchd_plist() used Path(shutil.which('node')).resolve().parent to find the node bin dir. When ~/.local/bin/node is a symlink into a specific profile's node install (e.g. ~/.hermes/profiles/

/node/bin/node), .resolve() chases it and bakes that one profile's path into EVERY profile's service definition. This breaks profile isolation and makes systemd_unit_is_current() perpetually False: each gateway rewrites its unit + daemon-reload on every boot, destabilizing multi-profile setups into a ~5-minute restart loop (observed NRestarts ~1600 across two gateways). Fix: use Path(resolved_node).parent — the directory where node is found on PATH — instead of chasing the symlink to its resolved target. This keeps generated service definitions profile-agnostic. Affects both the systemd (Linux) and launchd (macOS) unit generators. --- hermes_cli/gateway.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index bd970d9ca..dc988fa40 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -2655,7 +2655,15 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) path_entries = _build_service_path_dirs() resolved_node = shutil.which("node") if resolved_node: - resolved_node_dir = str(Path(resolved_node).resolve().parent) + # Use the directory where ``node`` is *found on PATH*, NOT the + # symlink's resolved target. ``~/.local/bin/node`` is often a symlink + # into a specific profile's node install (e.g. profiles/jarvis/node/ + # bin/node); calling .resolve() here would chase that symlink and bake + # one profile's node path into *every* profile's service unit. That + # cross-profile leak makes systemd_unit_is_current() perpetually false, + # so each gateway rewrites its unit + daemon-reload on every boot. Using + # the symlink's own parent keeps the generated unit profile-agnostic. + resolved_node_dir = str(Path(resolved_node).parent) if resolved_node_dir not in path_entries: path_entries.append(resolved_node_dir) @@ -3807,7 +3815,13 @@ def generate_launchd_plist() -> str: priority_dirs = _build_service_path_dirs() resolved_node = shutil.which("node") if resolved_node: - resolved_node_dir = str(Path(resolved_node).resolve().parent) + # Use the directory where ``node`` is *found on PATH*, NOT the symlink's + # resolved target. ``~/.local/bin/node`` is often a symlink into a + # specific profile's node install; calling .resolve() would chase it and + # bake one profile's path into every profile's service definition, + # breaking profile isolation and causing perpetual unit rewrites. See + # the matching fix in generate_systemd_unit(). + resolved_node_dir = str(Path(resolved_node).parent) if resolved_node_dir not in priority_dirs: priority_dirs.append(resolved_node_dir) sane_path = ":".join(