From 76961b61bd8c58a0bc4091aa5cb117dfc16bd6d7 Mon Sep 17 00:00:00 2001 From: Elisa Martinez Abad Date: Mon, 3 Aug 2026 05:54:20 -0700 Subject: [PATCH] fix(tools): isolate external project environments --- tools/code_execution_tool.py | 53 +++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/tools/code_execution_tool.py b/tools/code_execution_tool.py index e2853c0d15773..6342338f9f0cc 100644 --- a/tools/code_execution_tool.py +++ b/tools/code_execution_tool.py @@ -1455,16 +1455,6 @@ def execute_code( # with a C/POSIX locale (containers, minimal base images). child_env["PYTHONIOENCODING"] = "utf-8" child_env["PYTHONUTF8"] = "1" - # Ensure the hermes-agent root is importable in the sandbox so - # repo-root modules are available to child scripts. We also prepend - # the staging tmpdir so ``from hermes_tools import ...`` resolves even - # when the subprocess CWD is not tmpdir (project mode). - _hermes_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - _existing_pp = child_env.get("PYTHONPATH", "") - _pp_parts = [tmpdir, _hermes_root] - if _existing_pp: - _pp_parts.append(_existing_pp) - child_env["PYTHONPATH"] = os.pathsep.join(_pp_parts) # Inject user's configured timezone so datetime.now() in sandboxed # code reflects the correct wall-clock time. Only TZ is set — # HERMES_TIMEZONE is an internal Hermes setting and must not leak @@ -1487,6 +1477,22 @@ def execute_code( _child_cwd = _resolve_child_cwd(_mode, tmpdir, task_id=task_id or "") _script_path = os.path.join(tmpdir, "script.py") + # ``hermes_tools.py`` always lives in the staging directory, so that + # directory must be importable even when project mode changes CWD. + # Hermes's own package root is useful too, but only when the child + # uses the same Python environment. Project mode can select an + # external venv; exposing Hermes's site-packages to that interpreter + # can mix incompatible compiled extensions (for example, Python 3.12 + # NumPy with a Python 3.9 project interpreter). + _hermes_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + _existing_pp = child_env.get("PYTHONPATH", "") + _pp_parts = [tmpdir] + if _uses_hermes_python_environment(_child_python): + _pp_parts.append(_hermes_root) + if _existing_pp: + _pp_parts.append(_existing_pp) + child_env["PYTHONPATH"] = os.pathsep.join(_pp_parts) + proc = subprocess.Popen( [_child_python, _script_path], cwd=_child_cwd, @@ -1850,6 +1856,33 @@ def _is_usable_python(python_path: str) -> bool: return False +@functools.lru_cache(maxsize=32) +def _python_environment_prefix(python_path: str) -> str: + """Return the resolved ``sys.prefix`` reported by *python_path*, if any.""" + try: + from agent.delegation_context import delegated_child_subprocess_env + + result = subprocess.run( + [python_path, "-c", "import sys; print(sys.prefix)"], + timeout=5, + capture_output=True, + text=True, + creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0, + stdin=subprocess.DEVNULL, + env=delegated_child_subprocess_env(), + ) + if result.returncode == 0 and result.stdout.strip(): + return os.path.realpath(result.stdout.strip()) + except (OSError, subprocess.TimeoutExpired, subprocess.SubprocessError): + pass + return "" + + +def _uses_hermes_python_environment(python_path: str) -> bool: + """Whether *python_path* belongs to Hermes's active Python environment.""" + return _python_environment_prefix(python_path) == os.path.realpath(sys.prefix) + + def _resolve_child_python(mode: str) -> str: """Pick the Python interpreter for the execute_code subprocess.