Skip to content

Commit 1fc3321

Browse files
author
bcode
committed
fix(skills): correct cloud-browser stop body and cdpUrl resolution
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.
1 parent 639d61a commit 1fc3321

2 files changed

Lines changed: 26 additions & 26 deletions

File tree

packages/bcode-browser/skills/BROWSER.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ const r = await fetch("https://api.browser-use.com/api/v3/browsers", {
7979
headers: { "X-Browser-Use-API-Key": process.env.BROWSER_USE_API_KEY, "Content-Type": "application/json" },
8080
body: "{}",
8181
})
82-
const body = await r.json()
83-
const id = body.id
84-
const cdpUrl = body.cdp_url ?? body.cdpUrl // BU returns snake_case in some regions, camelCase in others
85-
const liveUrl = body.live_url ?? body.liveUrl
86-
await session.connect({ wsUrl: cdpUrl })
82+
const { id, cdpUrl, liveUrl } = await r.json()
83+
// BU's cdpUrl is the HTTP discovery endpoint (e.g. https://cdpN.browser-use.com),
84+
// not a WebSocket URL. Resolve it like a remote Chrome: fetch /json/version and
85+
// use the webSocketDebuggerUrl field.
86+
const ver = await fetch(`${cdpUrl}/json/version`).then(r => r.json())
87+
await session.connect({ wsUrl: ver.webSocketDebuggerUrl })
8788
console.log("liveUrl for the user to watch:", liveUrl)
8889
```
8990

packages/bcode-browser/skills/cloud-browser.md

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,12 @@ const r = await fetch("https://api.browser-use.com/api/v3/browsers", {
2525
headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" },
2626
body: JSON.stringify({
2727
// All optional — omit for an ephemeral fresh-profile browser with no proxy.
28-
// profile_id: "<uuid>", // attach an existing BU profile
29-
// proxy_country_code: "us", // geo-located proxy
28+
// profileId: "<uuid>", // attach an existing BU profile
29+
// proxyCountryCode: "us", // geo-located proxy (default "us"; null disables)
3030
}),
3131
})
3232
if (!r.ok) throw new Error(`provision failed: ${r.status} ${await r.text()}`)
33-
const body = await r.json()
34-
// Some BU regions return camelCase, others snake_case. Accept both.
35-
const id = body.id
36-
const cdpUrl = body.cdp_url ?? body.cdpUrl
37-
const liveUrl = body.live_url ?? body.liveUrl
33+
const { id, cdpUrl, liveUrl } = await r.json()
3834
```
3935

4036
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
4743

4844
## Connect
4945

46+
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`:
47+
5048
```js
51-
await session.connect({ wsUrl: cdpUrl })
49+
const ver = await fetch(`${cdpUrl}/json/version`).then(r => r.json())
50+
await session.connect({ wsUrl: ver.webSocketDebuggerUrl })
51+
5252
const targets = (await session.Target.getTargets({})).targetInfos
5353
const page = targets.find(t => t.type === "page")
5454
await session.use(page.targetId)
@@ -64,7 +64,7 @@ When you're done, stop the browser. BU's quotas and idle reclaim will eventually
6464
await fetch(`https://api.browser-use.com/api/v3/browsers/${id}`, {
6565
method: "PATCH",
6666
headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" },
67-
body: JSON.stringify({ state: "stop" }),
67+
body: JSON.stringify({ action: "stop" }),
6868
})
6969
```
7070

@@ -79,7 +79,7 @@ To switch from one cloud browser to another (e.g. different proxy country) withi
7979
await fetch(`https://api.browser-use.com/api/v3/browsers/${oldId}`, {
8080
method: "PATCH",
8181
headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" },
82-
body: JSON.stringify({ state: "stop" }),
82+
body: JSON.stringify({ action: "stop" }),
8383
})
8484

8585
// 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
106106
method: "POST",
107107
headers: { "X-Browser-Use-API-Key": key(), "Content-Type": "application/json" },
108108
body: JSON.stringify({
109-
profile_id: opts.profileId,
110-
proxy_country_code: opts.proxyCountryCode,
109+
profileId: opts.profileId,
110+
proxyCountryCode: opts.proxyCountryCode,
111111
}),
112112
})
113113
if (!r.ok) throw new Error(`provision failed: ${r.status} ${await r.text()}`)
114-
const body = await r.json()
115-
return {
116-
id: body.id as string,
117-
cdpUrl: (body.cdp_url ?? body.cdpUrl) as string,
118-
liveUrl: (body.live_url ?? body.liveUrl) as string,
119-
}
114+
const body = (await r.json()) as { id: string; cdpUrl: string; liveUrl: string }
115+
// BU's cdpUrl is an HTTP discovery endpoint; resolve to the WS URL once
116+
// here so callers can pass `wsUrl` straight to `session.connect`.
117+
const ver = await fetch(`${body.cdpUrl}/json/version`).then(r => r.json())
118+
return { id: body.id, wsUrl: ver.webSocketDebuggerUrl as string, liveUrl: body.liveUrl }
120119
}
121120

122121
export async function stop(id: string) {
123122
const r = await fetch(`${API}/${id}`, {
124123
method: "PATCH",
125124
headers: { "X-Browser-Use-API-Key": key(), "Content-Type": "application/json" },
126-
body: JSON.stringify({ state: "stop" }),
125+
body: JSON.stringify({ action: "stop" }),
127126
})
128127
if (!r.ok) throw new Error(`stop failed: ${r.status} ${await r.text()}`)
129128
}
@@ -133,9 +132,9 @@ Then any snippet does:
133132

134133
```js
135134
const { provision, stop } = await import(`${process.cwd()}/.bcode/agent-workspace/cloud.ts?t=${Date.now()}`)
136-
const { id, cdpUrl, liveUrl } = await provision({ proxyCountryCode: "us" })
135+
const { id, wsUrl, liveUrl } = await provision({ proxyCountryCode: "us" })
137136
console.log("Live view:", liveUrl)
138-
await session.connect({ wsUrl: cdpUrl })
137+
await session.connect({ wsUrl })
139138
// ... do work ...
140139
await stop(id)
141140
```

0 commit comments

Comments
 (0)