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:
21
config.py
21
config.py
@@ -4,7 +4,7 @@ Hold Slayer Gateway — Configuration
|
||||
All settings loaded from environment variables / .env file.
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, SecretStr
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class SIPTrunkSettings(BaseSettings):
|
||||
host: str = "sip.provider.com"
|
||||
port: int = 5060
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
password: SecretStr = SecretStr("")
|
||||
transport: str = "udp" # udp, tcp, tls
|
||||
did: str = "" # Your phone number (E.164)
|
||||
|
||||
@@ -58,7 +58,7 @@ class LLMSettings(BaseSettings):
|
||||
|
||||
base_url: str = "http://localhost:11434/v1"
|
||||
model: str = "llama3"
|
||||
api_key: str = "not-needed"
|
||||
api_key: SecretStr = SecretStr("not-needed")
|
||||
timeout: float = 30.0
|
||||
max_tokens: int = 1024
|
||||
temperature: float = 0.3
|
||||
@@ -84,7 +84,7 @@ class TTSSettings(BaseSettings):
|
||||
base_url: str = "http://localhost:8000"
|
||||
model: str = "speaches-ai/Kokoro-82M-v1.0-ONNX"
|
||||
voice: str = "af_heart"
|
||||
api_key: str = ""
|
||||
api_key: SecretStr = SecretStr("")
|
||||
timeout: float = 30.0
|
||||
sample_rate: int = 16000
|
||||
|
||||
@@ -117,15 +117,22 @@ class Settings(BaseSettings):
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
# Database
|
||||
database_url: str = "postgresql+asyncpg://holdslayer:changeme@localhost:5432/holdslayer"
|
||||
# Database — no default credentials; must be set in the environment
|
||||
database_url: str = ""
|
||||
|
||||
# Server
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
debug: bool = True
|
||||
debug: bool = False
|
||||
log_level: str = "info"
|
||||
|
||||
# Auth — one static bearer token shared by REST, WebSocket, and MCP.
|
||||
# Empty disables auth, which is only permitted on loopback binds.
|
||||
api_token: SecretStr = SecretStr("")
|
||||
|
||||
# Outbound-call safety cap (REST + MCP make_call)
|
||||
max_concurrent_calls: int = 4
|
||||
|
||||
# Notifications
|
||||
notify_sms_number: str = ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user