Skip to content

Commit c5ea60c

Browse files
author
bcode
committed
Option A: cloud browser via snippet, drop browser_open_cloud tool
The local-browser connect path is already snippet-side (the agent calls `await session.connect(...)` itself in a snippet, picks Way 1 / Way 2). `browser_open_cloud` was the only opaque path — it wrapped one HTTP call and hid the Browser Use API surface from the agent. That asymmetry was the root of the cubic P2 lifecycle finding (auto-stop wired but never invoked) and the broader "two patterns mixed" review feedback on PR #41. This commit makes cloud match local. The agent provisions, connects, stops, and swaps cloud browsers from inside `browser_execute` snippets using `fetch` against `https://api.browser-use.com/api/v3/browsers`. Removed: - packages/bcode-browser/src/cloud-browser.ts (-109) - packages/opencode/src/tool/browser-open-cloud.{ts,txt} (-47) - registry hookup for BrowserOpenCloudTool - SessionStore.onEvict + cleanup-callback machinery (~ -25 LOC, including the Entry wrapper type). evict() now just closes + deletes; kept because the test file uses it for cleanup between cases. Added: - packages/bcode-browser/skills/cloud-browser.md (~150 LOC). Way 3 documentation: provision/connect/stop/swap, plus a recommended reusable workspace helper (.bcode/agent-workspace/cloud.ts) for projects that use cloud browsers more than once. BU API auth via process.env.BROWSER_USE_API_KEY (stays as an env var; opencode's auth.json was considered and rejected — see decisions.md §3.10). Updated: - BROWSER.md restructured around Way 1 / Way 2 / Way 3 (real Chrome popup-gated / isolated debug-port / cloud). New "Switching browsers mid-session" section. Listability hint added (`read {{SKILLS_DIR}}/interaction-skills/` to enumerate without committing). Removed the misleading "first call connects automatically" line — agent always calls connect() explicitly, just sometimes with no args. - browser-execute.txt mentions cloud-browser.md and notes process.env is in snippet scope. - bcode-browser README + package.json + index.ts: cloud-browser.ts references removed; brief note that cloud is intentionally not its own Level-1 surface. - comments in browser-execute.ts: no more `browser_open_cloud` cross-references. Net LOC: -200 deletions vs +150 markdown additions = ~-50 net hand- written. Bigger win is one fewer tool surface (back to the original Phase H §3.2 single-tool target) and zero wrapper-PR treadmill as the BU API surface grows (profile sync, custom proxies, regional pools, recording, etc. all reachable from a snippet without bcode changes). Verification: - bun typecheck clean across all browsercode packages. - bun test from packages/bcode-browser/: 4 pass + 5 skip (chrome-gated). - embed-skills.ts now embeds 18 files (was 17, +1 for cloud-browser.md). Cubic P2 finding closed: no tool claims cloud auto-stop, so there is no false claim. Stop is the agent's responsibility, documented in cloud-browser.md "Stop". Stacked on feat/phase-h-ts-harness; PR-A (#41) lands first, then this rebases onto main and lands separately. v0.1.0 tag waits for both.
1 parent d5ddc78 commit c5ea60c

12 files changed

Lines changed: 239 additions & 217 deletions

File tree

