fix(browser): discover CDP websocket from bare ws:// URL before attach#68715
fix(browser): discover CDP websocket from bare ws:// URL before attach#68715
Conversation
Greptile SummaryThis PR fixes a real user-facing bug (#68027): a The fix is clean: a new Confidence Score: 5/5Safe to merge; the logic change is well-scoped, security invariants are preserved, and only a test-naming issue remains. All remaining findings are P2 (test description clarity / a missing companion test). The core fix is correct, SSRF policy is enforced on every code path, and existing tests are green. No P0/P1 issues found. cdp.test.ts — the 'still enforces SSRF policy for direct WebSocket URLs' test name no longer matches what it exercises.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ca73631a2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (isDirectCdpWebSocketEndpoint(cdpUrl)) { | ||
| // Handshake-ready direct WS endpoint — probe via WS handshake. | ||
| return await canOpenWebSocket(cdpUrl, timeoutMs); |
There was a problem hiding this comment.
Preserve direct handshake for non-/devtools WebSocket CDP URLs
This gate now treats only /devtools/<kind>/<id> URLs as direct CDP sockets, so a configured ws:///wss:// endpoint like wss://connect.browserbase.com?apiKey=... is forced into /json/version discovery instead of being handshaken directly. In environments where the provider exposes only a direct WebSocket endpoint (no /json/version route), isChromeReachable/getChromeWebSocketUrl will return false/null even though the socket is healthy, which blocks attach-only startup and downstream tab operations. Please keep a direct-WS fallback path for ws/wss URLs when discovery is unavailable.
Useful? React with 👍 / 👎.
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 SSRF guard bypass via permissive default policy when ssrfPolicy is omitted for CDP fetches
Description
This enables server-side requests to arbitrary hosts including private/internal networks (RFC1918, link-local such as Key points:
Vulnerable code: const policy = isLoopbackHost(parsedUrl.hostname)
? withAllowedHostname(ssrfPolicy, parsedUrl.hostname)
: (ssrfPolicy ?? { allowPrivateNetwork: true });
const guarded = await fetchWithSsrFGuard({ url, ... , policy });This risk is amplified by the new discovery-first logic that may convert RecommendationDo not default to a permissive policy when Options (choose one consistent with product requirements):
if (!ssrfPolicy && !isLoopbackHost(parsedUrl.hostname)) {
throw new BrowserCdpEndpointBlockedError({
cause: new Error("Missing SSRF policy for non-loopback CDP endpoint")
});
}
const policy = isLoopbackHost(parsedUrl.hostname)
? withAllowedHostname(ssrfPolicy, parsedUrl.hostname)
: ssrfPolicy;
Also ensure call sites consistently pass an SSRF policy when accepting user/config-controlled CDP URLs, especially for the 2. 🟡 CDP URL userinfo credentials preserved during ws→http normalization and passed to HTTP fetch/WebSocket URLs
Description
Impact:
Vulnerable flows:
Vulnerable code: export function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl: string): string {
const url = new URL(cdpUrl);
if (url.protocol === "ws:") url.protocol = "http:";
else if (url.protocol === "wss:") url.protocol = "https:";
// username/password are not cleared here
return url.toString().replace(/\/$/, "");
}
const res = await fetchWithSsrFGuard({
url, // may still include userinfo credentials
init: { ...init, headers },
...
});RecommendationAfter extracting credentials for an Suggested fix: export function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl: string): string {
try {
const url = new URL(cdpUrl);
if (url.protocol === "ws:") url.protocol = "http:";
else if (url.protocol === "wss:") url.protocol = "https:";
// Strip URL-embedded credentials to avoid leaks in logs/audits
url.username = "";
url.password = "";
url.pathname = url.pathname.replace(/\/devtools\/browser\/.*$/, "");
url.pathname = url.pathname.replace(/\/cdp$/, "");
return url.toString().replace(/\/$/, "");
} catch {
// Fallback: best-effort removal of userinfo as well
return cdpUrl
.replace(/^ws:/, "http:")
.replace(/^wss:/, "https:")
.replace(/\/\/[^@/]*@/, "//")
.replace(/\/devtools\/browser\/.*$/, "")
.replace(/\/cdp$/, "")
.replace(/\/$/, "");
}
}Additionally, consider using Analyzed PR: #68715 at commit Last updated on: 2026-04-19T09:46:41Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a779697954
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let wsUrl: string; | ||
| if (isWebSocketUrl(opts.cdpUrl)) { | ||
| // Direct WebSocket URL — skip /json/version discovery. | ||
| if (isDirectCdpWebSocketEndpoint(opts.cdpUrl)) { |
There was a problem hiding this comment.
Preserve direct WS fallback for non-/devtools CDP URLs
This new branch in createTargetViaCdp treats only /devtools/<kind>/<id> as direct WebSocket CDP and forces all other ws:///wss:// inputs through /json/version discovery, which regresses handshake-only providers (for example Browserbase-style wss://...?... endpoints and other non-/devtools WS routes). In those environments, discovery fails and tab creation now fails as well, even though the original WebSocket endpoint is valid; please keep a direct-WS fallback for ws/wss URLs when discovery is unavailable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4edbdcb8cc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return /\/devtools\/(?:browser|page|worker|shared_worker|service_worker)\/[^/]/i.test( | ||
| parsed.pathname, |
There was a problem hiding this comment.
Restore direct-WS fallback outside /devtools paths
isDirectCdpWebSocketEndpoint now rejects every ws/wss URL that is not /devtools/<kind>/<id>, and this predicate is used as the shared gate for isChromeReachable/getChromeWebSocketUrl and createTargetViaCdp. In environments that expose only a direct CDP socket (for example wss://...?... endpoints without /json/version), this forces HTTP discovery that cannot succeed, so attach-only reachability checks and target creation fail even though the WebSocket endpoint itself is healthy. Please keep a direct WebSocket fallback when discovery is unavailable.
Useful? React with 👍 / 👎.
|
Addressed all review feedback: P1 — WS fallback for non- The root issue: bare Fix (commit
This preserves the #68027 fix for Chrome's debug port (discovery normalises P2 — Misleading test name (Greptile) Renamed New tests cover all three fallback paths ( Note: the previous commit in this PR ( |
6af1018 to
0b912f4
Compare
When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as: Browser attachOnly is enabled and profile "openclaw" is not running. even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (#68027). Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip. Fixes #68027
Adds property-based / seeded-fuzz tests for the URL helpers the attachOnly CDP fix depends on (#68027): - isWebSocketUrl - isDirectCdpWebSocketEndpoint - normalizeCdpHttpBaseForJsonEndpoints - parseBrowserHttpUrl - redactCdpUrl - appendCdpPath - getHeadersWithAuth Follows the existing repo convention (see src/gateway/http-common.fuzz.test.ts): no fast-check dep, small mulberry32 PRNG + hand-rolled generators, deterministic per-describe seeds so failures are reproducible. Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements, 67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered lines are inside the WS sender internals (createCdpSender, withCdpSocket, fetchCdpChecked rate-limit branch), which require integration-style mocks and are unrelated to the attachOnly fix.
Lifts the three files touched by the #68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications.
When /json/version discovery is unavailable (or returns no webSocketDebuggerUrl), fall back to treating the original bare ws/wss URL as a direct WebSocket endpoint. This preserves the #68027 fix for Chrome's debug port while restoring compatibility with Browserless/ Browserbase-style providers that expose a direct WebSocket root without a /json/version endpoint. Priority order for bare ws/wss cdpUrl inputs: 1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged) 2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint 3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged) Affected call sites: isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp. Also renames a misleading test ('still enforces SSRF policy for direct WebSocket URLs') to accurately describe what it tests: SSRF enforcement on the navigation target URL, not on the CDP endpoint. New tests added for all three fallback paths. Coverage remains 100% on all three touched files (238 tests).
0b912f4 to
bb2fbce
Compare
|
Landed via temp rebase onto main.
LMK if you still have any issues after the next release. Thank you... @visionik |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb2fbce14e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (wsUrlRaw) { | ||
| wsUrl = normalizeCdpWsUrl(wsUrlRaw, discoveryUrl); |
There was a problem hiding this comment.
Retry direct WS when discovered debugger URL is unusable
For non-/devtools/* ws:///wss:// inputs, this branch now unconditionally prefers webSocketDebuggerUrl when /json/version returns any non-empty value, and only falls back to opts.cdpUrl when the field is missing. That regresses providers where discovery returns an internal/stale URL (for example a container-internal host) while the original direct WebSocket endpoint is the routable one: createTargetViaCdp will fail even though the supplied cdpUrl is healthy. Fresh evidence in this commit is that fallback is now gated solely on !wsUrlRaw, so bad-but-present discovery output no longer has a direct-WS recovery path.
Useful? React with 👍 / 👎.
* test: stabilize standalone Parallels smoke lanes * fix: strip orphaned OpenAI reasoning blocks before responses API call (openclaw#55787) Merged via squash. Prepared head SHA: 263b952 Co-authored-by: suboss87 <11032439+suboss87@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman * fix: stabilize release smoke reruns * test: tolerate empty fireworks live responses * test: make OpenWebUI smoke deterministic * tasks: extract detached task lifecycle runtime (openclaw#68886) * tasks: extract detached task lifecycle runtime * tests: relax gateway seam expectation --------- Co-authored-by: Mariano Belinky <mariano@mb-server-643.local> * test: complete workspace setup in update smokes * fix: parse PowerShell cron tools allow-list (openclaw#68858) (thanks @chen-zhang-cs-code) * fix(cron): parse PowerShell tools allow list * fix(cron): clarify tools allow-list help * fix: parse PowerShell cron tools allow-list (openclaw#68858) (thanks @chen-zhang-cs-code) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us> * fix(browser): discover CDP websocket from bare ws:// URL before attach (openclaw#68715) * fix(browser): discover CDP websocket from bare ws:// URL before attach When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as: Browser attachOnly is enabled and profile "openclaw" is not running. even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (openclaw#68027). Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip. Fixes openclaw#68027 * test(browser): add seeded fuzz coverage for CDP URL helpers Adds property-based / seeded-fuzz tests for the URL helpers the attachOnly CDP fix depends on (openclaw#68027): - isWebSocketUrl - isDirectCdpWebSocketEndpoint - normalizeCdpHttpBaseForJsonEndpoints - parseBrowserHttpUrl - redactCdpUrl - appendCdpPath - getHeadersWithAuth Follows the existing repo convention (see src/gateway/http-common.fuzz.test.ts): no fast-check dep, small mulberry32 PRNG + hand-rolled generators, deterministic per-describe seeds so failures are reproducible. Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements, 67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered lines are inside the WS sender internals (createCdpSender, withCdpSocket, fetchCdpChecked rate-limit branch), which require integration-style mocks and are unrelated to the attachOnly fix. * test(browser): drive cdp.helpers/cdp/chrome to 100% coverage Lifts the three files touched by the openclaw#68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications. * fix(browser): restore WS fallback for non-/devtools ws:// CDP URLs When /json/version discovery is unavailable (or returns no webSocketDebuggerUrl), fall back to treating the original bare ws/wss URL as a direct WebSocket endpoint. This preserves the openclaw#68027 fix for Chrome's debug port while restoring compatibility with Browserless/ Browserbase-style providers that expose a direct WebSocket root without a /json/version endpoint. Priority order for bare ws/wss cdpUrl inputs: 1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged) 2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint 3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged) Affected call sites: isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp. Also renames a misleading test ('still enforces SSRF policy for direct WebSocket URLs') to accurately describe what it tests: SSRF enforcement on the navigation target URL, not on the CDP endpoint. New tests added for all three fallback paths. Coverage remains 100% on all three touched files (238 tests). * fix: browser attachOnly bare ws CDP follow-ups (openclaw#68715) (thanks @visionik) * test(tasks): align detached runtime mock return types * browser: route existing-session user profile through browser nodes (openclaw#68891) * browser: route user profile through browser nodes * browser: align existing-session node docs * browser: preserve host fallback on node discovery errors * browser: preserve configured node pin errors * browser: widen config mock in node pin test * fix: default kimi thinking to off (openclaw#68907) Co-authored-by: termtek <termtek@ubuntu.tail2b72cd.ts.net> * docs(changelog): note kimi thinking default fix * docs(changelog): add thanks for kimi fix * tasks: add detached runtime plugin registration contract (openclaw#68915) * tasks: register detached runtime plugins * tasks: harden detached runtime ownership * tasks: extract detached runtime contract types * changelog: note detached runtime contract * changelog: attribute detached runtime contract * feat(ui): add a2a operator dashboard shell * fix(ui): align a2a baseline with current read contract * feat(ui): add A2A case inspector panels * CI: route small workflows to ubuntu-latest * CI: route fork-blocked workflows to GitHub-hosted runners * CI: fall back to GITHUB_TOKEN for fork automation * CI: skip app-token steps when fork secrets are absent * CI: gate fork app-token steps through env guards * CI: fix stale fallback env guard * fix(ui): sync A2A locale snapshots * fix(ui): sync A2A locale metadata --------- Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Subash Natarajan <suboss87@gmail.com> Co-authored-by: suboss87 <11032439+suboss87@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Co-authored-by: Mariano <132747814+mbelinky@users.noreply.github.com> Co-authored-by: Mariano Belinky <mariano@mb-server-643.local> Co-authored-by: ZC <chenzhangcode@163.com> Co-authored-by: Ayaan Zaidi <hi@obviy.us> Co-authored-by: Viz <visionik@pobox.com> Co-authored-by: Frank Yang <frank.ekn@gmail.com> Co-authored-by: termtek <termtek@ubuntu.tail2b72cd.ts.net> Co-authored-by: bangtong-ai <bangtong-ai@users.noreply.github.com> Co-authored-by: OpenClaw Agent <openclaw-agent@users.noreply.github.com> Co-authored-by: OpenClaw Bot <bot@openclaw.local>
openclaw#68715) * fix(browser): discover CDP websocket from bare ws:// URL before attach When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as: Browser attachOnly is enabled and profile "openclaw" is not running. even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (openclaw#68027). Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip. Fixes openclaw#68027 * test(browser): add seeded fuzz coverage for CDP URL helpers Adds property-based / seeded-fuzz tests for the URL helpers the attachOnly CDP fix depends on (openclaw#68027): - isWebSocketUrl - isDirectCdpWebSocketEndpoint - normalizeCdpHttpBaseForJsonEndpoints - parseBrowserHttpUrl - redactCdpUrl - appendCdpPath - getHeadersWithAuth Follows the existing repo convention (see src/gateway/http-common.fuzz.test.ts): no fast-check dep, small mulberry32 PRNG + hand-rolled generators, deterministic per-describe seeds so failures are reproducible. Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements, 67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered lines are inside the WS sender internals (createCdpSender, withCdpSocket, fetchCdpChecked rate-limit branch), which require integration-style mocks and are unrelated to the attachOnly fix. * test(browser): drive cdp.helpers/cdp/chrome to 100% coverage Lifts the three files touched by the openclaw#68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications. * fix(browser): restore WS fallback for non-/devtools ws:// CDP URLs When /json/version discovery is unavailable (or returns no webSocketDebuggerUrl), fall back to treating the original bare ws/wss URL as a direct WebSocket endpoint. This preserves the openclaw#68027 fix for Chrome's debug port while restoring compatibility with Browserless/ Browserbase-style providers that expose a direct WebSocket root without a /json/version endpoint. Priority order for bare ws/wss cdpUrl inputs: 1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged) 2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint 3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged) Affected call sites: isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp. Also renames a misleading test ('still enforces SSRF policy for direct WebSocket URLs') to accurately describe what it tests: SSRF enforcement on the navigation target URL, not on the CDP endpoint. New tests added for all three fallback paths. Coverage remains 100% on all three touched files (238 tests). * fix: browser attachOnly bare ws CDP follow-ups (openclaw#68715) (thanks @visionik)
openclaw#68715) * fix(browser): discover CDP websocket from bare ws:// URL before attach When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as: Browser attachOnly is enabled and profile "openclaw" is not running. even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (openclaw#68027). Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip. Fixes openclaw#68027 * test(browser): add seeded fuzz coverage for CDP URL helpers Adds property-based / seeded-fuzz tests for the URL helpers the attachOnly CDP fix depends on (openclaw#68027): - isWebSocketUrl - isDirectCdpWebSocketEndpoint - normalizeCdpHttpBaseForJsonEndpoints - parseBrowserHttpUrl - redactCdpUrl - appendCdpPath - getHeadersWithAuth Follows the existing repo convention (see src/gateway/http-common.fuzz.test.ts): no fast-check dep, small mulberry32 PRNG + hand-rolled generators, deterministic per-describe seeds so failures are reproducible. Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements, 67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered lines are inside the WS sender internals (createCdpSender, withCdpSocket, fetchCdpChecked rate-limit branch), which require integration-style mocks and are unrelated to the attachOnly fix. * test(browser): drive cdp.helpers/cdp/chrome to 100% coverage Lifts the three files touched by the openclaw#68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications. * fix(browser): restore WS fallback for non-/devtools ws:// CDP URLs When /json/version discovery is unavailable (or returns no webSocketDebuggerUrl), fall back to treating the original bare ws/wss URL as a direct WebSocket endpoint. This preserves the openclaw#68027 fix for Chrome's debug port while restoring compatibility with Browserless/ Browserbase-style providers that expose a direct WebSocket root without a /json/version endpoint. Priority order for bare ws/wss cdpUrl inputs: 1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged) 2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint 3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged) Affected call sites: isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp. Also renames a misleading test ('still enforces SSRF policy for direct WebSocket URLs') to accurately describe what it tests: SSRF enforcement on the navigation target URL, not on the CDP endpoint. New tests added for all three fallback paths. Coverage remains 100% on all three touched files (238 tests). * fix: browser attachOnly bare ws CDP follow-ups (openclaw#68715) (thanks @visionik)
openclaw#68715) * fix(browser): discover CDP websocket from bare ws:// URL before attach When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as: Browser attachOnly is enabled and profile "openclaw" is not running. even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (openclaw#68027). Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip. Fixes openclaw#68027 * test(browser): add seeded fuzz coverage for CDP URL helpers Adds property-based / seeded-fuzz tests for the URL helpers the attachOnly CDP fix depends on (openclaw#68027): - isWebSocketUrl - isDirectCdpWebSocketEndpoint - normalizeCdpHttpBaseForJsonEndpoints - parseBrowserHttpUrl - redactCdpUrl - appendCdpPath - getHeadersWithAuth Follows the existing repo convention (see src/gateway/http-common.fuzz.test.ts): no fast-check dep, small mulberry32 PRNG + hand-rolled generators, deterministic per-describe seeds so failures are reproducible. Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements, 67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered lines are inside the WS sender internals (createCdpSender, withCdpSocket, fetchCdpChecked rate-limit branch), which require integration-style mocks and are unrelated to the attachOnly fix. * test(browser): drive cdp.helpers/cdp/chrome to 100% coverage Lifts the three files touched by the openclaw#68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications. * fix(browser): restore WS fallback for non-/devtools ws:// CDP URLs When /json/version discovery is unavailable (or returns no webSocketDebuggerUrl), fall back to treating the original bare ws/wss URL as a direct WebSocket endpoint. This preserves the openclaw#68027 fix for Chrome's debug port while restoring compatibility with Browserless/ Browserbase-style providers that expose a direct WebSocket root without a /json/version endpoint. Priority order for bare ws/wss cdpUrl inputs: 1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged) 2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint 3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged) Affected call sites: isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp. Also renames a misleading test ('still enforces SSRF policy for direct WebSocket URLs') to accurately describe what it tests: SSRF enforcement on the navigation target URL, not on the CDP endpoint. New tests added for all three fallback paths. Coverage remains 100% on all three touched files (238 tests). * fix: browser attachOnly bare ws CDP follow-ups (openclaw#68715) (thanks @visionik)
openclaw#68715) * fix(browser): discover CDP websocket from bare ws:// URL before attach When browser.cdpUrl is set to a bare ws://host:port (no /devtools/ path), ensureBrowserAvailable would call isChromeReachable -> canOpenWebSocket against the URL verbatim. Chrome only accepts WebSocket upgrades at the specific path returned by /json/version, so the handshake failed immediately with HTTP 400. With attachOnly: true, that surfaced as: Browser attachOnly is enabled and profile "openclaw" is not running. even though the CDP endpoint was reachable and the profile was healthy. Reproduced by the new tests in chrome.test.ts and cdp.test.ts (openclaw#68027). Fix: introduce isDirectCdpWebSocketEndpoint(url) — true only when a ws/wss URL has a /devtools/<kind>/<id> handshake path. Route any other ws/wss cdpUrl (including the bare ws://host:port shape) through HTTP /json/version discovery by normalising the scheme via the existing normalizeCdpHttpBaseForJsonEndpoints helper. Apply this in isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp. Direct WS endpoints with a /devtools/ path are still opened without an extra discovery round-trip. Fixes openclaw#68027 * test(browser): add seeded fuzz coverage for CDP URL helpers Adds property-based / seeded-fuzz tests for the URL helpers the attachOnly CDP fix depends on (openclaw#68027): - isWebSocketUrl - isDirectCdpWebSocketEndpoint - normalizeCdpHttpBaseForJsonEndpoints - parseBrowserHttpUrl - redactCdpUrl - appendCdpPath - getHeadersWithAuth Follows the existing repo convention (see src/gateway/http-common.fuzz.test.ts): no fast-check dep, small mulberry32 PRNG + hand-rolled generators, deterministic per-describe seeds so failures are reproducible. Lifts cdp.helpers.ts coverage from 77.77% -> 89.54% statements, 67.9% -> 80.24% branches, 78% -> 90% lines. Remaining uncovered lines are inside the WS sender internals (createCdpSender, withCdpSocket, fetchCdpChecked rate-limit branch), which require integration-style mocks and are unrelated to the attachOnly fix. * test(browser): drive cdp.helpers/cdp/chrome to 100% coverage Lifts the three files touched by the openclaw#68027 attachOnly fix to 100% statements/branches/functions/lines across the extensions test suite. Adds cdp.helpers.internal.test.ts, cdp.internal.test.ts, and chrome.internal.test.ts covering error paths, branch matrices, CDP session helpers, Chrome spawn/launch/stop flows, and canRunCdpHealthCommand. Defensively unreachable guards are annotated with c8 ignore + inline justifications. * fix(browser): restore WS fallback for non-/devtools ws:// CDP URLs When /json/version discovery is unavailable (or returns no webSocketDebuggerUrl), fall back to treating the original bare ws/wss URL as a direct WebSocket endpoint. This preserves the openclaw#68027 fix for Chrome's debug port while restoring compatibility with Browserless/ Browserbase-style providers that expose a direct WebSocket root without a /json/version endpoint. Priority order for bare ws/wss cdpUrl inputs: 1. /devtools/<kind>/<id> URL \u2192 direct handshake, no discovery (unchanged) 2. bare ws/wss root \u2192 try HTTP discovery first; if discovery returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint 3. HTTP/HTTPS URL \u2192 HTTP discovery only, no fallback (unchanged) Affected call sites: isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp. Also renames a misleading test ('still enforces SSRF policy for direct WebSocket URLs') to accurately describe what it tests: SSRF enforcement on the navigation target URL, not on the CDP endpoint. New tests added for all three fallback paths. Coverage remains 100% on all three touched files (238 tests). * fix: browser attachOnly bare ws CDP follow-ups (openclaw#68715) (thanks @visionik)
Summary
Fixes #68027. A
browser.cdpUrlset to a barews://host:portwithout a/devtools/...path madeopenclaw browser start(and anyensureBrowserAvailablecaller) fail with:even when the CDP endpoint was reachable and healthy.
Root cause
isChromeReachable,getChromeWebSocketUrl, andcreateTargetViaCdpall treated anyws:///wss://URL as a handshake-ready direct WebSocket endpoint (viaisWebSocketUrl). For a barews://host:portthat assumption is wrong — Chrome only accepts WebSocket upgrades on the specific per-browser/per-target path returned byGET /json/version, so the probe handshake failed at the root and the availability check misreported the profile as down. WithattachOnly: true, that short-circuited the attach before any HTTP discovery was attempted.Fix
isDirectCdpWebSocketEndpoint(url)incdp.helpers.ts—trueonly when a ws/wss URL has a/devtools/<kind>/<id>path (browser/page/worker/shared_worker/service_worker).isChromeReachableandgetChromeWebSocketUrl(chrome.ts) andcreateTargetViaCdp(cdp.ts) now use the new helper with a discovery-first-then-fallback strategy for bare ws/wss URLs:/devtools/<kind>/<id>URLs → direct handshake, skip discovery (unchanged)ws:///wss://roots → try HTTP/json/versionfirst; if discovery returns awebSocketDebuggerUrluse it (Chrome case); otherwise fall back to the original URL as a direct WS endpoint (Browserless/Browserbase-style providers without/json/version)ws://host/devtools/browser/<uuid>) still skip discovery — no extra round-trip in the hot path.Tests
Tests written first; confirmed failing against the unpatched code, passing with the fix:
extensions/browser/src/browser/cdp.test.tsandchrome.test.ts: original fix tests plus new fallback-path tests for all three call sites.extensions/browser/src/browser/cdp.helpers.fuzz.test.ts: 200-iteration seeded property tests for all URL-parsing helpers (isWebSocketUrl,isDirectCdpWebSocketEndpoint,normalizeCdpHttpBaseForJsonEndpoints,parseBrowserHttpUrl,redactCdpUrl,appendCdpPath,getHeadersWithAuth).extensions/browser/src/browser/cdp.helpers.internal.test.ts,cdp.internal.test.ts,chrome.internal.test.ts: comprehensive unit tests drivingcdp.helpers.ts,cdp.ts, andchrome.tsto 100% statement / branch / function / line coverage (238 tests total across the three files).Related suites exercised and green:
cdp.helpers.test.ts,cdp.screenshot-params.test.ts,cdp-reachability-policy.test.ts,server-context.loopback-direct-ws.test.ts, and others. TypeScript typecheck (pnpm tsgo:extensions+pnpm tsgo:extensions:test) is clean.Compatibility
cdpUrlvalues that already include a/devtools/<kind>/<id>path continue to hit the fast WS handshake path.assertCdpEndpointAllowed) runs first in every path as before; the discovered WS URL is re-asserted before connect.http://host:portdiscovery URLs./json/versioncontinue to work via the new fallback path.Fixes #68027