mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(terminal): strip VIRTUAL_ENV/CONDA_PREFIX from terminal subprocess env
The Hermes gateway runs inside its own venv, so its process environment carries VIRTUAL_ENV (and possibly CONDA_PREFIX). The terminal tool spawned subprocesses inheriting those markers. When the agent ran `uv sync`, `uv pip install`, `poetry install`, etc. in ANY other project directory, those tools honored the inherited VIRTUAL_ENV and rebuilt/synced that project's dependencies into the Hermes venv path — wiping Hermes' own runtime deps (and, when the other project pinned a different Python, replacing the interpreter), bricking the gateway on the next restart (#23473). Strip VIRTUAL_ENV/CONDA_PREFIX in both subprocess-env construction points in tools/environments/local.py — `_sanitize_subprocess_env` and `_make_run_env` — via a shared `_ACTIVE_VENV_MARKER_VARS` constant. The Hermes venv stays reachable because its bin dir is already first on PATH, so removing the active-environment markers is safe and only prevents the cross-project clobber. Adds TestActiveVenvMarkerStripping: end-to-end (markers in os.environ don't reach the spawned subprocess) and unit coverage for both functions, plus a guard on the marker constant. Also adds the AUTHOR_MAP entry for the salvaged contributor. Closes #23473
This commit is contained in:
parent
d470ed0c4c
commit
dbbf102b8e
3 changed files with 67 additions and 0 deletions
|
|
@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
|
|||
|
||||
# Auto-extracted from noreply emails + manual overrides
|
||||
AUTHOR_MAP = {
|
||||
"dale@dalenguyen.me": "dalenguyen", # PR #53678 salvage (strip VIRTUAL_ENV/CONDA_PREFIX from terminal subprocess env; #23473)
|
||||
"blaryx@gmail.com": "Blaryxoff", # PR #32602 salvage (deep-merge PUT /api/config to preserve unrelated sections; #13396)
|
||||
"diamondeyesfox@gmail.com": "DiamondEyesFox", # PR #53351 salvage (rebaseline in-place compression flushes to prevent duplicate compacted rows; #9096)
|
||||
"piyrw9754@gmail.com": "rlaope", # PR #35075 salvage (align cron invisible-unicode set with install-time scanner; #35075)
|
||||
|
|
|
|||
|
|
@ -239,6 +239,54 @@ class TestForceEnvOptIn:
|
|||
assert result_env["OPENAI_BASE_URL"] == "http://intended/v1"
|
||||
|
||||
|
||||
class TestActiveVenvMarkerStripping:
|
||||
"""Active-virtualenv markers must not leak into terminal subprocesses (#23473).
|
||||
|
||||
The gateway runs inside its own venv, so its process environment carries
|
||||
VIRTUAL_ENV (and possibly CONDA_PREFIX). If those leak into commands the
|
||||
agent runs against ANOTHER Python project, ``uv``/``poetry`` treat the
|
||||
inherited value as the active environment and build that project's deps
|
||||
into the Hermes venv path instead of the project's own ``.venv`` —
|
||||
silently clobbering the Hermes environment (and, when the other project
|
||||
pins a different Python, breaking the gateway outright). The Hermes venv
|
||||
stays reachable via PATH, so stripping the markers is safe.
|
||||
"""
|
||||
|
||||
def test_virtualenv_marker_stripped_end_to_end(self):
|
||||
result_env = _run_with_env(extra_os_env={
|
||||
"VIRTUAL_ENV": "/home/user/.hermes/hermes-agent/venv",
|
||||
})
|
||||
assert "VIRTUAL_ENV" not in result_env
|
||||
|
||||
def test_conda_prefix_marker_stripped_end_to_end(self):
|
||||
result_env = _run_with_env(extra_os_env={
|
||||
"CONDA_PREFIX": "/opt/conda/envs/hermes",
|
||||
})
|
||||
assert "CONDA_PREFIX" not in result_env
|
||||
|
||||
def test_make_run_env_strips_markers(self):
|
||||
from tools.environments.local import _make_run_env
|
||||
poison = {"VIRTUAL_ENV": "/venv", "CONDA_PREFIX": "/conda", "PATH": "/usr/bin"}
|
||||
with patch.dict(os.environ, poison, clear=True):
|
||||
result = _make_run_env({})
|
||||
assert "VIRTUAL_ENV" not in result
|
||||
assert "CONDA_PREFIX" not in result
|
||||
|
||||
def test_sanitize_subprocess_env_strips_markers(self):
|
||||
from tools.environments.local import _sanitize_subprocess_env
|
||||
base = {"VIRTUAL_ENV": "/venv", "CONDA_PREFIX": "/conda", "HOME": "/home/user"}
|
||||
# Even an explicitly-passed extra marker is stripped.
|
||||
result = _sanitize_subprocess_env(base, {"VIRTUAL_ENV": "/also/venv"})
|
||||
assert "VIRTUAL_ENV" not in result
|
||||
assert "CONDA_PREFIX" not in result
|
||||
assert result.get("HOME") == "/home/user"
|
||||
|
||||
def test_markers_constant_contents(self):
|
||||
from tools.environments.local import _ACTIVE_VENV_MARKER_VARS
|
||||
assert "VIRTUAL_ENV" in _ACTIVE_VENV_MARKER_VARS
|
||||
assert "CONDA_PREFIX" in _ACTIVE_VENV_MARKER_VARS
|
||||
|
||||
|
||||
class TestBlocklistCoverage:
|
||||
"""Sanity checks that the blocklist covers all known providers."""
|
||||
|
||||
|
|
|
|||
|
|
@ -192,6 +192,18 @@ def _build_provider_env_blocklist() -> frozenset:
|
|||
|
||||
_HERMES_PROVIDER_ENV_BLOCKLIST = _build_provider_env_blocklist()
|
||||
|
||||
# Active-virtualenv markers that must NOT leak into terminal subprocesses.
|
||||
# The gateway runs inside its own venv, so its process environment carries
|
||||
# VIRTUAL_ENV (and possibly CONDA_PREFIX). If those leak into commands the
|
||||
# agent runs against OTHER Python projects, tools like ``uv``/``poetry`` treat
|
||||
# the inherited value as the active environment and build/sync that other
|
||||
# project's dependencies into the Hermes venv path instead of the project's own
|
||||
# ``.venv`` — silently clobbering the Hermes environment (e.g. a project pinned
|
||||
# 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).
|
||||
_ACTIVE_VENV_MARKER_VARS = ("VIRTUAL_ENV", "CONDA_PREFIX")
|
||||
|
||||
|
||||
def _inject_context_hermes_home(env: dict) -> None:
|
||||
"""Bridge the context-local Hermes home override into subprocess env."""
|
||||
|
|
@ -232,6 +244,9 @@ def _sanitize_subprocess_env(base_env: dict | None, extra_env: dict | None = Non
|
|||
from hermes_constants import apply_subprocess_home_env
|
||||
apply_subprocess_home_env(sanitized)
|
||||
|
||||
for _marker in _ACTIVE_VENV_MARKER_VARS:
|
||||
sanitized.pop(_marker, None)
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
|
|
@ -528,6 +543,9 @@ def _make_run_env(env: dict) -> dict:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
for _marker in _ACTIVE_VENV_MARKER_VARS:
|
||||
run_env.pop(_marker, None)
|
||||
|
||||
return run_env
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue