← Upstash RedisCONTENT HISTORY

Update to Upstash Redis

Snapshot Sep 30, 2026 · 23:07 UTC · version 1.2.1

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full 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