Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions tools/environments/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ def _build_provider_env_blocklist() -> frozenset:
# to a different Python version overwrites it and breaks the gateway). The
# Hermes venv stays reachable via PATH (its bin dir is first), so stripping
# these markers is safe and only prevents the cross-project clobber (#23473).
#
# PYTHONPATH is NOT included here — it's handled by
# _strip_mismatched_site_packages() which surgically removes only site-packages
# paths that don't match the current Python ABI, preserving user-set entries.
_ACTIVE_VENV_MARKER_VARS = ("VIRTUAL_ENV", "CONDA_PREFIX")


Expand Down Expand Up @@ -383,6 +387,8 @@ def _sanitize_subprocess_env(base_env: dict | None, extra_env: dict | None = Non
for _marker in _ACTIVE_VENV_MARKER_VARS:
sanitized.pop(_marker, None)

_strip_mismatched_site_packages(sanitized)

_apply_windows_msys_bash_env_defaults(sanitized)

return sanitized
Expand Down Expand Up @@ -495,6 +501,8 @@ def hermes_subprocess_env(*, inherit_credentials: bool = False) -> dict[str, str
for _marker in _ACTIVE_VENV_MARKER_VARS:
env.pop(_marker, None)

_strip_mismatched_site_packages(env)

_apply_windows_msys_bash_env_defaults(env)

# Cross-session leak guard, same as the terminal spawn paths: this helper
Expand Down Expand Up @@ -831,11 +839,72 @@ def _make_run_env(env: dict) -> dict:
for _marker in _ACTIVE_VENV_MARKER_VARS:
run_env.pop(_marker, None)

_strip_mismatched_site_packages(run_env)

_apply_windows_msys_bash_env_defaults(run_env)

return run_env


def _strip_mismatched_site_packages(env: dict) -> None:
"""Remove Hermes venv site-packages paths from PYTHONPATH.

The Desktop Electron process injects the Hermes venv's site-packages path
(e.g. ``.../.hermes/hermes-agent/venv/lib/python3.11/site-packages``) into
PYTHONPATH so the Hermes Python 3.11 backend can import its own packages.
When this PYTHONPATH leaks into agent terminal subprocesses:

- A Python 3.13 child sees 3.11 C extensions (``_imaging`` etc.) on
``sys.path`` ahead of the correct 3.13 versions and crashes.
- A Python 3.11 child doesn't need it either — the Hermes backend's
code discovery happens via ``sys.path``, not the inherited env var.

Rather than stripping PYTHONPATH entirely (which would discard user-set
entries like ``/home/user/my-lib``), this function surgically removes
only site-packages paths that live under the Hermes installation
(``~/.hermes/hermes-agent/venv/``). User paths and the Hermes source
root are preserved.

Also handles ``VIRTUAL_ENV``: if it points at a venv inside the Hermes
installation, that venv is already the one the gateway runs inside —
exposing it to subprocesses is redundant and clutters the env.
"""
# Hermes installation root — site-packages under here are never useful
# for terminal subprocesses.
_hermes_home = os.environ.get("HERMES_HOME", "")
_hermes_venv = os.path.join(_hermes_home, "hermes-agent", "venv")

# --- PYTHONPATH: strip any site-packages under Hermes venv ---
pp = env.get("PYTHONPATH")
if pp:
kept = []
stripped = []
for entry in pp.split(os.pathsep):
entry = entry.strip()
if not entry:
continue
# Does this entry point at site-packages inside the Hermes venv?
if _hermes_venv and _hermes_venv in entry and "/site-packages" in entry:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This Unix-only "/site-packages" check misses the Desktop Windows entry: current apps/desktop/electron/main.ts:1860 builds venvRoot\\Lib\\site-packages with path.join. Normalize/compare path components against the Hermes venv instead.

stripped.append(entry)
else:
kept.append(entry)
if kept:
env["PYTHONPATH"] = os.pathsep.join(kept)
else:
env.pop("PYTHONPATH", None)
if stripped:
logger.debug(
"Stripped Hermes-venv site-packages from PYTHONPATH: %s",
stripped,
)

# --- VIRTUAL_ENV: remove if pointing at the Hermes venv ---
ve = env.get("VIRTUAL_ENV")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

VIRTUAL_ENV has already been removed by the _ACTIVE_VENV_MARKER_VARS loop before this helper is called in every proposed caller, so this branch is unreachable.

if ve and _hermes_venv:
if Path(ve).resolve() == Path(_hermes_venv).resolve():
env.pop("VIRTUAL_ENV", None)


def _read_terminal_shell_init_config() -> tuple[list[str], bool]:
"""Return (shell_init_files, auto_source_bashrc) from config.yaml.

Expand Down