Add client service metadata sanitization and debug source check

This commit is contained in:
Xin Wang
2026-02-26 10:24:55 +08:00
parent f77f7c7531
commit 443f2d1541
2 changed files with 72 additions and 7 deletions

View File

@@ -71,6 +71,32 @@ class Session:
"configVersionId", "configVersionId",
"config_version_id", "config_version_id",
} }
_CLIENT_SERVICE_OVERRIDES = {
"llm": {
"provider",
"model",
"apiKey",
"baseUrl",
},
"asr": {
"provider",
"model",
"apiKey",
"baseUrl",
"interimIntervalMs",
"minAudioMs",
},
"tts": {
"enabled",
"provider",
"model",
"apiKey",
"baseUrl",
"voice",
"speed",
"mode",
},
}
def __init__( def __init__(
self, self,
@@ -853,13 +879,52 @@ class Session:
def _sanitize_client_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]: def _sanitize_client_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""Apply client metadata whitelist and remove forbidden secrets.""" """Apply client metadata whitelist and remove forbidden secrets."""
sanitized = self._sanitize_untrusted_runtime_metadata(metadata) sanitized = self._sanitize_untrusted_runtime_metadata(metadata)
if isinstance(metadata.get("services"), dict): services = metadata.get("services")
if isinstance(services, dict):
if self._is_debug_metadata_source(metadata):
sanitized_services = self._sanitize_client_services(services)
if sanitized_services:
sanitized["services"] = sanitized_services
else:
logger.warning( logger.warning(
"Session {} provided metadata.services from client; client-side service config is ignored", "Session {} provided metadata.services from client; client-side service config is ignored",
self.id, self.id,
) )
return sanitized return sanitized
@staticmethod
def _is_debug_metadata_source(metadata: Dict[str, Any]) -> bool:
source = str(metadata.get("source") or "").strip().lower()
if source == "debug":
return True
history = metadata.get("history")
if isinstance(history, dict):
history_source = str(history.get("source") or "").strip().lower()
if history_source == "debug":
return True
return False
@classmethod
def _sanitize_client_services(cls, services: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(services, dict):
return {}
sanitized_services: Dict[str, Any] = {}
for service_name, allowed_keys in cls._CLIENT_SERVICE_OVERRIDES.items():
payload = services.get(service_name)
if not isinstance(payload, dict):
continue
sanitized_payload = {
key: payload[key]
for key in allowed_keys
if key in payload
}
if sanitized_payload:
sanitized_services[service_name] = sanitized_payload
return sanitized_services
def _build_config_resolved(self, metadata: Dict[str, Any]) -> Dict[str, Any]: def _build_config_resolved(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""Build public resolved config payload (secrets removed).""" """Build public resolved config payload (secrets removed)."""
system_prompt = str(metadata.get("systemPrompt") or self.pipeline.conversation.system_prompt or "") system_prompt = str(metadata.get("systemPrompt") or self.pipeline.conversation.system_prompt or "")

View File

@@ -2296,10 +2296,10 @@ export const DebugDrawer: React.FC<{
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
// If core TTS-related settings changed while drawer stays open, // If core runtime settings changed while drawer stays open,
// reset the active WS session so the next launch uses new metadata. // reset the active WS session so the next launch uses new metadata.
closeWs(); closeWs();
}, [isOpen, assistant.id, assistant.voice, assistant.speed]); }, [isOpen, assistant.id, assistant.voice, assistant.speed, assistant.asrModelId]);
useEffect(() => { useEffect(() => {
if (!textTtsEnabled) { if (!textTtsEnabled) {