"""Frontend ABC + the runtime context handed to ``configure``. A frontend is anything that listens on a port and routes inbound traffic into the gateway. ``GatewayRuntime`` carries everything a frontend may need that isn't user-config: built registries, per-agent backends, and the in-memory token store. The user's ``/config/config.py`` defines a ``Gateway`` (lists); ``cli.main`` turns that into a ``GatewayRuntime`` and hands it to each frontend's ``configure``. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Mapping, Sequence from beaver_gateway.backends.base import Backend from beaver_gateway.core.auth import TokenStore from beaver_gateway.core.registry import AgentRegistry, McpRegistry from beaver_gateway.core.turn_record import TurnRecord from beaver_gateway.storage import Database TurnLogHandler = Callable[[TurnRecord], Awaitable[None]] @dataclass(frozen=True, slots=True) class GatewayRuntime: """Post-build state of the gateway, shared with every frontend. Backends are keyed by **agent name**, not type — one ``RaycastBackend`` instance can serve many ``RaycastAgent`` instances, but the lookup site (an inbound request with ``model=``) already has the name in hand, so the indirection lives one step earlier. ``mcp_internal_urls`` is filled in Phase 2.1: one loopback URL per declared ``McpServer`` so ``ClaudeSdkBackend`` can pass them to ``BackendOptions.mcp_servers`` without re-running discovery. ``db`` (Phase 4.1) is the shared :class:`Database` handle. Phase 4.2 will switch ``TokenStore`` to read from it; Phase 4.3 admin/audit write through it. Phase 4.1 only attaches it — existing frontends ignore it. """ agents: AgentRegistry mcps: McpRegistry backends: dict[str, Backend] token_store: TokenStore db: Database mcp_internal_urls: Mapping[str, str] = field(default_factory=dict) # Phase 4.3 — AdminFrontend reads creds + cookie-signing key from # the runtime so the user's ``config.py`` doesn't have to know # anything about env wiring. Defaulted to empty so existing tests / # call sites that don't touch the admin path keep building; the # admin frontend ``configure()`` itself rejects empty values. admin_user: str = "" admin_pass: str = "" session_secret: str = "" # The full sibling-frontends list, in declaration order. AdminFrontend # uses it to advertise concrete bearer-endpoint URLs (host/port) on # the dashboard so the operator can copy ready-to-use links / curl # snippets. Other frontends ignore it. frontends: Sequence[Frontend] = field(default_factory=tuple) # Frontends that finish a turn (Anthropic Messages, Markdown) iterate # this list and ``await`` each handler with a ``TurnRecord``. Handlers # are appended during ``configure()`` by frontends that want a # cross-frontend chat archive — currently the markdown frontend's # ``log_all_chats`` mode. Handler exceptions are caught at the call # site; they never block the user-visible response. # # The field is typed as ``list[Any]`` rather than the precise # ``list[TurnLogHandler]`` because the alias lives under TYPE_CHECKING # to keep ``anthropic.types`` out of the runtime import graph for # this base module. turn_log_handlers: list[TurnLogHandler] = field(default_factory=list) # M1b: conversations service, event bus and the shared session pool. # ``Any`` for the same import-graph reason as above; ``None`` only in # tests that build a runtime without them. conversations: Any = None bus: Any = None pool: Any = None class Frontend(ABC): """Listens on a port, dispatches into the gateway.""" @abstractmethod def configure(self, runtime: GatewayRuntime) -> None: ... @abstractmethod async def serve(self) -> None: ...