diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 16708e725697..f01703ec9398 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -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) @@ -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. diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index 0363208b784e..9975ae322b4f 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -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 # ---------------------------------------------------------------------------