packages/bcode-browser/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ See `decisions.md §1c` (three-level model) and `§1d` (this package) in the Bro
1010
|---|---|
1111
| `src/cdp/` | Vendored CDP layer (`session.ts`, `gen.ts`, `generated.ts`, protocol JSONs). Initial copy from `browser-use/browser-harness-js`; ours after — see `src/cdp/PROVENANCE.md`. |
1212
| `src/browser-execute.ts` | In-process JS-eval `browser_execute` body. |
13-
| `src/cloud-browser.ts` | Browser Use cloud-browser provision + attach. |
14-
| `src/session-store.ts` | Per-opencode-session CDP `Session` map shared by both browser tools. |
13+
| `src/session-store.ts` | Per-opencode-session CDP `Session` map. The agent calls `session.connect(...)` from a snippet; subsequent snippets find the same Session. |
1514
| `src/skills.ts` | Runtime resolver for embedded skills (extract on first call in compiled mode; in-tree path in dev). |
16-
| `skills/` | `BROWSER.md` (the agent's prompt for `browser_execute`) plus `interaction-skills/*.md` (UI mechanic reference docs). Embedded into the binary by `script/embed-skills.ts`. |
15+
| `skills/` | `BROWSER.md` (the agent's prompt for `browser_execute`), `cloud-browser.md` (Way 3 — provision/stop a Browser Use cloud browser via raw HTTP from inside a snippet), and `interaction-skills/*.md` (UI mechanic reference docs). Embedded into the binary by `script/embed-skills.ts`. |
1716
| `script/embed-skills.ts` | Build-time embed; emits `bcode-skills.gen.ts` consumed by the compiled binary. |
1817
| `test/` | `bun test` smoke coverage for the workspace dynamic-import pattern. |
1918

packages/bcode-browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json.schemastore.org/package.json",
33
"version": "0.0.0",
44
"name": "@browser-use/bcode-browser",
5-
"description": "BrowserCode Level-1 code: in-process CDP harness, browser_execute, cloud-browser attach, embedded skills",
5+
"description": "BrowserCode Level-1 code: in-process CDP harness, browser_execute, embedded skills",
66
"type": "module",
77
"license": "MIT",
88
"private": true,

packages/bcode-browser/skills/BROWSER.md

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,62 @@
11
# BROWSER.md — driving a real browser with `browser_execute`
22

3-
Use the `browser_execute` tool to run JavaScript against a connected browser via the Chrome DevTools Protocol. The snippet runs in-process; `session` is bound to a long-lived CDP `Session` that survives across calls within the same bcode session.
3+
Use the `browser_execute` tool to run JavaScript against a connected browser via the Chrome DevTools Protocol. The snippet runs in-process; `session` is bound to a long-lived CDP `Session` that persists across calls within the same bcode session. You connect once, drive many.
44

55
**Locations:**
66

77
- Workspace (read/write your reusable scripts): `<projectRoot>/.bcode/agent-workspace/`. The bcode CLI runs from the project root, so `./.bcode/agent-workspace/foo.ts` works directly with the `read`/`write`/`edit` tools.
8-
- Skills (read-only reference docs): `{{SKILLS_DIR}}/interaction-skills/`
8+
- Skills (read-only reference docs): `{{SKILLS_DIR}}/`. Run `read {{SKILLS_DIR}}/interaction-skills/` to list every available interaction skill before reading any one of them.
99

1010
## The model in one paragraph
1111

12-
`browser_execute` evaluates whatever JS you write against `session`. There is no auto-loaded library, no privileged file, no helper namespace — just `session` and standard JS globals. To reuse code from a previous snippet, save it as a `.ts` file under `./.bcode/agent-workspace/` (using the `write` tool) and `await import("/abs/path?t=" + Date.now())` it from a later snippet. The import takes an **absolute** path — construct it from `process.cwd()` inside the snippet, or shell out via the `bash` tool to get the project root. Same mechanism for a 5-line wrapper and a 500-line script. Skills under `{{SKILLS_DIR}}/interaction-skills/` are documentation you `read`, not modules you `import` — they teach you the CDP patterns; you write the code.
12+
`browser_execute` evaluates whatever JS you write against `session`. There is no auto-loaded library, no privileged file, no helper namespace — just `session` and standard JS globals. To reuse code from a previous snippet, save it as a `.ts` file under `./.bcode/agent-workspace/` (using the `write` tool) and `await import("/abs/path?t=" + Date.now())` it from a later snippet. The import takes an **absolute** path — construct it from `process.cwd()` inside the snippet. Same mechanism for a 5-line wrapper and a 500-line script. Skills under `{{SKILLS_DIR}}/` are documentation you `read`, not modules you `import` — they teach you the CDP patterns; you write the code.
1313

1414
## Connecting
1515

16-
The first `browser_execute` call connects automatically by scanning OS-typical Chrome profile dirs for a `DevToolsActivePort` file (Chrome must be running with `--remote-debugging-port`). To attach explicitly:
16+
You always call `session.connect(...)` once at the start of your work. The `Session` is fresh on the first `browser_execute` call of an opencode session; subsequent calls reuse it. Three connection methods, in order of preference for typical tasks:
17+
18+
**Way 1 — connect to the user's running Chrome (real profile, popup-gated).** Best when the task involves the user's actual logged-in sites.
19+
20+
```js
21+
// Auto-detect the most-recently-launched Chrome with remote debugging enabled.
22+
await session.connect()
23+
```
24+
25+
The user must have ticked "Allow remote debugging for this browser instance" once at `chrome://inspect/#remote-debugging` (sticky per-profile), and on Chrome 144+ click "Allow" on the in-browser popup at first attach. If `connect()` fails with a 403/permission message, ask the user to do this. To wait for the click instead of erroring fast, pass `{ profileDir: "/abs/path", timeoutMs: 30000 }`.
26+
27+
**Way 2 — connect to a Chrome you (or the user) launched with a debug port (isolated profile, no popups).** Best for unattended automation.
28+
29+
```bash
30+
# User runs this once (or you run it via the `bash` tool):
31+
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/bcode-chrome
32+
```
33+
34+
```js
35+
await session.connect({ wsUrl: "ws://127.0.0.1:9222/devtools/browser" })
36+
// or, if you know the profile dir:
37+
await session.connect({ profileDir: "/tmp/bcode-chrome" })
38+
```
39+
40+
The `--user-data-dir` must NOT be Chrome's platform default (`%LOCALAPPDATA%\Google\Chrome\User Data` on Windows, `~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux) — Chrome 136+ silently no-ops the port flag in that case.
41+
42+
**Way 3 — provision and connect to a Browser Use cloud browser.** Best when the user can't see the browser, you need a clean profile, geo-located proxy, or fingerprint isolation. Read `{{SKILLS_DIR}}/cloud-browser.md` for the full pattern (provision, stop, swap profile/proxy). Briefly:
1743

1844
```js
19-
await session.connect({ profileDir: "/abs/path/to/Chrome/Default" })
20-
// or
21-
await session.connect({ wsUrl: "ws://127.0.0.1:9222/devtools/browser/<id>" })
22-
// or for a Browser Use cloud browser, call the `browser_open_cloud` tool first.
45+
const r = await fetch("https://api.browser-use.com/api/v3/browsers", {
46+
method: "POST",
47+
headers: { "X-Browser-Use-API-Key": process.env.BROWSER_USE_API_KEY, "Content-Type": "application/json" },
48+
body: "{}",
49+
})
50+
const { id, cdp_url, live_url } = await r.json()
51+
await session.connect({ wsUrl: cdp_url })
52+
console.log("liveUrl for the user to watch:", live_url)
2353
```
2454

25-
After connect, attach to a page target:
55+
Requires `BROWSER_USE_API_KEY` in the environment (the user should have set this before launching bcode). If absent, tell the user to get a key at https://browser-use.com and `export BROWSER_USE_API_KEY=...`.
56+
57+
## Attaching to a target
58+
59+
After `connect()`, attach to a page target before driving the browser:
2660

2761
```js
2862
const targets = (await session.Target.getTargets({})).targetInfos
@@ -65,7 +99,18 @@ const { data } = await session.Page.captureScreenshot({ format: "png" })
6599
// data is base64; write with the `write` tool or process in JS.
66100
```
67101

68-
For the full menu of UI mechanics — dropdowns, dialogs, iframes, shadow DOM, uploads, scrolling, screenshots-with-highlights — read the relevant skill: `{{SKILLS_DIR}}/interaction-skills/<topic>.md`.
102+
For the full menu of UI mechanics — dropdowns, dialogs, iframes, shadow DOM, uploads, scrolling, screenshots-with-highlights — list `{{SKILLS_DIR}}/interaction-skills/` to see all available topics, then read the relevant one.
103+
104+
## Switching browsers mid-session
105+
106+
You own the connection. To swap:
107+
108+
```js
109+
await session.close()
110+
await session.connect({ /* new opts */ })
111+
```
112+
113+
Cloud cleanup is your responsibility — if you're done with a cloud browser, stop it explicitly (see `{{SKILLS_DIR}}/cloud-browser.md` for the PATCH call). Otherwise it persists until your API quota or BU's idle timer reclaims it.
69114

70115
## Reusing code: write to the workspace, import from snippet
71116

@@ -110,4 +155,5 @@ Cache-bust (`?t=${Date.now()}`) is your responsibility: without it, edits to the
110155
- **`session.Page.navigate` hangs forever** → the page is showing a native dialog. Use `session.Page.handleJavaScriptDialog({ accept: true })` to dismiss.
111156
- **Selectors don't find elements that you can see** → likely an iframe or shadow DOM. Read `{{SKILLS_DIR}}/interaction-skills/iframes.md` or `shadow-dom.md`.
112157
- **Actions silently no-op** → the page is mid-load. After `Page.navigate`, await `session.waitFor("Page.loadEventFired")` before driving inputs.
113-
- **Connection refused or 403 on connect()** → Chrome wasn't started with `--remote-debugging-port`, or the user hasn't clicked "Allow" on the remote-debugging prompt. Pass `{ timeoutMs: 30000 }` to wait for the click.
158+
- **Connection refused or 403 on connect()** → Chrome wasn't started with `--remote-debugging-port`, or the user hasn't clicked "Allow" on the remote-debugging prompt. Pass `{ profileDir, timeoutMs: 30000 }` to wait for the click, or fall back to Way 2.
159+
- **Cloud `connect()` fails after a successful provision** → check that `cdp_url` came back in the POST response; some BU regions return `cdpUrl` (camelCase) — accept both. See `{{SKILLS_DIR}}/cloud-browser.md`.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# cloud-browser.md — Browser Use cloud browser via raw HTTP
2+
3+
When BROWSER.md sent you here, the user wants a Browser Use cloud browser (Way 3): a clean isolated Chrome on BU's infrastructure, optionally with a geo-located proxy or a synced profile, with a `liveUrl` the user can open to watch you work.
4+
5+
There is no `browser_open_cloud` tool. You write the HTTP calls yourself in a `browser_execute` snippet. This keeps the connection model symmetric (you also call `session.connect()` for local browsers in Way 1 and Way 2) and gives you full control over the BU API surface — provision, stop, swap profiles, change proxies, anything BU exposes.
6+
7+
## Authentication
8+
9+
Every call to `https://api.browser-use.com/...` requires an API key in the `X-Browser-Use-API-Key` header. The key lives in the environment as `BROWSER_USE_API_KEY` (the user is expected to `export` it before launching bcode, the same way they'd set `AWS_BEDROCK_ACCESS_KEY_ID` for an LLM provider).
10+
11+
Read it once, fail clearly if missing:
12+
13+
```js
14+
const apiKey = process.env.BROWSER_USE_API_KEY
15+
if (!apiKey) {
16+
throw new Error("BROWSER_USE_API_KEY is not set. Get a key at https://browser-use.com and re-launch bcode with the key exported.")
17+
}
18+
```
19+
20+
## Provision
21+
22+
```js
23+
const r = await fetch("https://api.browser-use.com/api/v3/browsers", {
24+
method: "POST",
25+
headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" },
26+
body: JSON.stringify({
27+
// 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
30+
}),
31+
})
32+
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
38+
```
39+
40+
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:
41+
42+
```js
43+
console.log("Cloud browser ready. Live view:", liveUrl)
44+
```
45+
46+
Stash `id` somewhere (a `globalThis.cloudBrowserId = id` is fine, or the snippet's return value) — you need it to stop the browser later.
47+
48+
## Connect
49+
50+
```js
51+
await session.connect({ wsUrl: cdpUrl })
52+
const targets = (await session.Target.getTargets({})).targetInfos
53+
const page = targets.find(t => t.type === "page")
54+
await session.use(page.targetId)
55+
```
56+
57+
From here on `session.<Domain>.<method>(...)` drives the cloud browser exactly like a local Chrome.
58+
59+
## Stop
60+
61+
When you're done, stop the browser. BU's quotas and idle reclaim will eventually clean it up if you forget, but explicit stop is faster and frees the slot:
62+
63+
```js
64+
await fetch(`https://api.browser-use.com/api/v3/browsers/${id}`, {
65+
method: "PATCH",
66+
headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" },
67+
body: JSON.stringify({ state: "stop" }),
68+
})
69+
```
70+
71+
If you'll do this often within one project, save it as `./.bcode/agent-workspace/cloud.ts` (see BROWSER.md "Reusing code") and import it from later snippets.
72+
73+
## Swap
74+
75+
To switch from one cloud browser to another (e.g. different proxy country) within the same opencode session:
76+
77+
```js
78+
// Stop the old one first.
79+
await fetch(`https://api.browser-use.com/api/v3/browsers/${oldId}`, {
80+
method: "PATCH",
81+
headers: { "X-Browser-Use-API-Key": apiKey, "Content-Type": "application/json" },
82+
body: JSON.stringify({ state: "stop" }),
83+
})
84+
85+
// Close the local Session's WS so connect() opens a fresh one.
86+
await session.close()
87+
88+
// Provision and connect to the new one (provision block above, with new params).
89+
```
90+
91+
## A reusable workspace helper
92+
93+
Recommended pattern for any project that uses cloud browsers more than once:
94+
95+
```ts
96+
// ./.bcode/agent-workspace/cloud.ts
97+
const API = "https://api.browser-use.com/api/v3/browsers"
98+
const key = () => {
99+
const k = process.env.BROWSER_USE_API_KEY
100+
if (!k) throw new Error("BROWSER_USE_API_KEY is not set.")
101+
return k
102+
}
103+
104+
export async function provision(opts: { profileId?: string; proxyCountryCode?: string } = {}) {
105+
const r = await fetch(API, {
106+
method: "POST",
107+
headers: { "X-Browser-Use-API-Key": key(), "Content-Type": "application/json" },
108+
body: JSON.stringify({
109+
profile_id: opts.profileId,
110+
proxy_country_code: opts.proxyCountryCode,
111+
}),
112+
})
113+
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+
}
120+
}
121+
122+
export async function stop(id: string) {
123+
const r = await fetch(`${API}/${id}`, {
124+
method: "PATCH",
125+
headers: { "X-Browser-Use-API-Key": key(), "Content-Type": "application/json" },
126+
body: JSON.stringify({ state: "stop" }),
127+
})
128+
if (!r.ok) throw new Error(`stop failed: ${r.status} ${await r.text()}`)
129+
}
130+
```
131+
132+
Then any snippet does:
133+
134+
```js
135+
const { provision, stop } = await import(`${process.cwd()}/.bcode/agent-workspace/cloud.ts?t=${Date.now()}`)
136+
const { id, cdpUrl, liveUrl } = await provision({ proxyCountryCode: "us" })
137+
console.log("Live view:", liveUrl)
138+
await session.connect({ wsUrl: cdpUrl })
139+
// ... do work ...
140+
await stop(id)
141+
```
142+
143+
## Other BU API endpoints
144+
145+
The full BU cloud API (profile sync, profile list, custom proxies, recording on/off, etc.) is documented at https://browser-use.com — `read` the docs and write the matching `fetch` call. Anything BU's API exposes is reachable from a snippet without bcode-side wrapper code.

packages/bcode-browser/src/browser-execute.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ export type Parameters = Schema.Schema.Type<typeof parameters>
5555

5656
export interface ExecuteContext {
5757
// Identifies the per-opencode-session CDP Session to bind into the snippet.
58-
// Shared with `browser_open_cloud` via the SessionStore so a cloud-attach
59-
// call's Session is driven by subsequent `browser_execute` calls.
58+
// The same Session is reused across calls — the agent calls
59+
// `session.connect(...)` in one snippet and subsequent snippets find the
60+
// already-connected Session.
6061
readonly sessionID: string
6162
// Per-project workspace dir: <projectDir>/.bcode/agent-workspace/. Created
6263
// on first call. The agent reads/writes/edits .ts files here via the
@@ -97,8 +98,9 @@ const serialize = (v: unknown): string => {
9798
}
9899

99100
// Snippet executor. The CDP Session is resolved per-call from `SessionStore`
100-
// keyed on `ctx.sessionID` so a Session attached via `browser_open_cloud` is
101-
// the same one a follow-up `browser_execute` drives.
101+
// keyed on `ctx.sessionID`. The agent connects with `await session.connect(...)`
102+
// in one snippet (Way 1 / Way 2 / Way 3 in BROWSER.md); the Session persists
103+
// for follow-up snippets in the same opencode session.
102104
//
103105
// `dataDir` is opencode's XDG_DATA_HOME for bcode (~/.local/share/bcode/ on
104106
// Linux/Mac). Compiled-mode skills are extracted to `<dataDir>/skills/` once

0 commit comments

Comments
 (0)