Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions guides/browser-agent-in-sandbox/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# --- Browserbase (the cloud browser the agent drives) ---
# Get a key at https://www.browserbase.com/settings
BROWSERBASE_API_KEY=bb_live_xxxxxxxxxxxxxxxxxxxxxxxx

# --- Model auth: Vercel AI Gateway (no separate Anthropic key needed) ---
# The agent's model (anthropic/claude-sonnet-5) is served through Vercel AI
# Gateway, so the same Vercel auth that powers the Sandbox also powers the
# model. Pick ONE:
#
# 1. OIDC (recommended, keyless): run `vercel link` then `vercel env pull
# .env.local` in this directory. Vercel writes a short-lived
# VERCEL_OIDC_TOKEN to .env.local that both the Gateway and the Sandbox read
# automatically — no extra key to manage. (Refresh with `vercel env pull`
# when it expires.) When deployed to Vercel, OIDC is injected automatically.
#
# 2. AI Gateway API key: create one at https://vercel.com/dashboard/ai-gateway
# and set it here. Use this if you prefer a long-lived key over OIDC.
# AI_GATEWAY_API_KEY=vck_xxxxxxxxxxxxxxxxxxxxxxxx

# --- Optional: faster cold start with a prebuilt sandbox image ---
# Set this to a Vercel Container Registry image that already has the `browse`
# CLI installed (see the "Template image" section of the README). When unset,
# the route falls back to a node24 runtime and installs `browse` per request.
# BROWSE_VCR_IMAGE=vcr.vercel.com/<team>/<project>/browse-cli:v1
34 changes: 34 additions & 0 deletions guides/browser-agent-in-sandbox/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
/node_modules
/.pnp
.pnp.js

# Next.js
/.next/
/out/
next-env.d.ts

# Production
build
dist

# Misc
.DS_Store
*.pem

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local ENV files
.env
.env*.local

# Vercel
.vercel

# typescript
*.tsbuildinfo
4 changes: 4 additions & 0 deletions guides/browser-agent-in-sandbox/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}
10 changes: 10 additions & 0 deletions guides/browser-agent-in-sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Prebuilt sandbox image for faster cold starts, built FROM the official `browse`
# CLI image so the agent route can skip the per-request `npm install -g browse`.
#
# Build and push to your project's Vercel Container Registry, then set
# BROWSE_VCR_IMAGE to the pushed tag (see the README "Template image" section).
#
# Vercel Sandbox ignores ENTRYPOINT/CMD — the runtime drives the container — so
# we only need the CLI on PATH.
FROM ghcr.io/browserbase/browse:latest
WORKDIR /work
123 changes: 123 additions & 0 deletions guides/browser-agent-in-sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Browser Research Agent in a Vercel Sandbox

