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
This commit is contained in:
Christopher-Schulze 2026-06-23 19:27:22 +02:00 committed by Teknium
parent 7b45a22ddf
commit 8dcbc910bf
2 changed files with 75 additions and 2 deletions

View file

@ -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)"
)

View file

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