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
43 changes: 20 additions & 23 deletions gateway/platforms/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5367,30 +5367,12 @@ async def connect(self) -> bool:
self._app.router.add_get("/v1/runs/{run_id}/events", self._handle_run_events)
self._app.router.add_post("/v1/runs/{run_id}/approval", self._handle_run_approval)
self._app.router.add_post("/v1/runs/{run_id}/stop", self._handle_stop_run)
# Store the adapter after native routes are registered. Local Hermes-Relay
# bootstrap shims use this key as a feature-detection hook; registering
# native routes first lets those shims no-op instead of shadowing the
# upstream session-control handlers.
self._app["api_server_adapter"] = self

# Start background sweep to clean up orphaned (unconsumed) run streams
sweep_task = asyncio.create_task(self._sweep_orphaned_runs())
try:
self._background_tasks.add(sweep_task)
except TypeError:
pass
if hasattr(sweep_task, "add_done_callback"):
sweep_task.add_done_callback(self._background_tasks.discard)
self._app.router.add_get("/api/sessions", self._handle_list_sessions)
self._app.router.add_post("/api/sessions", self._handle_create_session)
# Axiom/Relay compatibility routes that are still outside upstream's
# baseline session-control surface. Register these before exposing
# the adapter through app["api_server_adapter"] so Hermes-Relay's
# bootstrap hook sees the native fork routes and no-ops instead of
# injecting shadow handlers.
self._app.router.add_get("/api/sessions/search", self._handle_search_sessions)
self._app.router.add_get("/api/sessions/{session_id}", self._handle_get_session)
self._app.router.add_get("/api/sessions/{session_id}/messages", self._handle_get_session_messages)
self._app.router.add_patch("/api/sessions/{session_id}", self._handle_update_session)
self._app.router.add_delete("/api/sessions/{session_id}", self._handle_delete_session)
self._app.router.add_post("/api/sessions/{session_id}/fork", self._handle_fork_session)
self._app.router.add_post("/api/sessions/{session_id}/chat", self._handle_session_chat)
self._app.router.add_post("/api/sessions/{session_id}/chat/stream", self._handle_session_chat_stream)
self._app.router.add_get("/api/memory", self._handle_get_memory)
self._app.router.add_post("/api/memory", self._handle_add_memory)
self._app.router.add_patch("/api/memory", self._handle_replace_memory)
Expand All @@ -5401,6 +5383,21 @@ async def connect(self) -> bool:
self._app.router.add_patch("/api/config", self._handle_update_config)
self._app.router.add_get("/api/available-models", self._handle_available_models)

# Store the adapter after all native routes are registered. Local
# Hermes-Relay bootstrap shims use this key as a feature-detection
# hook; registering native routes first lets those shims no-op
# instead of shadowing first-class handlers.
self._app["api_server_adapter"] = self

# Start background sweep to clean up orphaned (unconsumed) run streams
sweep_task = asyncio.create_task(self._sweep_orphaned_runs())
try:
self._background_tasks.add(sweep_task)
except TypeError:
pass
if hasattr(sweep_task, "add_done_callback"):
sweep_task.add_done_callback(self._background_tasks.discard)

# Refuse to start without authentication. The API server can
# dispatch terminal-capable agent work, so every deployment needs
# an explicit API_SERVER_KEY regardless of bind address.
Expand Down
36 changes: 36 additions & 0 deletions tests/gateway/test_api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,42 @@ def test_returns_false_without_aiohttp(self):
assert check_api_server_requirements() is False


# ---------------------------------------------------------------------------
# Route registration
# ---------------------------------------------------------------------------


def test_connect_registers_each_route_once():
"""Regression guard: upstream-native session routes must not be re-added.

Hermes-Relay's bootstrap detects native routes by method/path. Registering
duplicate aiohttp routes makes route ownership ambiguous and can leave the
live API surface depending on registration order instead of the upstream
canonical handlers.
"""
from collections import Counter
import inspect
import re

source = inspect.getsource(APIServerAdapter.connect)
registrations = [
(method.upper(), path)
for method, path in re.findall(
r'self\._app\.router\.add_(\w+)\("([^"]+)"',
source,
)
]

duplicate_routes = {
route: count
for route, count in Counter(registrations).items()
if count > 1
}

assert duplicate_routes == {}
assert ("GET", "/api/sessions/search") in registrations


# ---------------------------------------------------------------------------
# ResponseStore
# ---------------------------------------------------------------------------
Expand Down
Loading