Skip to content

fix(browser): discover CDP websocket from bare ws:// URL before attach#68715

Merged
visionik merged 5 commits intomainfrom
fix/browser-attach-only-bare-ws-cdp
Apr 19, 2026
Merged

fix(browser): discover CDP websocket from bare ws:// URL before attach#68715
visionik merged 5 commits intomainfrom
fix/browser-attach-only-bare-ws-cdp

Conversation

@visionik
Copy link
Copy Markdown
Contributor

@visionik visionik commented Apr 18, 2026

Summary

Fixes #68027. A browser.cdpUrl set to a bare ws://host:port without a /devtools/... path made openclaw browser start (and any ensureBrowserAvailable caller) fail with:

Browser attachOnly is enabled and profile "openclaw" is not running.

even when the CDP endpoint was reachable and healthy.

Root cause

isChromeReachable, getChromeWebSocketUrl, and createTargetViaCdp all treated any ws:// / wss:// URL as a handshake-ready direct WebSocket endpoint (via isWebSocketUrl). For a bare ws://host:port that assumption is wrong — Chrome only accepts WebSocket upgrades on the specific per-browser/per-target path returned by GET /json/version, so the probe handshake failed at the root and the availability check misreported the profile as down. With attachOnly: true, that short-circuited the attach before any HTTP discovery was attempted.

