diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 7ee26c138f8..82fd6c74f85 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -295,7 +295,13 @@ app.include_router(_memory_oauth_router) # 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 = os.environ.get("HERMES_DASHBOARD_SESSION_TOKEN") or secrets.token_urlsafe(32) + + +def _resolve_session_token() -> str: + return os.environ.get("HERMES_DASHBOARD_SESSION_TOKEN") or secrets.token_urlsafe(32) + + +_SESSION_TOKEN = _resolve_session_token() _SESSION_HEADER_NAME = "X-Hermes-Session-Token" _SSH_OWNER_NONCE: Optional[str] = None diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index dddb286d107..dc4bfe34503 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -191,16 +191,45 @@ class TestSessionTokenInjection: """ def test_honors_injected_token(self, monkeypatch): - import importlib import hermes_cli.web_server as ws + original_app = ws.app + original_token = ws._SESSION_TOKEN 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) + assert ws._resolve_session_token() == "desktop-seeded-token" + # No module reload: the loaded app and its adopted token are untouched. + assert ws.app is original_app + assert ws._SESSION_TOKEN == original_token + + def test_falls_back_to_random_token(self, monkeypatch): + import hermes_cli.web_server as ws + + monkeypatch.delenv("HERMES_DASHBOARD_SESSION_TOKEN", raising=False) + with patch.object( + ws.secrets, "token_urlsafe", return_value="generated-token" + ) as token_urlsafe: + assert ws._resolve_session_token() == "generated-token" + token_urlsafe.assert_called_once_with(32) + + def test_session_token_resolution_preserves_loaded_app_auth(self, monkeypatch): + import hermes_cli.web_server as ws + from starlette.testclient import TestClient + + original_app = ws.app + original_header_name = ws._SESSION_HEADER_NAME + original_token = ws._SESSION_TOKEN + monkeypatch.setenv("HERMES_DASHBOARD_SESSION_TOKEN", "desktop-seeded-token") + assert ws._resolve_session_token() == "desktop-seeded-token" + monkeypatch.delenv("HERMES_DASHBOARD_SESSION_TOKEN", raising=False) + with patch.object(ws.secrets, "token_urlsafe", return_value="generated-token"): + assert ws._resolve_session_token() == "generated-token" + + client = TestClient(original_app) + client.headers[original_header_name] = original_token + assert client.get("/api/__session_token_probe").status_code == 404 + assert ws.app is original_app + assert ws._SESSION_HEADER_NAME == original_header_name + assert ws._SESSION_TOKEN == original_token # ---------------------------------------------------------------------------