# Browser

Every sandbox has a browser in it that shows as a browser on an ordinary computer does, and the command-line tools agents drive it with. Your own code can drive it from outside as well.

There is nothing to install. The browser is run by commands, like anything else in the sandbox, with no call to turn on. A program on your own computer can drive it too, once it is turned on for that, as in [Drive it from your own computer](https://agental.io/docs/browser#drive-it-from-your-own-computer). A sandbox made before the browser was added does not have it: create a new one.

## Drive it with playwright-cli

`playwright-cli` is Playwright's command-line tool. It keeps one browser open between commands, so each command is short, and it writes what it reads to a file instead of printing a whole page.

```shell
playwright-cli open https://example.com
playwright-cli snapshot
playwright-cli click e6
playwright-cli screenshot
playwright-cli close
```

- `snapshot` writes the page as a list of what is on it, and says where. Each thing that can be used has a reference, such as `e6`, which `click`, `fill` and `type` take.
- It drives the Chrome in the sandbox, with no window.
- With `--headed` it opens a window on the [desktop](https://agental.io/docs/desktop), when that is on.
- `playwright-cli --help` lists every command.

Send the commands as you send any other, one request each or several in one line of shell:

```shell
curl -X POST https://agental.io/v1/sandboxes/{id}/exec \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":["sh","-c","playwright-cli open https://example.com && playwright-cli snapshot"]}'
```

## Chrome by itself

For one picture, one PDF, or a page as it is once its scripts have run, Chrome alone does it:

```shell
google-chrome --headless --screenshot=/opt/agental/page.png --window-size=1280,800 https://example.com
google-chrome --headless --print-to-pdf=/opt/agental/page.pdf https://example.com
google-chrome --headless --dump-dom https://example.com
```

- A file written under `/opt/agental` is read back with the [files](https://agental.io/docs/files) call.
- Commands run as root, and Chrome will not start as root with its own sandbox on. `google-chrome` and `playwright-cli` here start it without, since the machine is yours alone.
- A library that starts Chrome itself, such as Puppeteer, has to be told so: pass `--no-sandbox`.
- Playwright finds this Chrome as the channel `chrome`, so no other browser has to be downloaded.
- Anything that speaks the DevTools protocol can drive it: start Chrome with `--remote-debugging-port=9222` and connect to that port. While the browser is turned on as below, that port is its own, and the Chrome there is the one to connect to.

## What is in the image

| What | Which |
| --- | --- |
| Browser | BetterChromium, the Chromium of the BetterWright project, in Chrome's place: `google-chrome`, and Playwright's channel `chrome`, start it |
| BetterWright | `betterwright`, BetterWright's own tool for the same browser |
| Driver | `playwright-cli`, from the npm package `@playwright/cli` |
| Runtime | Node.js 24, with `npm` |
| Fonts | Liberation, Noto, Noto CJK and Noto Color Emoji |

## Drive it from your own computer

Playwright, Puppeteer, browser-use, Stagehand and anything else that speaks the Chrome DevTools Protocol can drive the Chrome in a sandbox from where they run. Turned on for that, Chrome runs in the sandbox's machine with its DevTools on, and keeps running between your scripts. It uses the sandbox's own CPU and memory, and is not charged for by itself.

`PUT /v1/sandboxes/{id}/browser`

```shell
curl -X PUT https://agental.io/v1/sandboxes/{id}/browser \
  -H "Authorization: Bearer $AGENTAL_API_KEY"
```

```json
{
  "enabled": true,
  "enabledAt": "2026-10-14T09:02:11.480Z",
  "running": true,
  "headless": true,
  "cdpUrl": "wss://b51e09c4d7a2f8e3c610.agental.site/devtools/browser",
  "localCdpUrl": "http://127.0.0.1:9222"
}
```

- The sandbox has to be running. The call answers when Chrome is up.
- `cdpUrl` is where to connect. The address is hard to guess and asks for no login: whoever has it has the whole browser, with what it is signed in to. Keep it as you would a password, and do not paste it into a chat or a log.
- It is another address each time the browser is turned on after being off. To cut off whoever has it, turn the browser off and on again.
- Asked again, the call changes nothing that is already so, and starts a Chrome that had ended.
- A sandbox made before this was added answers `409 browser_unavailable`. Create a new one.

## Connect to it

Playwright takes the address as it is:

```typescript
import { chromium } from "playwright";

const browser = await chromium.connectOverCDP("wss://b51e09c4d7a2f8e3c610.agental.site/devtools/browser");
const context = browser.contexts()[0];
const page = await context.newPage();
await page.goto("https://example.com");
console.log(await page.title());
await browser.close();
```

So does Puppeteer:

```javascript
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({ browserWSEndpoint: "wss://b51e09c4d7a2f8e3c610.agental.site/devtools/browser" });
const page = await browser.newPage();
await page.goto("https://example.com");
console.log(await page.title());
await browser.disconnect();
```

And browser-use, where `llm` is one of its chat models:

```python
import asyncio
from browser_use import Agent, Browser

async def main():
    browser = Browser(cdp_url="wss://b51e09c4d7a2f8e3c610.agental.site/devtools/browser")
    agent = Agent(task="Open example.com and say what it is for", llm=llm, browser=browser)
    await agent.run()

asyncio.run(main())
```

Stagehand takes it among its options for a local browser:

```typescript
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
  env: "LOCAL",
  localBrowserLaunchOptions: { cdpUrl: "wss://b51e09c4d7a2f8e3c610.agental.site/devtools/browser" },
});
await stagehand.init();
```

- A sandbox has one Chrome to drive, and many clients can be connected to it at once. Each sees the tabs the others open.
- In Playwright, `browser.contexts()[0]` is Chrome's own context, with the cookies and logins in its profile. One made with `browser.newContext()` starts empty.
- Playwright's `browser.close()` disconnects, and leaves Chrome running. Puppeteer's ends Chrome, which is started again within a second at the same address, with no tabs open. Call `browser.disconnect()` to leave it as it is.

A client that takes an `http` address instead, such as `chrome-devtools-mcp`, is given `https://` and the host of `cdpUrl`, with no path:

```shell
npx chrome-devtools-mcp@latest --browser-url=https://b51e09c4d7a2f8e3c610.agental.site
```

## With no window, or on the desktop

With no body, Chrome is on the desktop when one is turned on for the sandbox, and has no window when none is. A browser that is already on keeps what it has. To say which, send `headless`:

```shell
curl -X PUT https://agental.io/v1/sandboxes/{id}/browser \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"headless":false}'
```

- `false` puts Chrome on the [desktop](https://agental.io/docs/desktop), where it is watched as it is driven. A desktop has to be turned on first, or the call answers `409 desktop_not_running`.
- `true` gives it no window.
- Asked for the other, Chrome is started again so, which closes its tabs. `cdpUrl` stays the same.
- A Chrome on the desktop waits while the desktop is off, with `running` false, and comes back on it when the desktop is turned on again.

## The same Chrome from inside

`localCdpUrl` is where programs in the sandbox reach it. `playwright-cli` attaches to it, and its commands then drive the browser your own code drives:

```shell
playwright-cli attach --cdp=http://127.0.0.1:9222
playwright-cli snapshot
playwright-cli detach
```

- `playwright-cli detach` lets go of it, and leaves it running.
- So a script on your computer and an agent in the sandbox can use one browser, with one profile.

## After a stop

The browser stays turned on for the sandbox. Chrome ends when the sandbox stops, and is started again by itself a moment after the sandbox runs again, with no tabs open.

`GET /v1/sandboxes/{id}/browser`

- It changes nothing. `running` is false while the sandbox is not running, and for a moment after a start. `cdpUrl` is null then.
- The address is the same after a start, so a client that lost its connection connects to it again once `running` is true.
- Chrome's profile is in `/root/.config/agental-browser`, on the sandbox's disk. Its cookies and logins are kept through a stop, and go with snapshots and forks. A fork does not have the browser turned on.

## Turn it off

`DELETE /v1/sandboxes/{id}/browser`

Stops Chrome, which closes its tabs. Its address leads nowhere from then on, and what is connected through it is cut. The profile stays on the sandbox's disk, and is there when the browser is turned on again.

## In the console

A sandbox's Browser view turns it on, with no window or on the desktop, and off. While Chrome runs it shows the address, the lines that connect Playwright, Puppeteer and browser-use to it, and the command that attaches `playwright-cli` in the sandbox.

## Errors

| Answer | Meaning |
| --- | --- |
| `409 sandbox_not_running` | The sandbox is stopped. Start it, then repeat the request. |
| `409 desktop_not_running` | Chrome was to be on the desktop, and no desktop is turned on. Turn one on, or send `{"headless": true}`. |
| `409 browser_unavailable` | The machine was made from an image that cannot run the browser to be driven from outside, or Chrome did not start. `message` says which. |
| `501 browser_not_configured` | Sandboxes here have no browser to drive from outside. `GET /v1/policy` says so as `browser`. |

---

Every page of these docs as Markdown: [llms.txt](https://agental.io/llms.txt)
