feat(browser): support direct WebSocket CDP URLs for Browserbase

Browserbase uses direct WebSocket connections (wss://) rather than the
standard HTTP-based /json/version CDP discovery flow used by Browserless.
This change teaches the browser tool to accept ws:// and wss:// URLs as
cdpUrl values: when a WebSocket URL is detected, OpenClaw connects
directly instead of attempting HTTP discovery.

Changes:
- config.ts: accept ws:// and wss:// in cdpUrl validation
- cdp.helpers.ts: add isWebSocketUrl() helper
- cdp.ts: skip /json/version when cdpUrl is already a WebSocket URL
- chrome.ts: probe WSS endpoints via WebSocket handshake instead of HTTP
- cdp.test.ts: add test for direct WebSocket target creation
- docs/tools/browser.md: update Browserbase section with correct URL
  format and notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shrey150
2026-03-02 00:49:55 -08:00
committed by Peter Steinberger
parent 3cf75f760c
commit 75602014db
6 changed files with 106 additions and 17 deletions

View File

@@ -1,8 +1,20 @@
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { appendCdpPath, fetchJson, isLoopbackHost, withCdpSocket } from "./cdp.helpers.js";
import {
appendCdpPath,
fetchJson,
isLoopbackHost,
isWebSocketUrl,
withCdpSocket,
} from "./cdp.helpers.js";
import { assertBrowserNavigationAllowed, withBrowserNavigationPolicy } from "./navigation-guard.js";
export { appendCdpPath, fetchJson, fetchOk, getHeadersWithAuth } from "./cdp.helpers.js";
export {
appendCdpPath,
fetchJson,
fetchOk,
getHeadersWithAuth,
isWebSocketUrl,
} from "./cdp.helpers.js";
export function normalizeCdpWsUrl(wsUrl: string, cdpUrl: string): string {
const ws = new URL(wsUrl);
@@ -94,14 +106,21 @@ export async function createTargetViaCdp(opts: {
...withBrowserNavigationPolicy(opts.ssrfPolicy),
});
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
appendCdpPath(opts.cdpUrl, "/json/version"),
1500,
);
const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim();
const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : "";
if (!wsUrl) {
throw new Error("CDP /json/version missing webSocketDebuggerUrl");
let wsUrl: string;
if (isWebSocketUrl(opts.cdpUrl)) {
// Direct WebSocket URL (e.g. Browserbase) — skip /json/version discovery.
wsUrl = opts.cdpUrl;
} else {
// Standard HTTP(S) CDP endpoint — discover WebSocket URL via /json/version.
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(
appendCdpPath(opts.cdpUrl, "/json/version"),
1500,
);
const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim();
wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : "";
if (!wsUrl) {
throw new Error("CDP /json/version missing webSocketDebuggerUrl");
}
}
return await withCdpSocket(wsUrl, async (send) => {