From 1fc332179495db16db33c83ee3ce050081e43031 Mon Sep 17 00:00:00 2001 From: bcode Date: Sat, 9 May 2026 01:13:03 +0000 Subject: [PATCH] fix(skills): correct cloud-browser stop body and cdpUrl resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs surfaced by an agent testing Way 3 cloud browsers end-to-end, verified against the v3 OpenAPI spec at docs.browser-use.com. 1. Stop body was `{state: "stop"}`. The v3 `PATCH /browsers/{id}` schema (UpdateBrowserSessionRequest) requires `{action: "stop"}` — field name and enum both wrong before. Fixed in BROWSER.md (no instance), cloud-browser.md (Stop section, Swap section, workspace helper). 2. `cdpUrl` from BU was passed straight as `wsUrl`. BU returns the HTTP discovery endpoint (e.g. https://cdpN.browser-use.com), same shape as Chrome's :9222, not a WebSocket URL. Resolve via `/json/version` and use `webSocketDebuggerUrl` — same fix that the v0.1.0 Way 2 polish landed for raw Chrome. Updated all three call sites: BROWSER.md inline Way 3, cloud-browser.md Connect section, workspace helper now does the resolution once and returns wsUrl directly. Cleanups while here: `{id, cdpUrl, liveUrl} = await r.json()` (was hand-destructured snake-OR-camel; v3 spec is unambiguously camelCase). Inline comments updated profile_id/proxy_country_code -> profileId/ proxyCountryCode to match. No behavior change in source code; skill files only. Skills materialization tests still pass — content shape unchanged, just correct now. Typecheck clean, 8 pass + 5 chrome-gated skip. --- packages/bcode-browser/skills/BROWSER.md | 11 ++--- .../bcode-browser/skills/cloud-browser.md | 41 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/bcode-browser/skills/BROWSER.md b/packages/bcode-browser/skills/BROWSER.md index 6f3452652..15339b4d0 100644 --- a/packages/bcode-browser/skills/BROWSER.md +++ b/packages/bcode-browser/skills/BROWSER.md @@ -79,11 +79,12 @@ const r = await fetch("https://api.browser-use.com/api/v3/browsers", { headers: { "X-Browser-Use-API-Key": process.env.BROWSER_USE_API_KEY, "Content-Type": "application/json" }, body: "{}", }) -const body = await r.json() -const id = body.id -const cdpUrl = body.cdp_url ?? body.cdpUrl // BU returns snake_case in some regions, camelCase in others -const liveUrl = body.live_url ?? body.liveUrl -await session.connect({ wsUrl: cdpUrl }) +const { id, cdpUrl, liveUrl } = await r.json() +// BU's cdpUrl is the HTTP discovery endpoint (e.g. https://cdpN.browser-use.com), +// not a WebSocket URL. Resolve it like a remote Chrome: fetch /json/version and +// use the webSocketDebuggerUrl field. +const ver = await fetch(`${cdpUrl}/json/version`).then(r => r.json()) +await session.connect({ wsUrl: ver.webSocketDebuggerUrl }) console.log("liveUrl for the user to watch:", liveUrl) ``` diff --git a/packages/bcode-browser/skills/cloud-browser.md b/packages/bcode-browser/skills/cloud-browser.md index 6b1806f50..146c2f9c0 100644 --- a/packages/bcode-browser/skills/cloud-browser.md +++ b/packages/bcode-browser/skills/cloud-browser.md @@ -25,16 +25,12 @@ const r = await fetch("https://api.browser-use.com/api/v3/browsers", { headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" }, body: JSON.stringify({ // All optional — omit for an ephemeral fresh-profile browser with no proxy. - // profile_id: "", // attach an existing BU profile - // proxy_country_code: "us", // geo-located proxy + // profileId: "", // attach an existing BU profile + // proxyCountryCode: "us", // geo-located proxy (default "us"; null disables) }), }) if (!r.ok) throw new Error(`provision failed: ${r.status} ${await r.text()}`) -const body = await r.json() -// Some BU regions return camelCase, others snake_case. Accept both. -const id = body.id -const cdpUrl = body.cdp_url ?? body.cdpUrl -const liveUrl = body.live_url ?? body.liveUrl +const { id, cdpUrl, liveUrl } = await r.json() ``` The `liveUrl` is a viewer URL the user can open in their own browser to watch the cloud browser's pixels. **Print it to console** so the user can click it: @@ -47,8 +43,12 @@ Stash `id` somewhere (a `globalThis.cloudBrowserId = id` is fine, or the snippet ## Connect +The `cdpUrl` from BU is an HTTP discovery endpoint (e.g. `https://cdpN.browser-use.com`), the same shape Chrome's `:9222` exposes locally, **not** a WebSocket URL. Resolve it via `/json/version`: + ```js -await session.connect({ wsUrl: cdpUrl }) +const ver = await fetch(`${cdpUrl}/json/version`).then(r => r.json()) +await session.connect({ wsUrl: ver.webSocketDebuggerUrl }) + const targets = (await session.Target.getTargets({})).targetInfos const page = targets.find(t => t.type === "page") await session.use(page.targetId) @@ -64,7 +64,7 @@ When you're done, stop the browser. BU's quotas and idle reclaim will eventually await fetch(`https://api.browser-use.com/api/v3/browsers/${id}`, { method: "PATCH", headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" }, - body: JSON.stringify({ state: "stop" }), + body: JSON.stringify({ action: "stop" }), }) ``` @@ -79,7 +79,7 @@ To switch from one cloud browser to another (e.g. different proxy country) withi await fetch(`https://api.browser-use.com/api/v3/browsers/${oldId}`, { method: "PATCH", headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" }, - body: JSON.stringify({ state: "stop" }), + body: JSON.stringify({ action: "stop" }), }) // Close the local Session's WS so connect() opens a fresh one. @@ -106,24 +106,23 @@ export async function provision(opts: { profileId?: string; proxyCountryCode?: s method: "POST", headers: { "X-Browser-Use-API-Key": key(), "Content-Type": "application/json" }, body: JSON.stringify({ - profile_id: opts.profileId, - proxy_country_code: opts.proxyCountryCode, + profileId: opts.profileId, + proxyCountryCode: opts.proxyCountryCode, }), }) if (!r.ok) throw new Error(`provision failed: ${r.status} ${await r.text()}`) - const body = await r.json() - return { - id: body.id as string, - cdpUrl: (body.cdp_url ?? body.cdpUrl) as string, - liveUrl: (body.live_url ?? body.liveUrl) as string, - } + const body = (await r.json()) as { id: string; cdpUrl: string; liveUrl: string } + // BU's cdpUrl is an HTTP discovery endpoint; resolve to the WS URL once + // here so callers can pass `wsUrl` straight to `session.connect`. + const ver = await fetch(`${body.cdpUrl}/json/version`).then(r => r.json()) + return { id: body.id, wsUrl: ver.webSocketDebuggerUrl as string, liveUrl: body.liveUrl } } export async function stop(id: string) { const r = await fetch(`${API}/${id}`, { method: "PATCH", headers: { "X-Browser-Use-API-Key": key(), "Content-Type": "application/json" }, - body: JSON.stringify({ state: "stop" }), + body: JSON.stringify({ action: "stop" }), }) if (!r.ok) throw new Error(`stop failed: ${r.status} ${await r.text()}`) } @@ -133,9 +132,9 @@ Then any snippet does: ```js const { provision, stop } = await import(`${process.cwd()}/.bcode/agent-workspace/cloud.ts?t=${Date.now()}`) -const { id, cdpUrl, liveUrl } = await provision({ proxyCountryCode: "us" }) +const { id, wsUrl, liveUrl } = await provision({ proxyCountryCode: "us" }) console.log("Live view:", liveUrl) -await session.connect({ wsUrl: cdpUrl }) +await session.connect({ wsUrl }) // ... do work ... await stop(id) ```