The MCP server was created but never mounted — no client could reach
it. Mount it at /mcp/ over streamable HTTP with a combined lifespan,
resolving the gateway lazily so mounting happens at app construction.
Security and safety for the agent surface:
- One static API_TOKEN (SecretStr) enforced across REST (dependency),
WebSocket (query param/header before accept), and MCP
(StaticTokenVerifier). Startup refuses tokenless non-loopback binds.
- Emergency numbers (911/9911/112) always refused on make_call, plus a
MAX_CONCURRENT_CALLS cap; ValueError surfaces as 400/ToolError.
- Safe defaults: debug off, no credential in default DATABASE_URL,
SIP/LLM/TTS secrets as SecretStr.
Cleanups:
- Delete broken learn_call_flow tool (wrong ctor args, nonexistent
method) and the never-fed CallAnalytics service; keep
call_flow_learner for proper wiring later.
- Trim dial_plan to what is actually used (emergency guard, extension
allocation); delete the unreferenced matcher/normaliser.
- Register call_history before calls so /api/calls/history is no
longer shadowed by /api/calls/{call_id}.
- fastmcp pinned >=3.0 (http_app + StaticTokenVerifier).
New tests: MCP in-memory client (tool surface, lazy gateway, emergency
refusal, call cap) and API security (401 paths, route order, mount).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
40 lines
1.2 KiB
Python
40 lines
1.2 KiB
Python
"""
|
|
API Dependencies — Shared dependency injection for all routes.
|
|
"""
|
|
|
|
import secrets
|
|
|
|
from fastapi import Header, HTTPException, Request
|
|
|
|
from config import get_settings
|
|
from core.gateway import AIPSTNGateway
|
|
|
|
|
|
def get_gateway(request: Request) -> AIPSTNGateway:
|
|
"""Get the gateway instance from app state."""
|
|
gateway = getattr(request.app.state, "gateway", None)
|
|
if gateway is None:
|
|
raise HTTPException(status_code=503, detail="Gateway not initialized")
|
|
return gateway
|
|
|
|
|
|
def require_token(authorization: str | None = Header(default=None)) -> None:
|
|
"""
|
|
Enforce the static bearer token (API_TOKEN) on REST routes.
|
|
|
|
An empty configured token disables auth; startup refuses that
|
|
combination unless the server is bound to loopback.
|
|
"""
|
|
token = get_settings().api_token.get_secret_value()
|
|
if not token:
|
|
return
|
|
supplied = ""
|
|
if authorization and authorization.lower().startswith("bearer "):
|
|
supplied = authorization[7:]
|
|
if not secrets.compare_digest(supplied, token):
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Missing or invalid bearer token",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|