A full-stack [Next.js](https://nextjs.org) app: an autonomous browser-research agent that drives the Browserbase [`browse`](https://www.npmjs.com/package/browse) CLI inside a [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) and streams its steps to the UI live. The agent plans its own steps — it runs `browse` commands through a `bash` tool to navigate the web and answer a research question.

The `browse` CLI drives a real browser running on [Browserbase](https://www.browserbase.com), so the browser itself never runs in the sandbox — only the CLI does. That keeps the microVM cheap and the browser fully managed (stealth, CAPTCHA solving, proxies are all on the Browserbase side).

## Demo

[browser-agent-in-sandbox.vercel.app](https://browser-agent-in-sandbox.vercel.app)

Enter a research task and watch the agent stream its `browse` steps live, then render the final comparison table.

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/guides/browser-agent-in-sandbox&project-name=browser-agent-in-sandbox&repository-name=browser-agent-in-sandbox)

## How it works

```
┌────────────────────────────┐ ┌────────────────────────────┐ CDP/wss ┌────────────────────────────┐
│ Next.js app (your deploy) │ │ Vercel Sandbox (microVM) │ ─────────▶ │ Browserbase cloud browser │
│ │ │ node24 (or VCR image) │ │ │
│ /api/chat — agent loop │ ─bash─▶ │ `browse` CLI runs here │ ◀─────────── │ navigates / reads pages │
│ (AI SDK streamText) │ │ │ page data │ │
│ ▲ stream └────────────────────────────┘ └────────────────────────────┘
│ │
│ UI (useChat) renders each `browse` step + final answer
└────────────────────────────┘
```

- The **agent loop runs in the `/api/chat` route**. It provisions a sandbox, builds a `bash` tool with [`bash-tool`](https://github.com/vercel-labs/bash-tool) bound to that sandbox, and runs the AI SDK's `streamText` with `stopWhen: stepCountIs(40)`.
- The **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which run in the microVM.
- The **model is served through [Vercel AI Gateway](https://vercel.com/docs/ai-gateway)**. A bare `provider/model` string (`anthropic/claude-sonnet-5`) resolves through the default Gateway provider in `ai@6`, authenticated by Vercel's OIDC token — so the same Vercel auth that powers the Sandbox also powers the model, and there's no separate `ANTHROPIC_API_KEY`.
- The **UI streams live**. `useChat` from `@ai-sdk/react` renders each `browse` tool call (with a compact running/done/error indicator) as it happens, and the final answer as Markdown (the comparison table).
- The **system prompt is built fresh per request** (`buildSystemPrompt()`, not a static constant). It tells the agent it's running inside a sandbox microVM with no local browser (so it never passes `--local`), injects today's date at request time so it never guesses or hardcodes one, and tells it to give its best final answer from whatever it's gathered if it's nearing the step limit.

The default task is a genuinely browser-only example: on Amazon, search for the current top mechanical keyboards and, for the top 5 results, compare each product's title, price, star rating, and number of ratings — returning a comparison table with each product's URL. Amazon renders results client-side and returns nothing useful to a plain HTTP fetch, so the agent has to drive a real browser.

## Setup

The agent's model is served through AI Gateway, so **one Vercel auth covers both the Sandbox and the model — there is no separate Anthropic key.**

You need:

- `BROWSERBASE_API_KEY` — the cloud browser ([browserbase.com/settings](https://www.browserbase.com/settings)). It is passed to the sandbox as an encrypted environment variable, so it never lands in a file inside the sandbox.
- Model + Sandbox auth via Vercel — pick one:
- **OIDC (recommended, keyless):** link this directory to a Vercel project and pull its env. The `vercel` CLI writes a short-lived `VERCEL_OIDC_TOKEN` that both the Gateway and the Sandbox read automatically:
```bash
vercel link # link to your Vercel project
vercel env pull .env.local # writes VERCEL_OIDC_TOKEN
```
When deployed to Vercel, OIDC is injected automatically — nothing to set.
- **AI Gateway API key:** create one at [vercel.com/dashboard/ai-gateway](https://vercel.com/dashboard/ai-gateway) and set `AI_GATEWAY_API_KEY`. (Sandbox auth still comes from OIDC / the Vercel deployment.)

```bash
pnpm install
cp .env.example .env.local # add BROWSERBASE_API_KEY
vercel link # OIDC path: link the project…
vercel env pull .env.local # …and pull VERCEL_OIDC_TOKEN
pnpm dev # http://localhost:3000
```

> AI Gateway requires the Vercel team to have billing enabled to service model requests (free credits unlock once a card is on file). This is the only model-side prerequisite — there is no Anthropic account or key.

The default uses a plain `--remote` Browserbase session, which works on any plan. Verified browsers (residential IP + automatic CAPTCHA solving) are a Scale-plan upgrade — see [browserbase.com/pricing](https://www.browserbase.com/pricing).

### Environment variables

| Variable | Required | Description |
| --- | --- | --- |
| `BROWSERBASE_API_KEY` | yes | Authenticates `browse` against Browserbase. |
| `VERCEL_OIDC_TOKEN` | yes (keyless path) | Written by `vercel env pull`; powers both the Gateway and the Sandbox. Injected automatically on Vercel. |
| `AI_GATEWAY_API_KEY` | optional | Long-lived alternative to OIDC for the model only. |
| `BROWSE_VCR_IMAGE` | optional | Prebuilt sandbox image with `browse` baked in, for faster cold starts (see below). |

## Template image (faster cold start)

By default the route boots a `node24` sandbox and runs `npm install -g browse@latest` per request. To skip that install, point `BROWSE_VCR_IMAGE` at a prebuilt image — the `Dockerfile` here builds one from the official [`ghcr.io/browserbase/browse`](https://github.com/browserbase/stagehand/pkgs/container/browse) image:

```dockerfile
FROM ghcr.io/browserbase/browse
WORKDIR /work
```

(Vercel Sandbox base images must live in VCR, so you build/mirror from the official image into your project's registry — the runtime doesn't pull `ghcr.io` directly.)

Build and push it to your project's [Vercel Container Registry](https://vercel.com/docs/vercel-sandbox/images) (VCR), then set `BROWSE_VCR_IMAGE`:

```bash
# Pull the project's OIDC token, then authenticate Docker to the Vercel
# Container Registry with it.
vercel env pull .env.local
printf '%s' "$VERCEL_OIDC_TOKEN" | docker login vcr.vercel.com --username oidc --password-stdin

# Build for the sandbox platform (linux/amd64) with zstd compression and push
# straight to your project's registry.
docker buildx build --platform linux/amd64 \
--output "type=image,name=vcr.vercel.com/<team>/<project>/browse-cli:v1,push=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true" \
.

# Then set the env var (locally in .env.local, or in your Vercel project):
# BROWSE_VCR_IMAGE=vcr.vercel.com/<team>/<project>/browse-cli:v1
```

The image takes a moment to process after the push — `Sandbox.create` returns `image_not_ready` until it is Ready, after which the route boots from it.

When `BROWSE_VCR_IMAGE` is set, the route creates the sandbox from that image and skips the install step. When it's unset, it falls back to the runtime install — both paths set `BROWSERBASE_API_KEY` and `BROWSE_SESSION=agent` in the sandbox env so `browse` runs remote and shares one session flag-free.

## Project structure

```
ai/agent/
constants.ts # model id, default task, system prompt
sandbox.ts # dual-mode sandbox creation + bash-tool adapter
run.ts # agent loop (streamText + bash tool) → UI message stream
app/
api/chat/route.ts # POST endpoint useChat talks to
page.tsx # prompt box + live step stream + Markdown answer
layout.tsx # Geist fonts, theme
components/ # shadcn-style UI + chat rendering
scripts/smoke.ts # headless end-to-end smoke against real cloud
Dockerfile # prebuilt browse image for BROWSE_VCR_IMAGE
```

> Future direction: Vercel [`eve`](https://vercel.com/docs) (beta) is a purpose-built durable-agent framework to graduate to once it's GA — it would manage the agent's long-running lifecycle (provisioning, retries, durability) so the route wouldn't have to.
19 changes: 19 additions & 0 deletions guides/browser-agent-in-sandbox/ai/agent/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// The model is served through Vercel AI Gateway. A bare `provider/model` string
// resolves through the default Gateway provider in ai@6, authenticated by
// Vercel's OIDC token (or AI_GATEWAY_API_KEY) — so no separate Anthropic key is
// needed.
export const MODEL = 'anthropic/claude-sonnet-5'

export const DEFAULT_TASK =
"Using Amazon (https://www.amazon.com), research the current top mechanical keyboards: search the site, then for the top 5 results compare each product's title, price, star rating, and number of ratings. Return a comparison table including each product's URL."

// Built fresh on every request (not a static export) so the injected date is
// always today — the route calls this per-request, never at module load.
export function buildSystemPrompt(): string {
const today = new Date().toISOString().slice(0, 10)
return `You are an autonomous deep-research agent. You have a \`browse\` CLI (Browserbase browser automation) in your bash tool — it is installed, and its auth and a shared browser session are already configured via environment variables. Learn how to use it by running \`browse skills show\` (it prints the CLI's bundled usage guide); use \`browse --help\` (and \`browse <command> --help\`) for extra detail on any command. Then complete the task.

You are running inside a Vercel Sandbox microVM with no local browser — every \`browse\` session is remote and already configured via environment variables, so never pass \`--local\`. Today's date is ${today}; use it for anything time-sensitive and never hardcode or guess a date. If you're nearing the step limit, stop navigating and give the best final answer you can from what you've already gathered.

When you cite a document, link the direct document itself, not a viewer, preview, or index page that wraps it. Return a clear, well-sourced answer.`
}
74 changes: 74 additions & 0 deletions guides/browser-agent-in-sandbox/ai/agent/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Runs the browser-research agent and returns a UI message stream that
// `@ai-sdk/react`'s `useChat` consumes directly.
//
// The agent loop runs HERE on the host (Vercel AI SDK `streamText` with
// `stopWhen`). Its `bash` tool — built with `bash-tool` — executes commands
// INSIDE the Vercel Sandbox microVM. The model navigates the web by emitting
// `browse` commands through that `bash` tool. Tool calls + their results and the
// model's text stream to the client as standard AI SDK UI message parts.
import {
type ModelMessage,
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
stepCountIs,
streamText,
} from 'ai'
import { createBashTool } from 'bash-tool'
import { MODEL, buildSystemPrompt } from './constants'
import { createBrowseSandbox, toBashToolSandbox } from './sandbox'
import type { ChatUIMessage } from '@/lib/types'

interface RunAgentOptions {
/** Prior UI messages from `useChat` (includes the latest user turn). */
messages?: ChatUIMessage[]
/** Or a bare prompt string (used by the smoke-test script). */
prompt?: string
}

export async function runBrowserAgent({
messages,
prompt,
}: RunAgentOptions): Promise<Response> {
const sandbox = await createBrowseSandbox()

// Build the `bash` tool backed by the sandbox. `destination` is the working
// directory the bash tool runs commands from.
const { tools } = await createBashTool({
sandbox: toBashToolSandbox(sandbox),
destination: '/vercel/sandbox',
})

// Only expose the `bash` tool — that is all the agent needs to drive `browse`.
const agentTools = { bash: tools.bash }

const modelMessages: ModelMessage[] = messages
? await convertToModelMessages(messages)
: [{ role: 'user', content: prompt ?? '' }]

return createUIMessageStreamResponse({
stream: createUIMessageStream<ChatUIMessage>({
originalMessages: messages,
execute: ({ writer }) => {
const result = streamText({
model: MODEL,
system: buildSystemPrompt(),
messages: modelMessages,
tools: agentTools,
// The agent navigates the web over many steps; give it room.
stopWhen: stepCountIs(40),
onError: (error) => {
console.error('Agent error:', JSON.stringify(error, null, 2))
},
})

result.consumeStream()
writer.merge(result.toUIMessageStream({ sendStart: false }))
},
onFinish: async () => {
// Tear down the microVM once the run completes.
await sandbox.stop().catch(() => {})
},
}),
})
}
104 changes: 104 additions & 0 deletions guides/browser-agent-in-sandbox/ai/agent/sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Provisions a Vercel Sandbox (an ephemeral Firecracker microVM) with the
// Browserbase `browse` CLI available, and adapts it to `bash-tool`'s Sandbox
// interface so the agent's `bash` tool executes commands INSIDE the microVM.
//
// The `browse` CLI drives a real browser running on Browserbase, so the browser
// never runs in the microVM — only the CLI does.
import { Sandbox } from '@vercel/sandbox'
import type { Sandbox as BashToolSandbox } from 'bash-tool'

// Default sandbox env vars. Vercel stores them encrypted server-side and injects
// them into every command, so they reach the `browse` commands the bash tool
// runs — without ever being written to a committed file or echoed back.
// BROWSERBASE_API_KEY — authenticates `browse` against Browserbase.
// BROWSE_SESSION=agent — steers `browse` to run remotely and share one named
// browser session across commands, so the agent never has to pass
// --remote/--session flags itself.
function sandboxEnv(): Record<string, string> {
const browserbaseApiKey = process.env.BROWSERBASE_API_KEY
if (!browserbaseApiKey) {
throw new Error('Missing required env var: BROWSERBASE_API_KEY')
}
return { BROWSERBASE_API_KEY: browserbaseApiKey, BROWSE_SESSION: 'agent' }
}

/**
* Create a sandbox with `browse` available.
*
* Dual-mode:
* - If `BROWSE_VCR_IMAGE` is set, boot from that prebuilt image (the `browse`
* CLI is baked in — see the README "Template image" section). This skips the
* per-request npm install for a faster cold start.
* - Otherwise, boot the `node24` runtime and `npm install -g browse@latest`.
*/
export async function createBrowseSandbox(): Promise<Sandbox> {
const env = sandboxEnv()
const image = process.env.BROWSE_VCR_IMAGE

if (image) {
return Sandbox.create({ image, env, timeout: 600_000 })
}

const sandbox = await Sandbox.create({
runtime: 'node24',
env,
timeout: 600_000,
})

const install = await sandbox.runCommand('npm', [
'install',
'-g',
'browse@latest',
])
if (install.exitCode !== 0) {
await sandbox.stop()
throw new Error(`browse install failed (exit ${install.exitCode})`)
}

return sandbox
}

async function streamToString(
stream: NodeJS.ReadableStream
): Promise<string> {
const chunks: Buffer[] = []
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
return Buffer.concat(chunks).toString('utf-8')
}

/**
* Adapt a `@vercel/sandbox` v2 instance to `bash-tool`'s Sandbox interface
* (executeCommand / readFile / writeFiles). `bash-tool` ships a built-in
* wrapper, but its duck-typing detection doesn't recognize `@vercel/sandbox` v2,
* so we pass this small explicit adapter (documented in the bash-tool README).
* `bash` executes IN the microVM; the agent loop stays on the host.
*/
export function toBashToolSandbox(sandbox: Sandbox): BashToolSandbox {
return {
async executeCommand(command: string) {
const result = await sandbox.runCommand('bash', ['-c', command])
const [stdout, stderr] = await Promise.all([
result.stdout(),
result.stderr(),
])
return { stdout, stderr, exitCode: result.exitCode }
},
async readFile(filePath: string) {
const stream = await sandbox.readFile({ path: filePath })
if (stream === null) throw new Error(`File not found: ${filePath}`)
return streamToString(stream)
},
async writeFiles(files) {
await sandbox.writeFiles(
files.map((f) => ({
path: f.path,
content: Buffer.isBuffer(f.content)
? f.content
: Buffer.from(f.content),
}))
)
},
}
}
Loading