# Browser Use Browser Automation

Use the `system/browser-use` template to run web agents in UCloud Sandbox. The template includes Chromium, Playwright, browser-use, and a CDP reverse proxy, so you can start using it without installing browser dependencies.

## Common use cases

- Read web pages automatically and extract or summarize their content
- Click through pages, fill out forms, or complete a fixed sequence of actions
- Check whether a page loads, whether it reports errors, and whether images, scripts, and API requests load successfully
- Save screenshots in batches for visual regression checks or record keeping
- Run browser automation against untrusted pages in an isolated environment

> The `9223` CDP port does not provide authentication by itself. The examples on this page run the Agent inside the sandbox and disable public access when creating the sandbox. Do not use a public CDP endpoint to visit untrusted pages or sign in to important accounts. If you need to run browser-use on the host, see [Control a sandbox browser securely from the host](#control-a-sandbox-browser-securely-from-the-host).

## Prerequisites

Install the UCloud Sandbox Python SDK on your host machine:

```bash
pip install ucloud-sandbox
```

Then configure a UCloud Sandbox API Key by following [Prerequisites and authentication](/docs/agent-sandbox/product/prerequisites.md).

## Example 1: Read the AstraFlow Agent Sandbox documentation

The following demo creates a `system/browser-use` sandbox, opens the AstraFlow Agent Sandbox documentation, clicks the navigation items for **Prerequisites and certification**, **SDK quick start**, and **Overview** in sequence, and finally outputs the key points of the documentation and saves a screenshot after each step.

### Steps

#### 1. Save the Agent script

This script runs inside the sandbox. It connects to the Chromium instance started by the template, opens the AstraFlow Agent Sandbox documentation, clicks the navigation items for prerequisites and authentication, SDK quick start, and overview in order, and saves a screenshot after each action. When the task finishes, it writes the execution result, visited URLs, and screenshot list to `/home/user/browser-agent-run/result.json`.

Create `browser_agent_task.py`:

```python
import asyncio
import json
import os
from pathlib import Path

from browser_use import Agent, BrowserSession
from browser_use.llm import ChatOpenAI as LLM


TASK = (
    "The page has already been opened by the script. The site is the AstraFlow Agent Sandbox documentation. "
    "To demonstrate browser interaction, perform the following steps in order and make sure the page changes after each action: "
    "1. Click the left navigation item for prerequisites and authentication; "
    "2. Click the left navigation item for the SDK quick start; "
    "3. Click the left navigation item for the overview, then scroll to the core capabilities section. "
    "Finally, output the following in JSON: the positioning of UCloud Sandbox, key authentication details, "
    "the SDK quick start steps, and the list of core capabilities. Do not log in or submit anything."
)
START_URL = "https://astraflow.ucloud.cn/docs/agent-sandbox"
RUN_DIR = Path("/home/user/browser-agent-run")


async def main():
    RUN_DIR.mkdir(parents=True, exist_ok=True)

    # Reuse the Chromium instance started by the template instead of launching another browser.
    browser = BrowserSession(
        cdp_url="http://127.0.0.1:9222",
        keep_alive=True,
    )
    await browser.start()

    try:
        await browser.navigate_to(START_URL)
        await asyncio.sleep(2)
        await browser.take_screenshot(
            path=str(RUN_DIR / "step-000-start.png"),
            full_page=False,
        )

        step = 0

        async def on_step_end(agent):
            nonlocal step
            step += 1
            await asyncio.sleep(1)
            await browser.take_screenshot(
                path=str(RUN_DIR / f"step-{step:03d}.png"),
                full_page=False,
            )

        llm = LLM(
            model="deepseek-v4.1-flash-1017",
            api_key=os.environ["UCLOUD_API_KEY"],
            base_url=(
                os.environ.get("LLM_BASE_URL")
                or "https://api.modelverse.cn/v1"
            ),
        )
        agent = Agent(
            task=TASK,
            llm=llm,
            browser_session=browser,
            # DeepSeek text models do not read screenshots; set this to True for multimodal models.
            use_vision=False,
            max_actions_per_step=1,
            max_failures=3,
            step_timeout=120,
            enable_signal_handler=False,
            use_judge=False,
        )
        history = await agent.run(max_steps=15, on_step_end=on_step_end)

        await browser.take_screenshot(
            path=str(RUN_DIR / "step-final.png"),
            full_page=False,
        )
        summary = {
            "done": history.is_done(),
            "successful": history.is_successful(),
            "urls": history.urls(),
            "final_result": history.final_result(),
            "steps": step,
            "screenshots": sorted(
                path.name for path in RUN_DIR.glob("*.png")
            ),
        }
        (RUN_DIR / "result.json").write_text(
            json.dumps(summary, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        print(json.dumps(summary, ensure_ascii=False, indent=2))
    finally:
        await browser.kill()


if __name__ == "__main__":
    asyncio.run(main())
```

#### 2. Save the host-side runner script

Create `run_browser_agent.py`:

```python
import os
from pathlib import Path

from ucloud_sandbox import Sandbox
from ucloud_sandbox.sandbox.commands.command_handle import CommandExitException


OUTPUT_DIR = Path("browser-agent-run")
OUTPUT_DIR.mkdir(exist_ok=True)

sandbox = Sandbox.create(
    "system/browser-use",
    timeout=1800,
    network={
        "allow_public_traffic": False,
    },
)
try:
    # Verify that the sandbox can reach the target documentation site.
    sandbox.commands.run(
        "curl -sS -o /dev/null --max-time 15 "
        "https://astraflow.ucloud.cn/docs/agent-sandbox",
        timeout=25,
    )

    sandbox.files.write(
        "/home/user/browser_agent_task.py",
        Path("browser_agent_task.py").read_text(encoding="utf-8"),
    )

    command_error = None
    try:
        result = sandbox.commands.run(
            "/opt/browser-venv/bin/python /home/user/browser_agent_task.py",
            envs={
                "UCLOUD_API_KEY": os.environ["UCLOUD_API_KEY"],
                "LLM_BASE_URL": os.environ.get(
                    "LLM_BASE_URL",
                    "https://api.modelverse.cn/v1",
                ),
            },
            timeout=1200,
        )
    except CommandExitException as exc:
        result = exc
        command_error = exc

    print(result.stdout)
    if result.stderr:
        print(result.stderr)

    # Download screenshots and results before the sandbox is destroyed.
    for entry in sandbox.files.list(
        "/home/user/browser-agent-run",
        depth=1,
    ):
        if entry.name.endswith((".json", ".png")):
            (OUTPUT_DIR / entry.name).write_bytes(
                sandbox.files.read(entry.path, format="bytes")
            )

    if command_error is not None:
        raise SystemExit(
            f"Agent execution failed (exit {command_error.exit_code}); "
            f"artifacts were saved to {OUTPUT_DIR.resolve()}"
        )
finally:
    sandbox.kill()

print(f"Results saved to {OUTPUT_DIR.resolve()}")
```

The example sets `allow_public_traffic` to `False`, so external clients cannot access the sandbox's public URLs, including the `9223` CDP port. The Agent runs inside the sandbox and connects to the browser through `127.0.0.1:9222`, so it does not need public access. The sandbox can still reach the model service and the target documentation site. If you need to debug CDP from the host, use a controlled SSH tunnel or configure an access token for the public URL. See [Restrict public access](/docs/agent-sandbox/network/restrict-public-access.md).

#### 3. Run the example

The Agent example calls the UCloud ModelVerse model service. Set the model API key and service URL, and then run the host-side script:

```bash
export UCLOUD_API_KEY=<model-api-key>
export LLM_BASE_URL=https://api.modelverse.cn/v1

python run_browser_agent.py
```

> `UCLOUD_API_KEY` is the API key for the model service, not the UCloud Sandbox API key. It is passed only through command environment variables and is not written to the script files.

#### 4. View the results

The task artifacts are saved in the local `browser-agent-run/` directory:

```text
browser-agent-run/
├── result.json
├── step-000-start.png
├── step-001.png
├── step-002.png
├── step-003.png
└── step-final.png
```

`result.json` contains:

| Field | Description |
| --- | --- |
| `done` | Whether the Agent has finished |
| `successful` | Whether the task succeeded |
| `urls` | Pages visited during execution |
| `final_result` | The documentation summary returned by the Agent |
| `steps` | Number of steps executed |
| `screenshots` | Saved screenshot files |

### Modify the Agent example

The following settings modify the documentation-reading example above:

| Setting | Location | Description |
| --- | --- | --- |
| `START_URL` | `browser_agent_task.py` | Page to open |
| `TASK` | `browser_agent_task.py` | Task assigned to the Agent |
| `model` | `browser_agent_task.py` | Model name |
| `LLM_BASE_URL` | Runtime environment variable | Model service URL, defaults to `https://api.modelverse.cn/v1` |
| `use_vision` | `browser_agent_task.py` | Keep `False` for DeepSeek text models; set to `True` for multimodal models |
| `max_steps` | `browser_agent_task.py` | Maximum number of Agent steps |

When the Agent runs inside the sandbox, continue to use `http://127.0.0.1:9222`. If you need to run browser-use on the host, use the controlled `9223` access method described in [Control a sandbox browser securely from the host](#control-a-sandbox-browser-securely-from-the-host). Do not use the public `9223` address directly as `cdp_url`.

## Example 2: Page audit

This example opens a page you specify and checks it at desktop and mobile screen sizes. It reports whether the page loads, whether it reports errors, whether any resources fail to load, and approximately how long it takes to load. It also saves a full-page screenshot for each viewport.

This example does not call a model and does not require a model API key.

Create `site_audit.py`:

```python
import json
import sys
from pathlib import Path
from urllib.parse import urlparse

from ucloud_sandbox import Sandbox


SITE_AUDIT_SCRIPT = r'''
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("playwright");

const TARGET_URL = process.env.AUDIT_URL;
const OUTPUT_DIR = "/home/user/site-audit";

const VIEWPORTS = {
  desktop: {
    viewport: { width: 1440, height: 900 },
  },
  mobile: {
    viewport: { width: 390, height: 844 },
    deviceScaleFactor: 2,
    isMobile: true,
    hasTouch: true,
  },
};

const METRIC_KEYS = new Set([
  "DomContentLoaded",
  "LoadEventEnd",
  "JSHeapUsedSize",
  "Nodes",
  "TaskDuration",
  "ScriptDuration",
  "LayoutCount",
  "RecalcStyleCount",
]);


async function auditViewport(browser, label, options) {
  const context = await browser.newContext(options);
  const page = await context.newPage();
  const consoleErrors = [];
  const pageErrors = [];
  const failedRequests = [];

  page.on("console", (message) => {
    if (message.type() === "error") {
      consoleErrors.push(message.text());
    }
  });
  page.on("pageerror", (error) => pageErrors.push(String(error)));
  page.on("requestfailed", (request) => {
    failedRequests.push({
      url: request.url(),
      error: request.failure()?.errorText || "",
    });
  });

  const started = Date.now();

  try {
    const response = await page.goto(TARGET_URL, {
      waitUntil: "domcontentloaded",
      timeout: 60_000,
    });
    await page.waitForLoadState("networkidle", {
      timeout: 12_000,
    }).catch(() => {});
    await page.waitForTimeout(500);

    const screenshot = path.join(OUTPUT_DIR, `${label}.png`);
    await page.screenshot({ path: screenshot, fullPage: true });

    const dom = await page.evaluate(() => ({
      lang: document.documentElement.lang || "",
      description:
        document.querySelector('meta[name="description"]')?.content || "",
      links: document.links.length,
      images: document.images.length,
      forms: document.forms.length,
      buttons:
        document.querySelectorAll('button, input[type="submit"]').length,
      h1: Array.from(document.querySelectorAll("h1"))
        .map((element) => element.innerText.trim())
        .filter(Boolean)
        .slice(0, 10),
      scrollWidth: document.documentElement.scrollWidth,
      scrollHeight: document.documentElement.scrollHeight,
      bodyTextLength: (document.body?.innerText || "").length,
    }));

    const session = await context.newCDPSession(page);
    await session.send("Performance.enable");
    const rawMetrics = await session.send("Performance.getMetrics");
    const performance = Object.fromEntries(
      rawMetrics.metrics
        .filter((metric) => METRIC_KEYS.has(metric.name))
        .map((metric) => [metric.name, metric.value])
    );
    await session.detach();

    return {
      status: response ? response.status() : null,
      content_type:
        response?.headers()["content-type"] || null,
      final_url: page.url(),
      title: await page.title(),
      elapsed_ms: Date.now() - started,
      screenshot,
      dom,
      console_errors: consoleErrors.slice(0, 50),
      page_errors: pageErrors.slice(0, 50),
      failed_requests: failedRequests.slice(0, 50),
      performance,
    };
  } catch (error) {
    const screenshot = path.join(OUTPUT_DIR, `${label}.png`);
    try {
      await page.screenshot({ path: screenshot, fullPage: false });
    } catch {}

    return {
      status: null,
      final_url: page.url(),
      title: await page.title().catch(() => ""),
      screenshot,
      error: String(error?.stack || error),
      elapsed_ms: Date.now() - started,
      dom: {},
      console_errors: consoleErrors.slice(0, 50),
      page_errors: pageErrors.slice(0, 50),
      failed_requests: failedRequests.slice(0, 50),
      performance: {},
    };
  } finally {
    await context.close();
  }
}


(async () => {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  const browser = await chromium.connectOverCDP("http://127.0.0.1:9222");

  try {
    const audit = {
      url: TARGET_URL,
      generated_at: new Date().toISOString(),
    };
    for (const [label, options] of Object.entries(VIEWPORTS)) {
      audit[label] = await auditViewport(browser, label, options);
    }

    fs.writeFileSync(
      path.join(OUTPUT_DIR, "audit.json"),
      JSON.stringify(audit, null, 2)
    );
    console.log(JSON.stringify(audit, null, 2));
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exit(1);
});
'''


def normalize_url(value):
    if "://" not in value:
        value = "https://" + value
    parsed = urlparse(value)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        raise SystemExit(f"Unsupported URL: {value}")
    return value


if len(sys.argv) != 2:
    raise SystemExit("Usage: python site_audit.py <url>")

target_url = normalize_url(sys.argv[1])
output_dir = Path("site-audit")
output_dir.mkdir(exist_ok=True)

sandbox = Sandbox.create(
    "system/browser-use",
    timeout=600,
    network={
        "allow_public_traffic": False,
    },
)
try:
    sandbox.files.write("/home/user/site_audit.js", SITE_AUDIT_SCRIPT)
    result = sandbox.commands.run(
        "node /home/user/site_audit.js",
        cwd="/app",
        envs={
            "AUDIT_URL": target_url,
            "NODE_PATH": "/app/node_modules",
        },
        timeout=180,
    )
    print(result.stdout)
    if result.stderr:
        print(result.stderr)

    for filename in ("audit.json", "desktop.png", "mobile.png"):
        try:
            (output_dir / filename).write_bytes(
                sandbox.files.read(
                    f"/home/user/site-audit/{filename}",
                    format="bytes",
                )
            )
        except Exception as exc:
            print(f"Failed to generate {filename}: {exc}")

    audit = json.loads(
        (output_dir / "audit.json").read_text(encoding="utf-8")
    )
    for label in ("desktop", "mobile"):
        item = audit[label]
        print(
            f"{label}: HTTP {item.get('status')} | "
            f"title={item.get('title')!r} | "
            f"console_errors={len(item.get('console_errors', []))}"
        )
finally:
    sandbox.kill()

print(f"Results saved to {output_dir.resolve()}")
```

Run it with a URL:

```bash
python site_audit.py https://astraflow.ucloud.cn/docs/agent-sandbox
```

Output directory:

```text
site-audit/
├── audit.json
├── desktop.png
└── mobile.png
```

The audit results are saved in `audit.json`. `desktop` contains the desktop viewport result and `mobile` contains the mobile viewport result. The main fields are:

| Field | Description |
| --- | --- |
| `status` | Whether the page loads successfully |
| `title` | Page title |
| `final_url` | Final URL after redirects |
| `error` | Error message if the page cannot be opened |
| `dom` | Basic page information, such as the number of headings, links, images, and forms |
| `console_errors` | Browser console errors |
| `page_errors` | Page script errors |
| `failed_requests` | Images, scripts, or API requests that failed to load |
| `performance` | Page load and runtime performance metrics |

## Control a sandbox browser securely from the host

Use `9223` only when browser-use runs on the host or another machine. The following example runs the Agent on the host, sets `allow_public_traffic=False` when creating the sandbox, and connects securely to the sandbox Chromium through the `trafficAccessToken`.

In the SDK, the token is available as `sandbox.traffic_access_token`, and the HTTP header name is `e2b-traffic-access-token`. `BrowserSession(headers=...)` uses the header for both the `/json/version` request and the CDP WebSocket handshake. Do not put the token in a URL, log, or screenshot.

Install the host dependencies first:

```bash
pip install browser-use ucloud-sandbox
```

Create `secure_agent.py`:

```python
import asyncio
import json
import os
from urllib.error import HTTPError
from urllib.request import urlopen

from browser_use import Agent, BrowserSession
from browser_use.llm import ChatOpenAI as LLM
from ucloud_sandbox import Sandbox


TOKEN_HEADER = "e2b-traffic-access-token"
CDP_PORT = 9223
TASK = (
    "Open https://astraflow.ucloud.cn/docs/agent-sandbox, "
    "read the page title and above-the-fold content, and list the main sections in English. "
    "Do not sign in or submit anything."
)


def check_without_token(base_url):
    try:
        with urlopen(f"{base_url}/json/version", timeout=20):
            raise RuntimeError("Request without a token unexpectedly succeeded")
    except HTTPError as exc:
        if exc.code != 403:
            raise
        print("No token: HTTP 403")


async def main():
    sandbox = Sandbox.create(
        "system/browser-use",
        timeout=600,
        network={
            "allow_public_traffic": False,
        },
    )
    browser = None
    try:
        token = sandbox.traffic_access_token
        if not token:
            raise RuntimeError("The sandbox did not provide a trafficAccessToken")

        cdp_url = f"https://{sandbox.get_host(CDP_PORT)}"
        check_without_token(cdp_url)

        browser = BrowserSession(
            cdp_url=cdp_url,
            headers={TOKEN_HEADER: token},
            keep_alive=True,
        )
        await browser.start()

        llm = LLM(
            model="deepseek-v4.1-flash-1017",
            api_key=os.environ["UCLOUD_API_KEY"],
            base_url=(
                os.environ.get("LLM_BASE_URL")
                or "https://api.modelverse.cn/v1"
            ),
        )
        agent = Agent(
            task=TASK,
            llm=llm,
            browser_session=browser,
            use_vision=False,
            max_actions_per_step=1,
            max_failures=3,
            step_timeout=120,
            enable_signal_handler=False,
            use_judge=False,
        )
        history = await agent.run(max_steps=10)
        screenshot = "secure-agent.png"
        await browser.take_screenshot(path=screenshot, full_page=True)

        print(
            json.dumps(
                {
                    "successful": history.is_successful(),
                    "result": history.final_result(),
                },
                ensure_ascii=False,
                indent=2,
            )
        )
        print(f"Screenshot: {screenshot}")
    finally:
        try:
            if browser is not None:
                await browser.kill()
        finally:
            sandbox.kill()


if __name__ == "__main__":
    asyncio.run(main())
```

Set the model environment variables before running the Agent:

```bash
export UCLOUD_API_KEY=<model-api-key>
export LLM_BASE_URL=https://api.modelverse.cn/v1

python secure_agent.py
```

After it starts, browser-use on the host controls Chromium inside the sandbox through `9223`, while requests without a token cannot access the public CDP endpoint. The current browser-use version must support `BrowserSession(headers=...)`. If you use an older version, upgrade browser-use or use an SSH tunnel. For details about public URL access tokens, see [Restrict public access](/docs/agent-sandbox/network/restrict-public-access.md).

### Expected results

No visible browser window opens during the run, but you can verify that the connection and authentication work by checking the following results:

1. **Unauthorized access is rejected**: The terminal first prints `No token: HTTP 403`. This confirms that `9223` cannot be accessed without a `trafficAccessToken`.
2. **The token authorizes the connection**: browser-use sends the `e2b-traffic-access-token` header for both the `/json/version` request and the CDP WebSocket handshake, and starts controlling Chromium in the sandbox.
3. **The Agent completes the browser task**: The terminal prints JSON with `successful` set to `true` and `result` containing the page title and main sections read from the AstraFlow Agent Sandbox documentation.
4. **A page screenshot is generated**: The script saves `secure-agent.png` in the current directory. The screenshot is rendered by Chromium inside the sandbox and shows that the documentation page was actually opened.
5. **Resources are cleaned up**: After the script exits, browser-use and the sandbox are closed in sequence, and the `9223` address becomes unavailable.

Example output (the text returned by the Agent depends on the actual page content):

```text
No token: HTTP 403
{
  "successful": true,
  "result": "The page title is “Agent Sandbox”. The main sections include: ..."
}
Screenshot: secure-agent.png
```

If `BrowserSession` cannot connect and returns `403`, check that you are using a browser-use version that supports the `headers` argument and that the header name is `e2b-traffic-access-token`. If a request without a token does not return `403`, public access is not restricted correctly; do not continue running tasks in that environment.

## FAQ

| Symptom | Resolution |
| --- | --- |
| The sandbox cannot reach the documentation site | Check sandbox networking or change `START_URL` |
| The Agent fails | Check the stderr output from `run_browser_agent.py`, and verify `UCLOUD_API_KEY`, the model name, and `LLM_BASE_URL` |
| The screenshot is blank | Wait longer after navigation so the page body can finish rendering before taking the screenshot |
| The browser closes after `agent.run()` | Keep `keep_alive=True` in `BrowserSession(...)` |
| The public CDP endpoint returns `502` | Check `http://127.0.0.1:9223/health` inside the sandbox and verify that the port proxy supports WebSocket |
| The port URL is unreachable | Confirm that the sandbox is still running; the public CDP address becomes unavailable immediately after the sandbox is destroyed |

## Related documentation

- [Sandbox lifecycle](/docs/agent-sandbox/sandbox/lifecycle.md)
- [Internet access](/docs/agent-sandbox/network/internet-access.md)
- [Public URL](/docs/agent-sandbox/network/public-url.md)
- [File system](/docs/agent-sandbox/filesystem/overview.md)
