diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e25911d8fd..50679d405ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Control UI/Talk: fix Talk (OpenAI Realtime WebRTC) CORS failure by stripping server-side-only attribution headers (`originator`, `version`, `User-Agent`) from browser offer headers; `api.openai.com/v1/realtime/calls` only allows `authorization` and `content-type` in its CORS preflight, so forwarding these headers caused the browser SDP exchange to fail. Fixes #76435. Thanks @hclsys. - Plugins/onboarding: trust optional official plugin and web-search installs selected from the official catalog so npm security scanning treats them like other source-linked official install paths. Thanks @vincentkoc. - Microsoft Teams: persist sent-message markers across Gateway restarts so follow-up replies to recent bot messages keep resolving the original conversation instead of dropping out after restart, with marker TTLs preserved on best-effort recovery. (#75585) Thanks @amknight. - Matrix: persist pending approval reaction targets across Gateway restarts so room approvers can still approve or deny outstanding prompts after OpenClaw comes back online. (#75586) Thanks @amknight. diff --git a/extensions/openai/realtime-voice-provider.test.ts b/extensions/openai/realtime-voice-provider.test.ts index 197cfb7e209..b5554b65b05 100644 --- a/extensions/openai/realtime-voice-provider.test.ts +++ b/extensions/openai/realtime-voice-provider.test.ts @@ -192,14 +192,12 @@ describe("buildOpenAIRealtimeVoiceProvider", () => { transport: "webrtc-sdp", clientSecret: "client-secret-123", offerUrl: "https://api.openai.com/v1/realtime/calls", - offerHeaders: { - originator: "openclaw", - version: "2026.3.22", - }, }); - expect((session as { offerHeaders?: Record }).offerHeaders).not.toHaveProperty( - "User-Agent", - ); + // originator, version, and User-Agent are server-side attribution headers; they + // must not be forwarded to the browser so that the browser's direct SDP POST to + // api.openai.com passes the CORS preflight (only authorization,content-type + // allowed — #76435). All three are filtered, leaving no browser offer headers. + expect((session as { offerHeaders?: Record }).offerHeaders).toBeUndefined(); }); it("resolves keychain OPENAI_API_KEY refs before creating browser sessions", async () => { diff --git a/extensions/openai/realtime-voice-provider.ts b/extensions/openai/realtime-voice-provider.ts index b2deeb9e054..c0556eb9594 100644 --- a/extensions/openai/realtime-voice-provider.ts +++ b/extensions/openai/realtime-voice-provider.ts @@ -731,8 +731,12 @@ function resolveOpenAIRealtimeBrowserOfferHeaders(): Record | un transport: "http", defaultHeaders: {}, }); + // Strip server-side-only attribution headers: browser direct fetches to + // api.openai.com fail CORS preflight when these are present (only + // authorization,content-type are allowed by the endpoint's CORS policy). + const SERVER_ONLY_HEADERS = new Set(["user-agent", "originator", "version"]); const browserHeaders = Object.fromEntries( - Object.entries(headers ?? {}).filter(([key]) => key.toLowerCase() !== "user-agent"), + Object.entries(headers ?? {}).filter(([key]) => !SERVER_ONLY_HEADERS.has(key.toLowerCase())), ); return Object.keys(browserHeaders).length > 0 ? browserHeaders : undefined; }