From d9ec0bcf562a5b98eeccd8bedebc5bdd7ca0d794 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 23 Sep 2026 05:07:59 -0700 Subject: [PATCH] fix(relay): read segments config through the gateway's own reader, keep -z in its launch dir (#95577) Follow-up to the salvaged #88281 commits: read gateway.telemetry.session_segments through hermes_cli.config_effective.load_user_config_effective(), the exact primitive gateway.run._load_gateway_config() now delegates to, instead of read_raw_config() + managed overlay. The raw read dropped ${VAR} expansion (max_turns: ${SEG_TURNS} parsed as 0) and model-key canonicalization. The same import also rebound TERMINAL_CWD to the home dir in `hermes -z` (which does not pre-set it like cli.py): the launch dir's AGENTS.md was never loaded and terminal commands ran in $HOME (#95577). The subprocess guard now asserts TERMINAL_CWD / HERMES_QUIET / _HERMES_GATEWAY stay unset too, and the fixtures patch the effective reader, which both the old and new call paths go through. Found by the core entrypoint parity E2E matrix (oneshot row, context_file). --- agent/relay_runtime.py | 13 +++---- tests/agent/test_relay_session_segments.py | 42 +++++++++------------- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/agent/relay_runtime.py b/agent/relay_runtime.py index 1019e93fff..7433e1965b 100644 --- a/agent/relay_runtime.py +++ b/agent/relay_runtime.py @@ -177,15 +177,12 @@ def _load_segments_config() -> dict[str, Any]: on_compaction = False max_turns = 0 try: - # Do NOT import gateway.run from a non-gateway host: its module top level sets - # os.environ["_HERMES_GATEWAY"] / HERMES_QUIET — gateway-process semantics. Dragged - # into a CLI/TUI/desktop/cron process, _HERMES_GATEWAY flips tools/approval.py onto - # the gateway approval path with no notify_cb (#87183). - from hermes_cli import managed_scope - from hermes_cli.config import read_raw_config + # Never import gateway.run here: its import-time env setup (_HERMES_GATEWAY, HERMES_QUIET, + # TERMINAL_CWD := home) rebinds a CLI/TUI/cron host — hung approvals (#87183), `hermes -z` + # running in $HOME without the launch dir's AGENTS.md (#95577). Same reader it delegates to. + from hermes_cli.config_effective import load_user_config_effective - raw = managed_scope.apply_managed_overlay(read_raw_config()) - telemetry = ((raw.get("gateway") or {}).get("telemetry")) or {} + telemetry = (load_user_config_effective().get("gateway") or {}).get("telemetry") or {} segments = telemetry.get("session_segments") or {} on_compaction = bool(segments.get("on_compaction", False)) try: diff --git a/tests/agent/test_relay_session_segments.py b/tests/agent/test_relay_session_segments.py index 857286a835..7da8185f9e 100644 --- a/tests/agent/test_relay_session_segments.py +++ b/tests/agent/test_relay_session_segments.py @@ -138,18 +138,15 @@ def _fast_scope_timeout(monkeypatch): def _default_config(monkeypatch): """No config on disk by default; tests override _segments_config directly.""" monkeypatch.setattr( - "hermes_cli.config.read_raw_config", lambda: {} + "hermes_cli.config_effective.load_user_config_effective", lambda *_a, **_k: {} ) relay_runtime._reset_segments_config_for_tests() def _set_segments(monkeypatch, *, on_compaction=False, max_turns=0): - # _segments_config() reads via read_raw_config() (+ managed overlay) - # instead of importing gateway.run (whose module top-level setenv - # pollutes the calling process's environment, see #87183). monkeypatch.setattr( - "hermes_cli.config.read_raw_config", - lambda: { + "hermes_cli.config_effective.load_user_config_effective", + lambda *_a, **_k: { "gateway": { "telemetry": { "session_segments": { @@ -516,36 +513,29 @@ class TestRotationSafety: class TestGatewayRunStaysUnimported: """Guard against re-importing gateway.run from a non-gateway host. - relay_runtime._segments_config() must NEVER trigger ``gateway.run`` — - its module top level sets _HERMES_GATEWAY / HERMES_QUIET / HERMES_EXEC_ASK, - which flips a CLI/TUI/desktop/cron process onto the gateway path and hangs - dangerous commands in pending_approval (#87183). A plain monkeypatch of - read_raw_config wouldn't catch a future refactor that adds the import, so - this runs the real path in a fresh subprocess and asserts gateway.run never - enters sys.modules. + relay_runtime._segments_config() must NEVER trigger ``gateway.run`` — its + import-time env setup (_HERMES_GATEWAY, HERMES_QUIET, TERMINAL_CWD := home) + hangs CLI approvals (#87183) and runs ``hermes -z`` in $HOME (#95577). A + monkeypatch can't catch a refactor re-adding the import, so this runs the + real path in a fresh process with those vars unset. """ - def test_relay_runtime_never_imports_gateway_run(self) -> None: + def test_relay_runtime_never_imports_gateway_run(self, monkeypatch) -> None: + for var in ("TERMINAL_CWD", "HERMES_QUIET", "_HERMES_GATEWAY"): + monkeypatch.delenv(var, raising=False) result = _run_isolated( """ +import os import sys import agent.relay_runtime as rr -# _segments_config() is the only relay path that used to import gateway.run. rr._segments_config() rr._segments_config() # cached path too -if "gateway.run" in sys.modules: - print("FAIL: gateway.run was imported by a non-gateway host") - sys.exit(1) -print("PASS: gateway.run stays out of sys.modules") -sys.exit(0) +leaked = {v: os.environ[v] for v in ("TERMINAL_CWD", "HERMES_QUIET", "_HERMES_GATEWAY") if v in os.environ} +print("gateway.run imported:", "gateway.run" in sys.modules, "leaked env:", leaked) +sys.exit(1 if "gateway.run" in sys.modules or leaked else 0) """ ) - assert result.returncode == 0, ( - f"relay_runtime imported gateway.run:\\n" - f"stdout: {result.stdout}\\n" - f"stderr: {result.stderr}" - ) - assert "PASS" in result.stdout + assert result.returncode == 0, f"{result.stdout}\n{result.stderr}"