diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 86d7b3438..a50f615d6 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -3282,12 +3282,20 @@ def _spawn_hermes_action(subcommand: List[str], name: str) -> subprocess.Popen: cmd = [_dashboard_spawn_executable(), "-m", "hermes_cli.main", *subcommand] + # The dashboard runs *inside* the gateway process, so os.environ carries + # _HERMES_GATEWAY=1. Inheriting it makes a spawned `hermes gateway restart` + # trip the in-process restart-loop guard and exit 1 — silently failing the + # dashboard's auto-restart paths. The gateway's own restart watcher already + # drops it (gateway/run.py); mirror that here (#52470). + action_env = {**os.environ, "HERMES_NONINTERACTIVE": "1"} + action_env.pop("_HERMES_GATEWAY", None) + popen_kwargs: Dict[str, Any] = { "cwd": str(PROJECT_ROOT), "stdin": subprocess.DEVNULL, "stdout": log_file, "stderr": subprocess.STDOUT, - "env": {**os.environ, "HERMES_NONINTERACTIVE": "1"}, + "env": action_env, } if sys.platform == "win32": popen_kwargs["creationflags"] = windows_detach_flags() diff --git a/tests/hermes_cli/test_dashboard_admin_endpoints.py b/tests/hermes_cli/test_dashboard_admin_endpoints.py index 049ed4608..15bf21bae 100644 --- a/tests/hermes_cli/test_dashboard_admin_endpoints.py +++ b/tests/hermes_cli/test_dashboard_admin_endpoints.py @@ -1283,3 +1283,35 @@ class TestToolsConfigEndpoints: kwargs["json"] = payload r = fn(path, **kwargs) assert r.status_code == 401, f"{method} {path} not gated" + + +# --------------------------------------------------------------------------- +# _spawn_hermes_action env scrubbing (#52470) +# --------------------------------------------------------------------------- + +def test_spawn_hermes_action_scrubs_gateway_loop_guard_env(monkeypatch, tmp_path): + """The dashboard runs inside the gateway, so os.environ has + _HERMES_GATEWAY=1. Spawned actions (e.g. `gateway restart`) must NOT inherit + it, or the in-process restart-loop guard rejects the restart and it silently + fails (#52470). + """ + import hermes_cli.web_server as ws + + monkeypatch.setenv("_HERMES_GATEWAY", "1") + monkeypatch.setattr(ws, "_ACTION_LOG_DIR", tmp_path) + + captured = {} + + class _FakeProc: + pid = 1234 + + def _fake_popen(cmd, **kwargs): + captured["env"] = kwargs.get("env") + return _FakeProc() + + monkeypatch.setattr(ws.subprocess, "Popen", _fake_popen) + + ws._spawn_hermes_action(["gateway", "restart"], "gateway-restart") + + assert "_HERMES_GATEWAY" not in captured["env"] + assert captured["env"]["HERMES_NONINTERACTIVE"] == "1"