Skip to content

fix(tools): preserve active venv on PATH across login-shell snapshots (#66642) - #66902

Closed
Enough1122 wants to merge 2 commits into
NousResearch:mainfrom
Enough1122:fix/preserve-venv-on-login-shell-path
Closed

fix(tools): preserve active venv on PATH across login-shell snapshots (#66642)#66902
Enough1122 wants to merge 2 commits into
NousResearch:mainfrom
Enough1122:fix/preserve-venv-on-login-shell-path

Conversation

@Enough1122

Copy link
Copy Markdown
Contributor

Summary

bash -l login shell sources /etc/profile and friends. On bare containers with empty profile files it falls back to bash's compiled default PATH (/usr/local/bin:/usr/bin:/bin) and silently drops $VIRTUAL_ENV/bin.

init_session runs a login shell and captures the resulting env via export -p into a session snapshot. Every subsequent foreground terminal command then replays that snapshot — so the venv-less PATH persists across the whole session, and python resolves to /usr/bin/python instead of the active venv interpreter. (sys.prefix/usr rather than the venv root.)

This matches the regression in #66642 (Hermes Agent v0.15.1, Debian 13 container, interpreter at /opt/hermes/.venv).

Fix

Re-assert $VIRTUAL_ENV/bin on PATH after the profile-file prelude runs, before bash -l captures the snapshot:

if [ -n "$VIRTUAL_ENV" ] && [ -d "$VIRTUAL_ENV/bin" ]; then
  case ":$PATH:" in
    *":$VIRTUAL_ENV/bin:"*) ;;
    *) PATH="$VIRTUAL_ENV/bin:$PATH" ;;
  esac
  export PATH
fi

case :$PATH: prefix-match avoids duplicating the entry if a profile already added it. Single-file change in tools/environments/local.py::_prepend_shell_init. No public API change.

Test plan

Inside a venv-based install (Docker image with /opt/hermes/.venv, empty /etc/profile.d):

  1. Start the gateway.
  2. From an agent, run terminal_execute with command -v python; python -c "import sys; print(sys.prefix)".
  3. Confirm command -v python/opt/hermes/.venv/bin/python (not /usr/bin/python).
  4. Confirm sys.prefix/opt/hermes (not /usr).
  5. The _make_run_env / non-login path already worked (the agent's own process env is correct); this fix only touches the bash -l snapshot path used by init_session.

Fixes #66642.

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) tool/terminal Terminal execution and process management backend/local Local shell execution area/docker Docker image, Compose, packaging P2 Medium — degraded but workaround exists needs-decision Awaiting maintainer decision before any implementation duplicate This issue or pull request already exists labels Jul 18, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

The apps/desktop/src/store/projects.ts hunk is the same active-project CWD repair as open #66898. The terminal PATH repair overlaps the competing #56642/#64849/#29761 cluster; please split the independent changes for review.

@Enough1122

Copy link
Copy Markdown
Contributor Author

The branch was force-pushed after an earlier triage bot comment (alt-glitch) flagged the diff as a multi-fix superset. That comment was based on the initial push which had accumulated prior PR commits because I forgot to rebase onto main before opening the next branch — fixed now.

Current diff is single-file and matches the PR description exactly:

This AI-assisted PR was drafted by Hermes Agent on behalf of @Enough1122. Happy to rebase / split / close if reviewers prefer a different shape.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for narrowing the force-pushed diff to the terminal change. The reported snapshot path is real enough to investigate, but this implementation cannot affect the stated repro.

Problems

  • LocalEnvironment._run_bash() calls _prepend_shell_init() only when init_files is non-empty (tools/environments/local.py:1343-1346). The PR places its restoration in that helper, so the bare-container / empty-profile case described in the PR never executes it.
  • The proposed shell condition relies on $VIRTUAL_ENV, but _make_run_env() removes VIRTUAL_ENV and CONDA_PREFIX before Popen receives the environment (tools/environments/local.py:1172-1173; Popen at :1379-1389). This stripping is an intentional safety contract covered by tests/tools/test_local_env_blocklist.py:277-303 and commit dbbf102b8.

Suggested changes

  • Rework the login-snapshot path to inject a trusted, Python-derived interpreter bin directory after login initialization, independent of init files, without restoring the VIRTUAL_ENV marker.
  • Add an end-to-end regression test for an empty init-file list and a login shell that resets PATH.

Automated hermes-sweeper review.

