Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ def handler(args, **kw) -> str:

import json
import logging
import os
import time

from pathlib import Path
Expand Down Expand Up @@ -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"


Expand Down
10 changes: 3 additions & 7 deletions wsserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
Loading