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.
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
"""Hermes CLI - Unified command-line interface for Hermes Agent."""
|
"""Hermes CLI - Unified command-line interface for Hermes Agent."""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
__release_date__ = "2026.9.24"
|
__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")
|
return str(read_install_stamp(repo_root()).get("baseVersion") or "0.0.0")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_utf8():
|
def _ensure_utf8() -> bool:
|
||||||
"""Force UTF-8 stdout/stderr to prevent UnicodeEncodeError crashes.
|
"""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
|
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.
|
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
|
repaired = True
|
||||||
except (AttributeError, OSError, ValueError):
|
except (AttributeError, OSError, ValueError):
|
||||||
pass
|
pass
|
||||||
# Only nudge child processes toward UTF-8 when a non-UTF-8 locale was actually detected; on a
|
return repaired
|
||||||
# healthy UTF-8 host children inherit it from the locale already.
|
|
||||||
if repaired:
|
|
||||||
os.environ.setdefault("PYTHONUTF8", "1")
|
|
||||||
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
_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()
|
||||||
|
|||||||
@@ -3545,6 +3545,12 @@ def main():
|
|||||||
configure_windows_stdio()
|
configure_windows_stdio()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
# 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.
|
# CA bundle (agent/ssl_verify.py). Never raises; False just means OpenSSL's paths.
|
||||||
|
|||||||
@@ -228,6 +228,10 @@ else:
|
|||||||
assert result.returncode == 0, result.stderr
|
assert result.returncode == 0, result.stderr
|
||||||
assert result.stdout.strip() == "bootstrap-before-app"
|
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):
|
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
|
# 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
|
# that agent/gateway code and tests also import; there the bootstrap exported TMPDIR and
|
||||||
|
|||||||
Reference in New Issue
Block a user