From f32bf9f64258641a9f6707301ee0db29bf07d7c1 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 25 Jun 2026 14:44:28 -0700 Subject: [PATCH 01/11] Add BrowseCLI in a Vercel Sandbox example Signed-off-by: Shrey Pandya --- .../browsecli-vercel-sandbox/.env.example | 13 +++ solutions/browsecli-vercel-sandbox/.gitignore | 37 ++++++ solutions/browsecli-vercel-sandbox/README.md | 73 ++++++++++++ .../app/api/run/route.ts | 77 +++++++++++++ .../browsecli-demo.sh | 58 ++++++++++ .../browsecli-vercel-sandbox/package.json | 21 ++++ solutions/browsecli-vercel-sandbox/sandbox.ts | 109 ++++++++++++++++++ .../browsecli-vercel-sandbox/tsconfig.json | 16 +++ 8 files changed, 404 insertions(+) create mode 100644 solutions/browsecli-vercel-sandbox/.env.example create mode 100644 solutions/browsecli-vercel-sandbox/.gitignore create mode 100644 solutions/browsecli-vercel-sandbox/README.md create mode 100644 solutions/browsecli-vercel-sandbox/app/api/run/route.ts create mode 100755 solutions/browsecli-vercel-sandbox/browsecli-demo.sh create mode 100644 solutions/browsecli-vercel-sandbox/package.json create mode 100644 solutions/browsecli-vercel-sandbox/sandbox.ts create mode 100644 solutions/browsecli-vercel-sandbox/tsconfig.json diff --git a/solutions/browsecli-vercel-sandbox/.env.example b/solutions/browsecli-vercel-sandbox/.env.example new file mode 100644 index 0000000000..e6b6563c55 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/.env.example @@ -0,0 +1,13 @@ +# --- Browserbase (the Verified browser the sandbox connects out to) --- +# Get these at https://www.browserbase.com/settings +BROWSERBASE_API_KEY=bb_live_xxxxxxxxxxxxxxxxxxxxxxxx + +# --- Vercel Sandbox auth (to create the Firecracker microVM) --- +# Token: https://vercel.com/account/tokens +# Team + Project IDs: Vercel dashboard → Project → Settings +VERCEL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx +VERCEL_TEAM_ID=team_xxxxxxxxxxxxxxxxxxxxxxxx +VERCEL_PROJECT_ID=prj_xxxxxxxxxxxxxxxxxxxxxxxx + +# --- Optional: override the protected site the demo opens --- +# TARGET_URL=https://nowsecure.nl diff --git a/solutions/browsecli-vercel-sandbox/.gitignore b/solutions/browsecli-vercel-sandbox/.gitignore new file mode 100644 index 0000000000..89f4fb0855 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/.gitignore @@ -0,0 +1,37 @@ +# 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 +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md new file mode 100644 index 0000000000..dab7a61ad8 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -0,0 +1,73 @@ +# BrowseCLI in a Vercel Sandbox + +Run the **BrowseCLI** inside a [**Vercel Sandbox**](https://vercel.com/docs/vercel-sandbox) — an ephemeral Firecracker microVM (NOT a serverless function) — to reach **any** website through a **Verified Browserbase browser**: residential IP, no datacenter-IP blocking, and automatic CAPTCHA / challenge solving. + +## What it is + +A Vercel Sandbox is great at running an **agent loop**, but a vanilla Firecracker microVM can't browse the real web reliably — it has a datacenter IP (instantly blocked), no anti-bot fingerprint hardening, and no CAPTCHA solving. + +So we keep the browser **out** of the sandbox. The sandbox runs the [`browse`](https://github.com/browserbase/stagehand/tree/main/packages/cli) CLI, which connects out over CDP to a **Verified Browserbase browser** that: + +- uses a **residential / verified IP** — no datacenter-IP blocking +- runs in **Verified browser mode** — passes bot-detection fingerprinting +- **auto-solves CAPTCHAs / challenges** server-side + +``` +┌─────────────────────────────┐ CDP over wss ┌────────────────────────────┐ +│ Vercel Sandbox (microVM) │ ───────────────────▶ │ Browserbase Verified │ +│ node24 + `browse` CLI │ │ browser (residential IP, │ +│ your agent loop │ ◀───────────────────── │ stealth, CAPTCHA solve) │ +└─────────────────────────────┘ page data / refs └────────────────────────────┘ +``` + +The browser lives on Browserbase, so protected sites actually load — instead of running Chrome inside the sandbox, where a datacenter IP and bot walls block real sites. + +## Files + +| File | Purpose | +| --- | --- | +| `sandbox.ts` | **Primary, runnable artifact.** Standalone script: `Sandbox.create({ runtime: 'node24' })` → `npm i -g browse` → upload + run the demo with Browserbase creds, streaming stdout. | +| `app/api/run/route.ts` | Optional Next.js (App Router) `POST /api/run` variant. | +| `browsecli-demo.sh` | The load-bearing demo: create a Verified session (`--proxies --verified --solve-captchas`), open a Cloudflare-protected page over CDP, assert real content. | +| `package.json` / `tsconfig.json` | Deps (`@vercel/sandbox`) + TS config. | +| `.env.example` | Required env vars. | + +> **Note:** Verified browsers/sessions (residential IP + automatic CAPTCHA solving) require a Browserbase **Scale** plan — see https://www.browserbase.com/pricing and https://www.browserbase.com/verified. On lower plans, drop `--verified` (you'll get Basic stealth). + +## How to run + +1. Get a Browserbase API key at [browserbase.com/settings](https://www.browserbase.com/settings). +2. Get a Vercel token at [vercel.com/account/tokens](https://vercel.com/account/tokens), plus your team + project IDs. +3. Configure env and run the standalone script: + +```bash +pnpm i +cp .env.example .env # fill in Browserbase + Vercel creds +npx tsx sandbox.ts +``` + +Expected tail: + +``` +[browsecli-demo] page title : nowsecure.nl +[browsecli-demo] RESULT: ✅ PASS — reached real content through the protected site from inside the sandbox +✓ Done — BrowseCLI reached real content from inside the Vercel Sandbox. +``` + +Override the target with `TARGET_URL=https://… npx tsx sandbox.ts`. + +### Next.js variant + +Deploy this folder as a Next.js app and call: + +```bash +curl -X POST https:///api/run \ + -H 'content-type: application/json' \ + -d '{"targetUrl":"https://nowsecure.nl"}' +``` + +Set `BROWSERBASE_API_KEY` as a Vercel project env var. Vercel Sandbox auth (`VERCEL_TOKEN` / `VERCEL_TEAM_ID` / `VERCEL_PROJECT_ID`) resolves automatically when the route runs on Vercel. + +## Why Browserbase + +Running Chrome **inside** the sandbox gets a datacenter IP, no stealth fingerprint, and no CAPTCHA solving, so it gets walled on real sites. This example instead runs the browser **on Browserbase** (residential/verified IP, stealth, server-side CAPTCHA solve) and only runs the lightweight `browse` CLI in the microVM. Net result: the sandbox can actually reach protected, bot-defended sites. diff --git a/solutions/browsecli-vercel-sandbox/app/api/run/route.ts b/solutions/browsecli-vercel-sandbox/app/api/run/route.ts new file mode 100644 index 0000000000..ed29dd4bf6 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/app/api/run/route.ts @@ -0,0 +1,77 @@ +/** + * app/api/run/route.ts — Next.js (App Router) variant of sandbox.ts. + * + * POST /api/run → spins up a Vercel Sandbox, installs the `browse` CLI, runs + * the BrowseCLI demo against a protected site through a Verified Browserbase + * browser, and streams the combined stdout/stderr back as plain text. + * + * This is the Templates-Marketplace-friendly shape (Vercel favors Next.js). + * The standalone `sandbox.ts` remains the primary, directly-runnable artifact. + * + * Env (Project / Vercel env vars): BROWSERBASE_API_KEY. + * Vercel Sandbox auth (VERCEL_TOKEN / VERCEL_TEAM_ID / VERCEL_PROJECT_ID) is + * resolved automatically when this route runs on Vercel. + */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { Sandbox } from '@vercel/sandbox'; + +export const runtime = 'nodejs'; +export const maxDuration = 300; + +export async function POST(req: Request): Promise { + const { BROWSERBASE_API_KEY } = process.env; + if (!BROWSERBASE_API_KEY) { + return new Response( + 'Missing BROWSERBASE_API_KEY in environment.', + { status: 500 }, + ); + } + + let targetUrl: string | undefined; + try { + const body = await req.json(); + targetUrl = typeof body?.targetUrl === 'string' ? body.targetUrl : undefined; + } catch { + // no body → use the default target inside the demo script + } + + const stream = new ReadableStream({ + async start(controller) { + const enc = new TextEncoder(); + const emit = (line: string) => controller.enqueue(enc.encode(line)); + + const sandbox = await Sandbox.create({ runtime: 'node24', timeout: 300_000 }); + emit(`› Sandbox ready: ${sandbox.name}\n`); + + try { + await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'browse@latest'] }); + + const demo = readFileSync(join(process.cwd(), 'browsecli-demo.sh'), 'utf8'); + await sandbox.writeFiles([ + { path: 'browsecli-demo.sh', content: Buffer.from(demo) }, + ]); + + const cmd = await sandbox.runCommand({ + cmd: 'bash', + args: ['-lc', 'chmod +x browsecli-demo.sh && ./browsecli-demo.sh'], + env: { + BROWSERBASE_API_KEY, + ...(targetUrl ? { TARGET_URL: targetUrl } : {}), + }, + }); + + emit(await cmd.stdout()); + emit(await cmd.stderr()); + emit(`\n[exit ${cmd.exitCode}]\n`); + } finally { + await sandbox.stop(); + controller.close(); + } + }, + }); + + return new Response(stream, { + headers: { 'content-type': 'text/plain; charset=utf-8' }, + }); +} diff --git a/solutions/browsecli-vercel-sandbox/browsecli-demo.sh b/solutions/browsecli-vercel-sandbox/browsecli-demo.sh new file mode 100755 index 0000000000..dd172a98b4 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/browsecli-demo.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# browsecli-demo.sh — the load-bearing demo every sandbox template runs. +# +# Inside ANY sandbox (E2B, Modal, Daytona, Vercel, Cloudflare, Fly, ...), this +# script uses the `browse` CLI to drive a *remote* Browserbase session. The +# browser never runs in the sandbox — the sandbox runs the agent/CLI and +# connects out over CDP to a Verified Browserbase browser that: +# - uses a residential/verified IP (no datacenter-IP blocking) +# - runs in Verified browser mode (passes bot-detection fingerprinting) +# - auto-solves CAPTCHAs / challenges server-side +# +# Requires: BROWSERBASE_API_KEY in env. +# Optional: TARGET_URL (default https://nowsecure.nl, a Cloudflare-protected page) +# +set -euo pipefail + +TARGET="${TARGET_URL:-https://nowsecure.nl}" +: "${BROWSERBASE_API_KEY:?BROWSERBASE_API_KEY must be set}" + +log() { printf '[browsecli-demo] %s\n' "$*"; } + +# Parse a top-level JSON string field without requiring jq (node is always present). +jget() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(d)["'"$1"'"]??""))}catch(e){process.exit(2)}})'; } + +log "browse version: $(browse --version 2>/dev/null || echo unknown)" +log "creating Verified Browserbase session (proxies + verified + solve-captchas)..." +SJSON="$(browse cloud sessions create --proxies --verified --solve-captchas --keep-alive --timeout 300)" +CONNECT="$(printf '%s' "$SJSON" | jget connectUrl)" +SID="$(printf '%s' "$SJSON" | jget id)" +[ -n "$CONNECT" ] || { log "FAIL: no connectUrl returned (check BROWSERBASE_API_KEY)"; exit 1; } +log "session ready: ${SID}" + +cleanup() { browse stop --session demo >/dev/null 2>&1 || true; } +trap cleanup EXIT + +log "opening protected target: ${TARGET}" +browse open "$TARGET" --cdp "$CONNECT" --session demo --wait load --timeout 60000 >/dev/null +sleep 3 + +TITLE="$(browse get title --session demo | jget title)" +BODY="$(browse get text body --session demo | jget text)" +BLEN="${#BODY}" + +log "page title : ${TITLE:-}" +log "body length: ${BLEN} chars" +printf '%s\n' "----- first 400 chars of page text -----" +printf '%s\n' "${BODY:0:400}" +printf '%s\n' "----------------------------------------" + +# Heuristic pass/fail: a challenge wall returns an empty/short body or a +# "checking your browser" interstitial; a solved page returns real content. +shopt -s nocasematch +if [ "$BLEN" -lt 50 ] || [[ "$BODY" == *"just a moment"* ]] || [[ "$BODY" == *"checking your browser"* ]] || [[ "$BODY" == *"enable javascript and cookies"* ]]; then + log "RESULT: ❌ BLOCKED — looks like a challenge wall, not real content" + exit 1 +fi +log "RESULT: ✅ PASS — reached real content through the protected site from inside the sandbox" diff --git a/solutions/browsecli-vercel-sandbox/package.json b/solutions/browsecli-vercel-sandbox/package.json new file mode 100644 index 0000000000..2b0e30dafa --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/package.json @@ -0,0 +1,21 @@ +{ + "name": "browsecli-vercel-sandbox", + "version": "1.0.0", + "repository": "https://github.com/vercel/examples.git", + "license": "MIT", + "private": true, + "description": "Run the BrowseCLI inside a Vercel Sandbox to reach any site via a Verified Browserbase browser (residential IP, no datacenter blocking, auto CAPTCHA-solve).", + "type": "commonjs", + "scripts": { + "start": "tsx sandbox.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@vercel/sandbox": "^2.2.1" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "tsx": "^4.19.0", + "typescript": "^5.5.0" + } +} diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts new file mode 100644 index 0000000000..a7d15c2de7 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -0,0 +1,109 @@ +/** + * sandbox.ts — run the BrowseCLI inside a Vercel Sandbox to reach any site + * through a Verified Browserbase browser (residential IP, no datacenter + * blocking, auto CAPTCHA-solve). + * + * A Vercel Sandbox is an ephemeral Firecracker microVM (NOT a serverless + * function). It is great at running an agent loop, but a vanilla microVM has a + * datacenter IP and no anti-bot hardening, so it gets blocked on the real web. + * + * The fix: keep the browser OUT of the sandbox. The sandbox runs the `browse` + * CLI, which connects out over CDP to a Verified Browserbase browser that uses + * a residential/verified IP, passes bot-detection fingerprinting, and + * auto-solves CAPTCHAs server-side. + * + * Vercel Sandbox (node + `browse`) ──CDP/wss──▶ Browserbase Verified browser + * + * Run locally: + * pnpm i + * cp .env.example .env # fill in Browserbase + Vercel creds + * npx tsx sandbox.ts + */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { Sandbox } from '@vercel/sandbox'; + +const { + BROWSERBASE_API_KEY, + // Vercel Sandbox auth — see https://vercel.com/docs/vercel-sandbox + VERCEL_TOKEN, + VERCEL_TEAM_ID, + VERCEL_PROJECT_ID, + TARGET_URL, +} = process.env; + +function requireEnv(name: string, value: string | undefined): string { + if (!value) { + console.error(`Missing required env var: ${name} (see .env.example)`); + process.exit(1); + } + return value; +} + +async function main() { + requireEnv('BROWSERBASE_API_KEY', BROWSERBASE_API_KEY); + requireEnv('VERCEL_TOKEN', VERCEL_TOKEN); + requireEnv('VERCEL_TEAM_ID', VERCEL_TEAM_ID); + requireEnv('VERCEL_PROJECT_ID', VERCEL_PROJECT_ID); + + console.log('› Creating Vercel Sandbox (Firecracker microVM, runtime=node24)…'); + const sandbox = await Sandbox.create({ + runtime: 'node24', + // 5 min is plenty for: install browse + open a Verified session + assert. + timeout: 300_000, + // Vercel resolves these from process.env automatically, but we pass them + // explicitly so a single `tsx sandbox.ts` works without extra wiring. + token: VERCEL_TOKEN, + teamId: VERCEL_TEAM_ID, + projectId: VERCEL_PROJECT_ID, + }); + console.log(`› Sandbox ready: ${sandbox.name}`); + + try { + // 1) Install the unified Browserbase CLI inside the microVM. No Chrome is + // installed here — the browser lives on Browserbase. + console.log('› Installing `browse` CLI inside the sandbox…'); + const install = await sandbox.runCommand({ + cmd: 'npm', + args: ['install', '-g', 'browse@latest'], + stderr: process.stderr, + stdout: process.stdout, + }); + if (install.exitCode !== 0) { + throw new Error(`npm install failed with exit code ${install.exitCode}`); + } + + // 2) Upload the demo script into the sandbox filesystem. + const demo = readFileSync(join(__dirname, 'browsecli-demo.sh'), 'utf8'); + await sandbox.writeFiles([ + { path: 'browsecli-demo.sh', content: Buffer.from(demo) }, + ]); + + // 3) Run the demo: create a Verified Browserbase session, open a + // Cloudflare-protected page over CDP, and assert real content. + console.log('› Running BrowseCLI demo against a protected site…'); + const run = await sandbox.runCommand({ + cmd: 'bash', + args: ['-lc', 'chmod +x browsecli-demo.sh && ./browsecli-demo.sh'], + env: { + BROWSERBASE_API_KEY: BROWSERBASE_API_KEY!, + ...(TARGET_URL ? { TARGET_URL } : {}), + }, + stderr: process.stderr, + stdout: process.stdout, + }); + + if (run.exitCode !== 0) { + console.error(`\n✗ Demo exited with code ${run.exitCode}`); + process.exit(run.exitCode ?? 1); + } + console.log('\n✓ Done — BrowseCLI reached real content from inside the Vercel Sandbox.'); + } finally { + await sandbox.stop(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/solutions/browsecli-vercel-sandbox/tsconfig.json b/solutions/browsecli-vercel-sandbox/tsconfig.json new file mode 100644 index 0000000000..5ca94098a8 --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["sandbox.ts", "app/**/*.ts"] +} From d3c942cd91a38cf9ba25a37fbf5f0ec3014632dc Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Fri, 26 Jun 2026 14:25:37 -0700 Subject: [PATCH 02/11] Switch BrowseCLI Vercel Sandbox example to an AI SDK agent Replace the scripted single-page demo with an AI SDK agent that runs inside the Vercel Sandbox and whose only tool is the browse CLI driving a Browserbase cloud browser. Adds agent.mjs, rewrites sandbox.ts/README/ .env.example, and removes the obsolete demo script and Next.js route. Signed-off-by: Shrey Pandya --- .../browsecli-vercel-sandbox/.env.example | 12 +- solutions/browsecli-vercel-sandbox/README.md | 76 +++------ solutions/browsecli-vercel-sandbox/agent.mjs | 60 +++++++ .../app/api/run/route.ts | 77 --------- .../browsecli-demo.sh | 58 ------- .../browsecli-vercel-sandbox/package.json | 4 +- solutions/browsecli-vercel-sandbox/sandbox.ts | 157 ++++++------------ .../browsecli-vercel-sandbox/tsconfig.json | 6 +- 8 files changed, 146 insertions(+), 304 deletions(-) create mode 100644 solutions/browsecli-vercel-sandbox/agent.mjs delete mode 100644 solutions/browsecli-vercel-sandbox/app/api/run/route.ts delete mode 100755 solutions/browsecli-vercel-sandbox/browsecli-demo.sh diff --git a/solutions/browsecli-vercel-sandbox/.env.example b/solutions/browsecli-vercel-sandbox/.env.example index e6b6563c55..75c9b20547 100644 --- a/solutions/browsecli-vercel-sandbox/.env.example +++ b/solutions/browsecli-vercel-sandbox/.env.example @@ -1,5 +1,9 @@ -# --- Browserbase (the Verified browser the sandbox connects out to) --- -# Get these at https://www.browserbase.com/settings +# --- Anthropic (the model that drives the agent) --- +# Get a key at https://console.anthropic.com/settings/keys +ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxx + +# --- Browserbase (the cloud browser the agent drives) --- +# Get a key at https://www.browserbase.com/settings BROWSERBASE_API_KEY=bb_live_xxxxxxxxxxxxxxxxxxxxxxxx # --- Vercel Sandbox auth (to create the Firecracker microVM) --- @@ -9,5 +13,5 @@ VERCEL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx VERCEL_TEAM_ID=team_xxxxxxxxxxxxxxxxxxxxxxxx VERCEL_PROJECT_ID=prj_xxxxxxxxxxxxxxxxxxxxxxxx -# --- Optional: override the protected site the demo opens --- -# TARGET_URL=https://nowsecure.nl +# --- Optional: override the agent's task --- +# TASK=Go to Hacker News and summarize the top story's discussion. diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index dab7a61ad8..05c951d4e3 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -1,73 +1,37 @@ -# BrowseCLI in a Vercel Sandbox +# Browser agent in a Vercel Sandbox -Run the **BrowseCLI** inside a [**Vercel Sandbox**](https://vercel.com/docs/vercel-sandbox) — an ephemeral Firecracker microVM (NOT a serverless function) — to reach **any** website through a **Verified Browserbase browser**: residential IP, no datacenter-IP blocking, and automatic CAPTCHA / challenge solving. +Run an AI agent inside a [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) — an ephemeral Firecracker microVM — whose only tool is the [`browse`](https://www.npmjs.com/package/browse) CLI. The agent reasons with the [Vercel AI SDK](https://sdk.vercel.ai), and every action it takes is a `browse` command that drives a real browser running on [Browserbase](https://www.browserbase.com). The browser itself never runs in the microVM. -## What it is - -A Vercel Sandbox is great at running an **agent loop**, but a vanilla Firecracker microVM can't browse the real web reliably — it has a datacenter IP (instantly blocked), no anti-bot fingerprint hardening, and no CAPTCHA solving. - -So we keep the browser **out** of the sandbox. The sandbox runs the [`browse`](https://github.com/browserbase/stagehand/tree/main/packages/cli) CLI, which connects out over CDP to a **Verified Browserbase browser** that: - -- uses a **residential / verified IP** — no datacenter-IP blocking -- runs in **Verified browser mode** — passes bot-detection fingerprinting -- **auto-solves CAPTCHAs / challenges** server-side +## How it works ``` ┌─────────────────────────────┐ CDP over wss ┌────────────────────────────┐ -│ Vercel Sandbox (microVM) │ ───────────────────▶ │ Browserbase Verified │ -│ node24 + `browse` CLI │ │ browser (residential IP, │ -│ your agent loop │ ◀───────────────────── │ stealth, CAPTCHA solve) │ -└─────────────────────────────┘ page data / refs └────────────────────────────┘ +│ Vercel Sandbox (microVM) │ ───────────────────▶ │ Browserbase cloud browser │ +│ node24 │ │ │ +│ AI SDK agent loop │ ◀───────────────────── │ navigates / reads pages │ +│ tool = `browse` CLI │ page data │ │ +└─────────────────────────────┘ └────────────────────────────┘ ``` -The browser lives on Browserbase, so protected sites actually load — instead of running Chrome inside the sandbox, where a datacenter IP and bot walls block real sites. - -## Files +- `sandbox.ts` (runs locally) provisions the sandbox, installs the `browse` CLI plus the AI SDK inside it, and starts the agent loop. It streams the agent's output back to your terminal. +- `agent.mjs` (runs inside the sandbox) is a single `generateText` call from the AI SDK. Its only tool shells out to `browse `. The model navigates the web by emitting `browse` commands. -| File | Purpose | -| --- | --- | -| `sandbox.ts` | **Primary, runnable artifact.** Standalone script: `Sandbox.create({ runtime: 'node24' })` → `npm i -g browse` → upload + run the demo with Browserbase creds, streaming stdout. | -| `app/api/run/route.ts` | Optional Next.js (App Router) `POST /api/run` variant. | -| `browsecli-demo.sh` | The load-bearing demo: create a Verified session (`--proxies --verified --solve-captchas`), open a Cloudflare-protected page over CDP, assert real content. | -| `package.json` / `tsconfig.json` | Deps (`@vercel/sandbox`) + TS config. | -| `.env.example` | Required env vars. | +The default task asks the agent to find a discussion on Hacker News and summarize the debate from the top comments. Override it with the `TASK` env var. -> **Note:** Verified browsers/sessions (residential IP + automatic CAPTCHA solving) require a Browserbase **Scale** plan — see https://www.browserbase.com/pricing and https://www.browserbase.com/verified. On lower plans, drop `--verified` (you'll get Basic stealth). +## Setup -## How to run +You need: -1. Get a Browserbase API key at [browserbase.com/settings](https://www.browserbase.com/settings). -2. Get a Vercel token at [vercel.com/account/tokens](https://vercel.com/account/tokens), plus your team + project IDs. -3. Configure env and run the standalone script: +- `ANTHROPIC_API_KEY` — drives the agent ([console.anthropic.com](https://console.anthropic.com/settings/keys)). +- `BROWSERBASE_API_KEY` — the cloud browser ([browserbase.com/settings](https://www.browserbase.com/settings)). +- Vercel Sandbox credentials — `VERCEL_TOKEN` ([vercel.com/account/tokens](https://vercel.com/account/tokens)), `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID` (from your Vercel project settings). ```bash -pnpm i -cp .env.example .env # fill in Browserbase + Vercel creds +npm i +cp .env.example .env # fill in the keys above npx tsx sandbox.ts ``` -Expected tail: - -``` -[browsecli-demo] page title : nowsecure.nl -[browsecli-demo] RESULT: ✅ PASS — reached real content through the protected site from inside the sandbox -✓ Done — BrowseCLI reached real content from inside the Vercel Sandbox. -``` - -Override the target with `TARGET_URL=https://… npx tsx sandbox.ts`. - -### Next.js variant - -Deploy this folder as a Next.js app and call: - -```bash -curl -X POST https:///api/run \ - -H 'content-type: application/json' \ - -d '{"targetUrl":"https://nowsecure.nl"}' -``` - -Set `BROWSERBASE_API_KEY` as a Vercel project env var. Vercel Sandbox auth (`VERCEL_TOKEN` / `VERCEL_TEAM_ID` / `VERCEL_PROJECT_ID`) resolves automatically when the route runs on Vercel. - -## Why Browserbase +A successful run provisions a sandbox, prints each `browse` command the agent issues, and ends with a `===== FINAL ANSWER =====` summary. -Running Chrome **inside** the sandbox gets a datacenter IP, no stealth fingerprint, and no CAPTCHA solving, so it gets walled on real sites. This example instead runs the browser **on Browserbase** (residential/verified IP, stealth, server-side CAPTCHA solve) and only runs the lightweight `browse` CLI in the microVM. Net result: the sandbox can actually reach protected, bot-defended sites. +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). diff --git a/solutions/browsecli-vercel-sandbox/agent.mjs b/solutions/browsecli-vercel-sandbox/agent.mjs new file mode 100644 index 0000000000..e34b5fa2cc --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/agent.mjs @@ -0,0 +1,60 @@ +// Runs INSIDE the sandbox. A Vercel AI SDK agent whose ONLY tool is the `browse` +// CLI; the browser runs remotely on Browserbase. No custom browser tool needed — +// the agent just shells out `browse `. +import { generateText, tool, stepCountIs } from 'ai'; +import { anthropic } from '@ai-sdk/anthropic'; +import { execSync } from 'node:child_process'; +import { z } from 'zod'; + +const SESSION = 'agent'; +const TASK = process.env.TASK || + 'Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.'; + +const run = (args) => { + try { + return execSync(`browse ${args} --session ${SESSION}`, { + encoding: 'utf8', timeout: 45000, killSignal: 'SIGKILL', + env: process.env, stdio: ['ignore', 'pipe', 'ignore'], + }); + } catch (e) { return `ERROR: ${(e.stdout || '') + (e.stderr || e.message || '')}`.slice(0, 400); } +}; + +const system = `You drive a REAL web browser by running the browse CLI in a shell (the browser runs remotely on Browserbase). +Each tool call runs: browse --session ${SESSION} +Commands: + open --remote # navigate (ALWAYS include --remote so it uses the cloud browser) + get markdown body # read a page as markdown (keeps links/URLs) + get text body # read a page as PLAIN TEXT (cleaner for reading comment threads) + +Plan (be efficient, ~6 calls): +1. open https://news.ycombinator.com --remote, then "get markdown body" ONCE — the front page lists ~30 stories, each with points and an "N comments" link to https://news.ycombinator.com/item?id=NNNN. (Use markdown here because it contains the item URLs.) +2. Pick the most controversial = highest comment count (and/or a divisive topic). Take its exact item URL from the markdown. +3. open that item URL --remote, then "get text body" to read the post + comments. (Use TEXT here — HN comment pages are unreadable as markdown.) +4. Write a concise summary of the debate from the top 3 comments. + +Rules: +- If a page returns ERROR or looks empty, DO NOT retry it — pick a DIFFERENT story. +- As soon as you've read ONE comment thread successfully, STOP browsing and write the summary. +- Use exact item URLs from the markdown; never guess ids.`; + +const result = await generateText({ + model: anthropic('claude-sonnet-4-5'), + stopWhen: stepCountIs(20), + system, + prompt: TASK, + tools: { + browse: tool({ + description: 'Run a browse CLI command (omit leading "browse"). e.g. open https://news.ycombinator.com --remote ; get markdown body ; get text body', + inputSchema: z.object({ args: z.string() }), + execute: async ({ args }) => { + process.stdout.write(`-> browse ${args}\n`); + const out = run(args); + process.stdout.write(` <- ${out.length} chars${out.startsWith('ERROR') ? ' [ERR]' : ''}\n`); + return out.slice(0, 22000); + }, + }), + }, +}); + +console.log('\n===== FINAL ANSWER =====\n' + (result.text || '(empty)') + `\n\n(steps: ${result.steps.length})`); +run('stop'); diff --git a/solutions/browsecli-vercel-sandbox/app/api/run/route.ts b/solutions/browsecli-vercel-sandbox/app/api/run/route.ts deleted file mode 100644 index ed29dd4bf6..0000000000 --- a/solutions/browsecli-vercel-sandbox/app/api/run/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * app/api/run/route.ts — Next.js (App Router) variant of sandbox.ts. - * - * POST /api/run → spins up a Vercel Sandbox, installs the `browse` CLI, runs - * the BrowseCLI demo against a protected site through a Verified Browserbase - * browser, and streams the combined stdout/stderr back as plain text. - * - * This is the Templates-Marketplace-friendly shape (Vercel favors Next.js). - * The standalone `sandbox.ts` remains the primary, directly-runnable artifact. - * - * Env (Project / Vercel env vars): BROWSERBASE_API_KEY. - * Vercel Sandbox auth (VERCEL_TOKEN / VERCEL_TEAM_ID / VERCEL_PROJECT_ID) is - * resolved automatically when this route runs on Vercel. - */ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { Sandbox } from '@vercel/sandbox'; - -export const runtime = 'nodejs'; -export const maxDuration = 300; - -export async function POST(req: Request): Promise { - const { BROWSERBASE_API_KEY } = process.env; - if (!BROWSERBASE_API_KEY) { - return new Response( - 'Missing BROWSERBASE_API_KEY in environment.', - { status: 500 }, - ); - } - - let targetUrl: string | undefined; - try { - const body = await req.json(); - targetUrl = typeof body?.targetUrl === 'string' ? body.targetUrl : undefined; - } catch { - // no body → use the default target inside the demo script - } - - const stream = new ReadableStream({ - async start(controller) { - const enc = new TextEncoder(); - const emit = (line: string) => controller.enqueue(enc.encode(line)); - - const sandbox = await Sandbox.create({ runtime: 'node24', timeout: 300_000 }); - emit(`› Sandbox ready: ${sandbox.name}\n`); - - try { - await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'browse@latest'] }); - - const demo = readFileSync(join(process.cwd(), 'browsecli-demo.sh'), 'utf8'); - await sandbox.writeFiles([ - { path: 'browsecli-demo.sh', content: Buffer.from(demo) }, - ]); - - const cmd = await sandbox.runCommand({ - cmd: 'bash', - args: ['-lc', 'chmod +x browsecli-demo.sh && ./browsecli-demo.sh'], - env: { - BROWSERBASE_API_KEY, - ...(targetUrl ? { TARGET_URL: targetUrl } : {}), - }, - }); - - emit(await cmd.stdout()); - emit(await cmd.stderr()); - emit(`\n[exit ${cmd.exitCode}]\n`); - } finally { - await sandbox.stop(); - controller.close(); - } - }, - }); - - return new Response(stream, { - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }); -} diff --git a/solutions/browsecli-vercel-sandbox/browsecli-demo.sh b/solutions/browsecli-vercel-sandbox/browsecli-demo.sh deleted file mode 100755 index dd172a98b4..0000000000 --- a/solutions/browsecli-vercel-sandbox/browsecli-demo.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# -# browsecli-demo.sh — the load-bearing demo every sandbox template runs. -# -# Inside ANY sandbox (E2B, Modal, Daytona, Vercel, Cloudflare, Fly, ...), this -# script uses the `browse` CLI to drive a *remote* Browserbase session. The -# browser never runs in the sandbox — the sandbox runs the agent/CLI and -# connects out over CDP to a Verified Browserbase browser that: -# - uses a residential/verified IP (no datacenter-IP blocking) -# - runs in Verified browser mode (passes bot-detection fingerprinting) -# - auto-solves CAPTCHAs / challenges server-side -# -# Requires: BROWSERBASE_API_KEY in env. -# Optional: TARGET_URL (default https://nowsecure.nl, a Cloudflare-protected page) -# -set -euo pipefail - -TARGET="${TARGET_URL:-https://nowsecure.nl}" -: "${BROWSERBASE_API_KEY:?BROWSERBASE_API_KEY must be set}" - -log() { printf '[browsecli-demo] %s\n' "$*"; } - -# Parse a top-level JSON string field without requiring jq (node is always present). -jget() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(d)["'"$1"'"]??""))}catch(e){process.exit(2)}})'; } - -log "browse version: $(browse --version 2>/dev/null || echo unknown)" -log "creating Verified Browserbase session (proxies + verified + solve-captchas)..." -SJSON="$(browse cloud sessions create --proxies --verified --solve-captchas --keep-alive --timeout 300)" -CONNECT="$(printf '%s' "$SJSON" | jget connectUrl)" -SID="$(printf '%s' "$SJSON" | jget id)" -[ -n "$CONNECT" ] || { log "FAIL: no connectUrl returned (check BROWSERBASE_API_KEY)"; exit 1; } -log "session ready: ${SID}" - -cleanup() { browse stop --session demo >/dev/null 2>&1 || true; } -trap cleanup EXIT - -log "opening protected target: ${TARGET}" -browse open "$TARGET" --cdp "$CONNECT" --session demo --wait load --timeout 60000 >/dev/null -sleep 3 - -TITLE="$(browse get title --session demo | jget title)" -BODY="$(browse get text body --session demo | jget text)" -BLEN="${#BODY}" - -log "page title : ${TITLE:-}" -log "body length: ${BLEN} chars" -printf '%s\n' "----- first 400 chars of page text -----" -printf '%s\n' "${BODY:0:400}" -printf '%s\n' "----------------------------------------" - -# Heuristic pass/fail: a challenge wall returns an empty/short body or a -# "checking your browser" interstitial; a solved page returns real content. -shopt -s nocasematch -if [ "$BLEN" -lt 50 ] || [[ "$BODY" == *"just a moment"* ]] || [[ "$BODY" == *"checking your browser"* ]] || [[ "$BODY" == *"enable javascript and cookies"* ]]; then - log "RESULT: ❌ BLOCKED — looks like a challenge wall, not real content" - exit 1 -fi -log "RESULT: ✅ PASS — reached real content through the protected site from inside the sandbox" diff --git a/solutions/browsecli-vercel-sandbox/package.json b/solutions/browsecli-vercel-sandbox/package.json index 2b0e30dafa..e4ae86d4bc 100644 --- a/solutions/browsecli-vercel-sandbox/package.json +++ b/solutions/browsecli-vercel-sandbox/package.json @@ -4,8 +4,8 @@ "repository": "https://github.com/vercel/examples.git", "license": "MIT", "private": true, - "description": "Run the BrowseCLI inside a Vercel Sandbox to reach any site via a Verified Browserbase browser (residential IP, no datacenter blocking, auto CAPTCHA-solve).", - "type": "commonjs", + "description": "Run an AI agent inside a Vercel Sandbox whose only tool is the browse CLI, driving a Browserbase cloud browser.", + "type": "module", "scripts": { "start": "tsx sandbox.ts", "typecheck": "tsc --noEmit" diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index a7d15c2de7..ed3232602d 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -1,109 +1,58 @@ -/** - * sandbox.ts — run the BrowseCLI inside a Vercel Sandbox to reach any site - * through a Verified Browserbase browser (residential IP, no datacenter - * blocking, auto CAPTCHA-solve). - * - * A Vercel Sandbox is an ephemeral Firecracker microVM (NOT a serverless - * function). It is great at running an agent loop, but a vanilla microVM has a - * datacenter IP and no anti-bot hardening, so it gets blocked on the real web. - * - * The fix: keep the browser OUT of the sandbox. The sandbox runs the `browse` - * CLI, which connects out over CDP to a Verified Browserbase browser that uses - * a residential/verified IP, passes bot-detection fingerprinting, and - * auto-solves CAPTCHAs server-side. - * - * Vercel Sandbox (node + `browse`) ──CDP/wss──▶ Browserbase Verified browser - * - * Run locally: - * pnpm i - * cp .env.example .env # fill in Browserbase + Vercel creds - * npx tsx sandbox.ts - */ +// Driver (runs locally): provisions a Vercel Sandbox, installs `browse` + the AI +// SDK inside it, and runs the agent loop IN the sandbox. The agent drives a +// Browserbase browser over CDP — so the browser never runs in the microVM. import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; import { Sandbox } from '@vercel/sandbox'; -const { - BROWSERBASE_API_KEY, - // Vercel Sandbox auth — see https://vercel.com/docs/vercel-sandbox - VERCEL_TOKEN, - VERCEL_TEAM_ID, - VERCEL_PROJECT_ID, - TARGET_URL, -} = process.env; - -function requireEnv(name: string, value: string | undefined): string { - if (!value) { - console.error(`Missing required env var: ${name} (see .env.example)`); - process.exit(1); - } - return value; -} - -async function main() { - requireEnv('BROWSERBASE_API_KEY', BROWSERBASE_API_KEY); - requireEnv('VERCEL_TOKEN', VERCEL_TOKEN); - requireEnv('VERCEL_TEAM_ID', VERCEL_TEAM_ID); - requireEnv('VERCEL_PROJECT_ID', VERCEL_PROJECT_ID); - - console.log('› Creating Vercel Sandbox (Firecracker microVM, runtime=node24)…'); - const sandbox = await Sandbox.create({ - runtime: 'node24', - // 5 min is plenty for: install browse + open a Verified session + assert. - timeout: 300_000, - // Vercel resolves these from process.env automatically, but we pass them - // explicitly so a single `tsx sandbox.ts` works without extra wiring. - token: VERCEL_TOKEN, - teamId: VERCEL_TEAM_ID, - projectId: VERCEL_PROJECT_ID, +const __dirname = dirname(fileURLToPath(import.meta.url)); +const req = (n: string): string => { + const v = process.env[n]; + if (!v) { console.error(`Missing required env var: ${n}`); process.exit(1); } + return v; +}; + +const ANTHROPIC_API_KEY = req('ANTHROPIC_API_KEY'); +const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); +const { TASK } = process.env; + +console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…'); +const sandbox = await Sandbox.create({ + runtime: 'node24', + timeout: 600_000, + token: req('VERCEL_TOKEN'), + teamId: req('VERCEL_TEAM_ID'), + projectId: req('VERCEL_PROJECT_ID'), +}); +console.log(`› Sandbox ready: ${sandbox.name}`); + +try { + // 1) Upload the agent into the sandbox. + await sandbox.writeFiles([ + { path: 'agent.mjs', content: Buffer.from(readFileSync(join(__dirname, 'agent.mjs'))) }, + { path: 'package.json', content: Buffer.from('{"name":"browse-agent","type":"module","private":true}') }, + ]); + + // 2) Install the browse CLI (globally) + the AI SDK (locally) inside the sandbox. + console.log('› Installing `browse` + AI SDK inside the sandbox…'); + const install = await sandbox.runCommand({ + cmd: 'bash', + args: ['-lc', 'npm i -g browse@latest && npm i ai @ai-sdk/anthropic zod'], + stderr: process.stderr, }); - console.log(`› Sandbox ready: ${sandbox.name}`); - - try { - // 1) Install the unified Browserbase CLI inside the microVM. No Chrome is - // installed here — the browser lives on Browserbase. - console.log('› Installing `browse` CLI inside the sandbox…'); - const install = await sandbox.runCommand({ - cmd: 'npm', - args: ['install', '-g', 'browse@latest'], - stderr: process.stderr, - stdout: process.stdout, - }); - if (install.exitCode !== 0) { - throw new Error(`npm install failed with exit code ${install.exitCode}`); - } - - // 2) Upload the demo script into the sandbox filesystem. - const demo = readFileSync(join(__dirname, 'browsecli-demo.sh'), 'utf8'); - await sandbox.writeFiles([ - { path: 'browsecli-demo.sh', content: Buffer.from(demo) }, - ]); - - // 3) Run the demo: create a Verified Browserbase session, open a - // Cloudflare-protected page over CDP, and assert real content. - console.log('› Running BrowseCLI demo against a protected site…'); - const run = await sandbox.runCommand({ - cmd: 'bash', - args: ['-lc', 'chmod +x browsecli-demo.sh && ./browsecli-demo.sh'], - env: { - BROWSERBASE_API_KEY: BROWSERBASE_API_KEY!, - ...(TARGET_URL ? { TARGET_URL } : {}), - }, - stderr: process.stderr, - stdout: process.stdout, - }); - - if (run.exitCode !== 0) { - console.error(`\n✗ Demo exited with code ${run.exitCode}`); - process.exit(run.exitCode ?? 1); - } - console.log('\n✓ Done — BrowseCLI reached real content from inside the Vercel Sandbox.'); - } finally { - await sandbox.stop(); - } + if (install.exitCode !== 0) throw new Error(`install failed (exit ${install.exitCode})`); + + // 3) Run the agent loop IN the sandbox; stream its output. Keys are injected as env. + console.log('› Running the browser agent inside the sandbox…\n'); + const run = await sandbox.runCommand({ + cmd: 'node', + args: ['agent.mjs'], + env: { ANTHROPIC_API_KEY, BROWSERBASE_API_KEY, ...(TASK ? { TASK } : {}) }, + stdout: process.stdout, + stderr: process.stderr, + }); + process.exitCode = run.exitCode ?? 0; +} finally { + await sandbox.stop(); } - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/solutions/browsecli-vercel-sandbox/tsconfig.json b/solutions/browsecli-vercel-sandbox/tsconfig.json index 5ca94098a8..b9ed128ec6 100644 --- a/solutions/browsecli-vercel-sandbox/tsconfig.json +++ b/solutions/browsecli-vercel-sandbox/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", + "module": "ESNext", + "moduleResolution": "Bundler", "lib": ["ES2022", "DOM"], "strict": true, "esModuleInterop": true, @@ -12,5 +12,5 @@ "noEmit": true, "types": ["node"] }, - "include": ["sandbox.ts", "app/**/*.ts"] + "include": ["sandbox.ts"] } From 9e6834c47749d145aa1e4e5883c6139332eb8921 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Fri, 26 Jun 2026 16:03:33 -0700 Subject: [PATCH 03/11] Rebuild BrowseCLI Vercel Sandbox example on bash-tool + ToolLoopAgent Use Vercel's idiomatic bash-tool pattern: the AI SDK ToolLoopAgent loop runs on the host and its bash tool executes inside the Vercel Sandbox, where the browse CLI drives a Browserbase cloud browser. BROWSERBASE_API_KEY is passed via Sandbox.create({ env }) so it reaches the in-sandbox browse commands without being written to a file. Removes the old uploaded agent.mjs. Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/README.md | 24 +-- solutions/browsecli-vercel-sandbox/agent.mjs | 60 -------- .../browsecli-vercel-sandbox/package.json | 9 +- solutions/browsecli-vercel-sandbox/sandbox.ts | 145 ++++++++++++++---- 4 files changed, 129 insertions(+), 109 deletions(-) delete mode 100644 solutions/browsecli-vercel-sandbox/agent.mjs diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index 05c951d4e3..013aba5d03 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -1,20 +1,20 @@ -# Browser agent in a Vercel Sandbox +# Browser agent with bash-tool in a Vercel Sandbox -Run an AI agent inside a [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) — an ephemeral Firecracker microVM — whose only tool is the [`browse`](https://www.npmjs.com/package/browse) CLI. The agent reasons with the [Vercel AI SDK](https://sdk.vercel.ai), and every action it takes is a `browse` command that drives a real browser running on [Browserbase](https://www.browserbase.com). The browser itself never runs in the microVM. +A [Vercel AI SDK](https://ai-sdk.dev) agent that uses Vercel's [`bash-tool`](https://github.com/vercel-labs/bash-tool) to run the [`browse`](https://www.npmjs.com/package/browse) CLI inside a [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) — an ephemeral Firecracker microVM. The `browse` CLI drives a real browser running on [Browserbase](https://www.browserbase.com), so the browser itself never runs in the microVM. ## How it works ``` -┌─────────────────────────────┐ CDP over wss ┌────────────────────────────┐ -│ Vercel Sandbox (microVM) │ ───────────────────▶ │ Browserbase cloud browser │ -│ node24 │ │ │ -│ AI SDK agent loop │ ◀───────────────────── │ navigates / reads pages │ -│ tool = `browse` CLI │ page data │ │ -└─────────────────────────────┘ └────────────────────────────┘ +┌──────────────────────────────┐ ┌──────────────────────────────┐ CDP/wss ┌────────────────────────────┐ +│ Host (your machine / CI) │ │ Vercel Sandbox (microVM) │ ──────────▶ │ Browserbase cloud browser │ +│ │ │ node24 │ │ │ +│ ToolLoopAgent (AI SDK) │ ─bash─▶ │ browse CLI runs here │ ◀──────────── │ navigates / reads pages │ +│ reasoning + tool loop │ │ (installed by sandbox.ts) │ page data │ │ +└──────────────────────────────┘ └──────────────────────────────┘ └────────────────────────────┘ ``` -- `sandbox.ts` (runs locally) provisions the sandbox, installs the `browse` CLI plus the AI SDK inside it, and starts the agent loop. It streams the agent's output back to your terminal. -- `agent.mjs` (runs inside the sandbox) is a single `generateText` call from the AI SDK. Its only tool shells out to `browse `. The model navigates the web by emitting `browse` commands. +- The **agent loop runs on the host**. `sandbox.ts` provisions the sandbox, installs the `browse` CLI inside it, builds a `bash` tool with `bash-tool` (`createBashTool({ sandbox })`), and runs a `ToolLoopAgent`. +- Only the **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which `bash-tool` runs in the microVM. The default task asks the agent to find a discussion on Hacker News and summarize the debate from the top comments. Override it with the `TASK` env var. @@ -23,7 +23,7 @@ The default task asks the agent to find a discussion on Hacker News and summariz You need: - `ANTHROPIC_API_KEY` — drives the agent ([console.anthropic.com](https://console.anthropic.com/settings/keys)). -- `BROWSERBASE_API_KEY` — the cloud browser ([browserbase.com/settings](https://www.browserbase.com/settings)). +- `BROWSERBASE_API_KEY` — the cloud browser ([browserbase.com/settings](https://www.browserbase.com/settings)). It is passed to the sandbox as a default environment variable (stored encrypted by Vercel and injected into the `browse` commands), so it is never written to a file in the sandbox. - Vercel Sandbox credentials — `VERCEL_TOKEN` ([vercel.com/account/tokens](https://vercel.com/account/tokens)), `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID` (from your Vercel project settings). ```bash @@ -32,6 +32,6 @@ cp .env.example .env # fill in the keys above npx tsx sandbox.ts ``` -A successful run provisions a sandbox, prints each `browse` command the agent issues, and ends with a `===== FINAL ANSWER =====` summary. +A successful run provisions a sandbox, prints each `browse` command the agent issues through the `bash` tool, and ends with a `===== FINAL ANSWER =====` summary. 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). diff --git a/solutions/browsecli-vercel-sandbox/agent.mjs b/solutions/browsecli-vercel-sandbox/agent.mjs deleted file mode 100644 index e34b5fa2cc..0000000000 --- a/solutions/browsecli-vercel-sandbox/agent.mjs +++ /dev/null @@ -1,60 +0,0 @@ -// Runs INSIDE the sandbox. A Vercel AI SDK agent whose ONLY tool is the `browse` -// CLI; the browser runs remotely on Browserbase. No custom browser tool needed — -// the agent just shells out `browse `. -import { generateText, tool, stepCountIs } from 'ai'; -import { anthropic } from '@ai-sdk/anthropic'; -import { execSync } from 'node:child_process'; -import { z } from 'zod'; - -const SESSION = 'agent'; -const TASK = process.env.TASK || - 'Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.'; - -const run = (args) => { - try { - return execSync(`browse ${args} --session ${SESSION}`, { - encoding: 'utf8', timeout: 45000, killSignal: 'SIGKILL', - env: process.env, stdio: ['ignore', 'pipe', 'ignore'], - }); - } catch (e) { return `ERROR: ${(e.stdout || '') + (e.stderr || e.message || '')}`.slice(0, 400); } -}; - -const system = `You drive a REAL web browser by running the browse CLI in a shell (the browser runs remotely on Browserbase). -Each tool call runs: browse --session ${SESSION} -Commands: - open --remote # navigate (ALWAYS include --remote so it uses the cloud browser) - get markdown body # read a page as markdown (keeps links/URLs) - get text body # read a page as PLAIN TEXT (cleaner for reading comment threads) - -Plan (be efficient, ~6 calls): -1. open https://news.ycombinator.com --remote, then "get markdown body" ONCE — the front page lists ~30 stories, each with points and an "N comments" link to https://news.ycombinator.com/item?id=NNNN. (Use markdown here because it contains the item URLs.) -2. Pick the most controversial = highest comment count (and/or a divisive topic). Take its exact item URL from the markdown. -3. open that item URL --remote, then "get text body" to read the post + comments. (Use TEXT here — HN comment pages are unreadable as markdown.) -4. Write a concise summary of the debate from the top 3 comments. - -Rules: -- If a page returns ERROR or looks empty, DO NOT retry it — pick a DIFFERENT story. -- As soon as you've read ONE comment thread successfully, STOP browsing and write the summary. -- Use exact item URLs from the markdown; never guess ids.`; - -const result = await generateText({ - model: anthropic('claude-sonnet-4-5'), - stopWhen: stepCountIs(20), - system, - prompt: TASK, - tools: { - browse: tool({ - description: 'Run a browse CLI command (omit leading "browse"). e.g. open https://news.ycombinator.com --remote ; get markdown body ; get text body', - inputSchema: z.object({ args: z.string() }), - execute: async ({ args }) => { - process.stdout.write(`-> browse ${args}\n`); - const out = run(args); - process.stdout.write(` <- ${out.length} chars${out.startsWith('ERROR') ? ' [ERR]' : ''}\n`); - return out.slice(0, 22000); - }, - }), - }, -}); - -console.log('\n===== FINAL ANSWER =====\n' + (result.text || '(empty)') + `\n\n(steps: ${result.steps.length})`); -run('stop'); diff --git a/solutions/browsecli-vercel-sandbox/package.json b/solutions/browsecli-vercel-sandbox/package.json index e4ae86d4bc..2398cc999d 100644 --- a/solutions/browsecli-vercel-sandbox/package.json +++ b/solutions/browsecli-vercel-sandbox/package.json @@ -1,17 +1,18 @@ { "name": "browsecli-vercel-sandbox", "version": "1.0.0", - "repository": "https://github.com/vercel/examples.git", - "license": "MIT", "private": true, - "description": "Run an AI agent inside a Vercel Sandbox whose only tool is the browse CLI, driving a Browserbase cloud browser.", + "description": "A bash-tool agent that runs the browse CLI inside a Vercel Sandbox, driving a Browserbase cloud browser.", "type": "module", "scripts": { "start": "tsx sandbox.ts", "typecheck": "tsc --noEmit" }, "dependencies": { - "@vercel/sandbox": "^2.2.1" + "@ai-sdk/anthropic": "^3.0.0", + "@vercel/sandbox": "^2.2.1", + "ai": "^6.0.0", + "bash-tool": "^1.3.0" }, "devDependencies": { "@types/node": "^20.14.0", diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index ed3232602d..40f7446e2f 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -1,58 +1,137 @@ -// Driver (runs locally): provisions a Vercel Sandbox, installs `browse` + the AI -// SDK inside it, and runs the agent loop IN the sandbox. The agent drives a -// Browserbase browser over CDP — so the browser never runs in the microVM. -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; +// A bash-tool agent in a Vercel Sandbox. +// +// The agent loop runs HERE on the host (Vercel AI SDK `ToolLoopAgent`). Its +// `bash` tool — built with Vercel's `bash-tool` — executes commands INSIDE an +// ephemeral Vercel Sandbox microVM. The `browse` CLI is installed in that +// sandbox and drives a real browser running on Browserbase, so the browser +// never runs in the microVM. The model navigates the web by running `browse` +// commands through the `bash` tool. import { Sandbox } from '@vercel/sandbox'; +import { createBashTool } from 'bash-tool'; +import { ToolLoopAgent, stepCountIs } from 'ai'; +import { anthropic } from '@ai-sdk/anthropic'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const req = (n: string): string => { - const v = process.env[n]; - if (!v) { console.error(`Missing required env var: ${n}`); process.exit(1); } - return v; +const req = (name: string): string => { + const value = process.env[name]; + if (!value) { + console.error(`Missing required env var: ${name}`); + process.exit(1); + } + return value; }; const ANTHROPIC_API_KEY = req('ANTHROPIC_API_KEY'); const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); -const { TASK } = process.env; +const TASK = + process.env.TASK || + 'Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.'; + +const system = `You drive a REAL web browser by running the \`browse\` CLI through the bash tool. +The browser runs remotely on Browserbase; \`browse\` is already installed and BROWSERBASE_API_KEY is already set in the shell. +Use a single named session so commands share one browser: + browse open --remote --session agent # navigate (ALWAYS pass --remote for the cloud browser) + browse get markdown body --session agent # read a page as markdown (keeps links/URLs) + browse get text body --session agent # read a page as PLAIN TEXT (cleaner for comment threads) + +Plan (be efficient, ~6 bash calls): +1. browse open https://news.ycombinator.com --remote --session agent, then "browse get markdown body --session agent" ONCE. + The front page lists ~30 stories, each with points and an "N comments" link to https://news.ycombinator.com/item?id=NNNN. + Use markdown here because it contains the item URLs and comment counts. +2. Pick the most controversial = highest comment count (and/or a divisive topic). Take its exact item URL from the markdown. +3. browse open --remote --session agent, then "browse get text body --session agent" to read the post + comments. + Use TEXT here — HN comment pages are unreadable as markdown. +4. Write a concise summary of the debate from the top 3 comments. + +Rules: +- If a bash command errors or a page looks empty, DO NOT retry it unchanged — pick a DIFFERENT story. +- As soon as you've read ONE comment thread successfully, STOP browsing and write the summary. +- Use exact item URLs from the markdown; never guess ids. +- When finished, run "browse stop --session agent" to release the browser.`; console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…'); +// BROWSERBASE_API_KEY is passed as a default sandbox env var. Vercel stores it +// encrypted server-side and injects it into every command, so the key reaches +// the `browse` commands the bash tool runs — without ever being written to a +// committed file or echoed. const sandbox = await Sandbox.create({ runtime: 'node24', timeout: 600_000, token: req('VERCEL_TOKEN'), teamId: req('VERCEL_TEAM_ID'), projectId: req('VERCEL_PROJECT_ID'), + env: { BROWSERBASE_API_KEY }, }); console.log(`› Sandbox ready: ${sandbox.name}`); try { - // 1) Upload the agent into the sandbox. - await sandbox.writeFiles([ - { path: 'agent.mjs', content: Buffer.from(readFileSync(join(__dirname, 'agent.mjs'))) }, - { path: 'package.json', content: Buffer.from('{"name":"browse-agent","type":"module","private":true}') }, - ]); - - // 2) Install the browse CLI (globally) + the AI SDK (locally) inside the sandbox. - console.log('› Installing `browse` + AI SDK inside the sandbox…'); + // Install the browse CLI globally inside the sandbox (bash runs there). + console.log('› Installing the browse CLI inside the sandbox…'); const install = await sandbox.runCommand({ - cmd: 'bash', - args: ['-lc', 'npm i -g browse@latest && npm i ai @ai-sdk/anthropic zod'], + cmd: 'npm', + args: ['install', '-g', 'browse@latest'], stderr: process.stderr, }); - if (install.exitCode !== 0) throw new Error(`install failed (exit ${install.exitCode})`); - - // 3) Run the agent loop IN the sandbox; stream its output. Keys are injected as env. - console.log('› Running the browser agent inside the sandbox…\n'); - const run = await sandbox.runCommand({ - cmd: 'node', - args: ['agent.mjs'], - env: { ANTHROPIC_API_KEY, BROWSERBASE_API_KEY, ...(TASK ? { TASK } : {}) }, - stdout: process.stdout, - stderr: process.stderr, + if (install.exitCode !== 0) throw new Error(`browse install failed (exit ${install.exitCode})`); + + // Adapt the @vercel/sandbox 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 the small explicit adapter documented in the bash-tool + // README. `bash` executes IN the microVM; the agent loop stays on the host. + const streamToString = async (stream: NodeJS.ReadableStream): Promise => { + 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'); + }; + + const adapter = { + 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: Array<{ path: string; content: string | Buffer }>) { + await sandbox.writeFiles( + files.map((f) => ({ path: f.path, content: Buffer.isBuffer(f.content) ? f.content : Buffer.from(f.content) })), + ); + }, + }; + + // Run bash from the sandbox's home dir (the bash tool prepends `cd `). + const { tools } = await createBashTool({ sandbox: adapter, destination: '/vercel/sandbox' }); + + const agent = new ToolLoopAgent({ + model: anthropic('claude-sonnet-4-5'), + tools, + instructions: system, + stopWhen: stepCountIs(20), }); - process.exitCode = run.exitCode ?? 0; + + console.log('› Running the agent (loop on host, bash in the sandbox)…\n'); + const result = await agent.generate({ + prompt: TASK, + onStepFinish: ({ toolCalls }) => { + for (const call of toolCalls) { + if (call.toolName === 'bash') { + const cmd = (call.input as { command?: string })?.command ?? ''; + process.stdout.write(`-> bash: ${cmd}\n`); + } + } + }, + }); + + console.log( + '\n===== FINAL ANSWER =====\n' + + (result.text || '(empty)') + + `\n\n(steps: ${result.steps.length})`, + ); } finally { await sandbox.stop(); } From e1ae89f4d4604242a39ef29c774c4d834900fa50 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Fri, 26 Jun 2026 16:39:24 -0700 Subject: [PATCH 04/11] Fix URL quoting for SEC EDGAR (& in query strings); generic deep-research prompt Signed-off-by: Shrey Pandya --- .../browsecli-vercel-sandbox/.env.example | 2 +- solutions/browsecli-vercel-sandbox/README.md | 2 +- solutions/browsecli-vercel-sandbox/sandbox.ts | 31 ++++++------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/solutions/browsecli-vercel-sandbox/.env.example b/solutions/browsecli-vercel-sandbox/.env.example index 75c9b20547..91fd18e19a 100644 --- a/solutions/browsecli-vercel-sandbox/.env.example +++ b/solutions/browsecli-vercel-sandbox/.env.example @@ -14,4 +14,4 @@ VERCEL_TEAM_ID=team_xxxxxxxxxxxxxxxxxxxxxxxx VERCEL_PROJECT_ID=prj_xxxxxxxxxxxxxxxxxxxxxxxx # --- Optional: override the agent's task --- -# TASK=Go to Hacker News and summarize the top story's discussion. +# TASK=Find the latest Node.js LTS version and its release date from nodejs.org. diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index 013aba5d03..4ee9a62040 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -16,7 +16,7 @@ A [Vercel AI SDK](https://ai-sdk.dev) agent that uses Vercel's [`bash-tool`](htt - The **agent loop runs on the host**. `sandbox.ts` provisions the sandbox, installs the `browse` CLI inside it, builds a `bash` tool with `bash-tool` (`createBashTool({ sandbox })`), and runs a `ToolLoopAgent`. - Only the **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which `bash-tool` runs in the microVM. -The default task asks the agent to find a discussion on Hacker News and summarize the debate from the top comments. Override it with the `TASK` env var. +The default task is a deep-research example: pull the most recent 10-Q filing for Snowflake, Datadog, and MongoDB from SEC EDGAR and return a comparison of their quarterly revenue, growth, RPO, and top risk factor. The agent plans its own steps — there are no site-specific instructions in the prompt. Override the goal with the `TASK` env var. ## Setup diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index 40f7446e2f..980b79e3c2 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -24,29 +24,18 @@ const ANTHROPIC_API_KEY = req('ANTHROPIC_API_KEY'); const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); const TASK = process.env.TASK || - 'Go to Hacker News and find the most controversial post from today, then read the top 3 comments and summarize the debate.'; + "For Snowflake, Datadog, and MongoDB, find each company's most recent 10-Q filing on SEC EDGAR (start at https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany). Open the actual primary filing document — not the filing index, cover page, or an exhibit — and extract quarterly revenue, year-over-year revenue growth, remaining performance obligations (RPO), and the single most significant risk factor. Return a comparison table across all three companies and cite each filing's URL."; -const system = `You drive a REAL web browser by running the \`browse\` CLI through the bash tool. -The browser runs remotely on Browserbase; \`browse\` is already installed and BROWSERBASE_API_KEY is already set in the shell. -Use a single named session so commands share one browser: - browse open --remote --session agent # navigate (ALWAYS pass --remote for the cloud browser) - browse get markdown body --session agent # read a page as markdown (keeps links/URLs) - browse get text body --session agent # read a page as PLAIN TEXT (cleaner for comment threads) +const system = `You are an autonomous deep-research agent. You answer questions by investigating the live web with a real browser that runs remotely on Browserbase. You drive it by running the \`browse\` CLI through the bash tool — it is already installed and authenticated (BROWSERBASE_API_KEY is set in the shell). -Plan (be efficient, ~6 bash calls): -1. browse open https://news.ycombinator.com --remote --session agent, then "browse get markdown body --session agent" ONCE. - The front page lists ~30 stories, each with points and an "N comments" link to https://news.ycombinator.com/item?id=NNNN. - Use markdown here because it contains the item URLs and comment counts. -2. Pick the most controversial = highest comment count (and/or a divisive topic). Take its exact item URL from the markdown. -3. browse open --remote --session agent, then "browse get text body --session agent" to read the post + comments. - Use TEXT here — HN comment pages are unreadable as markdown. -4. Write a concise summary of the debate from the top 3 comments. +Use one named session so every command shares one browser: + browse open --remote --session agent # navigate to a URL + browse get markdown body --session agent # read the current page as markdown (keeps links) + browse get text body --session agent # read the current page as plain text +Run \`browse --help\` to discover more commands. +Since you're typing real shell commands, wrap any URL containing shell metacharacters (e.g. & or ?) in single quotes so the shell doesn't split it, e.g. browse open 'https://example.com/path?a=1&b=2' --remote --session agent. -Rules: -- If a bash command errors or a page looks empty, DO NOT retry it unchanged — pick a DIFFERENT story. -- As soon as you've read ONE comment thread successfully, STOP browsing and write the summary. -- Use exact item URLs from the markdown; never guess ids. -- When finished, run "browse stop --session agent" to release the browser.`; +Plan your own research: break the question into sub-questions, find and open relevant sources, follow links, and read pages to gather evidence. Use several independent sources and cross-check key facts. If a page errors or comes back empty, try a different source instead of retrying it unchanged. When you can answer thoroughly, run \`browse stop --session agent\` and return a concise, well-sourced synthesis that cites the URLs you used.`; console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…'); // BROWSERBASE_API_KEY is passed as a default sandbox env var. Vercel stores it @@ -111,7 +100,7 @@ try { model: anthropic('claude-sonnet-4-5'), tools, instructions: system, - stopWhen: stepCountIs(20), + stopWhen: stepCountIs(40), }); console.log('› Running the agent (loop on host, bash in the sandbox)…\n'); From f8bc5f5230fe3f8e5f033023e68fa96566e65743 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Fri, 26 Jun 2026 17:26:14 -0700 Subject: [PATCH 05/11] Use a reliable SEC EDGAR filing-research task; effectiveness prompt notes Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/README.md | 2 +- solutions/browsecli-vercel-sandbox/sandbox.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index 4ee9a62040..f1daf126b3 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -16,7 +16,7 @@ A [Vercel AI SDK](https://ai-sdk.dev) agent that uses Vercel's [`bash-tool`](htt - The **agent loop runs on the host**. `sandbox.ts` provisions the sandbox, installs the `browse` CLI inside it, builds a `bash` tool with `bash-tool` (`createBashTool({ sandbox })`), and runs a `ToolLoopAgent`. - Only the **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which `bash-tool` runs in the microVM. -The default task is a deep-research example: pull the most recent 10-Q filing for Snowflake, Datadog, and MongoDB from SEC EDGAR and return a comparison of their quarterly revenue, growth, RPO, and top risk factor. The agent plans its own steps — there are no site-specific instructions in the prompt. Override the goal with the `TASK` env var. +The default task is a deep-research example: for Snowflake, Datadog, and MongoDB, find each company's most recent 10-Q on SEC EDGAR (filing date, fiscal period, primary-document URL) plus the date of its most recent 10-K, and return a comparison table. The agent plans its own steps — there are no site-specific instructions in the prompt. Override the goal with the `TASK` env var. ## Setup diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index 980b79e3c2..f2ca184fc4 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -24,7 +24,7 @@ const ANTHROPIC_API_KEY = req('ANTHROPIC_API_KEY'); const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); const TASK = process.env.TASK || - "For Snowflake, Datadog, and MongoDB, find each company's most recent 10-Q filing on SEC EDGAR (start at https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany). Open the actual primary filing document — not the filing index, cover page, or an exhibit — and extract quarterly revenue, year-over-year revenue growth, remaining performance obligations (RPO), and the single most significant risk factor. Return a comparison table across all three companies and cite each filing's URL."; + "Using SEC EDGAR (start at https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany), research the recent SEC filing activity of Snowflake, Datadog, and MongoDB. For each company find its most recent 10-Q — the filing date, the fiscal period it covers, and the URL of the primary filing document — plus the date of its most recent 10-K. Return a comparison table across all three companies, citing the EDGAR pages you used."; const system = `You are an autonomous deep-research agent. You answer questions by investigating the live web with a real browser that runs remotely on Browserbase. You drive it by running the \`browse\` CLI through the bash tool — it is already installed and authenticated (BROWSERBASE_API_KEY is set in the shell). @@ -35,7 +35,12 @@ Use one named session so every command shares one browser: Run \`browse --help\` to discover more commands. Since you're typing real shell commands, wrap any URL containing shell metacharacters (e.g. & or ?) in single quotes so the shell doesn't split it, e.g. browse open 'https://example.com/path?a=1&b=2' --remote --session agent. -Plan your own research: break the question into sub-questions, find and open relevant sources, follow links, and read pages to gather evidence. Use several independent sources and cross-check key facts. If a page errors or comes back empty, try a different source instead of retrying it unchanged. When you can answer thoroughly, run \`browse stop --session agent\` and return a concise, well-sourced synthesis that cites the URLs you used.`; +Plan your own research: break the question into sub-questions, find and open relevant sources, follow links, and read pages to gather evidence. Use several independent sources and cross-check key facts. If a page errors or comes back empty, try a different source instead of retrying it unchanged. When you can answer thoroughly, run \`browse stop --session agent\` and return a concise, well-sourced synthesis that cites the URLs you used. + +To stay effective: +- Pages are fully rendered (JavaScript runs) before you read them — the text/markdown you get back IS the real content. Read it carefully and extract what you need; don't assume a page "needs JavaScript" or abandon a source that already has the answer. +- Read each page once. Don't fetch the same page twice or as both markdown and text (for long pages \`browse get text body\` is best), and don't chase detours when a page you already have answers the question. +- Your steps are limited: once you have what you need for one item, move on, and leave yourself a step to write the final answer.`; console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…'); // BROWSERBASE_API_KEY is passed as a default sandbox env var. Vercel stores it From 6beace1e29e5cc3b7cb2a5bf168df6a1c04a9995 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Mon, 29 Jun 2026 17:03:10 -0700 Subject: [PATCH 06/11] Polish Vercel example: package.json standards conformance + report-direct-URL/most-recent prompt nudges Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/package.json | 2 ++ solutions/browsecli-vercel-sandbox/sandbox.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/solutions/browsecli-vercel-sandbox/package.json b/solutions/browsecli-vercel-sandbox/package.json index 2398cc999d..04d5f8b7cf 100644 --- a/solutions/browsecli-vercel-sandbox/package.json +++ b/solutions/browsecli-vercel-sandbox/package.json @@ -2,6 +2,8 @@ "name": "browsecli-vercel-sandbox", "version": "1.0.0", "private": true, + "license": "MIT", + "repository": "https://github.com/vercel/examples.git", "description": "A bash-tool agent that runs the browse CLI inside a Vercel Sandbox, driving a Browserbase cloud browser.", "type": "module", "scripts": { diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index f2ca184fc4..b8c725c33e 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -37,6 +37,10 @@ Since you're typing real shell commands, wrap any URL containing shell metachara Plan your own research: break the question into sub-questions, find and open relevant sources, follow links, and read pages to gather evidence. Use several independent sources and cross-check key facts. If a page errors or comes back empty, try a different source instead of retrying it unchanged. When you can answer thoroughly, run \`browse stop --session agent\` and return a concise, well-sourced synthesis that cites the URLs you used. +When reporting: +- When a question asks for "the most recent" or "the latest" of something, first locate the full set of candidates, then identify the single most recent one before reporting it — don't report the first one you happen to find. +- When you report a document's URL, give the direct link to the document itself, not a viewer, preview, or index page that wraps it. + To stay effective: - Pages are fully rendered (JavaScript runs) before you read them — the text/markdown you get back IS the real content. Read it carefully and extract what you need; don't assume a page "needs JavaScript" or abandon a source that already has the answer. - Read each page once. Don't fetch the same page twice or as both markdown and text (for long pages \`browse get text body\` is best), and don't chase detours when a page you already have answers the question. From fc3538bfce9ef865e5a92a306e47f519e5557d18 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 30 Jun 2026 11:30:37 -0700 Subject: [PATCH 07/11] Use claude-sonnet-5 Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/sandbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index b8c725c33e..9956830288 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -106,7 +106,7 @@ try { const { tools } = await createBashTool({ sandbox: adapter, destination: '/vercel/sandbox' }); const agent = new ToolLoopAgent({ - model: anthropic('claude-sonnet-4-5'), + model: anthropic('claude-sonnet-5'), tools, instructions: system, stopWhen: stepCountIs(40), From 6ef444fa0ce358a969d3f6c39bcf67fc91151400 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 30 Jun 2026 11:45:15 -0700 Subject: [PATCH 08/11] Route model through Vercel AI Gateway (drop ANTHROPIC_API_KEY) Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/.env.example | 17 +++++++++++++---- solutions/browsecli-vercel-sandbox/README.md | 17 +++++++++++++++-- solutions/browsecli-vercel-sandbox/package.json | 1 - solutions/browsecli-vercel-sandbox/sandbox.ts | 9 ++++++--- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/solutions/browsecli-vercel-sandbox/.env.example b/solutions/browsecli-vercel-sandbox/.env.example index 91fd18e19a..07171d00aa 100644 --- a/solutions/browsecli-vercel-sandbox/.env.example +++ b/solutions/browsecli-vercel-sandbox/.env.example @@ -1,7 +1,3 @@ -# --- Anthropic (the model that drives the agent) --- -# Get a key at https://console.anthropic.com/settings/keys -ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxx - # --- Browserbase (the cloud browser the agent drives) --- # Get a key at https://www.browserbase.com/settings BROWSERBASE_API_KEY=bb_live_xxxxxxxxxxxxxxxxxxxxxxxx @@ -13,5 +9,18 @@ VERCEL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx VERCEL_TEAM_ID=team_xxxxxxxxxxxxxxxxxxxxxxxx VERCEL_PROJECT_ID=prj_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 one Vercel auth covers both the Sandbox and 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 the Gateway reads automatically — no +# extra key to manage. (Refresh it with `vercel env pull` when it expires.) +# +# 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: override the agent's task --- # TASK=Find the latest Node.js LTS version and its release date from nodejs.org. diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index f1daf126b3..361d479004 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -14,24 +14,37 @@ A [Vercel AI SDK](https://ai-sdk.dev) agent that uses Vercel's [`bash-tool`](htt ``` - The **agent loop runs on the host**. `sandbox.ts` provisions the sandbox, installs the `browse` CLI inside it, builds a `bash` tool with `bash-tool` (`createBashTool({ sandbox })`), and runs a `ToolLoopAgent`. +- 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 no `ANTHROPIC_API_KEY` is needed. - Only the **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which `bash-tool` runs in the microVM. The default task is a deep-research example: for Snowflake, Datadog, and MongoDB, find each company's most recent 10-Q on SEC EDGAR (filing date, fiscal period, primary-document URL) plus the date of its most recent 10-K, and return a comparison table. The agent plans its own steps — there are no site-specific instructions in the prompt. Override the goal with the `TASK` env var. ## Setup +The agent's model (`anthropic/claude-sonnet-5`) is served through [Vercel AI Gateway](https://vercel.com/docs/ai-gateway), so **one Vercel auth covers both the Sandbox and the model — there is no separate `ANTHROPIC_API_KEY`.** + You need: -- `ANTHROPIC_API_KEY` — drives the agent ([console.anthropic.com](https://console.anthropic.com/settings/keys)). - `BROWSERBASE_API_KEY` — the cloud browser ([browserbase.com/settings](https://www.browserbase.com/settings)). It is passed to the sandbox as a default environment variable (stored encrypted by Vercel and injected into the `browse` commands), so it is never written to a file in the sandbox. - Vercel Sandbox credentials — `VERCEL_TOKEN` ([vercel.com/account/tokens](https://vercel.com/account/tokens)), `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID` (from your Vercel project settings). +- Model auth via AI Gateway — 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 the Gateway reads automatically — nothing else to manage: + ```bash + vercel link # link to your Vercel project + vercel env pull .env.local # writes VERCEL_OIDC_TOKEN + ``` + - **AI Gateway API key:** create one at [vercel.com/dashboard/ai-gateway](https://vercel.com/dashboard/ai-gateway) and set `AI_GATEWAY_API_KEY` in `.env`. Use this if you prefer a long-lived key over OIDC. ```bash npm i -cp .env.example .env # fill in the keys above +cp .env.example .env # fill in BROWSERBASE_API_KEY + Vercel Sandbox creds +vercel link # OIDC path: link the project… +vercel env pull .env.local # …and pull VERCEL_OIDC_TOKEN (skip if using AI_GATEWAY_API_KEY) npx tsx sandbox.ts ``` +> AI Gateway requires the Vercel team to have a payment method / 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. + A successful run provisions a sandbox, prints each `browse` command the agent issues through the `bash` tool, and ends with a `===== FINAL ANSWER =====` summary. 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). diff --git a/solutions/browsecli-vercel-sandbox/package.json b/solutions/browsecli-vercel-sandbox/package.json index 04d5f8b7cf..23fa202c38 100644 --- a/solutions/browsecli-vercel-sandbox/package.json +++ b/solutions/browsecli-vercel-sandbox/package.json @@ -11,7 +11,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.0", "@vercel/sandbox": "^2.2.1", "ai": "^6.0.0", "bash-tool": "^1.3.0" diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index 9956830288..e95985423f 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -9,7 +9,6 @@ import { Sandbox } from '@vercel/sandbox'; import { createBashTool } from 'bash-tool'; import { ToolLoopAgent, stepCountIs } from 'ai'; -import { anthropic } from '@ai-sdk/anthropic'; const req = (name: string): string => { const value = process.env[name]; @@ -20,7 +19,6 @@ const req = (name: string): string => { return value; }; -const ANTHROPIC_API_KEY = req('ANTHROPIC_API_KEY'); const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); const TASK = process.env.TASK || @@ -105,8 +103,13 @@ try { // Run bash from the sandbox's home dir (the bash tool prepends `cd `). const { tools } = await createBashTool({ sandbox: adapter, destination: '/vercel/sandbox' }); + // The model is served through Vercel AI Gateway: a bare `provider/model` + // string resolves through the default Gateway provider in ai@6. Gateway reads + // its credentials from the environment automatically — `VERCEL_OIDC_TOKEN` + // (provided by `vercel env pull` / a linked Vercel project) or + // `AI_GATEWAY_API_KEY`. No separate Anthropic key is needed. const agent = new ToolLoopAgent({ - model: anthropic('claude-sonnet-5'), + model: 'anthropic/claude-sonnet-5', tools, instructions: system, stopWhen: stepCountIs(40), From a658bf0dead6469cb798b9114c1dcf2daa786098 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 30 Jun 2026 12:07:52 -0700 Subject: [PATCH 09/11] Minimal system prompt + BROWSE_SESSION env-steering Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/sandbox.ts | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index e95985423f..c9eaeaf60c 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -24,38 +24,23 @@ const TASK = process.env.TASK || "Using SEC EDGAR (start at https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany), research the recent SEC filing activity of Snowflake, Datadog, and MongoDB. For each company find its most recent 10-Q — the filing date, the fiscal period it covers, and the URL of the primary filing document — plus the date of its most recent 10-K. Return a comparison table across all three companies, citing the EDGAR pages you used."; -const system = `You are an autonomous deep-research agent. You answer questions by investigating the live web with a real browser that runs remotely on Browserbase. You drive it by running the \`browse\` CLI through the bash tool — it is already installed and authenticated (BROWSERBASE_API_KEY is set in the shell). - -Use one named session so every command shares one browser: - browse open --remote --session agent # navigate to a URL - browse get markdown body --session agent # read the current page as markdown (keeps links) - browse get text body --session agent # read the current page as plain text -Run \`browse --help\` to discover more commands. -Since you're typing real shell commands, wrap any URL containing shell metacharacters (e.g. & or ?) in single quotes so the shell doesn't split it, e.g. browse open 'https://example.com/path?a=1&b=2' --remote --session agent. - -Plan your own research: break the question into sub-questions, find and open relevant sources, follow links, and read pages to gather evidence. Use several independent sources and cross-check key facts. If a page errors or comes back empty, try a different source instead of retrying it unchanged. When you can answer thoroughly, run \`browse stop --session agent\` and return a concise, well-sourced synthesis that cites the URLs you used. - -When reporting: -- When a question asks for "the most recent" or "the latest" of something, first locate the full set of candidates, then identify the single most recent one before reporting it — don't report the first one you happen to find. -- When you report a document's URL, give the direct link to the document itself, not a viewer, preview, or index page that wraps it. - -To stay effective: -- Pages are fully rendered (JavaScript runs) before you read them — the text/markdown you get back IS the real content. Read it carefully and extract what you need; don't assume a page "needs JavaScript" or abandon a source that already has the answer. -- Read each page once. Don't fetch the same page twice or as both markdown and text (for long pages \`browse get text body\` is best), and don't chase detours when a page you already have answers the question. -- Your steps are limited: once you have what you need for one item, move on, and leave yourself a step to write the final answer.`; +const system = `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 --help\` (and \`browse --help\` as needed), then complete the task. 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.`; console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…'); -// BROWSERBASE_API_KEY is passed as a default sandbox env var. Vercel stores it -// encrypted server-side and injects it into every command, so the key reaches -// the `browse` commands the bash tool runs — without ever being written to a -// committed file or echoed. +// 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. +// 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. const sandbox = await Sandbox.create({ runtime: 'node24', timeout: 600_000, token: req('VERCEL_TOKEN'), teamId: req('VERCEL_TEAM_ID'), projectId: req('VERCEL_PROJECT_ID'), - env: { BROWSERBASE_API_KEY }, + env: { BROWSERBASE_API_KEY, BROWSE_SESSION: 'agent' }, }); console.log(`› Sandbox ready: ${sandbox.name}`); From e7e18bcadb164028839b0fa51545dc896fd5addd Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 30 Jun 2026 12:19:05 -0700 Subject: [PATCH 10/11] Switch demo task to Amazon product research (browser-only) Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/README.md | 2 +- solutions/browsecli-vercel-sandbox/sandbox.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index 361d479004..fcc36126f7 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -17,7 +17,7 @@ A [Vercel AI SDK](https://ai-sdk.dev) agent that uses Vercel's [`bash-tool`](htt - 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 no `ANTHROPIC_API_KEY` is needed. - Only the **`bash` tool executes inside the sandbox**. The model navigates the web by emitting `browse` commands, which `bash-tool` runs in the microVM. -The default task is a deep-research example: for Snowflake, Datadog, and MongoDB, find each company's most recent 10-Q on SEC EDGAR (filing date, fiscal period, primary-document URL) plus the date of its most recent 10-K, and return a comparison table. The agent plans its own steps — there are no site-specific instructions in the prompt. Override the goal with the `TASK` env var. +The default task is a product-research 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. It is a genuinely browser-only task: Amazon search renders its results client-side and returns nothing useful to a plain HTTP fetch, so the agent has to drive a real browser. The agent plans its own steps — there are no site-specific instructions in the prompt. Override the goal with the `TASK` env var. ## Setup diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index c9eaeaf60c..a4d4a15b40 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -22,7 +22,7 @@ const req = (name: string): string => { const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); const TASK = process.env.TASK || - "Using SEC EDGAR (start at https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany), research the recent SEC filing activity of Snowflake, Datadog, and MongoDB. For each company find its most recent 10-Q — the filing date, the fiscal period it covers, and the URL of the primary filing document — plus the date of its most recent 10-K. Return a comparison table across all three companies, citing the EDGAR pages you used."; + "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."; const system = `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 --help\` (and \`browse --help\` as needed), then complete the task. 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.`; From 658f5e762697da112500e6b5c619ce142e53989b Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 30 Jun 2026 13:05:36 -0700 Subject: [PATCH 11/11] Use pnpm + commit pnpm-lock.yaml to match repo convention Switch the example's install/run commands to pnpm (npm i -> pnpm install, npx tsx sandbox.ts -> pnpm start) and commit a pnpm-lock.yaml, matching the convention every other solutions/ example follows (packageManager pnpm@9.13.0, committed pnpm-lock.yaml, no package-lock.json). Add just-bash as an explicit dependency: bash-tool lists it as both a regular dependency and a peerDependency, so under pnpm's strict node_modules layout it is not installed for the consumer, causing ERR_MODULE_NOT_FOUND at runtime. npm's flat layout masked this. Declaring it directly makes the example install and run correctly under pnpm. Files reformatted with prettier v2 to satisfy the repo's pre-commit hook. Signed-off-by: Shrey Pandya --- solutions/browsecli-vercel-sandbox/README.md | 4 +- .../browsecli-vercel-sandbox/package.json | 3 +- .../browsecli-vercel-sandbox/pnpm-lock.yaml | 1361 +++++++++++++++++ solutions/browsecli-vercel-sandbox/sandbox.ts | 94 +- 4 files changed, 1419 insertions(+), 43 deletions(-) create mode 100644 solutions/browsecli-vercel-sandbox/pnpm-lock.yaml diff --git a/solutions/browsecli-vercel-sandbox/README.md b/solutions/browsecli-vercel-sandbox/README.md index fcc36126f7..6fc2483894 100644 --- a/solutions/browsecli-vercel-sandbox/README.md +++ b/solutions/browsecli-vercel-sandbox/README.md @@ -36,11 +36,11 @@ You need: - **AI Gateway API key:** create one at [vercel.com/dashboard/ai-gateway](https://vercel.com/dashboard/ai-gateway) and set `AI_GATEWAY_API_KEY` in `.env`. Use this if you prefer a long-lived key over OIDC. ```bash -npm i +pnpm install cp .env.example .env # fill in BROWSERBASE_API_KEY + Vercel Sandbox creds vercel link # OIDC path: link the project… vercel env pull .env.local # …and pull VERCEL_OIDC_TOKEN (skip if using AI_GATEWAY_API_KEY) -npx tsx sandbox.ts +pnpm start ``` > AI Gateway requires the Vercel team to have a payment method / 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. diff --git a/solutions/browsecli-vercel-sandbox/package.json b/solutions/browsecli-vercel-sandbox/package.json index 23fa202c38..a112fe285a 100644 --- a/solutions/browsecli-vercel-sandbox/package.json +++ b/solutions/browsecli-vercel-sandbox/package.json @@ -13,7 +13,8 @@ "dependencies": { "@vercel/sandbox": "^2.2.1", "ai": "^6.0.0", - "bash-tool": "^1.3.0" + "bash-tool": "^1.3.0", + "just-bash": "^3.0.1" }, "devDependencies": { "@types/node": "^20.14.0", diff --git a/solutions/browsecli-vercel-sandbox/pnpm-lock.yaml b/solutions/browsecli-vercel-sandbox/pnpm-lock.yaml new file mode 100644 index 0000000000..2d70f152ff --- /dev/null +++ b/solutions/browsecli-vercel-sandbox/pnpm-lock.yaml @@ -0,0 +1,1361 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@vercel/sandbox': + specifier: ^2.2.1 + version: 2.3.0 + ai: + specifier: ^6.0.0 + version: 6.0.217(zod@4.4.3) + bash-tool: + specifier: ^1.3.0 + version: 1.3.17(@vercel/sandbox@2.3.0)(ai@6.0.217(zod@4.4.3))(just-bash@3.0.2) + just-bash: + specifier: ^3.0.1 + version: 3.0.2 + devDependencies: + '@types/node': + specifier: ^20.14.0 + version: 20.19.43 + tsx: + specifier: ^4.19.0 + version: 4.22.4 + typescript: + specifier: ^5.5.0 + version: 5.9.3 + +packages: + + '@ai-sdk/gateway@3.0.141': + resolution: {integrity: sha512-BVisCihanCq+rXJZHY+aKVOSHe+gQDEitSexnvU9UuRTX/P16fu4x31AZLDNIR/bWAShT3Ct+dDltO1enBPK6Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.34': + resolution: {integrity: sha512-VL3tE0RV1ZwrtC8grTfcveFoyy9X96blfnRzIx5ayeGlCyTKpsZ4U4Ej3XpjOppAyAXKNflYSHAktOHE6gTLiw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.13': + resolution: {integrity: sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw==} + engines: {node: '>=18'} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jitl/quickjs-ffi-types@0.32.0': + resolution: {integrity: sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==} + + '@jitl/quickjs-wasmfile-debug-asyncify@0.32.0': + resolution: {integrity: sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==} + + '@jitl/quickjs-wasmfile-debug-sync@0.32.0': + resolution: {integrity: sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==} + + '@jitl/quickjs-wasmfile-release-asyncify@0.32.0': + resolution: {integrity: sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==} + + '@jitl/quickjs-wasmfile-release-sync@0.32.0': + resolution: {integrity: sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==} + + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + + '@mongodb-js/zstd@7.0.0': + resolution: {integrity: sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==} + engines: {node: '>= 20.19.0'} + + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + + '@vercel/sandbox@2.3.0': + resolution: {integrity: sha512-KLhc/sj2JuftgWEivPmmDq3iFLIoYU0XIv+44paTyhk5eGGaqNl6dQdxXSqOf+N31jh55m4UWY7i8oJy8oJRrQ==} + + '@workflow/serde@4.1.0-beta.2': + resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + + ai@6.0.217: + resolution: {integrity: sha512-BCA/1sNqQwfmZ8RxK2i+oezgzwfo1IAYt+wxBVpZlRLFswyi+t5TU0/5GvV6Hic7A2/UlnMtljz/Q3F94+2WzA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bash-tool@1.3.17: + resolution: {integrity: sha512-Pgmiu85adxMsNX5q/6dK4kPfPvOkuZX0nH1OOoVv7xqK1NegFQZzlWEqOZlCZuYxC+DFqiJ0V26bOGbdtoZTpA==} + engines: {node: '>=18'} + peerDependencies: + '@vercel/sandbox': '*' + ai: ^6.0.0 + just-bash: ^2.9.3 || ^3.0.0 + peerDependenciesMeta: + '@vercel/sandbox': + optional: true + just-bash: + optional: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.9.3: + resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-unsafe@1.0.1: + resolution: {integrity: sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + jsonlines@0.1.1: + resolution: {integrity: sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==} + + just-bash@3.0.2: + resolution: {integrity: sha512-uY8LMD2NpADp1HlYUcZSw9ffuq0tRhW76sg8xkeo+ZRDMu7mbekWbKbYDWquazVVi7pqpGZlic/liMFktQKy6w==} + hasBin: true + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + modern-tar@0.7.6: + resolution: {integrity: sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==} + engines: {node: '>=18.0.0'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.93.0: + resolution: {integrity: sha512-Cu6yUpX5Iavugm8BeX7c0wgU9CvOqfd1yM6A1d2q2ZMjym7GjpASv2GdRcTq3Fx+Sb5OgBkEEpw4VnAbY6Y5RA==} + engines: {node: '>=10'} + + node-addon-api@8.9.0: + resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-liblzma@2.2.0: + resolution: {integrity: sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g==} + engines: {node: '>=16.0.0'} + hasBin: true + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + os-paths@4.4.0: + resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} + engines: {node: '>= 6.0'} + + papaparse@5.5.4: + resolution: {integrity: sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==} + + path-expression-matcher@1.6.1: + resolution: {integrity: sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==} + engines: {node: '>=14.0.0'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quickjs-emscripten-core@0.32.0: + resolution: {integrity: sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==} + + quickjs-emscripten@0.32.0: + resolution: {integrity: sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==} + engines: {node: '>=16.0.0'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + re2js@1.3.3: + resolution: {integrity: sha512-s/I5zEAo79SUK0Qw4dpZKpiMwbQ6Gz0KU2NRr7eaO4x/p2g7Vvmn3hdeXDg8VsaUjfj/ora+e9oi27LX/C9+mw==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + seek-bzip@2.0.0: + resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} + engines: {node: '>= 18'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sql.js@1.14.1: + resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + turndown@7.2.4: + resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} + engines: {node: '>=18', npm: '>=9'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xdg-app-paths@5.1.0: + resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==} + engines: {node: '>=6'} + + xdg-portable@7.3.0: + resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==} + engines: {node: '>= 6.0'} + + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@ai-sdk/gateway@3.0.141(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.13 + '@ai-sdk/provider-utils': 4.0.34(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@4.0.34(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.13 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + zod: 4.4.3 + + '@ai-sdk/provider@3.0.13': + dependencies: + json-schema: 0.4.0 + + '@borewit/text-codec@0.2.2': {} + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@jitl/quickjs-ffi-types@0.32.0': {} + + '@jitl/quickjs-wasmfile-debug-asyncify@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@jitl/quickjs-wasmfile-debug-sync@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@jitl/quickjs-wasmfile-release-asyncify@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@jitl/quickjs-wasmfile-release-sync@0.32.0': + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + '@mixmark-io/domino@2.2.0': {} + + '@mongodb-js/zstd@7.0.0': + dependencies: + node-addon-api: 8.9.0 + prebuild-install: 7.1.3 + optional: true + + '@nodable/entities@2.2.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@opentelemetry/api@1.9.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@vercel/oidc@3.2.0': {} + + '@vercel/sandbox@2.3.0': + dependencies: + '@vercel/oidc': 3.2.0 + '@workflow/serde': 4.1.0-beta.2 + async-retry: 1.3.3 + jose: 6.2.3 + jsonlines: 0.1.1 + ms: 2.1.3 + picocolors: 1.1.1 + tar-stream: 3.1.7 + undici: 7.28.0 + xdg-app-paths: 5.1.0 + zod: 4.4.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + '@workflow/serde@4.1.0-beta.2': {} + + ai@6.0.217(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 3.0.141(zod@4.4.3) + '@ai-sdk/provider': 3.0.13 + '@ai-sdk/provider-utils': 4.0.34(zod@4.4.3) + '@opentelemetry/api': 1.9.1 + zod: 4.4.3 + + anynum@1.0.1: {} + + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + + b4a@1.8.1: {} + + balanced-match@4.0.4: {} + + bare-events@2.9.1: {} + + base64-js@1.5.1: + optional: true + + bash-tool@1.3.17(@vercel/sandbox@2.3.0)(ai@6.0.217(zod@4.4.3))(just-bash@3.0.2): + dependencies: + ai: 6.0.217(zod@4.4.3) + fast-glob: 3.3.3 + yaml: 2.9.0 + zod: 3.25.76 + optionalDependencies: + '@vercel/sandbox': 2.3.0 + just-bash: 3.0.2 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + + chownr@1.1.4: + optional: true + + commander@6.2.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + deep-extend@0.6.0: + optional: true + + detect-libc@2.1.2: + optional: true + + diff@8.0.4: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + + eventsource-parser@3.1.0: {} + + expand-template@2.0.3: + optional: true + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.6.1 + xml-naming: 0.1.0 + + fast-xml-parser@5.9.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + is-unsafe: 1.0.1 + path-expression-matcher: 1.6.1 + strnum: 2.4.1 + xml-naming: 0.1.0 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fs-constants@1.0.0: + optional: true + + fsevents@2.3.3: + optional: true + + github-from-package@0.0.0: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + ieee754@1.2.1: {} + + inherits@2.0.4: + optional: true + + ini@1.3.8: + optional: true + + ini@6.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-unsafe@1.0.1: {} + + jose@6.2.3: {} + + json-schema@0.4.0: {} + + jsonlines@0.1.1: {} + + just-bash@3.0.2: + dependencies: + diff: 8.0.4 + fast-xml-parser: 5.9.3 + file-type: 21.3.4 + ini: 6.0.0 + minimatch: 10.2.5 + modern-tar: 0.7.6 + papaparse: 5.5.4 + quickjs-emscripten: 0.32.0 + re2js: 1.3.3 + seek-bzip: 2.0.0 + smol-toml: 1.7.0 + sprintf-js: 1.1.3 + sql.js: 1.14.1 + turndown: 7.2.4 + yaml: 2.9.0 + optionalDependencies: + '@mongodb-js/zstd': 7.0.0 + node-liblzma: 2.2.0 + transitivePeerDependencies: + - supports-color + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mimic-response@3.1.0: + optional: true + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimist@1.2.8: + optional: true + + mkdirp-classic@0.5.3: + optional: true + + modern-tar@0.7.6: {} + + ms@2.1.3: {} + + napi-build-utils@2.0.0: + optional: true + + node-abi@3.93.0: + dependencies: + semver: 7.8.5 + optional: true + + node-addon-api@8.9.0: + optional: true + + node-gyp-build@4.8.4: + optional: true + + node-liblzma@2.2.0: + dependencies: + node-addon-api: 8.9.0 + node-gyp-build: 4.8.4 + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + os-paths@4.4.0: {} + + papaparse@5.5.4: {} + + path-expression-matcher@1.6.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.93.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + optional: true + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + queue-microtask@1.2.3: {} + + quickjs-emscripten-core@0.32.0: + dependencies: + '@jitl/quickjs-ffi-types': 0.32.0 + + quickjs-emscripten@0.32.0: + dependencies: + '@jitl/quickjs-wasmfile-debug-asyncify': 0.32.0 + '@jitl/quickjs-wasmfile-debug-sync': 0.32.0 + '@jitl/quickjs-wasmfile-release-asyncify': 0.32.0 + '@jitl/quickjs-wasmfile-release-sync': 0.32.0 + quickjs-emscripten-core: 0.32.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + re2js@1.3.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + retry@0.13.1: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: + optional: true + + seek-bzip@2.0.0: + dependencies: + commander: 6.2.1 + + semver@7.8.5: + optional: true + + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + smol-toml@1.7.0: {} + + sprintf-js@1.1.3: {} + + sql.js@1.14.1: {} + + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-json-comments@2.0.1: + optional: true + + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + tar-stream@3.1.7: + dependencies: + b4a: 1.8.1 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + turndown@7.2.4: + dependencies: + '@mixmark-io/domino': 2.2.0 + + typescript@5.9.3: {} + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + undici@7.28.0: {} + + util-deprecate@1.0.2: + optional: true + + wrappy@1.0.2: + optional: true + + xdg-app-paths@5.1.0: + dependencies: + xdg-portable: 7.3.0 + + xdg-portable@7.3.0: + dependencies: + os-paths: 4.4.0 + + xml-naming@0.1.0: {} + + yaml@2.9.0: {} + + zod@3.25.76: {} + + zod@4.4.3: {} diff --git a/solutions/browsecli-vercel-sandbox/sandbox.ts b/solutions/browsecli-vercel-sandbox/sandbox.ts index a4d4a15b40..4597cc3368 100644 --- a/solutions/browsecli-vercel-sandbox/sandbox.ts +++ b/solutions/browsecli-vercel-sandbox/sandbox.ts @@ -6,27 +6,27 @@ // sandbox and drives a real browser running on Browserbase, so the browser // never runs in the microVM. The model navigates the web by running `browse` // commands through the `bash` tool. -import { Sandbox } from '@vercel/sandbox'; -import { createBashTool } from 'bash-tool'; -import { ToolLoopAgent, stepCountIs } from 'ai'; +import { Sandbox } from '@vercel/sandbox' +import { createBashTool } from 'bash-tool' +import { ToolLoopAgent, stepCountIs } from 'ai' const req = (name: string): string => { - const value = process.env[name]; + const value = process.env[name] if (!value) { - console.error(`Missing required env var: ${name}`); - process.exit(1); + console.error(`Missing required env var: ${name}`) + process.exit(1) } - return value; -}; + return value +} -const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY'); +const BROWSERBASE_API_KEY = req('BROWSERBASE_API_KEY') const TASK = process.env.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."; + "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." -const system = `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 --help\` (and \`browse --help\` as needed), then complete the task. 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.`; +const system = `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 --help\` (and \`browse --help\` as needed), then complete the task. 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.` -console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…'); +console.log('› Creating Vercel Sandbox (Firecracker microVM, node24)…') // 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. @@ -41,52 +41,66 @@ const sandbox = await Sandbox.create({ teamId: req('VERCEL_TEAM_ID'), projectId: req('VERCEL_PROJECT_ID'), env: { BROWSERBASE_API_KEY, BROWSE_SESSION: 'agent' }, -}); -console.log(`› Sandbox ready: ${sandbox.name}`); +}) +console.log(`› Sandbox ready: ${sandbox.name}`) try { // Install the browse CLI globally inside the sandbox (bash runs there). - console.log('› Installing the browse CLI inside the sandbox…'); + console.log('› Installing the browse CLI inside the sandbox…') const install = await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'browse@latest'], stderr: process.stderr, - }); - if (install.exitCode !== 0) throw new Error(`browse install failed (exit ${install.exitCode})`); + }) + if (install.exitCode !== 0) + throw new Error(`browse install failed (exit ${install.exitCode})`) // Adapt the @vercel/sandbox 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 the small explicit adapter documented in the bash-tool // README. `bash` executes IN the microVM; the agent loop stays on the host. - const streamToString = async (stream: NodeJS.ReadableStream): Promise => { - const chunks: Buffer[] = []; + const streamToString = async ( + stream: NodeJS.ReadableStream + ): Promise => { + const chunks: Buffer[] = [] for await (const chunk of stream) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) } - return Buffer.concat(chunks).toString('utf-8'); - }; + return Buffer.concat(chunks).toString('utf-8') + } const adapter = { 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 }; + 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); + const stream = await sandbox.readFile({ path: filePath }) + if (stream === null) throw new Error(`File not found: ${filePath}`) + return streamToString(stream) }, async writeFiles(files: Array<{ path: string; content: string | Buffer }>) { await sandbox.writeFiles( - files.map((f) => ({ path: f.path, content: Buffer.isBuffer(f.content) ? f.content : Buffer.from(f.content) })), - ); + files.map((f) => ({ + path: f.path, + content: Buffer.isBuffer(f.content) + ? f.content + : Buffer.from(f.content), + })) + ) }, - }; + } // Run bash from the sandbox's home dir (the bash tool prepends `cd `). - const { tools } = await createBashTool({ sandbox: adapter, destination: '/vercel/sandbox' }); + const { tools } = await createBashTool({ + sandbox: adapter, + destination: '/vercel/sandbox', + }) // The model is served through Vercel AI Gateway: a bare `provider/model` // string resolves through the default Gateway provider in ai@6. Gateway reads @@ -98,26 +112,26 @@ try { tools, instructions: system, stopWhen: stepCountIs(40), - }); + }) - console.log('› Running the agent (loop on host, bash in the sandbox)…\n'); + console.log('› Running the agent (loop on host, bash in the sandbox)…\n') const result = await agent.generate({ prompt: TASK, onStepFinish: ({ toolCalls }) => { for (const call of toolCalls) { if (call.toolName === 'bash') { - const cmd = (call.input as { command?: string })?.command ?? ''; - process.stdout.write(`-> bash: ${cmd}\n`); + const cmd = (call.input as { command?: string })?.command ?? '' + process.stdout.write(`-> bash: ${cmd}\n`) } } }, - }); + }) console.log( '\n===== FINAL ANSWER =====\n' + (result.text || '(empty)') + - `\n\n(steps: ${result.steps.length})`, - ); + `\n\n(steps: ${result.steps.length})` + ) } finally { - await sandbox.stop(); + await sandbox.stop() }