diff --git a/gateway/run.py b/gateway/run.py index 40cb4a8de26e..3a1da1a9967b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -994,6 +994,28 @@ def _reload_runtime_env_preserving_config_authority() -> None: if isinstance(agent_cfg, dict) and "max_turns" in agent_cfg: os.environ["HERMES_MAX_ITERATIONS"] = str(agent_cfg["max_turns"]) + # Re-bridge terminal.* so long-lived gateways pick up config changes on + # /new or /reset without needing a full restart. Placeholder cwd values + # mean "use the startup fallback"; clear any stale explicit bridge first + # so explicit -> placeholder updates do not keep the old TERMINAL_CWD. + terminal_cfg = cfg.get("terminal") + if isinstance(terminal_cfg, dict): + raw_cwd = str(terminal_cfg.get("cwd", "")).strip() + if "cwd" not in terminal_cfg or raw_cwd in {".", "auto", "cwd"}: + os.environ.pop("TERMINAL_CWD", None) + else: + os.environ.pop("TERMINAL_CWD", None) + from hermes_cli.config import apply_terminal_config_to_env + apply_terminal_config_to_env(config=cfg, override=True) + _apply_terminal_cwd_fallback() + + +def _apply_terminal_cwd_fallback() -> None: + configured_cwd = os.environ.get("TERMINAL_CWD", "") + if not configured_cwd or configured_cwd in {".", "auto", "cwd"}: + fallback = os.getenv("MESSAGING_CWD") or str(Path.home()) + os.environ["TERMINAL_CWD"] = fallback + _DOCKER_VOLUME_SPEC_RE = re.compile(r"^(?P.+):(?P/[^:]+?)(?::(?P[^:]+))?$") _DOCKER_MEDIA_OUTPUT_CONTAINER_PATHS = {"/output", "/outputs"} @@ -1221,10 +1243,7 @@ def _reload_runtime_env_preserving_config_authority() -> None: # by the config bridge above). When it's unset or a placeholder, default # to home directory. MESSAGING_CWD is accepted as a backward-compat # fallback (deprecated — the warning above tells users to migrate). -_configured_cwd = os.environ.get("TERMINAL_CWD", "") -if not _configured_cwd or _configured_cwd in {".", "auto", "cwd"}: - _fallback = os.getenv("MESSAGING_CWD") or str(Path.home()) - os.environ["TERMINAL_CWD"] = _fallback +_apply_terminal_cwd_fallback() from gateway.config import ( Platform, diff --git a/tests/gateway/test_config_cwd_bridge.py b/tests/gateway/test_config_cwd_bridge.py index 05ffee9b8d2e..7be2f95180f1 100644 --- a/tests/gateway/test_config_cwd_bridge.py +++ b/tests/gateway/test_config_cwd_bridge.py @@ -12,6 +12,8 @@ import os import json +from hermes_cli.config import apply_terminal_config_to_env + def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None): """Simulate the gateway config bridge logic from gateway/run.py. @@ -75,6 +77,27 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None): return env +def _apply_terminal_cwd_fallback(env: dict): + configured_cwd = env.get("TERMINAL_CWD", "") + if not configured_cwd or configured_cwd in {".", "auto", "cwd"}: + env["TERMINAL_CWD"] = env.get("MESSAGING_CWD") or "/root" + + +def _simulate_runtime_env_reload(cfg: dict, initial_env: dict | None = None): + """Simulate _reload_runtime_env_preserving_config_authority cwd handling.""" + env = dict(initial_env or {}) + terminal_cfg = cfg.get("terminal") + if isinstance(terminal_cfg, dict): + raw_cwd = str(terminal_cfg.get("cwd", "")).strip() + if "cwd" not in terminal_cfg or raw_cwd in {".", "auto", "cwd"}: + env.pop("TERMINAL_CWD", None) + else: + env.pop("TERMINAL_CWD", None) + apply_terminal_config_to_env(env=env, config=cfg, override=True) + _apply_terminal_cwd_fallback(env) + return env + + class TestTopLevelCwdAlias: """Top-level `cwd:` should be treated as `terminal.cwd`.""" @@ -243,3 +266,32 @@ def test_tilde_with_nested_precedence(self): } result = _simulate_config_bridge(cfg) assert result["TERMINAL_CWD"] == os.path.expanduser("~/nested") + + +class TestRuntimeReloadTerminalCwd: + """Runtime config reload should match startup terminal.cwd fallback rules.""" + + def test_runtime_reload_updates_explicit_terminal_cwd(self): + cfg = {"terminal": {"cwd": "/new/project", "backend": "docker"}} + result = _simulate_runtime_env_reload( + cfg, + {"TERMINAL_CWD": "/old/project", "TERMINAL_ENV": "local"}, + ) + assert result["TERMINAL_CWD"] == "/new/project" + assert result["TERMINAL_ENV"] == "docker" + + def test_runtime_reload_placeholder_clears_stale_terminal_cwd(self): + cfg = {"terminal": {"cwd": "."}} + result = _simulate_runtime_env_reload( + cfg, + {"TERMINAL_CWD": "/old/project", "MESSAGING_CWD": "/fallback/project"}, + ) + assert result["TERMINAL_CWD"] == "/fallback/project" + + def test_runtime_reload_missing_cwd_clears_stale_terminal_cwd(self): + cfg = {"terminal": {"backend": "local"}} + result = _simulate_runtime_env_reload( + cfg, + {"TERMINAL_CWD": "/old/project", "MESSAGING_CWD": "/fallback/project"}, + ) + assert result["TERMINAL_CWD"] == "/fallback/project"