Fix

  • New helper isDirectCdpWebSocketEndpoint(url) in cdp.helpers.tstrue only when a ws/wss URL has a /devtools/<kind>/<id> path (browser/page/worker/shared_worker/service_worker).
  • isChromeReachable and getChromeWebSocketUrl (chrome.ts) and createTargetViaCdp (cdp.ts) now use the new helper with a discovery-first-then-fallback strategy for bare ws/wss URLs:
    1. /devtools/<kind>/<id> URLs → direct handshake, skip discovery (unchanged)
    2. Bare ws:///wss:// roots → try HTTP /json/version first; if discovery returns a webSocketDebuggerUrl use it (Chrome case); otherwise fall back to the original URL as a direct WS endpoint (Browserless/Browserbase-style providers without /json/version)
    3. HTTP/HTTPS URLs → HTTP discovery only, no fallback (unchanged)
  • Handshake-ready direct WS endpoints (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.ts and chrome.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 driving cdp.helpers.ts, cdp.ts, and chrome.ts to 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

  • No config changes required. Existing cdpUrl values that already include a /devtools/<kind>/<id> path continue to hit the fast WS handshake path.
  • SSRF policy enforcement (assertCdpEndpointAllowed) runs first in every path as before; the discovered WS URL is re-asserted before connect.
  • Backward-compatible for http://host:port discovery URLs.
  • Browserless/Browserbase-style direct WS providers that don't expose /json/version continue to work via the new fallback path.

Fixes #68027

@openclaw-barnacle openclaw-barnacle Bot added size: S maintainer Maintainer-authored PR labels Apr 18, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR fixes a real user-facing bug (#68027): a cdpUrl of ws://host:port (no /devtools/… path) caused ensureBrowserAvailable to misreport the browser as not running. The root cause was that isWebSocketUrl was used to decide whether to skip HTTP discovery, but Chrome only accepts WebSocket upgrades on the specific per-target path returned by GET /json/version, not at the bare root.

The fix is clean: a new isDirectCdpWebSocketEndpoint helper gates the fast-path on an explicit /devtools/<kind>/<id> regex, and callers that receive a bare ws:// URL now normalise it to http:// for discovery. SSRF policy is preserved on all paths (both via assertCdpEndpointAllowed and through fetchWithSsrFGuard inside fetchCdpChecked), and the discovered WS URL is re-asserted before connecting.

Confidence Score: 5/5

Safe 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.

Comments Outside Diff (1)

  1. extensions/browser/src/browser/cdp.test.ts, line 174-188 (link)

    P2 Test name and coverage mismatch

    The test is titled "still enforces SSRF policy for direct WebSocket URLs", but ws://127.0.0.1:9222 is no longer a "direct" WebSocket endpoint under the new isDirectCdpWebSocketEndpoint definition. With the new code, that bare URL routes through HTTP discovery; fetchSpy is never called only because assertBrowserNavigationAllowed throws SsrFBlockedError for the navigation target (http://127.0.0.1:8080) before any CDP URL processing. The test therefore validates navigation SSRF, not CDP-endpoint SSRF, for bare ws URLs.

    Consider renaming the test to reflect its actual coverage, and adding a sibling case (analogous to the existing "blocks the initial /json/version fetch when the cdpUrl host is outside strict SSRF policy" test for http://) that exercises a blocked bare ws cdpUrl with an allowed navigation target:

    it("blocks /json/version fetch when bare ws:// cdpUrl host is outside strict SSRF policy", async () => {
      await expect(
        createTargetViaCdp({
          cdpUrl: "ws://169.254.169.254:9222",
          url: "https://93.184.216.34",
          ssrfPolicy: {
            dangerouslyAllowPrivateNetwork: false,
            allowedHostnames: ["127.0.0.1"],
          },
        }),
      ).rejects.toBeInstanceOf(BrowserCdpEndpointBlockedError);
    });
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/browser/src/browser/cdp.test.ts
    Line: 174-188
    
    Comment:
    **Test name and coverage mismatch**
    
    The test is titled `"still enforces SSRF policy for direct WebSocket URLs"`, but `ws://127.0.0.1:9222` is no longer a "direct" WebSocket endpoint under the new `isDirectCdpWebSocketEndpoint` definition. With the new code, that bare URL routes through HTTP discovery; `fetchSpy` is never called only because `assertBrowserNavigationAllowed` throws `SsrFBlockedError` for the *navigation* target (`http://127.0.0.1:8080`) before any CDP URL processing. The test therefore validates navigation SSRF, not CDP-endpoint SSRF, for bare ws URLs.
    
    Consider renaming the test to reflect its actual coverage, and adding a sibling case (analogous to the existing `"blocks the initial /json/version fetch when the cdpUrl host is outside strict SSRF policy"` test for `http://`) that exercises a blocked *bare ws cdpUrl* with an allowed navigation target:
    
    ```ts
    it("blocks /json/version fetch when bare ws:// cdpUrl host is outside strict SSRF policy", async () => {
      await expect(
        createTargetViaCdp({
          cdpUrl: "ws://169.254.169.254:9222",
          url: "https://93.184.216.34",
          ssrfPolicy: {
            dangerouslyAllowPrivateNetwork: false,
            allowedHostnames: ["127.0.0.1"],
          },
        }),
      ).rejects.toBeInstanceOf(BrowserCdpEndpointBlockedError);
    });
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/browser/src/browser/cdp.test.ts
Line: 174-188

Comment:
**Test name and coverage mismatch**

The test is titled `"still enforces SSRF policy for direct WebSocket URLs"`, but `ws://127.0.0.1:9222` is no longer a "direct" WebSocket endpoint under the new `isDirectCdpWebSocketEndpoint` definition. With the new code, that bare URL routes through HTTP discovery; `fetchSpy` is never called only because `assertBrowserNavigationAllowed` throws `SsrFBlockedError` for the *navigation* target (`http://127.0.0.1:8080`) before any CDP URL processing. The test therefore validates navigation SSRF, not CDP-endpoint SSRF, for bare ws URLs.

Consider renaming the test to reflect its actual coverage, and adding a sibling case (analogous to the existing `"blocks the initial /json/version fetch when the cdpUrl host is outside strict SSRF policy"` test for `http://`) that exercises a blocked *bare ws cdpUrl* with an allowed navigation target:

```ts
it("blocks /json/version fetch when bare ws:// cdpUrl host is outside strict SSRF policy", async () => {
  await expect(
    createTargetViaCdp({
      cdpUrl: "ws://169.254.169.254:9222",
      url: "https://93.184.216.34",
      ssrfPolicy: {
        dangerouslyAllowPrivateNetwork: false,
        allowedHostnames: ["127.0.0.1"],
      },
    }),
  ).rejects.toBeInstanceOf(BrowserCdpEndpointBlockedError);
});
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(browser): discover CDP websocket fro..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +151 to 153
if (isDirectCdpWebSocketEndpoint(cdpUrl)) {
// Handshake-ready direct WS endpoint — probe via WS handshake.
return await canOpenWebSocket(cdpUrl, timeoutMs);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 18, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟠 High SSRF guard bypass via permissive default policy when ssrfPolicy is omitted for CDP fetches
2 🟡 Medium CDP URL userinfo credentials preserved during ws→http normalization and passed to HTTP fetch/WebSocket URLs
1. 🟠 SSRF guard bypass via permissive default policy when ssrfPolicy is omitted for CDP fetches
Property Value
Severity High
CWE CWE-918
Location extensions/browser/src/browser/cdp.helpers.ts:310-321

Description

fetchCdpChecked() falls back to an explicitly permissive SSRF policy ({ allowPrivateNetwork: true }) whenever the caller does not supply ssrfPolicy and the target hostname is not loopback.

This enables server-side requests to arbitrary hosts including private/internal networks (RFC1918, link-local such as 169.254.169.254, etc.) when upstream code paths accept an attacker-controlled cdpUrl and forget to pass an SSRF policy.

Key points:

  • Input: url (CDP base / discovery URL) can be attacker-controlled via configuration or API surface that accepts CDP endpoints.
  • Policy selection: when ssrfPolicy is undefined, non-loopback hosts use { allowPrivateNetwork: true }.
  • Sink: fetchWithSsrFGuard({ url, ... policy }) is invoked with the permissive policy, allowing private-network access.

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 ws://host:port inputs into HTTP discovery requests (/json/version) and therefore trigger fetchCdpChecked() more often.

Recommendation

Do not default to a permissive policy when ssrfPolicy is omitted.

Options (choose one consistent with product requirements):

  1. Fail closed when no policy is provided for non-loopback hosts:
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;
  1. Provide a restrictive default (e.g., disallow private network unless explicitly allowed) instead of { allowPrivateNetwork: true }.

Also ensure call sites consistently pass an SSRF policy when accepting user/config-controlled CDP URLs, especially for the /json/version discovery request path.

2. 🟡 CDP URL userinfo credentials preserved during ws→http normalization and passed to HTTP fetch/WebSocket URLs
Property Value
Severity Medium
CWE CWE-598
Location extensions/browser/src/browser/cdp.helpers.ts:183-203

Description

normalizeCdpHttpBaseForJsonEndpoints converts ws:/wss: CDP URLs to http:/https: for /json/version discovery, but does not remove URL userinfo (username:password@). The resulting discovery URL is then used by fetchCdpChecked() as the request URL.

Impact:

  • Credentials can be exposed via components that log URLs (e.g., SSRF guard auditing, proxy/access logs, error reporting), since the raw URL string still contains userinfo.
  • Requests may send credentials in multiple places (URL userinfo and Authorization: Basic ...), increasing the chance of accidental leakage.
  • URL-embedded credentials are broadly discouraged because they can be captured by intermediaries even when headers are protected/redacted.

Vulnerable flows:

  • Input: cdpUrl can include userinfo (e.g. ws(s)://user:pass@​host:port)
  • Transformation: normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) swaps schemes but keeps userinfo
  • Sink: fetchWithSsrFGuard({ url, ... }) is invoked with the unredacted URL

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 },
  ...
});

Recommendation

After extracting credentials for an Authorization header, strip userinfo from the URL before returning it from normalization and before passing it to any HTTP client / logging / auditing layers.

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 redactCdpUrl() whenever a CDP URL is included in error messages or logs, and prefer header-based auth over URL userinfo in configuration and docs.


Analyzed PR: #68715 at commit bb2fbce

Last updated on: 2026-04-19T09:46:41Z

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +84 to +85
return /\/devtools\/(?:browser|page|worker|shared_worker|service_worker)\/[^/]/i.test(
parsed.pathname,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@visionik
Copy link
Copy Markdown
Contributor Author

Addressed all review feedback:

P1 — WS fallback for non-/devtools providers (all three comments)

The root issue: bare ws:///wss:// URLs (e.g. wss://connect.browserbase.com?apiKey=...) were being forced through HTTP /json/version discovery which fails for providers that don't expose that endpoint, even though the WebSocket connection itself is valid.

Fix (commit 6af10184): Restored a direct-WS fallback at all three affected call sites with this priority order:

  1. /devtools/<kind>/<id> URLs → direct handshake, skip discovery (unchanged)
  2. Bare ws:///wss:// roots → try HTTP /json/version first; if it returns a webSocketDebuggerUrl use it; otherwise fall back to the original URL as a direct WS endpoint
  3. HTTP/HTTPS URLs → HTTP discovery only, no fallback (unchanged)

This preserves the #68027 fix for Chrome's debug port (discovery normalises ws://host:porthttp://host:port and gets the real /devtools/browser/<id> path) while restoring compatibility with Browserless/Browserbase-style providers.

P2 — Misleading test name (Greptile)

Renamed 'still enforces SSRF policy for direct WebSocket URLs''enforces SSRF policy on the navigation target URL before any CDP connection attempt', which is what the test actually exercises.

New tests cover all three fallback paths (isChromeReachable, getChromeWebSocketUrl, createTargetViaCdp) and coverage remains 100% on all three touched files (238 tests).


Note: the previous commit in this PR (4edbdcb8) added comprehensive unit tests (100% statement/branch/function/line coverage on cdp.helpers.ts, cdp.ts, chrome.ts) before these source changes were made, so all new branches introduced here are also covered.

@visionik visionik force-pushed the fix/browser-attach-only-bare-ws-cdp branch from 6af1018 to 0b912f4 Compare April 19, 2026 06:18
@visionik visionik self-assigned this Apr 19, 2026
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).
@visionik visionik force-pushed the fix/browser-attach-only-bare-ws-cdp branch from 0b912f4 to bb2fbce Compare April 19, 2026 09:43
@visionik visionik merged commit 4cfc8cd into main Apr 19, 2026
9 checks passed
@visionik visionik deleted the fix/browser-attach-only-bare-ws-cdp branch April 19, 2026 09:43
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Apr 19, 2026
@visionik
Copy link
Copy Markdown
Contributor Author

visionik commented Apr 19, 2026

Landed via temp rebase onto main.

  • Gate: pnpm lint ✅; pnpm test ✅; pnpm build ❌ (unrelated pre-existing failure in runtime postbuild staging for discord exact-version/runtime-deps resolution)
  • Land commit: bb2fbce
  • Merge commit: 4cfc8cd

LMK if you still have any issues after the next release.

Thank you... @visionik

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +218 to +219
if (wsUrlRaw) {
wsUrl = normalizeCdpWsUrl(wsUrlRaw, discoveryUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

jinon86 pushed a commit to jinon86/openclaw that referenced this pull request Apr 19, 2026
* 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>
Mquarmoc pushed a commit to Mquarmoc/openclaw that referenced this pull request Apr 20, 2026
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)
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
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)
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
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)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Browser attachOnly fails with "profile openclaw is not running" despite healthy CDP (2026.4.15)

1 participant