fix(gateway): remap all paths in system service unit to target user's home

When installing a system service via sudo, ExecStart, WorkingDirectory,
VIRTUAL_ENV, and PATH entries were not remapped to the target user's
home — only HERMES_HOME was. This caused the service to fail with
status=200/CHDIR because the target user cannot access /root/.

Adds _remap_path_for_user() helper and applies it to all path variables
in the system branch of generate_systemd_unit().

Closes #6989
This commit is contained in:
Evi Nova 2026-04-10 03:21:04 -07:00 committed by Teknium
parent 0f597dd127
commit 8dd738c2e6
2 changed files with 87 additions and 0 deletions

View file

@ -618,6 +618,24 @@ def _build_user_local_paths(home: Path, path_entries: list[str]) -> list[str]:
return [p for p in candidates if p not in path_entries and Path(p).exists()]
def _remap_path_for_user(path: str, target_home_dir: str) -> str:
"""Remap *path* from the current user's home to *target_home_dir*.
If *path* lives under ``Path.home()`` the corresponding prefix is swapped
to *target_home_dir*; otherwise the path is returned unchanged.
/root/.hermes/hermes-agent -> /home/alice/.hermes/hermes-agent
/opt/hermes -> /opt/hermes (kept as-is)
"""
current_home = Path.home().resolve()
resolved = Path(path).resolve()
try:
relative = resolved.relative_to(current_home)
return str(Path(target_home_dir) / relative)
except ValueError:
return str(resolved)
def _hermes_home_for_target_user(target_home_dir: str) -> str:
"""Remap the current HERMES_HOME to the equivalent under a target user's home.
@ -665,6 +683,15 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
username, group_name, home_dir = _system_service_identity(run_as_user)
hermes_home = _hermes_home_for_target_user(home_dir)
profile_arg = _profile_arg(hermes_home)
# Remap all paths that may resolve under the calling user's home
# (e.g. /root/) to the target user's home so the service can
# actually access them.
python_path = _remap_path_for_user(python_path, home_dir)
working_dir = _remap_path_for_user(working_dir, home_dir)
venv_dir = _remap_path_for_user(venv_dir, home_dir)
venv_bin = _remap_path_for_user(venv_bin, home_dir)
node_bin = _remap_path_for_user(node_bin, home_dir)
path_entries = [_remap_path_for_user(p, home_dir) for p in path_entries]
path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries))
path_entries.extend(common_bin_paths)
sane_path = ":".join(path_entries)

View file

@ -754,3 +754,63 @@ class TestProfileArg:
plist = gateway_cli.generate_launchd_plist()
assert "<string>--profile</string>" in plist
assert "<string>mybot</string>" in plist
class TestRemapPathForUser:
"""Unit tests for _remap_path_for_user()."""
def test_remaps_path_under_current_home(self, monkeypatch, tmp_path):
monkeypatch.setattr(Path, "home", lambda: tmp_path / "root")
(tmp_path / "root").mkdir()
result = gateway_cli._remap_path_for_user(
str(tmp_path / "root" / ".hermes" / "hermes-agent"),
str(tmp_path / "alice"),
)
assert result == str(tmp_path / "alice" / ".hermes" / "hermes-agent")
def test_keeps_system_path_unchanged(self, monkeypatch, tmp_path):
monkeypatch.setattr(Path, "home", lambda: tmp_path / "root")
(tmp_path / "root").mkdir()
result = gateway_cli._remap_path_for_user("/opt/hermes", str(tmp_path / "alice"))
assert result == "/opt/hermes"
def test_noop_when_same_user(self, monkeypatch, tmp_path):
monkeypatch.setattr(Path, "home", lambda: tmp_path / "alice")
(tmp_path / "alice").mkdir()
original = str(tmp_path / "alice" / ".hermes" / "hermes-agent")
result = gateway_cli._remap_path_for_user(original, str(tmp_path / "alice"))
assert result == original
class TestSystemUnitPathRemapping:
"""System units must remap ALL paths from the caller's home to the target user."""
def test_system_unit_has_no_root_paths(self, monkeypatch, tmp_path):
root_home = tmp_path / "root"
root_home.mkdir()
project = root_home / ".hermes" / "hermes-agent"
project.mkdir(parents=True)
venv_bin = project / "venv" / "bin"
venv_bin.mkdir(parents=True)
(venv_bin / "python").write_text("")
target_home = "/home/alice"
monkeypatch.setattr(Path, "home", lambda: root_home)
monkeypatch.setenv("HERMES_HOME", str(root_home / ".hermes"))
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: root_home / ".hermes")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", project)
monkeypatch.setattr(gateway_cli, "_detect_venv_dir", lambda: project / "venv")
monkeypatch.setattr(gateway_cli, "get_python_path", lambda: str(venv_bin / "python"))
monkeypatch.setattr(
gateway_cli, "_system_service_identity",
lambda run_as_user=None: ("alice", "alice", target_home),
)
unit = gateway_cli.generate_systemd_unit(system=True)
# No root paths should leak into the unit
assert str(root_home) not in unit
# Target user paths should be present
assert "/home/alice" in unit
assert "WorkingDirectory=/home/alice/.hermes/hermes-agent" in unit