fix(gateway): preserve symlinked venv python in systemd system unit - #8622
fix(gateway): preserve symlinked venv python in systemd system unit#8622whisky0809 wants to merge 1 commit into
Conversation
_remap_path_for_user() called Path.resolve() on its input, which follows symlinks. In uv-managed venvs, venv/bin/python is a symlink into uv's shared Python store (e.g. ~/.local/share/uv/python/cpython-.../bin/python3.11), so resolving emits the bare interpreter in ExecStart and the service launches with no venv site-packages — the gateway then crash-loops on ModuleNotFoundError: yaml (and any other dep) at import time. Prefer the lexical form for the remap, and only fall back to resolving when $HOME itself is a symlink. System paths like /opt/hermes continue to round-trip unchanged. Adds a regression test covering the uv symlink case.
There was a problem hiding this comment.
Pull request overview
This PR fixes systemd unit generation for --system installs in uv-managed virtualenvs by preventing _remap_path_for_user() from resolving symlinks (which previously caused ExecStart= to point at uv’s shared interpreter instead of the venv’s bin/python).
Changes:
- Update
_remap_path_for_user()to prefer lexical (unresolved) paths and only use resolved comparisons when needed. - Adjust fallback behavior to return the original path (not the resolved path) when remapping doesn’t apply.
- Add a regression test ensuring a symlinked
venv/bin/pythonthat points outside$HOMEis preserved.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
hermes_cli/gateway.py |
Avoids Path.resolve()-driven symlink following during home-dir remapping so systemd ExecStart= stays within the venv. |
tests/hermes_cli/test_gateway_service.py |
Adds regression coverage for uv-style venv interpreter symlink behavior during remapping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Simulate a uv-style venv: venv/bin/python -> external interpreter | ||
| venv_bin = root_home / "src" / "hermes-agent" / "venv" / "bin" | ||
| venv_bin.mkdir(parents=True) | ||
| external_python = tmp_path / "uv-store" / "python3.11" | ||
| external_python.parent.mkdir(parents=True) | ||
| external_python.write_text("") | ||
| venv_python = venv_bin / "python" | ||
| venv_python.symlink_to(external_python) | ||
|
|
There was a problem hiding this comment.
This regression test creates a real filesystem symlink via symlink_to(), which can raise OSError/NotImplementedError on platforms or CI environments where symlinks aren’t supported (notably Windows without dev-mode/admin). Other tests in the repo guard symlink creation and pytest.skip when unavailable; doing the same here will prevent platform-specific failures while still exercising the behavior when symlinks work.
What
_remap_path_for_user()inhermes_cli/gateway.pycalledPath.resolve()on its input, which follows symlinks. In uv-managed venvs,venv/bin/pythonis a symlink into uv's shared Python store:get_python_path()correctly returns.../venv/bin/python, but whengenerate_systemd_unit(system=True)runs that path through_remap_path_for_user,.resolve()swaps it for the bare uv interpreter. The generated unit'sExecStart=then launches Python outside the venv, so none of the venv's site-packages are importable and the service crash-loops on import:hermes gateway start --systemreports success (systemd accepted the start request) butsystemctl status hermes-gatewayshowsactivating (auto-restart)briefly before settling intofailed. The CLI and the actual state disagree, which makes it genuinely confusing to diagnose.Only affects uv-managed venvs combined with
--systeminstalls — a traditionalpython -m venvcopies the interpreter, so.resolve()stays inside the venv and the bug is invisible. User-scoped installs skip_remap_path_for_userentirely and are unaffected.Fix
Prefer the lexical (unresolved) form of the path for the remap, and only fall back to resolving when
$HOMEitself is a symlink (e.g./home/alice -> /mnt/users/alice). System paths like/opt/hermescontinue to round-trip unchanged. In the unreachable-by-remap fallback branch, return the original path rather than the resolved form — otherwise we'd still leak resolved symlinks for paths outside$HOME.Test plan
pytest tests/hermes_cli/test_gateway_service.py— 58 passed, including new regression testtest_preserves_symlink_pointing_outside_home_remap_path_for_usertests (test_remaps_path_under_current_home,test_keeps_system_path_unchanged,test_noop_when_same_user) still passsystemctl cat hermes-gatewayshowedExecStart=<uv-store-path>/python3.11 ...and the service crash-looped onModuleNotFoundError: yaml. After the fix,ExecStart=<project>/venv/bin/python ...and the service runs cleanly (Active: active (running)).pathlib, so cross-platform impact should be minimal.Platforms
Tested on Linux (systemd).