Skip to content

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

Merged
phernandez merged 1 commit into
basicmachines-co:mainfrom
tonydzi:fix/1093-sanitize-child-env
Aug 4, 2026
Merged

fix(integrations): stop bm subprocesses inheriting Hermes's Python env#1179
phernandez merged 1 commit into
basicmachines-co:mainfrom
tonydzi:fix/1093-sanitize-child-env

Conversation

@tonydzi

@tonydzi tonydzi commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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.

@CLAassistant

CLAassistant commented Aug 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@phernandez

Copy link
Copy Markdown
Member

thanks for the PR @Palo-Alto-AI-Research-Lab
We'll take a look at this

@tonydzi

tonydzi commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

thanks — and one thing cleared on our side since you wrote: the CLA is signed, so license/cla is green and the PR is passing everything. nothing is blocked on us.

whenever you get to the review, the two scope calls in the description are one-word answers, and i'll push either the same day:

  • strip LD_LIBRARY_PATH / DYLD_LIBRARY_PATH as well, or leave them alone?
  • keep __PYVENV_LAUNCHER__ (added defensively — unlike the other three i did not reproduce a failure from it), or drop it so the list is only what's proven?

no urgency on either; the fix stands as it is. the diagnosis in #1093 was @tellus1019's and already complete — this is that, implemented, with a repro that runs in CI.

@phernandez phernandez left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at 33c2011e; no findings.

I also applied the PR cleanly to current main (eedce9bb) and verified:

  • just package-check-hermes: 267 passed, 12 skipped
  • focused child-environment regression tests: 10 passed
  • Ruff check/format, Hermes manifest validation, and git diff --check: clean

For the two scope questions:

  • Leave LD_LIBRARY_PATH / DYLD_LIBRARY_PATH alone. They can carry legitimate native-library configuration and are not implicated in this failure.
  • Keep __PYVENV_LAUNCHER__ in the strip list. It is parent-interpreter launcher state and should not steer the independent bm child.

The boundary here looks right: isolate the child from Hermes's Python installation while preserving unrelated environment such as PATH, HOME, proxies, and credentials.

Thanks for the careful diagnosis, complete spawn-site sweep, and strong regression coverage.

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>
@phernandez
phernandez force-pushed the fix/1093-sanitize-child-env branch from 33c2011 to 1b80ee3 Compare August 4, 2026 22:41
@phernandez

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 1b80ee34c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@phernandez
phernandez merged commit 46104c8 into basicmachines-co:main Aug 4, 2026
8 checks passed
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

3 participants