← Upstash RedisCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Upstash Redis
Snapshot Sep 30, 2026 · 23:07 UTC · version 1.2.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "upstash-box-js",
"description": "Work with the @upstash/box TypeScript/JavaScript SDK for sandboxed cloud containers with AI agents, shell, filesystem, git, cron schedules, snapshots, and a headless browser. Use when building with Upstash Box, creating a sandbox or isolated environment to run untrusted or agent-generated code, running AI coding agents in containers, giving an agent a cloud dev environment with a shell and repository, browser automation from a box, scheduling recurring jobs inside a box, saving and restoring snapshots, or orchestrating parallel boxes.",
"included_files": [],
"skill_md_contents": "---\nname: upstash-box-js\ndescription: Work with the @upstash/box TypeScript/JavaScript SDK for sandboxed cloud containers with AI agents, shell, filesystem, git, cron schedules, snapshots, and a headless browser. Use when building with Upstash Box, creating a sandbox or isolated environment to run untrusted or agent-generated code, running AI coding agents in containers, giving an agent a cloud dev environment with a shell and repository, browser automation from a box, scheduling recurring jobs inside a box, saving and restoring snapshots, or orchestrating parallel boxes.\nlicense: MIT\nmetadata:\n author: Upstash\n homepage: https://upstash.com\n---\n\n# @upstash/box SDK\n\nSandboxed cloud containers with built-in AI agents, shell, filesystem, git, cron schedules, and an optional headless browser.\n\nThe Python SDK (`upstash-box`) mirrors this API with snake_case names — see the\n`upstash-box-py` skill for the Python spelling of everything below.\n\n## Install & Setup\n\n```bash\nnpm install @upstash/box\nnpm install zod # peer dependency, only needed for responseSchema / browser schemas\n```\n\nSet `UPSTASH_BOX_API_KEY` env var or pass `apiKey` to constructors.\n\nAnonymous telemetry headers are sent by default. Opt out with the\n`UPSTASH_DISABLE_TELEMETRY` env var, or `enableTelemetry: false` in the config\n(the only option on runtimes without `process.env`, e.g. Cloudflare Workers).\n\n## Box Lifecycle\n\n```ts\nimport { Box, Agent, ClaudeCode, BoxApiKey } from \"@upstash/box\"\n\n// Create with agent + git + env vars\nconst box = await Box.create({\n name: \"my-box\",\n runtime: \"node\", // \"node\" | \"python\" | \"golang\" | \"ruby\" | \"rust\" (+ \"-alpine\" variants)\n size: \"small\", // \"small\" (2 CPU/4GB) | \"medium\" (4/8) | \"large\" (8/16)\n labels: [\"beta\", \"x-team\"], // max 5, ≤20 chars each\n keepAlive: true, // don't idle-pause the box\n initCommand: \"npm install && npm run dev\", // keep-alive boxes only\n browser: true, // provision headless Chromium for box.browser\n agent: {\n harness: Agent.ClaudeCode, // Agent.Codex | Agent.OpenCode | Agent.Cursor | Agent.Custom\n model: ClaudeCode.Sonnet_4_5,\n // apiKey options:\n // omit → server decides which key to use\n // BoxApiKey.UpstashKey → use Upstash-provided LLM key\n // BoxApiKey.StoredKey → use key previously stored via Upstash Console\n // \"sk-...\" → direct API key string\n apiKey: BoxApiKey.UpstashKey,\n },\n git: { // all fields optional\n token: process.env.GITHUB_TOKEN, // alternatively link your GitHub account via Upstash Console\n userName: \"Bot\",\n userEmail: \"bot@example.com\",\n },\n env: { DATABASE_URL: \"...\" },\n skills: [\"upstash/qstash-js/qstash-js\"], // owner/repo/skill-name\n timeout: 600_000, // request timeout in ms\n debug: false,\n})\n\n// Reconnect, list, delete, pause/resume\n// Box.get / Box.getByName take { apiKey, baseUrl, gitToken, timeout, debug }\nconst same = await Box.get(box.id, { gitToken: process.env.GITHUB_TOKEN })\nconst byName = await Box.getByName(\"my-box\")\nconst all = await Box.list()\nconst beta = await Box.list({ label: \"beta\" }) // filter by label\nawait box.pause() // throws on keep-alive boxes — they are never idle-paused\nawait box.resume()\nawait box.delete() // irreversible\nconst { status } = await box.getStatus()\n\nbox.id; box.size; box.keepAlive; box.cwd; box.networkPolicy\n\n// Init command (keep-alive boxes only — throws otherwise)\nawait box.setInitCommand(\"npm run dev\")\nconst script = await box.getInitCommand()\nawait box.deleteInitCommand()\n\n// Bulk delete (static, by ID)\nawait Box.delete({ boxIds: [\"box_1\", \"box_2\"] })\nconst { deleted } = await Box.deleteSnapshots({ snapshotIds: [\"snap_1\"] }) // omit ids → delete all\n```\n\n### Account-level env vars\n\nInjected into every box you create.\n\n```ts\nawait Box.setEnv(\"API_TOKEN\", \"secret\")\nconst env = await Box.listEnv() // values are masked\nawait Box.setAllEnv({ A: \"1\", B: \"2\" }) // full replace — unlisted keys are removed\nawait Box.deleteEnv(\"API_TOKEN\")\n```\n\n## Agent Runs\n\n```ts\nimport { z } from \"zod\"\n\n// Structured output with Zod schema\nconst run = await box.agent.run({\n prompt: \"Review the code for security issues\",\n responseSchema: z.object({\n verdict: z.enum([\"approved\", \"changes_requested\"]),\n findings: z.array(z.object({\n severity: z.enum([\"high\", \"medium\", \"low\"]),\n file: z.string(),\n issue: z.string(),\n })),\n }),\n timeout: 120_000,\n maxRetries: 2,\n options: { maxTurns: 20, maxBudgetUsd: 1.0, effort: \"high\" }, // harness-specific\n onToolUse: (tool) => console.log(tool.name, tool.input),\n onToolResult: (result) => console.log(result.toolCallId, result.output),\n})\n\nrun.status // \"running\" | \"completed\" | \"failed\" | \"cancelled\" | \"detached\"\nrun.result // typed from schema\nrun.cost // { inputTokens, outputTokens, cachedInputTokens, computeMs, totalUsd }\n\n// Attach files to a prompt (max 10 files, 10 MB each)\nawait box.agent.run({ prompt: \"Describe this\", files: [\"./screenshot.png\"] })\nawait box.agent.run({\n prompt: \"Describe this\",\n files: [{ data: base64, mediaType: \"image/png\", filename: \"shot.png\" }],\n})\n\n// Streaming — chunk is a discriminated union\nconst stream = await box.agent.stream({ prompt: \"Build a REST API\" })\nfor await (const chunk of stream) {\n if (chunk.type === \"text-delta\") process.stdout.write(chunk.text)\n if (chunk.type === \"reasoning\") process.stdout.write(chunk.text)\n if (chunk.type === \"tool-call\") console.log(chunk.toolName, chunk.input)\n if (chunk.type === \"tool-result\") console.log(chunk.output)\n if (chunk.type === \"finish\") console.log(chunk.output, chunk.usage, chunk.sessionId)\n // also: { type: \"start\", runId } | { type: \"stats\", cpuNs, memoryPeakBytes } | { type: \"unknown\" }\n}\nstream.status // \"completed\" after iteration finishes\nstream.result // final output\n\n// stream() takes the same prompt/files/options/timeout/onToolUse/onToolResult as run().\n// It has no responseSchema, maxRetries, or webhook — use run() for those.\n\n// Fire-and-forget with webhook\nawait box.agent.run({\n prompt: \"Run tests\",\n webhook: { url: \"https://example.com/hook\", headers: { Authorization: \"Bearer ...\" } },\n})\n```\n\n### Agent options (per harness)\n\n`options` is forwarded to the harness — the accepted keys depend on which one\nthe box runs. Typing the box (`Box.create<Agent.ClaudeCode>({...})`) narrows\n`options` to that harness's shape.\n\n```ts\n// Agent.ClaudeCode → ClaudeCodeAgentOptions\n{\n maxTurns: 20,\n maxBudgetUsd: 1.0,\n effort: \"high\", // \"low\" | \"medium\" | \"high\" | \"max\"\n thinking: { type: \"adaptive\" }, // | { type: \"enabled\", budgetTokens: 8000 } | { type: \"disabled\" }\n disallowedTools: [\"Bash\"],\n agents: { reviewer: { /* custom subagent definition */ } },\n promptSuggestions: false,\n fallbackModel: \"anthropic/claude-sonnet-4-5\",\n systemPrompt: \"You are a release engineer.\",\n}\n\n// Agent.Codex → CodexAgentOptions\n{\n modelReasoningEffort: \"high\", // \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"\n modelReasoningSummary: \"concise\", // \"auto\" | \"concise\" | \"detailed\" | \"none\"\n personality: \"pragmatic\", // \"friendly\" | \"pragmatic\" | \"none\"\n webSearch: \"live\", // or true / false\n}\n\n// Agent.OpenCode → OpenCodeAgentOptions\n{\n reasoningEffort: \"high\", // \"low\" | \"medium\" | \"high\"\n textVerbosity: \"low\", // \"low\" | \"medium\" | \"high\"\n reasoningSummary: \"auto\", // \"auto\" | \"concise\" | \"detailed\" | \"none\"\n thinking: { type: \"enabled\", budgetTokens: 8000 }, // Anthropic-backed models\n}\n\n// Agent.Cursor → free-form Record<string, unknown>\n```\n\nCodex keys are converted to the backend's snake_case for you — always write them camelCase.\n\n### Harness & model\n\n`harness` is required (`provider` / `runner` are deprecated aliases). Model\nenums: `ClaudeCode`, `OpenAICodex`, `OpenCodeModel`, `CursorModel`,\n`OpenRouterModel`, `VercelModel` — or any plain provider-prefixed string.\n\n```ts\nimport { ClaudeCode, OpenAICodex, OpenCodeModel, CursorModel, OpenRouterModel, VercelModel } from \"@upstash/box\"\n\nClaudeCode.Fable_5_1 // \"anthropic/claude-fable-5-1\"\nClaudeCode.Opus_5 // \"anthropic/claude-opus-5\"\nClaudeCode.Sonnet_5 // \"anthropic/claude-sonnet-5\"\nOpenAICodex.GPT_6_Astra // \"openai/gpt-6-astra\"\nOpenAICodex.GPT_5_6 // \"openai/gpt-5.6\"\nOpenCodeModel.Claude_Opus_5 // \"opencode/claude-opus-5\"\nCursorModel.Composer_2_5 // \"cursor/composer-2.5\"\nOpenRouterModel.Claude_Opus_5 // \"openrouter/anthropic/claude-opus-5\"\nVercelModel.GPT_5_5 // \"vercel/openai/gpt-5.5\"\n\n// Read / change the box's harness + model at runtime\nconst { harness, model } = box.modelConfig\nawait box.configureModel(\"anthropic/claude-opus-4-8\")\n\n// Which harness a bare model string implies (prefix-based)\nimport { inferDefaultProvider } from \"@upstash/box\"\ninferDefaultProvider(\"openai/gpt-5.6\") // Agent.Codex\ninferDefaultProvider(\"cursor/default\") // Agent.Cursor\n```\n\n### Custom harness\n\nRun your own agent binary inside the box instead of a managed harness.\n\n```ts\nimport { Box, Agent, runCustomHarness } from \"@upstash/box\"\n\nconst box = await Box.create({\n agent: {\n harness: Agent.Custom,\n model: \"my-agent\", // label forwarded to the process\n // command: name on PATH, or an absolute path under /workspace/home or /home/boxuser\n customHarness: { command: \"node\", args: [\"/workspace/home/agent.js\"], protocol: \"box-sse-v1\" },\n },\n})\nawait box.configureCustomHarness({ command: \"node\", args: [\"/workspace/home/agent2.js\"] })\n\n// Inside the box, agent.js emits box-sse-v1 events. The backend appends\n// `-p <prompt> --model <model> --stream` (+ `--session <id>` when resuming).\nawait runCustomHarness(async ({ prompt, model, sessionId, stream, args }, emit) => {\n emit.text(\"working...\")\n emit.reasoning(\"thinking out loud\") // -> `thinking` event\n emit.tool({ toolCallId: \"1\", name: \"Bash\", input: { command: \"ls\" } })\n emit.toolResult({ toolCallId: \"1\", output: \"file.txt\" })\n emit.emit(\"custom-event\", { any: \"payload\" }) // raw escape hatch\n // emit.error(new Error(\"boom\")) to fail the run\n return {\n output: \"done\",\n inputTokens: 10,\n outputTokens: 5,\n cachedInputTokens: 0,\n totalCostUsd: 0.01,\n sessionId,\n } // returning a plain string is shorthand for { output }\n})\n```\n\n## Run Fields\n\nEvery `run` (agent, command, or code) returns a `Run<T>`:\n\n```ts\nconst run = await box.exec.command(\"npm test\")\nrun.id // run ID\nrun.status // \"completed\" | \"failed\" | ...\nrun.result // stdout on success, stderr on failure (or typed T with responseSchema)\nrun.stdout // raw stdout (command/code runs)\nrun.stderr // raw stderr (command/code runs)\nrun.exitCode // number | null (null for agent runs)\nrun.cost // { inputTokens, outputTokens, cachedInputTokens, computeMs, totalUsd }\n\nawait run.cancel() // cancel a running run\nconst logs = await run.logs() // [{ timestamp, level, message }]\n\n// Box-level history\nconst entries = await box.logs({ limit: 100, offset: 0 }) // [{ timestamp, level, source, message }]\nconst runs = await box.listRuns() // backend run records, newest first\n```\n\n## Shell Execution\n\n```ts\n// Run commands\nconst run = await box.exec.command(\"echo hello && ls -la\")\n\n// Run code snippets — lang: \"js\" | \"ts\" | \"python\"\nconst run2 = await box.exec.code({ code: \"console.log(1+1)\", lang: \"js\", timeout: 10_000 })\n\n// Streaming shell / code\nconst stream = await box.exec.stream(\"npm run build\")\nconst stream2 = await box.exec.streamCode({ code: \"print('hi')\", lang: \"python\" })\nfor await (const chunk of stream) {\n // chunk: { type: \"output\", data } | { type: \"exit\", exitCode, cpuNs }\n}\n```\n\n### Live sessions\n\n`exec.session()` opens a WebSocket to a process that is *still running* — stdin,\nstreamed stdout/stderr, PTY resize, and signals. Node-only: auth is a handshake\nheader, which browsers cannot set (`ws` ships as an SDK dependency, nothing to\ninstall). Available on `Box` and `EphemeralBox`.\n\n```ts\nconst session = await box.exec.session({\n cmd: \"sort\", // run via `bash -lc`; `argv: [\"sort\"]` runs the program with\n // no shell and takes precedence over cmd\n cwd: \"/workspace/home\", // defaults to the box's tracked cwd\n env: [\"LOG_LEVEL=debug\"], // KEY=VALUE entries overlaid on the box environment\n onStdout: (bytes) => process.stdout.write(bytes), // Uint8Array\n onStderr: (bytes) => process.stderr.write(bytes), // separate stream unless tty\n})\n\nsession.pid // in-box PID, always non-zero\nsession.execId // server-side exec id\nsession.write(\"banana\\napple\\n\")\nsession.endStdin() // EOF — a command that reads to EOF now exits by itself\nconst exitCode = await session.wait() // -1 if torn down while still running\nsession.close() // hang up; also kills the process\n\n// Interactive programs / TUIs — tty allocates a real PTY, merging stderr into stdout\nconst shell = await box.exec.session({ argv: [\"bash\", \"-i\"], tty: true, rows: 40, cols: 120 })\nshell.resize(50, 160)\nshell.kill(\"INT\") // allowlist: TERM KILL INT HUP TSTP QUIT USR1 USR2 (default TERM)\nshell.terminate(5000) // server-side SIGTERM, then SIGKILL after the grace (first call wins)\n```\n\nThe session owns the process: `close()`, a dropped connection, or your process\nexiting all kill the command, and a session cannot be reattached. Use `wait()`\nto run something to completion.\n\n## Filesystem\n\n```ts\nawait box.files.write({ path: \"/workspace/home/app.js\", content: \"console.log('hi')\" })\nconst content = await box.files.read(\"/workspace/home/app.js\")\nconst entries = await box.files.list(\"/workspace/home\") // [{ name, path, size, is_dir, mod_time }]\n\n// Binary files — use encoding: \"base64\" for read and write\nawait box.files.write({ path: \"/workspace/home/image.png\", content: base64String, encoding: \"base64\" })\nconst b64 = await box.files.read(\"/workspace/home/image.png\", { encoding: \"base64\" })\n\n// Bounded byte-range read — the *presence* of `length` selects the range, so\n// { length: 0 } reads zero bytes rather than the whole file. Server caps it at 8 MiB.\nconst head = await box.files.read(\"/workspace/home/big.log\", { length: 64 * 1024 })\nconst slice = await box.files.read(\"/workspace/home/big.log\", { offset: 1024, length: 512 })\n\n// Metadata — defaults to lstat, so a symlink reports type \"symlink\"\nconst stat = await box.files.stat(\"/workspace/home/app.js\")\n// stat: { type: \"file\" | \"directory\" | \"symlink\" | \"other\", size, mod_time, inode, version }\n// `version` is an opaque freshness token (inode + mtime + size) for optimistic-concurrency\n// guards — compare it for equality, never parse it.\nconst target = await box.files.stat(\"/workspace/home/link\", { follow: true }) // dereference\n\n// Directories, moves, deletes\nawait box.files.mkdir(\"build/cache\", { parents: true }) // parents mirrors `mkdir -p`\nawait box.files.rename(\"draft.md\", \"docs/final.md\") // move/rename\nawait box.files.remove(\"build/cache\", { recursive: true }) // recursive required for a directory\n\n// Upload local files\nawait box.files.upload([{ path: \"./local/file.txt\", destination: \"/workspace/home/file.txt\" }])\n\n// Download — `folder` is a path INSIDE the box; files land in ./<basename>\nawait box.files.download({ folder: \"src\" }) // → ./src\nawait box.files.download() // whole cwd → ./workspace\n```\n\n## cd / Working Directory\n\nThe SDK tracks `cwd` client-side. All operations (exec, files, git, agent) run relative to it.\n\n```ts\nbox.cwd // current working directory (starts at /workspace/home)\nawait box.cd(\"my-repo\") // relative to current cwd\nawait box.cd(\"/workspace/home/other\") // absolute path\n```\n\n## Git\n\nClones land inside the box's isolated container, never on the caller's machine. Cloned\ncode is data until something runs it — treat an untrusted repo as untrusted input, and\npair it with a restrictive `networkPolicy` (see below) before running its build or tests.\n\nEvery git call except `clone` runs in the box's current directory, so `cd` into the\nclone first. At the workspace root there is no repository, and `status` comes back\nempty, which reads as a clean tree.\n\n```ts\nawait box.git.clone({ repo: \"github.com/org/repo\", branch: \"main\" })\nawait box.git.clone({ repo: \"github.com/org/repo\", depth: 1 }) // shallow clone\nawait box.git.clone({ repo: \"github.com/org/repo\", folder: \"my-app\" }) // destination\nawait box.cd(\"repo\") // the clone lands in a directory named after the repo\n\nconst status = await box.git.status()\nconst diff = await box.git.diff()\nconst { sha } = await box.git.commit({\n message: \"fix: resolve bug\",\n authorName: \"Jane Doe\", // optional per-commit override\n authorEmail: \"jane@example.com\",\n})\nawait box.git.push({ branch: \"feature/fix\" })\n\nawait box.git.checkout({ branch: \"release/v2\" })\nconst pr = await box.git.createPR({ title: \"Fix bug\", body: \"...\", base: \"main\" })\n// pr: { url, number, title, base }\n\n// Update the box-wide git identity\nconst cfg = await box.git.updateConfig({ userName: \"Bot\", userEmail: \"bot@example.com\" })\n// cfg: { git_user_name, git_user_email }\n\n// Arbitrary git commands. Check exit_code: 128 means the cwd is not a repository.\nconst { output, exit_code } = await box.git.exec({ args: [\"log\", \"--oneline\", \"-5\"] })\n```\n\n## Schedules\n\nCron tasks on a box — shell commands or agent prompts. Available on `Box` and `EphemeralBox`. Cron is UTC.\n\n```ts\nconst execSchedule = await box.schedule.exec({\n cron: \"* * * * *\",\n command: [\"bash\", \"-c\", \"date >> /workspace/home/cron.log\"],\n folder: \"/workspace/home\", // optional cwd override\n webhookUrl: \"https://example.com/hook\",\n webhookHeaders: { Authorization: \"Bearer ...\" },\n})\n\nconst agentSchedule = await box.schedule.agent({\n cron: \"0 9 * * *\",\n prompt: \"Run the test suite and fix any failures\",\n folder: \"/workspace/home/repo\", // optional cwd override\n model: \"anthropic/claude-sonnet-5\", // optional override\n options: { maxBudgetUsd: 1.0, effort: \"high\" },\n timeout: 300_000,\n webhookUrl: \"https://example.com/hook\",\n webhookHeaders: { Authorization: \"Bearer ...\" },\n})\n\nconst schedules = await box.schedule.list()\nconst one = await box.schedule.get(agentSchedule.id)\n\n// Partial update — omitted fields keep their value, \"\" / [] / {} clear a field,\n// `options: null` clears agent options. The schedule's type cannot change.\n// Updatable: cron, command, prompt, folder, model, options, timeout, webhookUrl, webhookHeaders\nawait box.schedule.update(agentSchedule.id, { cron: \"0 18 * * *\", webhookUrl: \"\" })\n\nawait box.schedule.pause(agentSchedule.id)\nawait box.schedule.resume(agentSchedule.id)\nawait box.schedule.delete(agentSchedule.id)\n```\n\n## Snapshots\n\n```ts\n// Snapshot — checkpoint workspace state\nconst snap = await box.snapshot({ name: \"after-setup\" })\n// snap: { id, name, box_id, size_bytes, status, created_at }\n\n// fromSnapshot takes a BoxConfig: name, labels, size, keepAlive, initCommand, runtime,\n// agent, git, env, attachHeaders, networkPolicy. It does NOT send `browser`, `skills`,\n// or `mcpServers` (the Python SDK does) — add skills with box.skills.add() afterwards,\n// and use Box.create({ browser: true }) when you need Chromium.\nconst restored = await Box.fromSnapshot(snap.id, {\n size: \"medium\",\n keepAlive: true,\n // git identity is forwarded, not just the token\n git: { token: process.env.GITHUB_TOKEN, userName: \"Bot\", userEmail: \"bot@example.com\" },\n env: { DATABASE_URL: \"...\" },\n})\nconst snaps = await box.listSnapshots()\nawait box.deleteSnapshot(snap.id)\n```\n\n## Browser\n\nCreate the box with `browser: true` to drive a headless Chromium. Tab management\nlives on `box.browser`; every page operation lives on the `Tab` handle.\n`extract` / `observe` / `act(instruction)` are AI-powered and metered;\n`act(action)` replays an already-resolved action with no LLM call and no tokens.\n\n```ts\nimport { z } from \"zod\"\n\nconst box = await Box.create({\n browser: true,\n agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },\n})\n\n// Tabs\nconst tab = await box.browser.tab.create(\"https://example.com\", { waitUntil: \"load\", timeout: 30_000 })\nconst tabs = await box.browser.listTabs()\nconst again = box.browser.getTab(tab.id) // no network call\ntab.id; tab.url; tab.title // handle metadata, no network call\n\n// Page operations\nconst content = await tab.goto(\"https://news.ycombinator.com\") // { title, url, text, links }\nconst current = await tab.content()\nconst png = await tab.screenshot() // Uint8Array\nconst b64 = await tab.screenshot({ type: \"base64\", fullPage: true })\n\n// AI operations (metered) — extract/observe/act take an optional { model } override,\n// defaulting to the box's model (or anthropic/claude-sonnet-4-5 when it has none)\nconst data = await tab.extract(\n \"Top story title and points\",\n z.object({ title: z.string(), points: z.number() }),\n { model: \"anthropic/claude-sonnet-4-5\" },\n)\n// observe → actionable elements, each carrying a replayable method + arguments\nconst { elements } = await tab.observe(\"What can I click?\", { model: \"openai/gpt-5.6\" })\n// elements: [{ description, selector?, url?, method?, arguments? }]\n\nconst acted = await tab.act(\"Click the first headline\")\n// acted: { success, message, actionDescription, actions, cacheStatus?, inputTokens, outputTokens }\n\n// Replay a pre-resolved action — no LLM call, no tokens, no model provider key.\n// Takes a BrowserAction (= BrowserObserveElement | BrowserActAction); `model` is\n// ignored in this form, and an action without a `selector` throws.\nawait tab.act(elements[0])\nawait tab.act(acted.actions[0])\n\n// Live view + raw CDP\nconst liveUrl = await tab.liveViewUrl() // view-only screencast page/iframe\nconst cdpUrl = await box.browser.cdpUrl() // wss://…?token=… — no extra auth wiring\nawait tab.close()\n\n// Drive the same browser from Playwright / Puppeteer / Stagehand\nimport { chromium } from \"playwright-core\"\nconst remote = await chromium.connectOverCDP(cdpUrl)\nconst context = remote.contexts()[0] ?? (await remote.newContext())\nconst page = context.pages()[0] ?? (await context.newPage())\nawait page.goto(\"https://example.com\")\n// Stagehand: new Stagehand({ env: \"LOCAL\", localBrowserLaunchOptions: { cdpUrl } })\n\n// Session recordings (HLS playback URL + MP4 download, chapter markers).\n// One active recording per box; captures all tabs and follows the foreground.\n// Auto-stops after maxDurationSeconds or ~3 minutes of no on-screen activity.\nconst handle = await box.browser.recordings.start({ maxDurationSeconds: 600 }) // default & max 600\nconst recording = await handle.stop()\n// or stop whatever is recording on the box, without a handle:\n// const recording = await box.browser.recordings.stop()\n// recording: { id, boxId, status, startedAt, endedAt, durationMs, sizeBytes, mp4SizeBytes,\n// segmentCount, markers, stoppedReason, maxDurationSeconds, expiresAt, playlistUrl }\n// markers: { type: \"tab_switch\", atMs, endMs?, label?, tabId? }\n// expiresAt is epoch ms (videos retained 14 days); playlistUrl is API-served — fetch it\n// with an `X-Box-Api-Key: <apiKey>` header (hls.js / Safari / ffplay).\nconst all = await box.browser.recordings.list()\nconst one = await box.browser.recordings.get(recording.id)\n\n// Download the video to a local file — returns the path written.\n// Defaults to ./box-recording-<id>.mp4 (.ts for recordings captured before MP4 support).\nconst file = await box.browser.recordings.download(recording.id)\nawait box.browser.recordings.download(recording.id, { path: \"./out/demo.mp4\" })\n```\n\n### Multi-step browser goals\n\n`tab.run()` — the autonomous multi-step browser agent — was **removed in 0.7.0**,\nalong with the `BrowserRunOptions` / `BrowserRunResult` / `BrowserRunStep` types\n(Stagehand v4 dropped the underlying agent primitive). The DOM-aware browser now\nexposes `observe`, `act`, and `extract` only. Three replacements:\n\n**1. Drive your own loop** — resolve steps once with `observe`, then replay them\nwith `act(action)` so the model stays out of the hot path; `extract` is the stop check.\n\n```ts\nconst { elements } = await tab.observe(\"the product links in the listing\")\nconst actions = elements.filter((e) => e.selector)\n\nfor (const action of actions.slice(0, 5)) {\n await tab.goto(START) // deterministic reset, no browser-AI tokens\n await tab.act(action) // replay the resolved click: no LLM, no tokens\n const item = await tab.extract(\"title and price\", z.object({ title: z.string() }))\n}\n```\n\n**2. Hand the goal to the in-box agent** — `browser: true` auto-wires the\nchrome-devtools MCP (Chromium already warmed on 127.0.0.1:9222) into the box's\ncoding agent, so `box.agent.run({ prompt })` drives the browser itself and iterates\nuntil done. No `tab.create()` needed first. This bills coding-agent model tokens\nrather than browser-AI metering, and needs an agent harness + key.\n\n**3. Connect over CDP** with Playwright / Puppeteer via `box.browser.cdpUrl()` when\nthe flow is fully deterministic.\n\n## EphemeralBox\n\nLightweight, short-lived boxes (max 3 days). Supports `exec`, `files`, `schedule`, `cd`, network policy, and snapshots. No agent, git, skills, labels namespace, browser, or public URLs.\n\n```ts\nimport { EphemeralBox } from \"@upstash/box\"\n\nconst ebox = await EphemeralBox.create({\n name: \"scratch-box\",\n runtime: \"python\",\n size: \"small\",\n ttl: 3600, // seconds, max 259200 (3 days), default 259200\n env: { API_KEY: \"...\" },\n labels: [\"scratch\"], // settable at create time; filter via Box.list({ label })\n networkPolicy: { mode: \"deny-all\" },\n attachHeaders: { \"api.stripe.com\": { Authorization: \"Bearer sk_live_...\" } },\n})\n\nebox.networkPolicy\n\nebox.expiresAt // unix timestamp when auto-deleted\nawait ebox.exec.command(\"python -c 'print(1+1)'\")\nawait ebox.exec.code({ code: \"print('hi')\", lang: \"python\" })\nawait ebox.exec.session({ argv: [\"bash\", \"-i\"], tty: true }) // whole exec namespace, session included\nawait ebox.files.write({ path: \"/workspace/home/data.json\", content: \"{}\" })\nawait ebox.files.stat(\"/workspace/home/data.json\") // whole files namespace, stat/mkdir/rename/remove included\nawait ebox.schedule.exec({ cron: \"* * * * *\", command: [\"bash\", \"-c\", \"date\"] })\nawait ebox.cd(\"subdir\")\nconst snap = await ebox.snapshot({ name: \"checkpoint\" })\nawait ebox.listSnapshots()\nawait ebox.deleteSnapshot(snap.id)\nconst { status } = await ebox.getStatus()\nawait ebox.delete()\n\n// Restore from snapshot\nconst ebox2 = await EphemeralBox.fromSnapshot(snap.id, { ttl: 7200 })\n\n// Statics: EphemeralBox.delete({ boxIds }) and EphemeralBox.deleteSnapshots() are the\n// Box ones. EphemeralBox.getByName() is Box.get — it returns a full `Box`, not an\n// `EphemeralBox` (quirk mirrored in the Python SDK).\n```\n\n## Public URLs\n\nExpose box ports as public URLs with optional auth.\n\n```ts\nconst publicURL = await box.getPublicURL(3000)\n// publicURL: { url: \"https://{id}-3000.preview.box.upstash.com\", port }\n\nconst authed = await box.getPublicURL(3000, { bearerToken: true })\n// authed: { url, port, token }\n\nconst basic = await box.getPublicURL(3000, { basicAuth: true })\n// basic: { url, port, username, password }\n\nconst { publicURLs } = await box.listPublicURLs()\nawait box.deletePublicURL(3000)\n```\n\n## Skills\n\nInstall agent skills from the Context7 registry. Format: `owner/repo/skill-name`.\n\nAn installed skill becomes instructions for the box's agent, so pin skills to owners you\ntrust the same way you would a dependency. Skills resolve from the registry at box\ncreation, not from arbitrary URLs, and they only ever run inside the box's container.\n\n```ts\nconst box = await Box.create({ skills: [\"upstash/qstash-js/qstash-js\"] })\n\nawait box.skills.add(\"upstash/workflow-js/workflow-js\")\nconst enabled = await box.skills.list()\nawait box.skills.remove(\"upstash/workflow-js/workflow-js\")\n```\n\n## Labels\n\n```ts\nconst labels = await box.labels.add(\"prod\") // returns the updated set\nawait box.labels.remove(\"beta\")\nconst current = await box.labels.list()\nconst prodBoxes = await Box.list({ label: \"prod\" })\n```\n\n## Network Policy & Outbound Headers\n\n```ts\nconst box = await Box.create({\n // mode: \"allow-all\" (default) | \"deny-all\" | \"custom\"\n // custom takes any of allowedDomains / allowedCidrs / deniedCidrs\n networkPolicy: {\n mode: \"custom\",\n allowedDomains: [\"api.example.com\"],\n allowedCidrs: [\"203.0.113.0/24\"],\n deniedCidrs: [\"10.0.0.0/8\"],\n },\n\n // Inject secret headers into matching outbound HTTPS requests (write-only, never read back)\n attachHeaders: {\n \"api.stripe.com\": { Authorization: \"Bearer sk_live_...\" },\n \"*.example.com\": { \"X-Custom-Token\": \"secret123\" },\n },\n})\n\nbox.networkPolicy\nawait box.updateNetworkPolicy({ mode: \"deny-all\" })\n```\n\n## MCP Servers\n\nAttach MCP servers to the box agent. An attached server supplies tools the agent can call,\nso use servers you control or trust — and keep `networkPolicy` restrictive when the agent\nalso handles untrusted input.\n\n```ts\nconst box = await Box.create({\n agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },\n mcpServers: [\n { name: \"fs\", package: \"@modelcontextprotocol/server-filesystem\", args: [] },\n { name: \"custom\", url: \"<your-mcp-server-url>\", headers: { Authorization: \"...\" } },\n ],\n})\n```\n\n## Errors & SSH\n\n```ts\nimport { BoxError } from \"@upstash/box\"\n\ntry {\n await box.agent.run({ prompt: \"...\" })\n} catch (e) {\n if (e instanceof BoxError) console.error(e.message, e.statusCode)\n}\n```\n\nShell into a box directly (Box API key is the SSH password):\n\n```bash\nssh <box-id>@us-east-1.box.upstash.com\n```\n\n## Gotchas\n\n- Default working directory is `/workspace/home`, not `/home` or `/`\n- `box.cd()` is client-side tracking — it validates the path exists but doesn't change the box's shell cwd. All SDK methods use it automatically.\n- `agent.harness` is required; `provider` / `runner` still work but are deprecated\n- There is **no** `box.fork()` — it was removed from the SDK. Snapshot the box and use `Box.fromSnapshot()` instead.\n- `EphemeralBox` does NOT support `agent`, `git`, `skills`, `browser`, or public URLs — use full `Box` for those (it does support `schedule` and snapshots)\n- `run.exitCode` is `null` for agent runs, only available for exec commands\n- `run.result` is stdout on success and stderr on failure — a command that exits 0 writing only to stderr yields `\"\"`; read `run.stderr` for it\n- `files.download({ folder })` takes a path *inside the box*; output lands in `./<basename>` locally\n- `files.read()` slices only when `length` is present — `{ offset }` alone reads the whole file, and `{ length: 0 }` reads nothing\n- `files.stat()` is an lstat by default: a symlink reports `type: \"symlink\"` unless you pass `{ follow: true }`\n- `files.remove()` needs `{ recursive: true }` for a directory, and `files.mkdir()` needs `{ parents: true }` for nested paths\n- `exec.session()` is Node-only (the WebSocket handshake carries an auth header) and the handle owns the process — `close()` or a dropped connection kills the command, and sessions cannot be reattached\n- `exec.session({ tty: true })` merges stderr into stdout, so `onStderr` never fires for a PTY session\n- `box.browser` requires a box created with `browser: true`\n- There is **no** `tab.run()` — the autonomous browser agent was removed in 0.7.0. Loop `observe` + `act(action)` + `extract` yourself, hand the goal to the in-box agent, or drive Playwright over `cdpUrl()`\n- `tab.act(action)` (replaying an `observe()` result) costs no tokens and needs no model provider key; only `act(instruction)` with a string is metered\n- `getInitCommand` / `setInitCommand` / `deleteInitCommand` throw unless the box was created with `keepAlive: true`\n- `box.delete()` is irreversible — snapshot first if you need the state\n- Git operations require `git.token` in `BoxConfig` for private repos and PRs\n- `Box.fromSnapshot()` creates a new box — it does not modify the original, and it does not forward `browser`, `skills`, or `mcpServers` from the config you pass\n- `EphemeralBox` has no `updateNetworkPolicy` — set `networkPolicy` at create time\n- `responseSchema` and browser `schema` need `zod` installed (peer dependency, v3 or v4)\n- All `timeout` values are milliseconds\n"
}SHA-256: 2f733615f35b3a9d4275f20c447c49ba9cbd3103f7c574d15882a8342f19e622