Skip to content

fix(integrations): stop bm subprocesses inheriting Hermes's Python env - #1179

Open
Palo-Alto-AI-Research-Lab wants to merge 1 commit into
basicmachines-co:mainfrom
Palo-Alto-AI-Research-Lab:fix/1093-sanitize-child-env
Open

fix(integrations): stop bm subprocesses inheriting Hermes's Python env#1179
Palo-Alto-AI-Research-Lab wants to merge 1 commit into
basicmachines-co:mainfrom
Palo-Alto-AI-Research-Lab:fix/1093-sanitize-child-env

Conversation

@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown

Fixes #1093.

What

The Hermes plugin spawns three external processes, and every one of them inherited the parent's Python environment. Hermes ships its own interpreter and exports PYTHONPATH; bm is a separate uv-managed tool on 3.12+. The child therefore resolved imports against Hermes's site-packages and failed during interpreter startup with ModuleNotFoundError: No module named 'pydantic_core._pydantic_core' — a compiled-extension mismatch between two Python versions, not a missing dependency, which is why reinstalling doesn't help.

_clean_child_env() drops the four variables that name a specific interpreter (PYTHONPATH, PYTHONHOME, VIRTUAL_ENV, __PYVENV_LAUNCHER__) and keeps everything else — PATH, HOME, proxies, credentials — because the child still needs them.

Applied at all three sites:

site spawns before
_BmMcpActor.__init__StdioServerParameters(env=…) bm mcp os.environ.copy()
_install_bm_via_uv uv tool install basic-memory no env= — inherited implicitly
_ensure_local_project bm project add no env= — inherited implicitly

The bottom two are the easy ones to miss: with no env= argument there is nothing to grep for, and uv tool install is what builds the environment the MCP server later runs in.

Testing

tests/test_child_env.py, 10 tests. The four covering the spawn sites were confirmed to fail before the fix and pass after — a sanitizer that did nothing would not go green.

The issue asked for an integration test with a contaminated PYTHONPATH and noted that unit mocks can't reproduce a native-extension ABI failure. That's right, but the failure doesn't need a second interpreter to reproduce deterministically — a directory on PYTHONPATH shadowing a real module does it on one Python:

# hermes_abi_canary/__init__.py, on the poisoned path
raise ImportError("No module named 'pydantic_core._pydantic_core'")

A child spawned with the inherited environment imports the shadow (ImportError); a child spawned through _clean_child_env() cannot see it at all (ModuleNotFoundError). Both directions are asserted, so the test proves the contamination is real before it proves it is gone. No ABI mismatch to stage in CI and no second interpreter needed.

Local runs: 267 passed on Python 3.11, 3.12 and 3.14; just manifest-check clean; ruff check and ruff format --check clean.

Two scope decisions, in case you'd rather they went the other way

  • LD_LIBRARY_PATH / DYLD_LIBRARY_PATH are deliberately not stripped. They can break a child the same way, but they also carry legitimate local configuration and were not implicated here. Say the word and I'll add them.
  • __PYVENV_LAUNCHER__ is included defensively. The macOS framework launcher exports it to steer a child at a specific interpreter, but I did not reproduce a failure caused by it, unlike the other three. Happy to drop it if you'd rather keep the list to what's proven.

I also swept integrations/openclaw for the same pattern — it has no subprocess spawns, so nothing to fix there.

Credit to @tellus1019: the diagnosis in the issue was already correct and complete, including all three scopes. This is that diagnosis implemented, plus a repro that runs in CI.


Disclosure: written by an AI agent acting on behalf of the account holder, as part of Palo Alto AI Research Lab. Everything reported above was run, not assumed; the one untested item is flagged as such.

Hermes runs on its own interpreter and exports PYTHONPATH; bm is a separate
uv-managed tool on Python 3.12+. Every process the plugin spawned inherited
those variables, so the child resolved imports against Hermes's site-packages
and died during interpreter startup:

    ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'

That is a compiled-extension mismatch between two Python versions, not a
missing dependency, so it does not respond to reinstalling anything.

Adds _clean_child_env() and applies it at all three spawn sites:

  - _BmMcpActor.__init__  -> StdioServerParameters(env=...) for 'bm mcp'
  - _install_bm_via_uv    -> 'uv tool install basic-memory'
  - _ensure_local_project -> 'bm project add'

The last two passed no env= at all, so they inherited implicitly and are easy
to miss when patching only the actor.

PATH, HOME, proxies and credentials are preserved; only the four variables
that name a specific interpreter are dropped.

Tests spawn real subprocesses with a deliberately contaminated PYTHONPATH, as
requested in the issue: a mock cannot reproduce this because the failure
happens in interpreter startup rather than in our code. One test asserts the
contamination reaches an inherited child, so a no-op sanitizer cannot pass.

Fixes basicmachines-co#1093

Signed-off-by: Palo-Alto-AI-Research-Lab <Palo-Alto-AI-Research-Lab@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hermes provider inherits PYTHONPATH and fails to start bm MCP with mixed Python versions

2 participants