diff --git a/CHANGELOG.md b/CHANGELOG.md index b9104467e1d..dc34e8ec9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai - Browser/Extension relay CORS: handle `/json*` `OPTIONS` preflight before auth checks, allow Chrome extension origins, and return extension-origin CORS headers on relay HTTP responses so extension token validation no longer fails cross-origin. Landed from contributor PR #23962 by @miloudbelarebia. (#23842) - Browser/Extension relay auth: allow `?token=` query-param auth on relay `/json*` endpoints (consistent with relay WebSocket auth) so curl/devtools-style `/json/version` and `/json/list` probes work without requiring custom headers. Landed from contributor PR #26015 by @Sid-Qin. (#25928) - Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay `stop()` before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng. +- Browser/Chrome extension handshake: bind relay WS message handling before `onopen` and add non-blocking `connect.challenge` response handling for gateway-style handshake frames, avoiding stuck `…` badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553) - Auth/Auth profiles: normalize `auth-profiles.json` alias fields (`mode -> type`, `apiKey -> key`) before credential validation so entries copied from `openclaw.json` auth examples are no longer silently dropped. (#26950) thanks @byungsker. - Cron/Hooks isolated routing: preserve canonical `agent:*` session keys in isolated runs so already-qualified keys are not double-prefixed (for example `agent:main:main` no longer becomes `agent:main:agent:main:main`). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282) - iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman. diff --git a/assets/chrome-extension/background.js b/assets/chrome-extension/background.js index 60f50d6551e..c78f2c7c452 100644 --- a/assets/chrome-extension/background.js +++ b/assets/chrome-extension/background.js @@ -13,6 +13,9 @@ const BADGE = { let relayWs = null /** @type {Promise|null} */ let relayConnectPromise = null +let relayGatewayToken = '' +/** @type {string|null} */ +let relayConnectRequestId = null let nextSession = 1 @@ -143,6 +146,13 @@ async function ensureRelayConnection() { const ws = new WebSocket(wsUrl) relayWs = ws + relayGatewayToken = gatewayToken + // Bind message handler before open so an immediate first frame (for example + // gateway connect.challenge) cannot be missed. + ws.onmessage = (event) => { + if (ws !== relayWs) return + void whenReady(() => onRelayMessage(String(event.data || ''))) + } await new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error('WebSocket connect timeout')), 5000) @@ -162,10 +172,6 @@ async function ensureRelayConnection() { // Bind permanent handlers. Guard against stale socket: if this WS was // replaced before its close fires, the handler is a no-op. - ws.onmessage = (event) => { - if (ws !== relayWs) return - void whenReady(() => onRelayMessage(String(event.data || ''))) - } ws.onclose = () => { if (ws !== relayWs) return onRelayClosed('closed') @@ -188,6 +194,8 @@ async function ensureRelayConnection() { // Debugger sessions are kept alive so they survive transient WS drops. function onRelayClosed(reason) { relayWs = null + relayGatewayToken = '' + relayConnectRequestId = null for (const [id, p] of pending.entries()) { pending.delete(id) @@ -308,6 +316,33 @@ function sendToRelay(payload) { ws.send(JSON.stringify(payload)) } +function ensureGatewayHandshakeStarted(payload) { + if (relayConnectRequestId) return + const nonce = typeof payload?.nonce === 'string' ? payload.nonce.trim() : '' + relayConnectRequestId = `ext-connect-${Date.now()}-${Math.random().toString(16).slice(2, 8)}` + sendToRelay({ + type: 'req', + id: relayConnectRequestId, + method: 'connect', + params: { + minProtocol: 3, + maxProtocol: 3, + client: { + id: 'chrome-relay-extension', + version: '1.0.0', + platform: 'chrome-extension', + mode: 'webchat', + }, + role: 'operator', + scopes: ['operator.read', 'operator.write'], + caps: [], + commands: [], + nonce: nonce || undefined, + auth: relayGatewayToken ? { token: relayGatewayToken } : undefined, + }, + }) +} + async function maybeOpenHelpOnce() { try { const stored = await chrome.storage.local.get(['helpOnErrorShown']) @@ -349,6 +384,33 @@ async function onRelayMessage(text) { return } + if (msg && msg.type === 'event' && msg.event === 'connect.challenge') { + try { + ensureGatewayHandshakeStarted(msg.payload) + } catch (err) { + console.warn('gateway connect handshake start failed', err instanceof Error ? err.message : String(err)) + relayConnectRequestId = null + const ws = relayWs + if (ws && ws.readyState === WebSocket.OPEN) { + ws.close(1008, 'gateway connect failed') + } + } + return + } + + if (msg && msg.type === 'res' && relayConnectRequestId && msg.id === relayConnectRequestId) { + relayConnectRequestId = null + if (!msg.ok) { + const detail = msg?.error?.message || msg?.error || 'gateway connect failed' + console.warn('gateway connect handshake rejected', String(detail)) + const ws = relayWs + if (ws && ws.readyState === WebSocket.OPEN) { + ws.close(1008, 'gateway connect failed') + } + } + return + } + if (msg && msg.method === 'ping') { try { sendToRelay({ method: 'pong' })