From 960ea8a849dee4d4c4475d321b82e6da314dd2eb Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 29 May 2026 23:26:31 -0500 Subject: [PATCH] fix(dashboard): honor injected HERMES_DASHBOARD_SESSION_TOKEN The desktop shell mints a session token and signs its /api + /api/ws calls with it via HERMES_DASHBOARD_SESSION_TOKEN, but the main-merge restored a web_server.py that ignored the env var and minted its own random _SESSION_TOKEN -- so every desktop request 401'd and the UI reported "gateway offline". Read the injected token (fall back to a fresh random one) so loopback HTTP + WS auth line up. Adds a regression test so a future merge can't silently drop the read. --- hermes_cli/web_server.py | 9 ++++++--- tests/hermes_cli/test_web_server.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 40dd9e2efff..10f20dc35ad 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -85,10 +85,13 @@ app = FastAPI(title="Hermes Agent", version=__version__) # --------------------------------------------------------------------------- # Session token for protecting sensitive endpoints (reveal). -# Generated fresh on every server start — dies when the process exits. -# Injected into the SPA HTML so only the legitimate web UI can use it. +# The desktop shell mints the token and injects it via +# HERMES_DASHBOARD_SESSION_TOKEN so its main process can authenticate the +# /api calls it makes on the user's behalf; otherwise we generate one fresh +# on every server start. Either way it dies when the process exits and is +# injected into the SPA HTML so only the legitimate web UI can use it. # --------------------------------------------------------------------------- -_SESSION_TOKEN = secrets.token_urlsafe(32) +_SESSION_TOKEN = os.environ.get("HERMES_DASHBOARD_SESSION_TOKEN") or secrets.token_urlsafe(32) _SESSION_HEADER_NAME = "X-Hermes-Session-Token" # In-browser Chat tab (/chat, /api/pty, …). Off unless ``hermes dashboard --tui`` diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 04774113c63..7bc5c5185ac 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -89,6 +89,35 @@ class TestRedactKey: assert "not set" in result.lower() or result == "***" or "\x1b" in result +class TestSessionTokenInjection: + """The desktop shell mints HERMES_DASHBOARD_SESSION_TOKEN and signs its + /api + /api/ws calls with it. The backend must adopt that token, else every + desktop request 401s ("gateway is offline"). A main-merge once silently + dropped this read — this guards the contract, not a literal value. + """ + + def test_honors_injected_token(self, monkeypatch): + import importlib + import hermes_cli.web_server as ws + + monkeypatch.setenv("HERMES_DASHBOARD_SESSION_TOKEN", "desktop-seeded-token") + try: + importlib.reload(ws) + assert ws._SESSION_TOKEN == "desktop-seeded-token" + finally: + monkeypatch.delenv("HERMES_DASHBOARD_SESSION_TOKEN", raising=False) + importlib.reload(ws) + + def test_falls_back_to_random_token(self, monkeypatch): + import importlib + import hermes_cli.web_server as ws + + monkeypatch.delenv("HERMES_DASHBOARD_SESSION_TOKEN", raising=False) + importlib.reload(ws) + + assert ws._SESSION_TOKEN and len(ws._SESSION_TOKEN) >= 32 + + # --------------------------------------------------------------------------- # web_server tests (FastAPI endpoints) # ---------------------------------------------------------------------------