From 4cfa859aa9a469d885c353b0104f985848f7abc1 Mon Sep 17 00:00:00 2001 From: Blasius Patrick Date: Mon, 20 Jul 2026 18:49:15 +0700 Subject: [PATCH] fix: revert to shared internal auth token, defer generation until after bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes: 1. _internal_token_path() always returns ~/.hermes/nodes-internal-token (removes HERMES_HOME profile-scoping) in both wsserver/server.py and tools.py 2. Token generation moved from before _serve() to after the bind-wait loop in lifecycle.py — if we bound the port, generate fresh token; if port was occupied (kate profile), read the existing shared token. 3. Removed unused 'import os' from tools.py This fixes the profile collision: default gateway owns port 7000 and generates the token; kate gateway reads it and authenticates against the running server. Signed-off-by: Blasius Patrick --- lifecycle.py | 53 +++++++++++++++++++++++++++++++++++----------- tools.py | 8 ++----- wsserver/server.py | 10 +++------ 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/lifecycle.py b/lifecycle.py index 341c7cc..ffd2ec2 100644 --- a/lifecycle.py +++ b/lifecycle.py @@ -213,18 +213,10 @@ async def start(self) -> None: self._server = uvicorn.Server(uvicorn_config) self._server.lifespan = uvicorn_config.lifespan_class(uvicorn_config) - # The FastAPI app's lifespan handler normally generates the - # internal auth token via ASGI lifespan.startup, but we use - # lifespan="off" (import-chain issues with "on" in this plugin - # context). Generate the token directly instead. - from .server import _ensure_internal_token, _internal_token_path - - token_path = _internal_token_path() - logger.info( - "hermes-node: generating internal auth token at %s", token_path - ) - self._app.state.internal_token = _ensure_internal_token() - logger.info("hermes-node: internal auth token generated") + # Token generation is deferred until AFTER the bind succeeds. + # If we're the first to bind (default gateway), we generate a + # fresh token. If the port is already occupied (kate profile), + # we read the existing shared token — we must NOT overwrite it. async def _serve() -> None: try: @@ -276,6 +268,43 @@ async def _serve() -> None: f"{self._config.port} within 5s" ) + # Token setup: generate if we bound the port, read existing if + # another gateway already owns it. This must happen AFTER the + # bind-wait so we don't overwrite the token when the port is + # already occupied by the default gateway. + from .server import _ensure_internal_token, _internal_token_path + + if self._server.started: + token_path = _internal_token_path() + logger.info( + "hermes-node: generating internal auth token at %s", + token_path, + ) + self._app.state.internal_token = _ensure_internal_token() + logger.info("hermes-node: internal auth token generated") + else: + token_path = _internal_token_path() + try: + existing = token_path.read_text().strip().partition("\n")[0] + if existing: + self._app.state.internal_token = existing + logger.info( + "hermes-node: using existing token from %s", + token_path, + ) + else: + logger.warning( + "hermes-node: token file %s is empty — " + "internal endpoints will return 503", + token_path, + ) + except (FileNotFoundError, OSError): + logger.warning( + "hermes-node: cannot read token from %s — " + "internal endpoints will return 503", + token_path, + ) + if not self._server.started and self._server.should_exit: # Server couldn't start (EADDRINUSE, etc.). Surface the # error so the caller can log it; do not raise past diff --git a/tools.py b/tools.py index 6be3322..26fbe21 100644 --- a/tools.py +++ b/tools.py @@ -21,7 +21,6 @@ def handler(args, **kw) -> str: import json import logging -import os import time from pathlib import Path @@ -81,12 +80,9 @@ def _read_internal_token() -> str | None: def _internal_token_path() -> Path: """Return the path for the internal auth token file. - Uses HERMES_HOME if set (profile-scoped), otherwise falls back to - ~/.hermes/nodes-internal-token. Must match wsserver/server.py. + Always uses the shared path ``~/.hermes/nodes-internal-token``. + Must match wsserver/server.py. """ - hermes_home = os.environ.get("HERMES_HOME") - if hermes_home: - return Path(hermes_home) / "nodes-internal-token" return Path.home() / ".hermes" / "nodes-internal-token" diff --git a/wsserver/server.py b/wsserver/server.py index 0cb6b84..06b12ad 100644 --- a/wsserver/server.py +++ b/wsserver/server.py @@ -84,14 +84,10 @@ def _internal_token_path() -> Path: """Return the path for the internal auth token file. - Uses HERMES_HOME if set (profile-scoped), otherwise falls back to - ~/.hermes/nodes-internal-token (shared). This prevents one profile's - WSS server from overwriting another profile's token when two gateway - processes run side by side. + Always uses the shared path ``~/.hermes/nodes-internal-token``. + All profiles share the same token — the default gateway owns the + WSS server and generates the token; other profiles read it. """ - hermes_home = os.environ.get("HERMES_HOME") - if hermes_home: - return Path(hermes_home) / "nodes-internal-token" return Path.home() / ".hermes" / "nodes-internal-token"