From bd00212337343150f2ee831d0e8738b417e03ae8 Mon Sep 17 00:00:00 2001 From: Devorun <130918800+devorun@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:52:53 +0300 Subject: [PATCH] fix(dashboard): drop _HERMES_GATEWAY when spawning hermes actions (#52482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web dashboard runs inside the gateway process, so `os.environ` carries `_HERMES_GATEWAY=1`. `_spawn_hermes_action` spread that into the subprocess env, so a spawned `hermes gateway restart` (dashboard "Enable webhooks", Telegram QR apply) tripped the in-process restart-loop guard and exited 1 — the gateway never restarted, but the dashboard reported `restart_started: true` because it only checks that the spawn succeeded. Scrub `_HERMES_GATEWAY` from the spawned action's env, matching what the gateway's own restart watcher already does (gateway/run.py). Fixes #52470. Adds a test asserting the spawned env drops the loop-guard var while keeping HERMES_NONINTERACTIVE. --- hermes_cli/web_server.py | 10 +++++- .../test_dashboard_admin_endpoints.py | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) 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"