TUI crashes when cwd contains shadowing Python packages
NousResearch/hermes-agent · cluster 15989 · gateway
Data as of
Nothing open on GitHub. Fix merged in #19885. Check it covers your case.
The short answer
- Known?
- Yes. 8 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #19885. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PRs #19885, #51693. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and fixes address a bug where the TUI or its gateway child process fails to launch when the current working directory contains a Python package (e.g., 'utils', 'proxy', 'ui') that shadows internal modules via sys.path. The fixes guard sys.path in the slash_worker and tui_gateway to prevent local packages from interfering with internal imports.
Open issues0
Open PRs0
Fixes on record
-
#19885 fix(tui_gateway): guard sys.path against local package shadowing
files:
tests/tui_gateway/test_entry_sys_path.py,tui_gateway/entry.pyShow the diff
-
tests/tui_gateway/test_entry_sys_path.py@@ -0,0 +1,101 @@ +"""Tests for tui_gateway/entry.py sys.path hardening (issue #15989). + +When the TUI backend is spawned by Node.js, the Python interpreter may have +'' or '.' at the front of sys.path, allowing a local utils/ directory in CWD +to shadow the installed utils module. entry.py must sanitize sys.path before +any non-stdlib import is resolved. +""" + +import importlib +import os +import sys +from unittest.mock import patch + + +def _reload_entry_with_env(env_overrides: dict) -> None: + """Re-execute entry.py's module-level path setup under a controlled env.""" + # We only want to exercise the sys.path fixup block, not the signal/import + # machinery that follows. We do this by running the fixup code verbatim in + # a fresh copy of sys.path rather than importing the real module (which + # would trigger tui_gateway.server imports requiring heavy mocks). + original_path = sys.path[:] + original_env = {k: os.environ.get(k) for k in env_overrides} + try: + with patch.dict(os.environ, env_overrides, clear=False): + _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "") + if _src_root and _src_root not in sys.path: + sys.path.insert(0, _src_root) + sys.path = [p for p in sys.path if p not in ("", ".")] + return sys.path[:] + finally: + sys.path = original_path + for k, v in original_env.items(): + if v is None: + os.environ.pop(k, None) + else: + os.environ[k] = v + + +def test_empty_string_and_dot_removed_from_sys_path(): + original = sys.path[:] + try: + sys.path.insert(0, "") + sys.path.insert(0, ".") + assert "" in sys.path + assert "." in sys.path + + # Run the entry.py fixup logic directly + sys.path = [p for p in sys.path if p not in ("", ".")] + + assert "" not in sys.path + assert "." not in sys.path + finally: + sys.path = original + + +def test_hermes_src_root_inserted_at_front(): + original = sys.path[:] + try: + fake_root = "/fake/hermes/src" + with patch.dict(os.environ, {"HERMES_PYTHON_SRC_ROOT": fake_root}): + _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "") + if _src_root and _src_root not in sys.path: + sys.path.insert(0, _src_root) + sys.path = [p for p in sys.path if p not in ("", ".")] + + assert sys.path[0] == fake_root + finally: + sys.path = original + + +def test_src_root_not_duplicated_if_already_present(): + original = sys.path[:] + try: + fake_root = "/already/present" + sys.path.insert(0, fake_root) + count_before = sys.path.count(fake_root) + + with patch.dict(os.environ, {"HERMES_PYTHON_SRC_ROOT": fake_root}): + _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "") + if _src_root and _src_root not in sys.path: + sys.path.insert(0, _src_root) + sys.path = [p for p in sys.path if p not in ("", ".")] + + assert sys.path.count(fake_root) == count_before + finally: + sys.path = original + + +def test_no_src_root_env_does_not_crash(): + original = sys.path[:] + try: + env = {k: v for k, v in os.environ.items() if k != "HERMES_PYTHON_SRC_ROOT"} + with patch.dict(os.environ, {}, clear=True): + os.environ.update(env) + _src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "") + if _src_root and _src_root not in sys.path: + sys.path.insert(0, _src_root) + sys.path = [p for p in sys.path if p not in ("", ".")] + # No exception raised + finally: + sys.path = original -
tui_gateway/entry.py@@ -1,7 +1,18 @@ -import json import os -import signal import sys + +# Guard against a local utils/ (or other package) in CWD shadowing installed +# hermes modules. hermes_cli sets HERMES_PYTHON_SRC_ROOT before spawning this +# subprocess; inserting it first ensures the installed packages win. +_src_root = os.environ.get("HERMES_PYTHON_SRC_ROOT", "") +if _src_root and _src_root not in sys.path: + sys.path.insert(0, _src_root) +# Strip '' and '.' — both resolve to CWD at import time and can let a local +# directory shadow installed packages. +sys.path = [p for p in sys.path if p not in ("", ".")] + +import json +import signal import time import traceback
-
-
#51693 fix(tui): stop a cwd package named utils/proxy/ui from crashing the gateway child
files:
acp_adapter/entry.py,hermes_bootstrap.py,tests/test_hermes_bootstrap.py,tests/tui_gateway/test_entry_sys_path.py,tui_gateway/entry.py,ui-tui/src/gatewayClient.ts -
#56379 fix(tui): guard slash_worker sys.path against local package shadowing (#51286)
files:
tests/tui_gateway/test_slash_worker_sys_path.py,tui_gateway/slash_worker.py
3 PRs closed without merging
From the close comments: merged via another change 2 · done elsewhere 1
-
#16141 fix(tui_gateway): guard sys.path against local package shadowing (#15989) · merged via another change →
#19885 -
#51304 fix(tui_gateway): prevent cwd packages from shadowing internal modules · done elsewhere →
c39b2b5,#51286,#51693 -
#51484 fix(tui): guard slash_worker sys.path against local package shadowing · merged via another change →
#56379,#51693
Closed issues · 2
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.23: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab