mcpField
U
upstash-box-js

upstash-box-js

github.com/upstashクラウド・DevOps341.3k 回インストール1 日前に更新
リポジトリ

upstash-box-js について

upstash-box-jsは、エージェント、シェル、ファイルシステム、オプションのブラウザー機能を備えたサンドボックス型クラウドコンテナを作成・操作するTypeScript SDKです。隔離環境でコードやエージェントタスクを実行する開発者向けです。

主な機能

  1. 01サンドボックスの作成と管理
  2. 02エージェント、シェルコマンド、コードの実行
  3. 03コンテナ内ファイルの読み書き
  4. 04リポジトリのクローンとGit状態の管理
  5. 05コマンドやエージェントプロンプトのスケジュール
  6. 06ヘッドレスChromiumの操作

活用シーン

  1. 01隔離環境でエージェントによるコードレビューを実行
  2. 02クローンしたリポジトリでビルドやテストを実行
  3. 03Chromiumを使ったブラウザータスクの自動化
  4. 04コンテナ内で定期コマンドをスケジュール

MCP 対応

READMEによると、ブラウザーを有効にするとchrome-devtools MCPがコンテナ内のコーディングエージェントに自動接続されます。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/upstash/skills --skill upstash-box-js

インストール後、Claude Code で/upstash-box-jsを入力して読み込まれたか確認します。

詳細

@upstash/box SDK

AI エージェント、シェル、ファイルシステム、git、cron スケジュール、オプションのヘッドレスブラウザーを標準搭載した、サンドボックス化されたクラウドコンテナです。

Python SDK(upstash-box)は、この API を snake_case の名前で提供します。以下の各項目の Python での表記については、 upstash-box-py スキルをご覧ください。

同梱ドキュメントとソースコード

SDK コードを作成またはデバッグする前に、同梱ドキュメントは node_modules/@upstash/box/docs/、読みやすい TypeScript ソースは node_modules/@upstash/box/src/ で確認してください。該当するファイルがあれば、読み込んで検索してください。ない場合は、このスキル内のリファレンスを使用してください。

インストールとセットアップ

npm install @upstash/box
npm install zod   # peer dependency, only needed for responseSchema / browser schemas

UPSTASH_BOX_API_KEY 環境変数を設定するか、コンストラクターに apiKey を渡してください。

匿名のテレメトリヘッダーはデフォルトで送信されます。UPSTASH_DISABLE_TELEMETRY 環境変数を設定するか、設定で enableTelemetry: false を指定すると無効にできます(process.env がないランタイム(Cloudflare Workers など)では、後者のみ使用できます)。

Box のライフサイクル

import { Box, Agent, ClaudeCode, BoxApiKey } from "@upstash/box"

// Create with agent + git + env vars
const box = await Box.create({
  name: "my-box",
  runtime: "node", // "node" | "python" | "golang" | "ruby" | "rust" (+ "-alpine" variants)
  size: "small",   // "small" (2 CPU/4GB) | "medium" (4/8) | "large" (8/16)
  labels: ["beta", "x-team"], // max 5, ≤20 chars each
  keepAlive: true,            // don't idle-pause the box
  initCommand: "npm install && npm run dev", // keep-alive boxes only
  browser: true,              // provision headless Chromium for box.browser
  agent: {
    harness: Agent.ClaudeCode, // Agent.Codex | Agent.OpenCode | Agent.Cursor | Agent.Custom
    model: ClaudeCode.Sonnet_4_5,
    // apiKey options:
    //   omit          → server decides which key to use
    //   BoxApiKey.UpstashKey  → use Upstash-provided LLM key
    //   BoxApiKey.StoredKey   → use key previously stored via Upstash Console
    //   "sk-..."      → direct API key string
    apiKey: BoxApiKey.UpstashKey,
  },
  git: { // all fields optional
    token: process.env.GITHUB_TOKEN, // alternatively link your GitHub account via Upstash Console
    userName: "Bot",
    userEmail: "[email protected]",
  },
  env: { DATABASE_URL: "..." },
  skills: ["upstash/qstash-js/qstash-js"], // owner/repo/skill-name
  timeout: 600_000, // request timeout in ms
  debug: false,
})

// Reconnect, list, delete, pause/resume
// Box.get / Box.getByName take { apiKey, baseUrl, gitToken, timeout, debug }
const same = await Box.get(box.id, { gitToken: process.env.GITHUB_TOKEN })
const byName = await Box.getByName("my-box")
const all = await Box.list()
const beta = await Box.list({ label: "beta" }) // filter by label
await box.pause()   // throws on keep-alive boxes — they are never idle-paused
await box.resume()
await box.delete()  // irreversible
const { status } = await box.getStatus()

box.id; box.size; box.keepAlive; box.cwd; box.networkPolicy

// Init command (keep-alive boxes only — throws otherwise)
await box.setInitCommand("npm run dev")
const script = await box.getInitCommand()
await box.deleteInitCommand()

// Bulk delete (static, by ID). An empty or blank id list throws — it is never
// read as "everything". Omit snapshotIds entirely to delete all snapshots.
await Box.delete({ boxIds: ["box_1", "box_2"] })
const { deleted } = await Box.deleteSnapshots({ snapshotIds: ["snap_1"] })
await Box.deleteSnapshots() // delete all
アカウントレベルの環境変数

作成するすべての box に注入されます。

await Box.setEnv("API_TOKEN", "secret")
const env = await Box.listEnv()          // values are masked
await Box.setAllEnv({ A: "1", B: "2" })  // full replace — unlisted keys are removed
await Box.deleteEnv("API_TOKEN")

エージェントの実行

import { z } from "zod"

// Structured output with Zod schema
const run = await box.agent.run({
  prompt: "Review the code for security issues",
  responseSchema: z.object({
    verdict: z.enum(["approved", "changes_requested"]),
    findings: z.array(z.object({
      severity: z.enum(["high", "medium", "low"]),
      file: z.string(),
      issue: z.string(),
    })),
  }),
  timeout: 120_000,
  maxRetries: 2,
  options: { maxTurns: 20, maxBudgetUsd: 1.0, effort: "high" }, // harness-specific
  onToolUse: (tool) => console.log(tool.name, tool.input),
  onToolResult: (result) => console.log(result.toolCallId, result.output),
})

run.status  // "running" | "completed" | "failed" | "cancelled" | "detached"
run.result  // typed from schema
run.cost    // { inputTokens, outputTokens, cachedInputTokens, computeMs, totalUsd }

// Attach files to a prompt (max 10 files, 10 MB each)
await box.agent.run({ prompt: "Describe this", files: ["./screenshot.png"] })
await box.agent.run({
  prompt: "Describe this",
  files: [{ data: base64, mediaType: "image/png", filename: "shot.png" }],
})

// Streaming — chunk is a discriminated union
const stream = await box.agent.stream({ prompt: "Build a REST API" })
for await (const chunk of stream) {
  if (chunk.type === "text-delta") process.stdout.write(chunk.text)
  if (chunk.type === "reasoning") process.stdout.write(chunk.text)
  if (chunk.type === "tool-call") console.log(chunk.toolName, chunk.input)
  if (chunk.type === "tool-result") console.log(chunk.output)
  if (chunk.type === "finish") console.log(chunk.output, chunk.usage, chunk.sessionId)
  // also: { type: "start", runId } | { type: "stats", cpuNs, memoryPeakBytes } | { type: "unknown" }
}
stream.status // "completed" after iteration finishes
stream.result // final output

// stream() takes the same prompt/files/options/timeout/onToolUse/onToolResult as run().
// It has no responseSchema, maxRetries, or webhook — use run() for those.

// Fire-and-forget with webhook
await box.agent.run({
  prompt: "Run tests",
  webhook: { url: "https://example.com/hook", headers: { Authorization: "Bearer ..." } },
})
エージェントのオプション(ハーネスごと)

options はハーネスに渡されます。受け付けるキーは、box で実行するハーネスによって異なります。box の型(Box.create<Agent.ClaudeCode>({...}))を指定すると、options の型がそのハーネスの形式に絞り込まれます。

// Agent.ClaudeCode → ClaudeCodeAgentOptions
{
  maxTurns: 20,
  maxBudgetUsd: 1.0,
  effort: "high",                 // "low" | "medium" | "high" | "max"
  thinking: { type: "adaptive" }, // | { type: "enabled", budgetTokens: 8000 } | { type: "disabled" }
  disallowedTools: ["Bash"],
  agents: { reviewer: { /* custom subagent definition */ } },
  promptSuggestions: false,
  fallbackModel: "anthropic/claude-sonnet-4-5",
  systemPrompt: "You are a release engineer.",
}

// Agent.Codex → CodexAgentOptions
{
  modelReasoningEffort: "high",   // "none" | "minimal" | "low" | "medium" | "high" | "xhigh"
  modelReasoningSummary: "concise", // "auto" | "concise" | "detailed" | "none"
  personality: "pragmatic",       // "friendly" | "pragmatic" | "none"
  webSearch: "live",              // or true / false
}

// Agent.OpenCode → OpenCodeAgentOptions
{
  reasoningEffort: "high",        // "low" | "medium" | "high"
  textVerbosity: "low",           // "low" | "medium" | "high"
  reasoningSummary: "auto",       // "auto" | "concise" | "detailed" | "none"
  thinking: { type: "enabled", budgetTokens: 8000 }, // Anthropic-backed models
}

// Agent.Cursor → free-form Record<string, unknown>

Codex のキーはバックエンドの snake_case に自動変換されるため、常に camelCase で記述してください。

ハーネスとモデル

harness は必須です(provider / runner は非推奨のエイリアスです)。モデルの列挙型は ClaudeCode、OpenAICodex、OpenCodeModel、CursorModel、OpenRouterModel、VercelModel、またはプロバイダー名を接頭辞にした任意の文字列です。

import { ClaudeCode, OpenAICodex, OpenCodeModel, CursorModel, OpenRouterModel, VercelModel } from "@upstash/box"

ClaudeCode.Fable_5_1     // "anthropic/claude-fable-5-1"
ClaudeCode.Opus_5        // "anthropic/claude-opus-5"
ClaudeCode.Sonnet_5      // "anthropic/claude-sonnet-5"
OpenAICodex.GPT_6_Astra  // "openai/gpt-6-astra"
OpenAICodex.GPT_5_6      // "openai/gpt-5.6"
OpenCodeModel.Claude_Opus_5   // "opencode/claude-opus-5"
CursorModel.Composer_2_5 // "cursor/composer-2.5"
OpenRouterModel.Claude_Opus_5 // "openrouter/anthropic/claude-opus-5"
VercelModel.GPT_5_5      // "vercel/openai/gpt-5.5"

// Read / change the box's harness + model at runtime
const { harness, model } = box.modelConfig
await box.configureModel("anthropic/claude-opus-4-8")

// Which harness a bare model string implies (prefix-based)
import { inferDefaultProvider } from "@upstash/box"
inferDefaultProvider("openai/gpt-5.6")   // Agent.Codex
inferDefaultProvider("cursor/default")   // Agent.Cursor
カスタムハーネス

管理対象のハーネスの代わりに、独自のエージェントバイナリを box 内で実行します。

import { Box, Agent, runCustomHarness } from "@upstash/box"

const box = await Box.create({
  agent: {
    harness: Agent.Custom,
    model: "my-agent",                                  // label forwarded to the process
    // command: name on PATH, or an absolute path under /workspace/home or /home/boxuser
    customHarness: { command: "node", args: ["/workspace/home/agent.js"], protocol: "box-sse-v1" },
  },
})
await box.configureCustomHarness({ command: "node", args: ["/workspace/home/agent2.js"] })

// Inside the box, agent.js emits box-sse-v1 events. The backend appends
// `-p <prompt> --model <model> --stream` (+ `--session <id>` when resuming).
await runCustomHarness(async ({ prompt, model, sessionId, stream, args }, emit) => {
  emit.text("working...")
  emit.reasoning("thinking out loud")            // -> `thinking` event
  emit.tool({ toolCallId: "1", name: "Bash", input: { command: "ls" } })
  emit.toolResult({ toolCallId: "1", output: "file.txt" })
  emit.emit("custom-event", { any: "payload" })  // raw escape hatch
  // emit.error(new Error("boom")) to fail the run
  return {
    output: "done",
    inputTokens: 10,
    outputTokens: 5,
    cachedInputTokens: 0,
    totalCostUsd: 0.01,
    sessionId,
  } // returning a plain string is shorthand for { output }
})

実行フィールド

エージェント、コマンド、コードのいずれを実行した場合も、run は Run<T> を返します。

const run = await box.exec.command("npm test")
run.id        // run ID
run.status    // "completed" | "failed" | ...
run.result    // stdout on success, stderr on failure (or typed T with responseSchema)
run.stdout    // raw stdout (command/code runs)
run.stderr    // raw stderr (command/code runs)
run.exitCode  // number | null (null for agent runs)
run.cost      // { inputTokens, outputTokens, cachedInputTokens, computeMs, totalUsd }

await run.cancel()          // the pending run()/stream() rejects with the abort, and is not retried
const logs = await run.logs() // [{ timestamp, level, message }]

// Box-level history
const entries = await box.logs({ limit: 100, offset: 0 }) // [{ timestamp, level, source, message }]
const runs = await box.listRuns()              // backend run records, newest first

シェルの実行

// Run commands
const run = await box.exec.command("echo hello && ls -la")

// Run code snippets — lang: "js" | "ts" | "python"
const run2 = await box.exec.code({ code: "console.log(1+1)", lang: "js", timeout: 10_000 })

// Streaming shell / code
const stream = await box.exec.stream("npm run build")
const stream2 = await box.exec.streamCode({ code: "print('hi')", lang: "python" })
for await (const chunk of stream) {
  // chunk: { type: "output", data } | { type: "exit", exitCode, cpuNs }
}
ライブセッション

exec.session() は、実行中のプロセスへの WebSocket を開きます。stdin、ストリーミングされる stdout/stderr、PTY のサイズ変更、シグナルを利用できます。Node.js 専用です。認証にはハンドシェイクヘッダーを使用しますが、ブラウザーからは設定できません(ws は SDK の依存関係として同梱されており、インストールは不要です)。Box と EphemeralBox で利用できます。

const session = await box.exec.session({
  cmd: "sort",              // run via `bash -lc`; `argv: ["sort"]` runs the program with
                            // no shell and takes precedence over cmd
  cwd: "/workspace/home",   // defaults to the box's tracked cwd
  env: ["LOG_LEVEL=debug"], // KEY=VALUE entries overlaid on the box environment
  onStdout: (bytes) => process.stdout.write(bytes), // Uint8Array
  onStderr: (bytes) => process.stderr.write(bytes), // separate stream unless tty
})

session.pid                        // in-box PID, always non-zero
session.execId                     // server-side exec id
session.write("banana\napple\n")
session.endStdin()                 // EOF — a command that reads to EOF now exits by itself
const exitCode = await session.wait()  // -1 if torn down while still running
session.close()                    // hang up; also kills the process

// Interactive programs / TUIs — tty allocates a real PTY, merging stderr into stdout
const shell = await box.exec.session({ argv: ["bash", "-i"], tty: true, rows: 40, cols: 120 })
shell.resize(50, 160)
shell.kill("INT")     // allowlist: TERM KILL INT HUP TSTP QUIT USR1 USR2 (default TERM)
shell.terminate(5000) // server-side SIGTERM, then SIGKILL after the grace (first call wins)

セッションはプロセスを所有します。close() の呼び出し、接続の切断、またはプロセスの終了によって、いずれもコマンドは終了します。また、セッションに再接続することはできません。処理が完了するまで実行するには、wait() を使用してください。

ファイルシステム

await box.files.write({ path: "/workspace/home/app.js", content: "console.log('hi')" })
const content = await box.files.read("/workspace/home/app.js")
const entries = await box.files.list("/workspace/home") // [{ name, path, size, is_dir, mod_time }]

// Binary files — use encoding: "base64" for read and write
await box.files.write({ path: "/workspace/home/image.png", content: base64String, encoding: "base64" })
const b64 = await box.files.read("/workspace/home/image.png", { encoding: "base64" })

// Bounded byte-range read — the *presence* of `length` selects the range, so
// { length: 0 } reads zero bytes rather than the whole file. Server caps it at 8 MiB.
const head = await box.files.read("/workspace/home/big.log", { length: 64 * 1024 })
const slice = await box.files.read("/workspace/home/big.log", { offset: 1024, length: 512 })

// Metadata — defaults to lstat, so a symlink reports type "symlink"
const stat = await box.files.stat("/workspace/home/app.js")
// stat: { type: "file" | "directory" | "symlink" | "other", size, mod_time, inode, version }
// `version` is an opaque freshness token (inode + mtime + size) for optimistic-concurrency
// guards — compare it for equality, never parse it.
const target = await box.files.stat("/workspace/home/link", { follow: true }) // dereference

// Directories, moves, deletes
await box.files.mkdir("build/cache", { parents: true }) // parents mirrors `mkdir -p`
await box.files.rename("draft.md", "docs/final.md")     // move/rename
await box.files.remove("build/cache", { recursive: true }) // recursive required for a directory

// Upload local files
await box.files.upload([{ path: "./local/file.txt", destination: "/workspace/home/file.txt" }])

// Download — `folder` is a path INSIDE the box; files land in ./<basename>
await box.files.download({ folder: "src" }) // → ./src
await box.files.download()                  // whole cwd → ./workspace

cd / 作業ディレクトリ

SDK は cwd をクライアント側で追跡します。すべての操作(exec、files、git、agent)は、このディレクトリを基準に実行されます。

box.cwd // current working directory (starts at /workspace/home)
await box.cd("my-repo")     // relative to current cwd
await box.cd("/workspace/home/other") // absolute path

Git

クローンは box の分離されたコンテナ内に作成され、呼び出し元のマシンには作成されません。クローンしたコードは、何かを実行するまではデータです。信頼できないリポジトリは信頼できない入力として扱い、ビルドやテストを実行する前に、制限的な networkPolicy(下記参照)と組み合わせてください。

clone を除くすべての git 呼び出しは、box の現在のディレクトリで実行されるため、まずクローン先に cd してください。ワークスペースのルートにはリポジトリがないため、status は空で返され、クリーンな作業ツリーとして表示されます。

await box.git.clone({ repo: "github.com/org/repo", branch: "main" })
await box.git.clone({ repo: "github.com/org/repo", depth: 1 }) // shallow clone
await box.git.clone({ repo: "github.com/org/repo", folder: "my-app" }) // destination
await box.cd("repo") // the clone lands in a directory named after the repo

const status = await box.git.status()
const diff = await box.git.diff()
const { sha } = await box.git.commit({
  message: "fix: resolve bug",
  authorName: "Jane Doe",      // optional per-commit override
  authorEmail: "[email protected]",
})
await box.git.push({ branch: "feature/fix" })

await box.git.checkout({ branch: "release/v2" })
const pr = await box.git.createPR({ title: "Fix bug", body: "...", base: "main" })
// pr: { url, number, title, base, warning? }
const issue = await box.git.createIssue({ title: "Flaky test in CI", body: "..." })
// issue: { url, number, title, warning? }

// Attach images or videos to a PR or issue. Paths are relative to the box's
// working directory; alt text for an image is written as `shot.png#alt text`
// (a video takes none). Reference an attachment from the body as
// ![alt](./shot.png) and GitHub rewrites it to the uploaded asset.
await box.git.createPR({
  title: "Fix the tower layout",
  body: "![before](./before.png)\n![after](./after.png)",
  attach: ["before.png#before", "after.png#after"],
})
// `warning` is set when `gh` exited non-zero but still returned a URL: the item
// exists while an attachment failed to upload, or the PR was already open.

// Update the box-wide git identity
const cfg = await box.git.updateConfig({ userName: "Bot", userEmail: "[email protected]" })
// cfg: { git_user_name, git_user_email }

// Arbitrary git commands. Check exit_code: 128 means the cwd is not a repository.
const { output, exit_code } = await box.git.exec({ args: ["log", "--oneline", "-5"] })

スケジュール

box 上の cron タスク(シェルコマンドまたはエージェントへのプロンプト)です。Box と EphemeralBox で利用できます。cron は UTC です。

const execSchedule = await box.schedule.exec({
  cron: "* * * * *",
  command: ["bash", "-c", "date >> /workspace/home/cron.log"],
  folder: "/workspace/home",            // optional cwd override
  webhookUrl: "https://example.com/hook",
  webhookHeaders: { Authorization: "Bearer ..." },
})

const agentSchedule = await box.schedule.agent({
  cron: "0 9 * * *",
  prompt: "Run the test suite and fix any failures",
  folder: "/workspace/home/repo",       // optional cwd override
  model: "anthropic/claude-sonnet-5",   // optional override
  options: { maxBudgetUsd: 1.0, effort: "high" },
  timeout: 300_000,
  webhookUrl: "https://example.com/hook",
  webhookHeaders: { Authorization: "Bearer ..." },
})

const schedules = await box.schedule.list()
const one = await box.schedule.get(agentSchedule.id)

// Partial update — omitted fields keep their value, "" / [] / {} clear a field,
// `options: null` clears agent options. The schedule's type cannot change.
// Updatable: cron, command, prompt, folder, model, options, timeout, webhookUrl, webhookHeaders
await box.schedule.update(agentSchedule.id, { cron: "0 18 * * *", webhookUrl: "" })

await box.schedule.pause(agentSchedule.id)
await box.schedule.resume(agentSchedule.id)
await box.schedule.delete(agentSchedule.id)

スナップショット

// Snapshot — checkpoint workspace state
const snap = await box.snapshot({ name: "after-setup" })
// snap: { id, name, box_id, size_bytes, status, created_at }

// fromSnapshot takes a BoxConfig: name, labels, size, keepAlive, initCommand, runtime,
// agent, git, env, attachHeaders, networkPolicy. It does NOT send `browser`, `skills`,
// or `mcpServers` (the Python SDK does) — add skills with box.skills.add() afterwards,
// and use Box.create({ browser: true }) when you need Chromium.
const restored = await Box.fromSnapshot(snap.id, {
  size: "medium",
  keepAlive: true,
  // git identity is forwarded, not just the token
  git: { token: process.env.GITHUB_TOKEN, userName: "Bot", userEmail: "[email protected]" },
  env: { DATABASE_URL: "..." },
})
const snaps = await box.listSnapshots()
await box.deleteSnapshot(snap.id)

ブラウザー

ヘッドレス Chromium を操作するには、browser: true を指定して box を作成します。タブの管理は box.browser で行い、各ページの操作は Tab ハンドルで行います。extract / observe / act(instruction) は AI を利用し、使用量が計測されます。act(action) はすでに解決済みのアクションを LLM の呼び出しやトークン消費なしで再生します。

import { z } from "zod"

const box = await Box.create({
  browser: true,
  agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },
})

// Tabs
const tab = await box.browser.tab.create("https://example.com", { waitUntil: "load", timeout: 30_000 })
const tabs = await box.browser.listTabs()
const again = box.browser.getTab(tab.id) // no network call
tab.id; tab.url; tab.title                // handle metadata, no network call

// Page operations
const content = await tab.goto("https://news.ycombinator.com") // { title, url, text, links }
const current = await tab.content()
const png = await tab.screenshot()                                  // Uint8Array
const b64 = await tab.screenshot({ type: "base64", fullPage: true })

// AI operations (metered) — extract/observe/act take an optional { model } override,
// defaulting to the box's model (or anthropic/claude-sonnet-4-5 when it has none)
const data = await tab.extract(
  "Top story title and points",
  z.object({ title: z.string(), points: z.number() }),
  { model: "anthropic/claude-sonnet-4-5" },
)
// observe → actionable elements, each carrying a replayable method + arguments
const { elements } = await tab.observe("What can I click?", { model: "openai/gpt-5.6" })
// elements: [{ description, selector?, url?, method?, arguments? }]

const acted = await tab.act("Click the first headline")
// acted: { success, message, actionDescription, actions, cacheStatus?, inputTokens, outputTokens }

// Replay a pre-resolved action — no LLM call, no tokens, no model provider key.
// Takes a BrowserAction (= BrowserObserveElement | BrowserActAction); `model` is
// ignored in this form, and an action without a `selector` throws.
await tab.act(elements[0])
await tab.act(acted.actions[0])

// Live view + raw CDP
const liveUrl = await tab.liveViewUrl()      // view-only screencast page/iframe
const cdpUrl = await box.browser.cdpUrl()    // wss://…?token=… — no extra auth wiring
await tab.close()

// Drive the same browser from Playwright / Puppeteer / Stagehand
import { chromium } from "playwright-core"
const remote = await chromium.connectOverCDP(cdpUrl)
const context = remote.contexts()[0] ?? (await remote.newContext())
const page = context.pages()[0] ?? (await context.newPage())
await page.goto("https://example.com")
// Stagehand: new Stagehand({ env: "LOCAL", localBrowserLaunchOptions: { cdpUrl } })

// Session recordings (HLS playback URL + MP4 download, chapter markers).
// One active recording per box; captures all tabs and follows the foreground.
// Auto-stops after maxDurationSeconds or ~3 minutes of no on-screen activity.
const handle = await box.browser.recordings.start({ maxDurationSeconds: 600 }) // default & max 600
const recording = await handle.stop()
// or stop whatever is recording on the box, without a handle:
// const recording = await box.browser.recordings.stop()
// recording: { id, boxId, status, startedAt, endedAt, durationMs, sizeBytes, mp4SizeBytes,
//              segmentCount, markers, stoppedReason, maxDurationSeconds, expiresAt, playlistUrl }
// markers: { type: "tab_switch", atMs, endMs?, label?, tabId? }
// expiresAt is epoch ms (videos retained 14 days); playlistUrl is API-served — fetch it
// with an `X-Box-Api-Key: <apiKey>` header (hls.js / Safari / ffplay).
const all = await box.browser.recordings.list()
const one = await box.browser.recordings.get(recording.id)

// Download the video to a local file — returns the path written.
// Defaults to ./box-recording-<id>.mp4 (.ts for recordings captured before MP4 support).
const file = await box.browser.recordings.download(recording.id)
await box.browser.recordings.download(recording.id, { path: "./out/demo.mp4" })
マルチステップのブラウザゴール

tab.run() — 自律的なマルチステップブラウザエージェント — は 0.7.0 で削除されました。 BrowserRunOptions / BrowserRunResult / BrowserRunStep 型も同様です (Stagehand v4 で基盤となるエージェントプリミティブが廃止されました)。DOM を認識するブラウザは 現在、observe、act、extract のみを公開しています。代替手段は 3 つあります。

1. 独自のループを回す — observe でステップを一度解決し、act(action) でそれらを 再生することで、モデルをホットパスから遠ざけます。extract が停止チェックです。

const { elements } = await tab.observe("the product links in the listing")
const actions = elements.filter((e) => e.selector)

for (const action of actions.slice(0, 5)) {
  await tab.goto(START) // deterministic reset, no browser-AI tokens
  await tab.act(action) // replay the resolved click: no LLM, no tokens
  const item = await tab.extract("title and price", z.object({ title: z.string() }))
}

2. ゴールをインボックスエージェントに任せる — browser: true は chrome-devtools MCP (Chromium はすでに 127.0.0.1:9222 でウォームアップ済み) をボックスの コーディングエージェントに自動接続するため、box.agent.run({ prompt }) がブラウザ自体を 操作し、完了するまで反復します。事前に tab.create() は不要です。これはブラウザ AI の メータリングではなくコーディングエージェントのモデルトークンとして課金され、 エージェントハーネスとキーが必要です。

3. フローが完全に決定論的な場合は box.browser.cdpUrl() を介して Playwright / Puppeteer で CDP 経由で接続します。

EphemeralBox

軽量で短命なボックス (最長 3 日)。exec、files、schedule、cd、ネットワークポリシー、 スナップショットをサポートします。エージェント、git、スキル、ラベル名前空間、ブラウザ、 パブリック URL はありません。

import { EphemeralBox } from "@upstash/box"

const ebox = await EphemeralBox.create({
  name: "scratch-box",
  runtime: "python",
  size: "small",
  ttl: 3600,  // seconds, max 259200 (3 days), default 259200
  env: { API_KEY: "..." },
  labels: ["scratch"], // settable at create time; filter via Box.list({ label })
  networkPolicy: { mode: "deny-all" },
  attachHeaders: { "api.stripe.com": { Authorization: "Bearer sk_live_..." } },
})

ebox.networkPolicy

ebox.expiresAt // unix timestamp when auto-deleted
await ebox.exec.command("python -c 'print(1+1)'")
await ebox.exec.code({ code: "print('hi')", lang: "python" })
await ebox.exec.session({ argv: ["bash", "-i"], tty: true }) // whole exec namespace, session included
await ebox.files.write({ path: "/workspace/home/data.json", content: "{}" })
await ebox.files.stat("/workspace/home/data.json")           // whole files namespace, stat/mkdir/rename/remove included
await ebox.schedule.exec({ cron: "* * * * *", command: ["bash", "-c", "date"] })
await ebox.cd("subdir")
const snap = await ebox.snapshot({ name: "checkpoint" })
await ebox.listSnapshots()
await ebox.deleteSnapshot(snap.id)
const { status } = await ebox.getStatus()
await ebox.delete()

// Restore from snapshot
const ebox2 = await EphemeralBox.fromSnapshot(snap.id, { ttl: 7200 })

// Statics: EphemeralBox.delete({ boxIds }) and EphemeralBox.deleteSnapshots() are the
// Box ones. EphemeralBox.getByName() is Box.get — it returns a full `Box`, not an
// `EphemeralBox` (quirk mirrored in the Python SDK).

パブリック URL

ボックスのポートを、任意の認証付きでパブリック URL として公開します。

const publicURL = await box.getPublicURL(3000)
// publicURL: { url: "https://{id}-3000.preview.box.upstash.com", port }

const authed = await box.getPublicURL(3000, { bearerToken: true })
// authed: { url, port, token }

const basic = await box.getPublicURL(3000, { basicAuth: true })
// basic: { url, port, username, password }

const { publicURLs } = await box.listPublicURLs()
await box.deletePublicURL(3000)

スキル

Context7 レジストリからエージェントスキルをインストールします。形式: owner/repo/skill-name。

インストールされたスキルはボックスのエージェントへの指示となるため、依存関係と同じように、 信頼できるオーナーにスキルを固定してください。スキルは任意の URL からではなくボックス作成時に レジストリから解決され、ボックスのコンテナ内でのみ実行されます。

const box = await Box.create({ skills: ["upstash/qstash-js/qstash-js"] })

await box.skills.add("upstash/workflow-js/workflow-js")
const enabled = await box.skills.list()
await box.skills.remove("upstash/workflow-js/workflow-js")

ラベル

const labels = await box.labels.add("prod")     // returns the updated set
await box.labels.remove("beta")
const current = await box.labels.list()
const prodBoxes = await Box.list({ label: "prod" })

ネットワークポリシーと送信ヘッダー

const box = await Box.create({
  // mode: "allow-all" (default) | "deny-all" | "custom"
  // custom takes any of allowedDomains / allowedCidrs / deniedCidrs
  networkPolicy: {
    mode: "custom",
    allowedDomains: ["api.example.com"],
    allowedCidrs: ["203.0.113.0/24"],
    deniedCidrs: ["10.0.0.0/8"],
  },

  // Inject secret headers into matching outbound HTTPS requests (write-only, never read back)
  attachHeaders: {
    "api.stripe.com": { Authorization: "Bearer sk_live_..." },
    "*.example.com": { "X-Custom-Token": "secret123" },
  },
})

