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": {
"summary": "Get Session",
"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": {
"200": {
"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}": {
"patch": {
"post": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch",
"operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
@ -1258,9 +1286,9 @@
}
}
},
"get": {
"patch": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch",
"operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
@ -1295,42 +1323,7 @@
},
"delete": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch",
"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",
"operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
@ -1365,42 +1358,7 @@
},
"head": {
"summary": "Proxy Static",
"operationId": "proxy_static_ai_voicebot__path__patch",
"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",
"operationId": "proxy_static_ai_voicebot__path__post",
"parameters": [
{
"name": "path",
@ -1435,7 +1393,77 @@
},
"put": {
"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": [
{
"name": "path",
@ -2134,7 +2162,7 @@
"properties": {
"providers": {
"items": {
"$ref": "#/components/schemas/BotProviderModel"
"$ref": "#/components/schemas/BotProviderPublicModel"
},
"type": "array",
"title": "Providers"
@ -2147,16 +2175,12 @@
"title": "BotProviderListResponse",
"description": "Response listing all registered bot providers"
},
"BotProviderModel": {
"BotProviderPublicModel": {
"properties": {
"provider_id": {
"type": "string",
"title": "Provider Id"
},
"base_url": {
"type": "string",
"title": "Base Url"
},
"name": {
"type": "string",
"title": "Name"
@ -2166,10 +2190,6 @@
"title": "Description",
"default": ""
},
"provider_key": {
"type": "string",
"title": "Provider Key"
},
"registered_at": {
"type": "number",
"title": "Registered At"
@ -2182,14 +2202,12 @@
"type": "object",
"required": [
"provider_id",
"base_url",
"name",
"provider_key",
"registered_at",
"last_seen"
],
"title": "BotProviderModel",
"description": "Bot provider registration information"
"title": "BotProviderPublicModel",
"description": "Public bot provider information (safe for frontend)"
},
"BotProviderRegisterRequest": {
"properties": {
@ -2209,13 +2227,18 @@
"provider_key": {
"type": "string",
"title": "Provider Key"
},
"provider_id": {
"type": "string",
"title": "Provider Id"
}
},
"type": "object",
"required": [
"base_url",
"name",
"provider_key"
"provider_key",
"provider_id"
],
"title": "BotProviderRegisterRequest",
"description": "Request to register a bot provider"

View File

@ -27,7 +27,7 @@ import {
Refresh as RefreshIcon,
Settings as SettingsIcon,
} 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 BotConfigComponent from "./BotConfig";
@ -40,7 +40,7 @@ interface BotManagerProps {
const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
const [bots, setBots] = useState<BotInfoModel[]>([]);
const [providers, setProviders] = useState<Record<string, string>>({});
const [botProviders, setBotProviders] = useState<BotProviderModel[]>([]);
const [botProviders, setBotProviders] = useState<BotProviderPublicModel[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [addDialogOpen, setAddDialogOpen] = useState(false);
@ -77,7 +77,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
setAddingBot(true);
setError(""); // Clear any previous errors
try {
const request: BotJoinLobbyRequest = {
lobby_id: lobbyId,
@ -88,21 +88,21 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
// Retry logic for handling service restart scenarios
let retries = 3;
let response;
while (retries > 0) {
try {
response = await botsApi.requestJoinLobby(selectedBot, request);
break; // Success, exit retry loop
} catch (err: any) {
retries--;
// If it's a 404 error and we have retries left, wait and retry
if (err?.status === 404 && retries > 0) {
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;
}
// If it's not a 404 or we're out of retries, throw the error
throw err;
}
@ -272,9 +272,6 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
<Typography variant="body2" component="div">
{provider.description || "No description"}
</Typography>
<Typography variant="caption" color="text.secondary">
{provider.base_url}
</Typography>
</Box>
}
secondaryTypographyProps={{ component: "div" }}

View File

@ -28,7 +28,7 @@ export type BotLeaveLobbyResponse = components["schemas"]["BotLeaveLobbyResponse
export type BotListResponse = components["schemas"]["BotListResponse"];
export type BotLobbyConfig = components["schemas"]["BotLobbyConfig"];
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 BotProviderRegisterResponse = components["schemas"]["BotProviderRegisterResponse"];
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" });
}
async updateProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" });
async createProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" });
}
async getProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" });
async updateProxyStatic(path: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" });
}
async deleteProxyStatic(path: string): Promise<any> {
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> {
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> {
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

View File

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

View File

@ -19,6 +19,7 @@ sys.path.append(
)
from shared.models import (
BotProviderModel,
BotProviderPublicModel,
BotProviderRegisterRequest,
BotProviderRegisterResponse,
BotProviderListResponse,
@ -190,23 +191,20 @@ class BotManager:
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.")
# Check if there's already an active provider with this key and remove it
providers_to_remove: List[str] = []
with self.lock:
for existing_provider_id, existing_provider in self.bot_providers.items():
if existing_provider.provider_key == request.provider_key:
providers_to_remove.append(existing_provider_id)
logger.info(f"Removing stale bot provider: {existing_provider.name} (ID: {existing_provider_id})")
# Require provider_id for static registration
if not request.provider_id:
logger.warning("Rejected bot provider registration without provider_id")
raise ValueError("provider_id is required for bot provider registration.")
# 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 = request.provider_id
provider_id = str(uuid.uuid4())
# Remove any existing provider with the same provider_id (this handles restarts/re-registration)
with self.lock:
if provider_id in self.bot_providers:
logger.info(
f"Removing existing provider with ID: {provider_id} for re-registration"
)
del self.bot_providers[provider_id]
now = time.time()
provider = BotProviderModel(
@ -232,9 +230,21 @@ class BotManager:
return BotProviderRegisterResponse(provider_id=provider_id)
def list_providers(self) -> BotProviderListResponse:
"""List all registered bot providers"""
"""List all registered bot providers (public information only)"""
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:
"""List all available bots from all registered providers"""

View File

@ -447,6 +447,7 @@ class BotProviderRegisterRequest(BaseModel):
name: str
description: str = ""
provider_key: str
provider_id: str # Required static provider ID
class BotProviderRegisterResponse(BaseModel):
@ -456,10 +457,20 @@ class BotProviderRegisterResponse(BaseModel):
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):
"""Response listing all registered bot providers"""
providers: List[BotProviderModel]
providers: List[BotProviderPublicModel]
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
# Get provider key from environment variable
provider_key = os.getenv('VOICEBOT_PROVIDER_KEY', 'default-voicebot')
# Get provider key and ID from environment variables
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 = {
"base_url": voicebot_url.rstrip('/'),
"name": "voicebot-provider",
"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}")
# Prepare SSL context if needed

View File

@ -265,6 +265,7 @@ async def handle_track_received(peer: Peer, track: MediaStreamTrack):
# Receive audio frame
frame = await track.recv()
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
audio_data = frame.to_ndarray()
@ -292,6 +293,8 @@ async def handle_track_received(peer: Peer, track: MediaStreamTrack):
# Send to audio processor
if _audio_processor:
_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:
logger.error(f"Error processing audio track from {peer.peer_name}: {e}", exc_info=True)