fix(tools): isolate external project environments

This commit is contained in:
Elisa Martinez Abad 2026-08-03 05:54:20 -07:00 committed by kshitij
parent a3bcb2c232
commit 76961b61bd
1 changed files with 43 additions and 10 deletions

View File

@ -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.