From 8dcbc910bfd131dcfa7b83bf61e11e4807dce4ec Mon Sep 17 00:00:00 2001 From: Christopher-Schulze <210261288+Christopher-Schulze@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:27:22 +0200 Subject: [PATCH] fix(tui): guard slash_worker sys.path against local package shadowing The slash-command worker is spawned as `-m tui_gateway.slash_worker` and inherits the user's CWD. A local package in that CWD (e.g. a project shipping its own `utils/`, `proxy/`, or `ui/`) shadows the installed hermes module, so `import cli` crashes the worker with: ImportError: cannot import name 'atomic_replace' from 'utils' The child then exits 1 in a crash loop. #15989 added this sys.path guard to the sibling entrypoint tui_gateway/entry.py but not to this worker, which is spawned as a separate process and so starts with CWD back on sys.path. Apply the same guard (insert HERMES_PYTHON_SRC_ROOT, strip ''/'.') before the first non-stdlib import. Add a regression test that imports the worker from a CWD containing colliding packages. Fixes #51286 --- .../tui_gateway/test_slash_worker_sys_path.py | 60 +++++++++++++++++++ tui_gateway/slash_worker.py | 17 +++++- 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/tui_gateway/test_slash_worker_sys_path.py diff --git a/tests/tui_gateway/test_slash_worker_sys_path.py b/tests/tui_gateway/test_slash_worker_sys_path.py new file mode 100644 index 000000000..4b8262239 --- /dev/null +++ b/tests/tui_gateway/test_slash_worker_sys_path.py @@ -0,0 +1,60 @@ +"""Regression tests for tui_gateway/slash_worker.py sys.path hardening (issue #51286). + +The slash-command worker is spawned as ``-m tui_gateway.slash_worker`` and +inherits the user's CWD. A local package (e.g. ``utils/``) in that CWD shadows +the installed hermes ``utils`` module and crashes the worker on ``import cli`` +(``ImportError: cannot import name 'atomic_replace' from 'utils'``). + +#15989 added this guard to the sibling entrypoint ``tui_gateway/entry.py`` but +missed this child, so the crash still reproduced. slash_worker.py must sanitize +sys.path before its first non-stdlib import. +""" + +import os +import subprocess +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +def test_slash_worker_imports_from_cwd_with_colliding_utils(tmp_path): + """Importing the worker from a CWD that ships its own ``utils/`` package + must succeed — the guard strips CWD so the installed module wins.""" + # Mimic the user's project (tg-ws-proxy ships utils/, proxy/, ui/). + for pkg in ("utils", "proxy", "ui"): + (tmp_path / pkg).mkdir() + (tmp_path / pkg / "__init__.py").write_text("") # no atomic_replace, etc. + + env = {k: v for k, v in os.environ.items() if k != "HERMES_PYTHON_SRC_ROOT"} + # Keep the source importable via PYTHONPATH; CWD ('') still precedes it on + # sys.path for ``-c``, so the shadow (and thus the guard) is still exercised. + env["PYTHONPATH"] = str(PROJECT_ROOT) + + result = subprocess.run( + [sys.executable, "-c", "import tui_gateway.slash_worker"], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + timeout=120, + ) + + assert result.returncode == 0, ( + "slash_worker failed to import from a CWD containing a colliding " + "utils/ package — sys.path guard regressed (issue #51286).\n" + f"stderr:\n{result.stderr}" + ) + + +def test_sys_path_guard_runs_before_cli_import(): + """The guard must execute before ``import cli`` — reordering it below the + import would re-introduce the shadowing crash.""" + src = (PROJECT_ROOT / "tui_gateway" / "slash_worker.py").read_text() + guard = 'sys.path = [p for p in sys.path if p not in {"", "."}]' + cli_import = "import cli as cli_mod" + assert guard in src, "sys.path shadowing guard missing from slash_worker.py" + assert cli_import in src, "expected 'import cli as cli_mod' in slash_worker.py" + assert src.index(guard) < src.index(cli_import), ( + "sys.path guard must run before 'import cli' (issue #51286)" + ) diff --git a/tui_gateway/slash_worker.py b/tui_gateway/slash_worker.py index fce8ec3e2..f2708c541 100644 --- a/tui_gateway/slash_worker.py +++ b/tui_gateway/slash_worker.py @@ -3,12 +3,25 @@ Protocol: reads JSON lines from stdin {id, command}, writes {id, ok, output|error} to stdout. """ +import os +import sys + +# Guard against a local ``utils/`` (or other) package in the spawn CWD shadowing +# installed hermes modules. This worker is spawned as ``-m tui_gateway.slash_worker`` +# and inherits the user's CWD, so the ``import cli`` below would otherwise resolve +# ``utils`` to a colliding local package and crash the child (issue #51286). The +# sibling entrypoint ``tui_gateway/entry.py`` applies the same guard; #15989 added +# it there but missed this child. +_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) +# '' and '.' both resolve to CWD at import time and can shadow installed packages. +sys.path = [p for p in sys.path if p not in {"", "."}] + import argparse import contextlib import io import json -import os -import sys import threading import time