From 8308d1833935c372b4d79f181baf8165ddcefd91 Mon Sep 17 00:00:00 2001 From: altmazza0-star <256974976+altmazza0-star@users.noreply.github.com> Date: Sun, 3 May 2026 18:53:57 +0800 Subject: [PATCH] fix(gateway): preserve max turns after env reload --- gateway/run.py | 40 +++++++++++--- ...est_runtime_env_reload_config_authority.py | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 tests/gateway/test_runtime_env_reload_config_authority.py diff --git a/gateway/run.py b/gateway/run.py index 303e030177..9f792c3e5d 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -299,6 +299,36 @@ _env_path = _hermes_home / '.env' load_hermes_dotenv(hermes_home=_hermes_home, project_env=Path(__file__).resolve().parents[1] / '.env') +def _reload_runtime_env_preserving_config_authority() -> None: + """Reload .env for fresh credentials without letting stale .env override config. + + Gateway processes are long-lived, so per-turn code reloads ~/.hermes/.env to + pick up rotated API keys. config.yaml remains authoritative for agent budget + settings such as agent.max_turns; otherwise a stale HERMES_MAX_ITERATIONS in + .env can replace the startup bridge on later turns. + """ + load_hermes_dotenv( + hermes_home=_hermes_home, + project_env=Path(__file__).resolve().parents[1] / '.env', + ) + + config_path = _hermes_home / 'config.yaml' + if not config_path.exists(): + return + try: + import yaml as _yaml + with open(config_path, encoding="utf-8") as f: + cfg = _yaml.safe_load(f) or {} + from hermes_cli.config import _expand_env_vars + cfg = _expand_env_vars(cfg) + except Exception: + return + + agent_cfg = cfg.get("agent", {}) + if isinstance(agent_cfg, dict) and "max_turns" in agent_cfg: + os.environ["HERMES_MAX_ITERATIONS"] = str(agent_cfg["max_turns"]) + + _DOCKER_VOLUME_SPEC_RE = re.compile(r"^(?P.+):(?P/[^:]+?)(?::(?P[^:]+))?$") _DOCKER_MEDIA_OUTPUT_CONTAINER_PATHS = {"/output", "/outputs"} @@ -13524,13 +13554,9 @@ class GatewayRunner: combined_ephemeral = (combined_ephemeral + "\n\n" + self._ephemeral_system_prompt).strip() # Re-read .env and config for fresh credentials (gateway is long-lived, - # keys may change without restart). - try: - load_dotenv(_env_path, override=True, encoding="utf-8") - except UnicodeDecodeError: - load_dotenv(_env_path, override=True, encoding="latin-1") - except Exception: - pass + # keys may change without restart). Keep config.yaml authoritative for + # runtime budget settings bridged into env vars. + _reload_runtime_env_preserving_config_authority() try: model, runtime_kwargs = self._resolve_session_agent_runtime( diff --git a/tests/gateway/test_runtime_env_reload_config_authority.py b/tests/gateway/test_runtime_env_reload_config_authority.py new file mode 100644 index 0000000000..92d54b8863 --- /dev/null +++ b/tests/gateway/test_runtime_env_reload_config_authority.py @@ -0,0 +1,53 @@ +"""Regression tests for gateway per-turn env reload preserving config authority. + +Issue #19158: startup bridges config.yaml agent.max_turns into +HERMES_MAX_ITERATIONS, but a later per-turn load_dotenv(..., override=True) +can restore a stale .env HERMES_MAX_ITERATIONS value before the next turn. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import yaml + +from gateway import run as gateway_run + + +def test_reload_runtime_env_preserves_config_max_turns(tmp_path: Path, monkeypatch) -> None: + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text( + yaml.safe_dump({"agent": {"max_turns": 9000}}), + encoding="utf-8", + ) + (hermes_home / ".env").write_text( + "HERMES_MAX_ITERATIONS=90\nOPENROUTER_API_KEY=fresh-key\n", + encoding="utf-8", + ) + + monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home) + monkeypatch.setenv("HERMES_MAX_ITERATIONS", "9000") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + + gateway_run._reload_runtime_env_preserving_config_authority() + + assert os.environ["OPENROUTER_API_KEY"] == "fresh-key" + assert os.environ["HERMES_MAX_ITERATIONS"] == "9000" + + +def test_reload_runtime_env_keeps_env_max_iterations_when_config_omits_key( + tmp_path: Path, monkeypatch +) -> None: + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text(yaml.safe_dump({"agent": {}}), encoding="utf-8") + (hermes_home / ".env").write_text("HERMES_MAX_ITERATIONS=123\n", encoding="utf-8") + + monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home) + monkeypatch.delenv("HERMES_MAX_ITERATIONS", raising=False) + + gateway_run._reload_runtime_env_preserving_config_authority() + + assert os.environ["HERMES_MAX_ITERATIONS"] == "123"