feat: mount MCP server, add bearer auth, and guard outbound calls
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>
This commit is contained in:
@@ -11,7 +11,7 @@ from typing import Optional
|
||||
|
||||
from config import Settings, get_settings
|
||||
from core.call_manager import CallManager
|
||||
from core.dial_plan import next_extension
|
||||
from core.dial_plan import is_emergency_number, next_extension
|
||||
from core.event_bus import EventBus
|
||||
from core.media_pipeline import MediaPipeline
|
||||
from core.sip_engine import MockSIPEngine, SIPEngine
|
||||
@@ -52,7 +52,7 @@ def _build_sip_engine(settings: Settings, gateway: "AIPSTNGateway") -> SIPEngine
|
||||
trunk_host=trunk.host,
|
||||
trunk_port=trunk.port,
|
||||
trunk_username=trunk.username,
|
||||
trunk_password=trunk.password,
|
||||
trunk_password=trunk.password.get_secret_value(),
|
||||
trunk_transport=trunk.transport,
|
||||
domain=gw_sip.domain,
|
||||
did=trunk.did,
|
||||
@@ -228,6 +228,19 @@ class AIPSTNGateway:
|
||||
- hold_slayer: Navigate IVR, wait on hold, transfer when human detected
|
||||
- ai_assisted: Connect with transcription, recording, noise cancel
|
||||
"""
|
||||
if is_emergency_number(number):
|
||||
raise ValueError(
|
||||
f"Refusing to dial emergency number '{number}'. Emergency calls "
|
||||
"must be placed from a phone with E911 location service, not "
|
||||
"through the gateway API."
|
||||
)
|
||||
active = len(self.call_manager.active_calls)
|
||||
if active >= self.settings.max_concurrent_calls:
|
||||
raise ValueError(
|
||||
f"Concurrent-call limit reached ({active}/{self.settings.max_concurrent_calls}). "
|
||||
"End an active call or raise MAX_CONCURRENT_CALLS."
|
||||
)
|
||||
|
||||
# Create call in manager
|
||||
call = await self.call_manager.create_call(
|
||||
remote_number=number,
|
||||
|
||||
Reference in New Issue
Block a user