fix(dashboard): drop _HERMES_GATEWAY when spawning hermes actions (#52482)

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.
This commit is contained in:
Devorun 2026-07-17 02:52:53 +03:00 committed by GitHub
parent d4c3f98140
commit bd00212337
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 1 deletions

View file

@ -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()

View file

@ -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"