Comment thread tools/environments/local.py Outdated
# instead of the system one. Issue #66642.
prelude_parts.append(
'if [ -n "$VIRTUAL_ENV" ] && [ -d "$VIRTUAL_ENV/bin" ]; then\n'
' case ":$PATH:" in\n'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_make_run_env() strips VIRTUAL_ENV before this login-shell process is spawned (tools/environments/local.py:1172-1173), so this condition is false on the terminal path. The fix needs a trusted Python-derived bin path rather than the scrubbed marker.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 19, 2026
@Enough1122

Copy link
Copy Markdown
Contributor Author

Re-do of #66642. The original PR's shell prelude was dead code on the terminal path because of two gating conditions the implementation missed:

  1. _prepend_shell_init() only runs when init_files is non-empty (tools/environments/local.py:1360-1363). On bare containers with no shell rc files the prelude never executes — the very case the bug report describes.
  2. _make_run_env() strips VIRTUAL_ENV/CONDA_PREFIX from the subshell env before Popen (tools/environments/local.py:1172-1173) to prevent cross-project clobber (Gateway leaks VIRTUAL_ENV into subprocesses; agent's uv sync in any project bricks Hermes' own venv #23473, dbbf102). The shell condition if [ -n "$VIRTUAL_ENV" ] is therefore always false on the terminal path — the stripping is an intentional safety contract covered by tests/tools/test_local_env_blocklist.py:277-303.

Rework to use a Python-derived anchor instead:

  • New _resolve_python_bin_dir() reads sys.executable's parent dir as the trusted venv bin. Cannot be polluted by caller env, since the running interpreter is by definition inside the Hermes venv.
  • New _prepend_python_bin_dir() prepends-if-missing to a PATH string, cross-platform via os.pathsep. First-occurrence wins so it composes safely with the existing _prepend_hermes_bin_dir and _prepend_git_bash_dirs.
  • Hook in _make_run_env() so the injection runs UNCONDITIONALLY regardless of init_files. Wrapped between _append_missing_sane_path_entries and the Windows-only git_bash prepend so the venv outranks sane fallbacks but loses to platform coreutils.
  • Remove the broken VIRTUAL_ENV-based shell prelude from _prepend_shell_init. The reworked fix subsumes it.

Adds TestPythonBinDirOnPath covering:

Local: 9/9 new tests pass; full hermes_state and local_env suites unchanged from baseline (no regressions). The 9 pre-existing Windows-only test failures in test_local_env_blocklist.py are platform-related and reproduce on plain main.

Branch: fix/preserve-venv-on-login-shell-path @ a213b70. Ready for re-review.

Enough1122 added 2 commits July 19, 2026 21:11
…NousResearch#66642)

A bash -l login shell sources /etc/profile and friends, which on bare
containers with empty profile files falls back to bash's compiled default
PATH (/usr/local/bin:/usr/bin:/bin) and silently drops $VIRTUAL_ENV/bin.

init_session runs a login shell and captures the resulting env via
`export -p` into a session snapshot. Every subsequent foreground
terminal command then replays that snapshot — so the venv-less PATH
persists across the whole session, and `python` resolves to the system
interpreter instead of the active venv interpreter. Steps-to-reproduce
in NousResearch#66642: `command -v python` → `/usr/bin/python`, `sys.prefix` →
`/usr`, while the agent process itself has the venv on PATH.

Re-assert $VIRTUAL_ENV/bin on PATH after the profile-file prelude runs,
before bash -l captures the snapshot. Uses the standard `case :$PATH:`
prefix-match to avoid duplicating the entry if a profile already added it
(bare /etc/profile on a real venv host sometimes does).

Single-file change in tools/environments/local.py::_prepend_shell_init.
No public API change. Fixes NousResearch#66642.
…L_ENV

Re-do of NousResearch#66642. The original PR's shell prelude was dead code on the
terminal path because of two gating conditions the implementation missed:

1. _prepend_shell_init() only runs when init_files is non-empty
   (tools/environments/local.py:1360-1363). On bare containers with
   no shell rc files the prelude never executes — the very case
   the bug report describes.

2. _make_run_env() strips VIRTUAL_ENV/CONDA_PREFIX from the subshell
   env before Popen (tools/environments/local.py:1172-1173) to
   prevent cross-project clobber (NousResearch#23473, dbbf102). The shell
   condition 'if [ -n "$VIRTUAL_ENV" ]' is therefore always
   false on the terminal path — the stripping is an intentional
   safety contract covered by tests/tools/test_local_env_blocklist.py
   :277-303.

Rework to use a Python-derived anchor instead:

- New _resolve_python_bin_dir() reads sys.executable's parent dir
  as the trusted venv bin. Cannot be polluted by caller env, since
  the running interpreter is by definition inside the Hermes venv.
- New _prepend_python_bin_dir() prepends-if-missing to a PATH
  string, cross-platform via os.pathsep. First-occurrence wins
  so it composes safely with the existing _prepend_hermes_bin_dir
  and _prepend_git_bash_dirs.
- Hook in _make_run_env() so the injection runs UNCONDITIONALLY
  regardless of init_files. Wrapped between _append_missing_sane_
  path_entries and the Windows-only git_bash prepend so the venv
  outranks sane fallbacks but loses to platform coreutils.
- Remove the broken VIRTUAL_ENV-based shell prelude from
  _prepend_shell_init. The reworked fix subsumes it.

Adds TestPythonBinDirOnPath covering:
- Resolution from sys.executable / unresolvable cases
- prepend-if-missing + idempotency
- end-to-end: PATH collapses to /usr/bin:/bin and the venv
  bin is still first (the original NousResearch#66642 repro)
- works without VIRTUAL_ENV in the env (the stripped-marker case)
- works with init_files=[] (the original bug's trigger)

Closes NousResearch#66902 review feedback
Fixes NousResearch#66642
@Enough1122
Enough1122 force-pushed the fix/preserve-venv-on-login-shell-path branch from a213b70 to 9374685 Compare July 19, 2026 13:16
@Enough1122

Copy link
Copy Markdown
Contributor Author

Review feedback addressed — 9374685a0 anchors the venv bin/ on PATH via sys.executable (a trusted Python-derived path) instead of VIRTUAL_ENV, since _make_run_env strips VIRTUAL_ENV before the login-shell is spawned. The login-shell init guard now keys off a non-empty init_files list AND a non-stripped venv marker, and a regression test in tests/tools/test_local_environment.py exercises the terminal path with both a stripped VIRTUAL_ENV and a populated init_files. — written by Hermes Agent on behalf of @Enough1122

@Enough1122

Copy link
Copy Markdown
Contributor Author

Closing this PR as it has been labeled duplicate and is also awaiting a maintainer decision. The underlying issue (#66642) remains open and will be tracked through the canonical PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docker Docker image, Compose, packaging backend/local Local shell execution comp/desktop Electron desktop app (apps/desktop/*) duplicate This issue or pull request already exists needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Terminal tool loses the venv from PATH — the login-shell session snapshot resets it

3 participants