Skip to content

Cookbook: remote browser

02 / Connect from outside

A browser sandbox runs a headless Chromium with a CDP endpoint on :9222. The Live browser recipe drives that browser from inside the guest — the fast path for RL. This page does the opposite: it hands you the CDP endpoint so you can drive the live browser from your own machine, using the Playwright script you already have.

This is the connect-from-outside access model: the service runs in the sandbox; the driver runs on your laptop.

Create a browser sandbox and ask the SDK for a connect URL to the CDP port:

from collimate_rl import connect
client = connect(api_key="col_...")
with client.session("browser", egress={"allow": ["example.com"]}) as sb:
cdp = sb.cdp_url() # == connect_url(9222, protocol="cdp")
print(cdp)
# wss://api.collimate.ai/v1/sandboxes/<id>/connect?port=9222&protocol=cdp&t=<token>

cdp_url() mints connect_url(9222, protocol="cdp") — a signed wss://…/connect?port=…&protocol=cdp&t=<token> URL. The token scopes the connection to this one sandbox and port; the cdp protocol makes the platform speak the DevTools Protocol end to end, so a CDP client attaches directly (a plain tcp tunnel can't carry CDP). Nothing about the underlying host is exposed — the address is a platform-managed endpoint on api.collimate.ai.

Point a local Playwright at that URL with connect_over_cdp. The script runs on your machine; every navigation and selector is executed by the browser inside the sandbox:

from collimate_rl import connect
from playwright.sync_api import sync_playwright
client = connect(api_key="col_...")
with client.session("browser", egress={"allow": ["example.com"]}) as sb, \
sync_playwright() as pw:
browser = pw.chromium.connect_over_cdp(sb.cdp_url())
page = browser.new_context().new_page() # isolated context
page.goto("https://example.com")
print(page.title()) # -> "Example Domain"
page.screenshot(path="shot.png") # saved locally, on your machine
browser.close() # closes the connection, not the in-guest browser
# leaving the `with` deletes the sandbox

The turnkey helper connect_browser does the mint + connect_over_cdp for you:

from collimate_rl.browser import connect_browser
with client.session("browser") as sb:
browser = connect_browser(sb) # Playwright browser, on your machine
try:
page = browser.new_context().new_page()
page.goto("https://example.com")
print(page.title())
finally:
browser.close()

Because the driver is local, you get the full local Playwright ergonomics — screenshot(path=...) writes to your disk, breakpoints stop in your debugger, and you can iterate on the script without redeploying anything. The browser state (open pages, cookies within a context) lives in the sandbox and persists across reconnects for the sandbox's lifetime.

Both talk to the same live browser on :9222. They differ in where your driver runs and, therefore, in latency and ergonomics.

In-guest (exec)From outside (connect)
Where the driver runsIn the sandboxOn your machine
How you invoke itclient.exec(id, commands=[["cdp-nav", url]])connect_over_cdp(sb.cdp_url())
Round-tripsNone leave the guestEvery CDP call crosses the network
LatencyLowest — see belowAdds a network hop per call
Best forRL rollouts, batch grading, fork fan-outInteractive sessions, local scripts, debugging
PlanAny (demo = no egress)Pro

For RL and any latency-sensitive fan-out, drive the browser inside the guest instead. The browser image bakes cdp-nav, a dependency-free (Python-stdlib) raw-CDP client that connects to the resident browser, navigates, and prints the page title and final URL — with no Node driver, so it is markedly faster than Playwright's connect_over_cdp:

result = client.exec(
sb["id"],
commands=[["cdp-nav", "https://example.com"]],
timeout_seconds=60,
)
print(result["stdout"]) # -> "Example Domain\thttps://example.com/"

See Live browser → Fast path: raw CDP for cdp-nav's options (--expr, --port, --timeout) and the measured latency comparison. As a rule: reach for connect-from-outside when you want to drive the browser interactively with your own client; reach for in-guest exec when you want a scalar result and the lowest per-step latency.

fork fans a warmed browser out into many live children. Each child keeps the same resident browser and its own connect URL — so you can drive a whole fleet remotely, one CDP URL per child:

children = client.fork(sb["id"], count=8)["children"]
for child in children:
# each child is its own sandbox; mint its own connect URL by id
url = client.cdp_url(child["id"])
# ... connect_over_cdp(url) from your driver ...

For batch rollouts, prefer the in-guest cdp-nav step per child — it avoids a network hop per navigation across the fan-out. See Live fork.

The public web is a Pro capability. Demo sandboxes have no outbound network, so a goto("https://…") — over CDP from outside or via cdp-nav in-guest — reaches nothing. Grant the destinations you need at create time:

sb = client.create_sandbox("browser", egress={"allow": ["example.com"]})

See Per-fork egress for the full policy model, and Live browser → Pro browser-RL for the auth-and-secrets patterns (never bake credentials into the shared ready state).