diff --git a/docs/src/app/next/page.mdx b/docs/src/app/next/page.mdx index 85d1a6f..677ae0a 100644 --- a/docs/src/app/next/page.mdx +++ b/docs/src/app/next/page.mdx @@ -16,23 +16,58 @@ pnpm add @vercel/sandbox ## Server action +The Vercel Sandbox runs Amazon Linux. Chromium requires system libraries +that are not installed by default, so fresh sandboxes need a `dnf install` +step before agent-browser can launch Chrome. Use a sandbox snapshot +(below) to skip this entirely in production. + ```ts "use server"; import { Sandbox } from "@vercel/sandbox"; const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID; +const CHROMIUM_SYSTEM_DEPS = [ + "nss", "nspr", "libxkbcommon", "atk", "at-spi2-atk", "at-spi2-core", + "libXcomposite", "libXdamage", "libXrandr", "libXfixes", "libXcursor", + "libXi", "libXtst", "libXScrnSaver", "libXext", "mesa-libgbm", "libdrm", + "mesa-libGL", "mesa-libEGL", "cups-libs", "alsa-lib", "pango", "cairo", + "gtk3", "dbus-libs", +]; + +function getSandboxCredentials() { + if ( + process.env.VERCEL_TOKEN && + process.env.VERCEL_TEAM_ID && + process.env.VERCEL_PROJECT_ID + ) { + return { + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_TEAM_ID, + projectId: process.env.VERCEL_PROJECT_ID, + }; + } + return {}; +} + async function withBrowser( fn: (sandbox: InstanceType) => Promise, ): Promise { + const credentials = getSandboxCredentials(); + const sandbox = snapshotId ? await Sandbox.create({ + ...credentials, source: { type: "snapshot", snapshotId }, timeout: 120_000, }) - : await Sandbox.create({ runtime: "node24", timeout: 120_000 }); + : await Sandbox.create({ ...credentials, runtime: "node24", timeout: 120_000 }); if (!snapshotId) { + await sandbox.runCommand("sh", [ + "-c", + `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`, + ]); await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]); await sandbox.runCommand("npx", ["agent-browser", "install"]); } @@ -48,13 +83,15 @@ export async function screenshotUrl(url: string) { return withBrowser(async (sandbox) => { await sandbox.runCommand("agent-browser", ["open", url]); - const result = await sandbox.runCommand("agent-browser", [ + const ssResult = await sandbox.runCommand("agent-browser", [ "screenshot", "--json", ]); - const data = JSON.parse(await result.stdout()); + const ssPath = JSON.parse(await ssResult.stdout())?.data?.path; + const b64Result = await sandbox.runCommand("base64", ["-w", "0", ssPath]); + const screenshot = (await b64Result.stdout()).trim(); await sandbox.runCommand("agent-browser", ["close"]); - return { ok: true, screenshot: data.data.base64 }; + return { ok: true, screenshot }; }); } @@ -75,11 +112,12 @@ export async function snapshotUrl(url: string) { ## Sandbox snapshots -Without optimization, each Sandbox run installs agent-browser + Chromium -from scratch (~30 seconds). A **sandbox snapshot** is a saved VM image -with everything pre-installed -- like a Docker image for Vercel Sandbox. -When `AGENT_BROWSER_SNAPSHOT_ID` is set, the sandbox boots from that -image instead of installing, bringing startup down to sub-second. +Without optimization, each Sandbox run installs system dependencies + +agent-browser + Chromium from scratch (~30 seconds). A **sandbox snapshot** +is a saved VM image with everything pre-installed -- like a Docker image +for Vercel Sandbox. When `AGENT_BROWSER_SNAPSHOT_ID` is set, the sandbox +boots from that image instead of installing, bringing startup down to +sub-second. This is different from an agent-browser *accessibility snapshot* (which dumps a page's accessibility tree). A sandbox snapshot is a Vercel @@ -91,8 +129,8 @@ Create a sandbox snapshot by running the helper script once: npx tsx scripts/create-snapshot.ts ``` -The script spins up a fresh sandbox, installs agent-browser + Chromium, -saves the VM state, and prints the snapshot ID: +The script spins up a fresh sandbox, installs system dependencies + +agent-browser + Chromium, saves the VM state, and prints the snapshot ID: ``` AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx @@ -101,6 +139,25 @@ AGENT_BROWSER_SNAPSHOT_ID=snap_xxxxxxxxxxxx Add this to your Vercel project environment variables (or `.env.local` for local development). Recommended for any production deployment. +## Authentication + +On Vercel deployments, the Sandbox SDK authenticates automatically via +OIDC. For local development, provide explicit credentials: + + + + + + + + + + +
VariableDescription
VERCEL_TOKENVercel personal access token
VERCEL_TEAM_IDVercel team ID
VERCEL_PROJECT_IDVercel project ID
+ +When all three are set, they are passed to `Sandbox.create()`. When +absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel). + ## Scheduled workflows (cron) For recurring tasks like daily monitoring, use Vercel Cron Jobs: @@ -141,10 +198,14 @@ export async function GET() { AGENT_BROWSER_SNAPSHOT_IDSandbox snapshot ID for sub-second startup (see above) + VERCEL_TOKENVercel personal access token (for local dev; OIDC is automatic on Vercel) + VERCEL_TEAM_IDVercel team ID (for local dev) + VERCEL_PROJECT_IDVercel project ID (for local dev) ## Demo app -A working demo with a UI and deploy-to-Vercel button is at +A working demo with streaming progress UI, rate limiting, and a +deploy-to-Vercel button is at [`examples/environments/`](https://github.com/agent-browser/agent-browser/tree/main/examples/environments). diff --git a/examples/environments/README.md b/examples/environments/README.md index ea3ef54..979e5a1 100644 --- a/examples/environments/README.md +++ b/examples/environments/README.md @@ -1,11 +1,13 @@ # agent-browser Environments -A demo of agent-browser running in a Vercel Sandbox. Enter a URL and take a screenshot or accessibility snapshot. +A demo of agent-browser running in a Vercel Sandbox. Pick a URL, take a screenshot or accessibility snapshot, and watch each command execute in real time. ## How It Works The app runs agent-browser + Chrome inside an ephemeral Vercel Sandbox microVM. A Linux VM spins up on demand, executes agent-browser commands, and shuts down. No binary size limits, no Chromium bundling complexity. +The UI streams progress via Server-Sent Events so you can see each step as it runs (sandbox creation, browser startup, navigation, screenshot/snapshot, cleanup). + ## Getting Started ```bash @@ -14,9 +16,11 @@ pnpm install pnpm dev ``` +For local development, set `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID` in `.env.local` so the Sandbox SDK can authenticate. + ## Sandbox Snapshots -Without optimization, each Sandbox run installs agent-browser + Chromium from scratch (~30s). A **sandbox snapshot** is a saved VM image with everything pre-installed -- the sandbox boots from the image instead of installing, bringing startup down to sub-second. (This is unrelated to agent-browser's *accessibility snapshot* feature, which dumps a page's accessibility tree.) +Without optimization, each Sandbox run installs system dependencies + agent-browser + Chromium from scratch (~30s). A **sandbox snapshot** is a saved VM image with everything pre-installed -- the sandbox boots from the image instead of installing, bringing startup down to sub-second. (This is unrelated to agent-browser's *accessibility snapshot* feature, which dumps a page's accessibility tree.) Create a sandbox snapshot by running the helper script once: @@ -32,6 +36,9 @@ Add the ID to your Vercel project environment variables or `.env.local`. Recomme | Variable | Description | |---|---| | `AGENT_BROWSER_SNAPSHOT_ID` | Sandbox snapshot ID for sub-second startup (see above) | +| `VERCEL_TOKEN` | Vercel personal access token (for local dev; OIDC is automatic on Vercel) | +| `VERCEL_TEAM_ID` | Vercel team ID (for local dev) | +| `VERCEL_PROJECT_ID` | Vercel project ID (for local dev) | | `KV_REST_API_URL` | Upstash Redis URL for rate limiting (optional) | | `KV_REST_API_TOKEN` | Upstash Redis token for rate limiting (optional) | | `RATE_LIMIT_PER_MINUTE` | Max requests per minute per IP (default: 10) | @@ -42,11 +49,11 @@ Add the ID to your Vercel project environment variables or `.env.local`. Recomme ``` examples/environments/ app/ - page.tsx # Demo UI - actions/browse.ts # Server actions - api/browse/route.ts # API route for programmatic access + page.tsx # Demo UI with streaming progress + actions/browse.ts # Server action (env status check) + api/browse/route.ts # Streaming SSE endpoint lib/ - agent-browser-sandbox.ts # Vercel Sandbox client + agent-browser-sandbox.ts # Vercel Sandbox client with progress callbacks constants.ts # Allowed URLs rate-limit.ts # Upstash rate limiting scripts/ diff --git a/skills/vercel-sandbox/SKILL.md b/skills/vercel-sandbox/SKILL.md index b29451f..3de43de 100644 --- a/skills/vercel-sandbox/SKILL.md +++ b/skills/vercel-sandbox/SKILL.md @@ -13,26 +13,56 @@ Run agent-browser + headless Chrome inside ephemeral Vercel Sandbox microVMs. A pnpm add @vercel/sandbox ``` -The sandbox VM installs agent-browser and Chrome on first run. Use sandbox snapshots (below) to skip this step. +The sandbox VM needs system dependencies for Chromium plus agent-browser itself. Use sandbox snapshots (below) to pre-install everything for sub-second startup. ## Core Pattern ```ts import { Sandbox } from "@vercel/sandbox"; +// System libraries required by Chromium on the sandbox VM (Amazon Linux / dnf) +const CHROMIUM_SYSTEM_DEPS = [ + "nss", "nspr", "libxkbcommon", "atk", "at-spi2-atk", "at-spi2-core", + "libXcomposite", "libXdamage", "libXrandr", "libXfixes", "libXcursor", + "libXi", "libXtst", "libXScrnSaver", "libXext", "mesa-libgbm", "libdrm", + "mesa-libGL", "mesa-libEGL", "cups-libs", "alsa-lib", "pango", "cairo", + "gtk3", "dbus-libs", +]; + +function getSandboxCredentials() { + if ( + process.env.VERCEL_TOKEN && + process.env.VERCEL_TEAM_ID && + process.env.VERCEL_PROJECT_ID + ) { + return { + token: process.env.VERCEL_TOKEN, + teamId: process.env.VERCEL_TEAM_ID, + projectId: process.env.VERCEL_PROJECT_ID, + }; + } + return {}; +} + async function withBrowser( fn: (sandbox: InstanceType) => Promise, ): Promise { const snapshotId = process.env.AGENT_BROWSER_SNAPSHOT_ID; + const credentials = getSandboxCredentials(); const sandbox = snapshotId ? await Sandbox.create({ + ...credentials, source: { type: "snapshot", snapshotId }, timeout: 120_000, }) - : await Sandbox.create({ runtime: "node24", timeout: 120_000 }); + : await Sandbox.create({ ...credentials, runtime: "node24", timeout: 120_000 }); if (!snapshotId) { + await sandbox.runCommand("sh", [ + "-c", + `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`, + ]); await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]); await sandbox.runCommand("npx", ["agent-browser", "install"]); } @@ -47,6 +77,8 @@ async function withBrowser( ## Screenshot +The `screenshot --json` command saves to a file and returns the path. Read the file back as base64: + ```ts export async function screenshotUrl(url: string) { return withBrowser(async (sandbox) => { @@ -60,7 +92,9 @@ export async function screenshotUrl(url: string) { const ssResult = await sandbox.runCommand("agent-browser", [ "screenshot", "--json", ]); - const screenshot = JSON.parse(await ssResult.stdout())?.data?.base64 || ""; + const ssPath = JSON.parse(await ssResult.stdout())?.data?.path; + const b64Result = await sandbox.runCommand("base64", ["-w", "0", ssPath]); + const screenshot = (await b64Result.stdout()).trim(); await sandbox.runCommand("agent-browser", ["close"]); @@ -118,7 +152,9 @@ export async function fillAndSubmitForm(url: string, data: Record { const sandbox = await Sandbox.create({ runtime: "node24", timeout: 300_000, }); + await sandbox.runCommand("sh", [ + "-c", + `sudo dnf clean all 2>&1 && sudo dnf install -y --skip-broken ${CHROMIUM_SYSTEM_DEPS.join(" ")} 2>&1 && sudo ldconfig 2>&1`, + ]); await sandbox.runCommand("npm", ["install", "-g", "agent-browser"]); await sandbox.runCommand("npx", ["agent-browser", "install"]); @@ -168,6 +218,18 @@ npx tsx examples/environments/scripts/create-snapshot.ts Recommended for any production deployment using the Sandbox pattern. +## Authentication + +On Vercel deployments, the Sandbox SDK authenticates automatically via OIDC. For local development or explicit control, set: + +```bash +VERCEL_TOKEN= +VERCEL_TEAM_ID= +VERCEL_PROJECT_ID= +``` + +These are spread into `Sandbox.create()` calls. When absent, the SDK falls back to `VERCEL_OIDC_TOKEN` (automatic on Vercel). + ## Scheduled Workflows (Cron) Combine with Vercel Cron Jobs for recurring browser tasks: @@ -197,8 +259,9 @@ export async function GET() { | Variable | Required | Description | |---|---|---| | `AGENT_BROWSER_SNAPSHOT_ID` | No (but recommended) | Pre-built sandbox snapshot ID for sub-second startup (see above) | - -The Vercel Sandbox SDK handles OIDC authentication automatically when deployed on Vercel. For local development, run `vercel link` and `vercel env pull` to get the required tokens. +| `VERCEL_TOKEN` | No | Vercel personal access token (for local dev; OIDC is automatic on Vercel) | +| `VERCEL_TEAM_ID` | No | Vercel team ID (for local dev) | +| `VERCEL_PROJECT_ID` | No | Vercel project ID (for local dev) | ## Framework Examples @@ -214,4 +277,4 @@ The pattern works identically across frameworks. The only difference is where yo ## Example -See `examples/environments/` in the agent-browser repo for a working app with the Vercel Sandbox pattern, including a sandbox snapshot creation script and demo UI. +See `examples/environments/` in the agent-browser repo for a working app with the Vercel Sandbox pattern, including a sandbox snapshot creation script, streaming progress UI, and rate limiting. diff --git a/src/browser.ts b/src/browser.ts index a0e0499..6697cd8 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -2523,9 +2523,11 @@ export class BrowserManager { ); this.browser = null; } else if (this.kernelSessionId) { - await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey ?? undefined).catch((error) => { - console.error('Failed to close Kernel session:', error); - }); + await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey ?? undefined).catch( + (error) => { + console.error('Failed to close Kernel session:', error); + } + ); this.browser = null; } else if (this.cdpEndpoint !== null) { // CDP: only disconnect, don't close external app's pages