From a021b61bfc0605ff5a75b764166d2c4eb8313bae Mon Sep 17 00:00:00 2001 From: ethernet Date: Thu, 24 Sep 2026 14:34:01 -0400 Subject: [PATCH] fix(cli): keep hermes_cli import off os.environ; run the import-purity test on Windows Importing hermes_cli on a host whose stdout is not UTF-8 (a cp1252 pipe on Windows, a latin-1 locale on a Pi) set PYTHONUTF8/PYTHONIOENCODING in os.environ as a side effect. Every library importer -- gateway.relay, the compute host, agent.auxiliary_client -- then leaked that into each child it spawned, which is what test_library_imports_of_dual_use_entry_modules_stay_side_effect_free catches on Windows (changed == [PYTHONIOENCODING, PYTHONUTF8]). The package import now only repairs its own streams and records whether it had to. hermes_cli.main.main() turns that record into the child-process hint, so `hermes` still steers its Python children to UTF-8 on such a host. On Windows the entry-point bootstrap and configure_windows_stdio() already export both variables; the tool subprocess env builders set them for children independently. The test was unmarked, so the OS lanes (`-m "platforms and not integration"`) deselected it and Windows never ran it. Mark it platforms("any"): list_os_marked_tests.py lists the file for every lane and the -m expression now selects the test. --- hermes_cli/__init__.py | 16 +++++++--------- hermes_cli/main.py | 6 ++++++ tests/test_hermes_bootstrap.py | 4 ++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/hermes_cli/__init__.py b/hermes_cli/__init__.py index ed527a0585..9ccc4b297d 100644 --- a/hermes_cli/__init__.py +++ b/hermes_cli/__init__.py @@ -1,6 +1,5 @@ """Hermes CLI - Unified command-line interface for Hermes Agent.""" -import os import sys __release_date__ = "2026.9.24" @@ -40,8 +39,8 @@ def __getattr__(name: str) -> str: return str(read_install_stamp(repo_root()).get("baseVersion") or "0.0.0") -def _ensure_utf8(): - """Force UTF-8 stdout/stderr to prevent UnicodeEncodeError crashes. +def _ensure_utf8() -> bool: + """Force UTF-8 stdout/stderr to prevent UnicodeEncodeError crashes; True when a stream was repaired. The CLI prints box-drawing characters and the ☤ glyph in the setup wizard, doctor, and status banners; under a non-UTF-8 codec that raises before the command can even start (e.g. @@ -68,11 +67,10 @@ def _ensure_utf8(): repaired = True except (AttributeError, OSError, ValueError): pass - # Only nudge child processes toward UTF-8 when a non-UTF-8 locale was actually detected; on a - # healthy UTF-8 host children inherit it from the locale already. - if repaired: - os.environ.setdefault("PYTHONUTF8", "1") - os.environ.setdefault("PYTHONIOENCODING", "utf-8") + return repaired -_ensure_utf8() +# Import repairs only this process's streams. Gateway, compute host, and test code import this +# package as a library; rewriting their os.environ would leak into every child they spawn, so the +# child-process UTF-8 hint is applied by the CLI entry point (hermes_cli.main.main) instead. +_stdio_repaired = _ensure_utf8() diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 48f5d837c6..ed29ef5e44 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3545,6 +3545,12 @@ def main(): configure_windows_stdio() except Exception: pass + # A non-UTF-8 locale that the package import had to repair would crash Python children the + # same way. Only on that host, so a healthy UTF-8 locale keeps its children untouched. + from hermes_cli import _stdio_repaired + if _stdio_repaired: + os.environ.setdefault("PYTHONUTF8", "1") + os.environ.setdefault("PYTHONIOENCODING", "utf-8") # One TLS authority: trust the OS store before any outbound call resolves a # CA bundle (agent/ssl_verify.py). Never raises; False just means OpenSSL's paths. diff --git a/tests/test_hermes_bootstrap.py b/tests/test_hermes_bootstrap.py index 8aaa79271b..3713ab280c 100644 --- a/tests/test_hermes_bootstrap.py +++ b/tests/test_hermes_bootstrap.py @@ -228,6 +228,10 @@ else: assert result.returncode == 0, result.stderr assert result.stdout.strip() == "bootstrap-before-app" + +# "any": the OS lanes select only platforms-marked tests, and Windows is where hermes_cli's +# stdio repair fires on a cp1252 pipe. +@pytest.mark.platforms("any") def test_library_imports_of_dual_use_entry_modules_stay_side_effect_free(tmp_path): # The codex hermes-tools MCP server and the compute host are ``python -m`` entry points # that agent/gateway code and tests also import; there the bootstrap exported TMPDIR and