diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index b19ceaac9..1ca487364 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -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) diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index aa21793ae..23ad21b36 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -754,3 +754,63 @@ class TestProfileArg: plist = gateway_cli.generate_launchd_plist() assert "--profile" in plist assert "mybot" 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