Locked down bot provider secrets

This commit is contained in:
James Ketr 2025-09-05 13:06:10 -07:00
parent 64c5d8c590
commit af5e45fb38
8 changed files with 213 additions and 152 deletions

View File

@ -213,6 +213,24 @@
"get": { "get": {
"summary": "Get Session", "summary": "Get Session",
"operationId": "get_session_ai_voicebot_api_session_get", "operationId": "get_session_ai_voicebot_api_session_get",
"parameters": [
{
"name": "session_id",
"in": "cookie",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Session Id"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "Successful Response", "description": "Successful Response",
@ -223,6 +241,16 @@
} }
} }
} }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
} }
} }
} }
@ -1223,9 +1251,9 @@
} }
}, },
"/ai-voicebot/{path}": { "/ai-voicebot/{path}": {
"patch": { "post": {
"summary": "Proxy Static", "summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch", "operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [ "parameters": [
{ {
"name": "path", "name": "path",
@ -1258,9 +1286,9 @@
} }
} }
}, },
"get": { "patch": {
"summary": "Proxy Static", "summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch", "operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [ "parameters": [
{ {
"name": "path", "name": "path",
@ -1295,42 +1323,7 @@
}, },
"delete": { "delete": {
"summary": "Proxy Static", "summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch", "operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Path"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"options": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch",
"parameters": [ "parameters": [
{ {
"name": "path", "name": "path",
@ -1365,42 +1358,7 @@
}, },
"head": { "head": {
"summary": "Proxy Static", "summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch", "operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Path"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch",
"parameters": [ "parameters": [
{ {
"name": "path", "name": "path",
@ -1435,7 +1393,77 @@
}, },
"put": { "put": {
"summary": "Proxy Static", "summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch", "operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Path"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"options": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Path"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"get": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [ "parameters": [
{ {
"name": "path", "name": "path",
@ -2134,7 +2162,7 @@
"properties": { "properties": {
"providers": { "providers": {
"items": { "items": {
"$ref": "#/components/schemas/BotProviderModel" "$ref": "#/components/schemas/BotProviderPublicModel"
}, },
"type": "array", "type": "array",
"title": "Providers" "title": "Providers"
@ -2147,16 +2175,12 @@
"title": "BotProviderListResponse", "title": "BotProviderListResponse",
"description": "Response listing all registered bot providers" "description": "Response listing all registered bot providers"
}, },
"BotProviderModel": { "BotProviderPublicModel": {
"properties": { "properties": {
"provider_id": { "provider_id": {
"type": "string", "type": "string",
"title": "Provider Id" "title": "Provider Id"
}, },
"base_url": {
"type": "string",
"title": "Base Url"
},
"name": { "name": {
"type": "string", "type": "string",
"title": "Name" "title": "Name"
@ -2166,10 +2190,6 @@
"title": "Description", "title": "Description",
"default": "" "default": ""
}, },
"provider_key": {
"type": "string",
"title": "Provider Key"
},
"registered_at": { "registered_at": {
"type": "number", "type": "number",
"title": "Registered At" "title": "Registered At"
@ -2182,14 +2202,12 @@
"type": "object", "type": "object",
"required": [ "required": [
"provider_id", "provider_id",
"base_url",
"name", "name",
"provider_key",
"registered_at", "registered_at",
"last_seen" "last_seen"
], ],
"title": "BotProviderModel", "title": "BotProviderPublicModel",
"description": "Bot provider registration information" "description": "Public bot provider information (safe for frontend)"
}, },
"BotProviderRegisterRequest": { "BotProviderRegisterRequest": {
"properties": { "properties": {
@ -2209,13 +2227,18 @@
"provider_key": { "provider_key": {
"type": "string", "type": "string",
"title": "Provider Key" "title": "Provider Key"
},
"provider_id": {
"type": "string",
"title": "Provider Id"
} }
}, },
"type": "object", "type": "object",
"required": [ "required": [
"base_url", "base_url",
"name", "name",
"provider_key" "provider_key",
"provider_id"
], ],
"title": "BotProviderRegisterRequest", "title": "BotProviderRegisterRequest",
"description": "Request to register a bot provider" "description": "Request to register a bot provider"

View File

@ -27,7 +27,7 @@ import {
Refresh as RefreshIcon, Refresh as RefreshIcon,
Settings as SettingsIcon, Settings as SettingsIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { botsApi, BotInfoModel, BotProviderModel, BotJoinLobbyRequest } from "./api-client"; import { botsApi, BotInfoModel, BotProviderPublicModel, BotJoinLobbyRequest } from "./api-client";
import BotConfig from "./BotConfig"; import BotConfig from "./BotConfig";
import BotConfigComponent from "./BotConfig"; import BotConfigComponent from "./BotConfig";
@ -40,7 +40,7 @@ interface BotManagerProps {
const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => { const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
const [bots, setBots] = useState<BotInfoModel[]>([]); const [bots, setBots] = useState<BotInfoModel[]>([]);
const [providers, setProviders] = useState<Record<string, string>>({}); const [providers, setProviders] = useState<Record<string, string>>({});
const [botProviders, setBotProviders] = useState<BotProviderModel[]>([]); const [botProviders, setBotProviders] = useState<BotProviderPublicModel[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
@ -99,7 +99,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
// If it's a 404 error and we have retries left, wait and retry // If it's a 404 error and we have retries left, wait and retry
if (err?.status === 404 && retries > 0) { if (err?.status === 404 && retries > 0) {
console.log(`Bot join failed with 404, retrying... (${retries} attempts left)`); console.log(`Bot join failed with 404, retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
continue; continue;
} }
@ -272,9 +272,6 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
<Typography variant="body2" component="div"> <Typography variant="body2" component="div">
{provider.description || "No description"} {provider.description || "No description"}
</Typography> </Typography>
<Typography variant="caption" color="text.secondary">
{provider.base_url}
</Typography>
</Box> </Box>
} }
secondaryTypographyProps={{ component: "div" }} secondaryTypographyProps={{ component: "div" }}

View File

@ -28,7 +28,7 @@ export type BotLeaveLobbyResponse = components["schemas"]["BotLeaveLobbyResponse
export type BotListResponse = components["schemas"]["BotListResponse"]; export type BotListResponse = components["schemas"]["BotListResponse"];
export type BotLobbyConfig = components["schemas"]["BotLobbyConfig"]; export type BotLobbyConfig = components["schemas"]["BotLobbyConfig"];
export type BotProviderListResponse = components["schemas"]["BotProviderListResponse"]; export type BotProviderListResponse = components["schemas"]["BotProviderListResponse"];
export type BotProviderModel = components["schemas"]["BotProviderModel"]; export type BotProviderPublicModel = components["schemas"]["BotProviderPublicModel"];
export type BotProviderRegisterRequest = components["schemas"]["BotProviderRegisterRequest"]; export type BotProviderRegisterRequest = components["schemas"]["BotProviderRegisterRequest"];
export type BotProviderRegisterResponse = components["schemas"]["BotProviderRegisterResponse"]; export type BotProviderRegisterResponse = components["schemas"]["BotProviderRegisterResponse"];
export type ChatMessageModel = components["schemas"]["ChatMessageModel"]; export type ChatMessageModel = components["schemas"]["ChatMessageModel"];
@ -277,33 +277,33 @@ export class ApiClient {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics/export`), { method: "GET" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics/export`), { method: "GET" });
} }
async updateProxyStatic(path: string): Promise<any> { async createProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" }); return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" });
} }
async getProxyStatic(path: string): Promise<any> { async updateProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" }); return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" });
} }
async deleteProxyStatic(path: string): Promise<any> { async deleteProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "DELETE" }); return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "DELETE" });
} }
async optionsProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "OPTIONS" });
}
async headProxyStatic(path: string): Promise<any> { async headProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "HEAD" }); return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "HEAD" });
} }
async createProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" });
}
async replaceProxyStatic(path: string): Promise<any> { async replaceProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PUT" }); return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PUT" });
} }
async optionsProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "OPTIONS" });
}
async getProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" });
}
} }
// Default client instance // Default client instance

View File

@ -263,19 +263,19 @@ export interface paths {
}; };
"/ai-voicebot/{path}": { "/ai-voicebot/{path}": {
/** Proxy Static */ /** Proxy Static */
get: operations["proxy_static_ai_voicebot__path__patch"]; get: operations["proxy_static_ai_voicebot__path__post"];
/** Proxy Static */ /** Proxy Static */
put: operations["proxy_static_ai_voicebot__path__patch"]; put: operations["proxy_static_ai_voicebot__path__post"];
/** Proxy Static */ /** Proxy Static */
post: operations["proxy_static_ai_voicebot__path__patch"]; post: operations["proxy_static_ai_voicebot__path__post"];
/** Proxy Static */ /** Proxy Static */
delete: operations["proxy_static_ai_voicebot__path__patch"]; delete: operations["proxy_static_ai_voicebot__path__post"];
/** Proxy Static */ /** Proxy Static */
options: operations["proxy_static_ai_voicebot__path__patch"]; options: operations["proxy_static_ai_voicebot__path__post"];
/** Proxy Static */ /** Proxy Static */
head: operations["proxy_static_ai_voicebot__path__patch"]; head: operations["proxy_static_ai_voicebot__path__post"];
/** Proxy Static */ /** Proxy Static */
patch: operations["proxy_static_ai_voicebot__path__patch"]; patch: operations["proxy_static_ai_voicebot__path__post"];
}; };
} }
@ -587,17 +587,15 @@ export interface components {
*/ */
BotProviderListResponse: { BotProviderListResponse: {
/** Providers */ /** Providers */
providers: components["schemas"]["BotProviderModel"][]; providers: components["schemas"]["BotProviderPublicModel"][];
}; };
/** /**
* BotProviderModel * BotProviderPublicModel
* @description Bot provider registration information * @description Public bot provider information (safe for frontend)
*/ */
BotProviderModel: { BotProviderPublicModel: {
/** Provider Id */ /** Provider Id */
provider_id: string; provider_id: string;
/** Base Url */
base_url: string;
/** Name */ /** Name */
name: string; name: string;
/** /**
@ -605,8 +603,6 @@ export interface components {
* @default * @default
*/ */
description?: string; description?: string;
/** Provider Key */
provider_key: string;
/** Registered At */ /** Registered At */
registered_at: number; registered_at: number;
/** Last Seen */ /** Last Seen */
@ -628,6 +624,8 @@ export interface components {
description?: string; description?: string;
/** Provider Key */ /** Provider Key */
provider_key: string; provider_key: string;
/** Provider Id */
provider_id: string;
}; };
/** /**
* BotProviderRegisterResponse * BotProviderRegisterResponse
@ -944,6 +942,11 @@ export interface operations {
}; };
/** Get Session */ /** Get Session */
get_session_ai_voicebot_api_session_get: { get_session_ai_voicebot_api_session_get: {
parameters: {
cookie?: {
session_id?: string | null;
};
};
responses: { responses: {
/** @description Successful Response */ /** @description Successful Response */
200: { 200: {
@ -951,6 +954,12 @@ export interface operations {
"application/json": components["schemas"]["SessionResponse"]; "application/json": components["schemas"]["SessionResponse"];
}; };
}; };
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
}; };
}; };
/** List Lobbies */ /** List Lobbies */
@ -1568,7 +1577,7 @@ export interface operations {
}; };
}; };
/** Proxy Static */ /** Proxy Static */
proxy_static_ai_voicebot__path__patch: { proxy_static_ai_voicebot__path__post: {
parameters: { parameters: {
path: { path: {
path: string; path: string;

View File

@ -19,6 +19,7 @@ sys.path.append(
) )
from shared.models import ( from shared.models import (
BotProviderModel, BotProviderModel,
BotProviderPublicModel,
BotProviderRegisterRequest, BotProviderRegisterRequest,
BotProviderRegisterResponse, BotProviderRegisterResponse,
BotProviderListResponse, BotProviderListResponse,
@ -190,23 +191,20 @@ class BotManager:
logger.warning(f"Rejected bot provider registration with invalid key: {request.provider_key}") logger.warning(f"Rejected bot provider registration with invalid key: {request.provider_key}")
raise ValueError("Invalid provider key. Bot provider is not authorized to register.") raise ValueError("Invalid provider key. Bot provider is not authorized to register.")
# Check if there's already an active provider with this key and remove it # Require provider_id for static registration
providers_to_remove: List[str] = [] if not request.provider_id:
logger.warning("Rejected bot provider registration without provider_id")
raise ValueError("provider_id is required for bot provider registration.")
provider_id = request.provider_id
# Remove any existing provider with the same provider_id (this handles restarts/re-registration)
with self.lock: with self.lock:
for existing_provider_id, existing_provider in self.bot_providers.items(): if provider_id in self.bot_providers:
if existing_provider.provider_key == request.provider_key: logger.info(
providers_to_remove.append(existing_provider_id) f"Removing existing provider with ID: {provider_id} for re-registration"
logger.info(f"Removing stale bot provider: {existing_provider.name} (ID: {existing_provider_id})") )
del self.bot_providers[provider_id]
# Remove stale providers
for provider_id_to_remove in providers_to_remove:
del self.bot_providers[provider_id_to_remove]
# Save after removing stale providers
if providers_to_remove:
self._save_bot_providers()
provider_id = str(uuid.uuid4())
now = time.time() now = time.time()
provider = BotProviderModel( provider = BotProviderModel(
@ -232,9 +230,21 @@ class BotManager:
return BotProviderRegisterResponse(provider_id=provider_id) return BotProviderRegisterResponse(provider_id=provider_id)
def list_providers(self) -> BotProviderListResponse: def list_providers(self) -> BotProviderListResponse:
"""List all registered bot providers""" """List all registered bot providers (public information only)"""
with self.lock: with self.lock:
return BotProviderListResponse(providers=list(self.bot_providers.values())) # Create safe public versions of provider info
public_providers: List[BotProviderPublicModel] = []
for provider in self.bot_providers.values():
public_provider = BotProviderPublicModel(
provider_id=provider.provider_id,
name=provider.name,
description=provider.description,
registered_at=provider.registered_at,
last_seen=provider.last_seen
)
public_providers.append(public_provider)
return BotProviderListResponse(providers=public_providers)
async def list_bots(self) -> BotListResponse: async def list_bots(self) -> BotListResponse:
"""List all available bots from all registered providers""" """List all available bots from all registered providers"""

View File

@ -447,6 +447,7 @@ class BotProviderRegisterRequest(BaseModel):
name: str name: str
description: str = "" description: str = ""
provider_key: str provider_key: str
provider_id: str # Required static provider ID
class BotProviderRegisterResponse(BaseModel): class BotProviderRegisterResponse(BaseModel):
@ -456,10 +457,20 @@ class BotProviderRegisterResponse(BaseModel):
status: str = "registered" status: str = "registered"
class BotProviderPublicModel(BaseModel):
"""Public bot provider information (safe for frontend)"""
provider_id: str
name: str
description: str = ""
registered_at: float
last_seen: float
class BotProviderListResponse(BaseModel): class BotProviderListResponse(BaseModel):
"""Response listing all registered bot providers""" """Response listing all registered bot providers"""
providers: List[BotProviderModel] providers: List[BotProviderPublicModel]
class BotListResponse(BaseModel): class BotListResponse(BaseModel):

View File

@ -529,16 +529,24 @@ async def register_with_server(server_url: str, voicebot_url: str, insecure: boo
# Import httpx locally to avoid dependency issues # Import httpx locally to avoid dependency issues
import httpx import httpx
# Get provider key from environment variable # Get provider key and ID from environment variables
provider_key = os.getenv('VOICEBOT_PROVIDER_KEY', 'default-voicebot') provider_key = os.getenv('VOICEBOT_PROVIDER_KEY')
provider_id = os.getenv('VOICEBOT_PROVIDER_ID')
if not provider_key:
raise ValueError("VOICEBOT_PROVIDER_KEY environment variable is required")
if not provider_id:
raise ValueError("VOICEBOT_PROVIDER_ID environment variable is required")
payload = { payload = {
"base_url": voicebot_url.rstrip('/'), "base_url": voicebot_url.rstrip('/'),
"name": "voicebot-provider", "name": "voicebot-provider",
"description": "AI voicebot provider with speech recognition and synthetic media capabilities", "description": "AI voicebot provider with speech recognition and synthetic media capabilities",
"provider_key": provider_key "provider_key": provider_key,
"provider_id": provider_id
} }
logger.info(f"📍 Using static provider ID: {provider_id}")
logger.info(f"📤 Sending registration payload: {payload}") logger.info(f"📤 Sending registration payload: {payload}")
# Prepare SSL context if needed # Prepare SSL context if needed

View File

@ -265,6 +265,7 @@ async def handle_track_received(peer: Peer, track: MediaStreamTrack):
# Receive audio frame # Receive audio frame
frame = await track.recv() frame = await track.recv()
if isinstance(frame, AudioFrame): if isinstance(frame, AudioFrame):
logger.info(f"Received audio frame: {frame.sample_rate}Hz, {frame.format.name}, {frame.layout.name}")
# Convert AudioFrame to numpy array # Convert AudioFrame to numpy array
audio_data = frame.to_ndarray() audio_data = frame.to_ndarray()
@ -292,6 +293,8 @@ async def handle_track_received(peer: Peer, track: MediaStreamTrack):
# Send to audio processor # Send to audio processor
if _audio_processor: if _audio_processor:
_audio_processor.add_audio_data(audio_data_float32) _audio_processor.add_audio_data(audio_data_float32)
else:
logger.warning(f"Received non-audio frame on audio track from {peer.peer_name}")
except Exception as e: except Exception as e:
logger.error(f"Error processing audio track from {peer.peer_name}: {e}", exc_info=True) logger.error(f"Error processing audio track from {peer.peer_name}: {e}", exc_info=True)