box.networkPolicy
await box.updateNetworkPolicy({ mode: "deny-all" })

MCP サーバー

MCP サーバーをボックスエージェントに接続します。接続されたサーバーはエージェントが呼び出せる ツールを提供するため、自分が管理または信頼するサーバーを使用してください。また、エージェントが 信頼できない入力を扱う場合には networkPolicy を制限的に保ってください。

const box = await Box.create({
  agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },
  mcpServers: [
    { name: "fs", package: "@modelcontextprotocol/server-filesystem", args: [] },
    { name: "custom", url: "<your-mcp-server-url>", headers: { Authorization: "..." } },
  ],
})

エラーと SSH

import { BoxError } from "@upstash/box"

try {
  await box.agent.run({ prompt: "..." })
} catch (e) {
  if (e instanceof BoxError) console.error(e.message, e.statusCode)
}

ボックスに直接シェル接続します (Box API キーが SSH パスワードです):

ssh <box-id>@us-east-1.box.upstash.com

注意点

  • デフォルトの作業ディレクトリは /workspace/home で、/home や / ではありません
  • box.cd() はクライアント側のトラッキングです — パスが存在するかどうかを検証しますが、ボックスのシェル cwd は変更しません。すべての SDK メソッドはこれを自動的に使用します。
  • agent.harness は必須です。provider / runner は今でも動作しますが非推奨です
  • box.fork() は 存在しません — SDK から削除されました。ボックスをスナップショットして Box.fromSnapshot() を使用してください。
  • EphemeralBox は agent、git、skills、browser、パブリック URL をサポートして いません — これらには完全な Box を使用してください(schedule とスナップショットはサポートしています)
  • run.exitCode はエージェント実行では null で、exec コマンドでのみ利用可能です
  • run.result は成功時は stdout、失敗時は stderr です — 終了コード 0 で stderr にのみ書き込むコマンドは "" を返します。その場合は run.stderr を読んでください
  • maxRetries は一時的な障害のみをカバーします — run.cancel() や自身の timeout によって中断された実行は、課金が続く 2 回目の実行を開始せずに即座に失敗します。タイムアウトは BoxError("Run timed out") / "Stream timed out" を発生させ、中断を cause として保持します
  • Box.delete({ boxIds: [] }) と Box.deleteSnapshots({ snapshotIds: [] }) は例外をスローします — 空のリストが「すべて削除」と解釈されることは決してありません。その場合は id を指定せずに Box.deleteSnapshots() を呼び出してください
  • files.download({ folder }) は ボックス内 のパスを受け取ります。出力はローカルの ./<basename> に配置されます
  • files.read() は length が指定されている場合にのみスライスします — { offset } だけではファイル全体を読み取り、{ length: 0 } では何も読み取りません
  • files.stat() はデフォルトで lstat です。シンボリックリンクは { follow: true } を渡さない限り type: "symlink" を報告します
  • files.remove() はディレクトリには { recursive: true } が必要で、files.mkdir() はネストされたパスには { parents: true } が必要です
  • exec.session() は Node 専用です(WebSocket ハンドシェイクが認証ヘッダーを運びます)、そしてハンドルがプロセスを所有します — close() または接続の切断がコマンドを強制終了し、セッションに再接続することはできません
  • exec.session({ tty: true }) は stderr を stdout にマージするため、PTY セッションでは onStderr が決して発火しません
  • box.browser には browser: true で作成されたボックスが必要です
  • tab.run() は 存在しません — 自律ブラウザエージェントは 0.7.0 で削除されました。observe + act(action) + extract を自分でループするか、ゴールをボックス内エージェントに渡すか、cdpUrl() 経由で Playwright を操作してください
  • tab.act(action)(observe() の結果を再生する)はトークンを消費せず、モデルプロバイダーのキーも不要です。文字列を渡す act(instruction) のみが課金対象です
  • getInitCommand / setInitCommand / deleteInitCommand は、ボックスが keepAlive: true で作成されていない限り例外をスローします
  • box.delete() は取り消せません — 状態が必要な場合は先にスナップショットを取得してください
  • Git 操作では、プライベートリポジトリと PR のために BoxConfig 内の git.token が必要です
  • Box.fromSnapshot() は新しいボックスを作成します — 元のボックスを変更せず、渡した設定から browser、skills、mcpServers を引き継ぎません
  • EphemeralBox には updateNetworkPolicy がありません — 作成時に networkPolicy を設定してください
  • responseSchema とブラウザの schema には zod のインストール(ピア依存関係、v3 または v4)が必要です
  • すべての timeout の値はミリ秒です

コメント