关于 upstash-box-js
upstash-box-js 是用于创建和管理沙箱云容器的 TypeScript SDK,提供 Agent、命令行、文件系统及可选浏览器功能,面向需要隔离运行代码和 Agent 任务的开发者。
主要功能
- 01创建和管理沙箱容器
- 02运行 Agent、Shell 命令和代码
- 03读写容器文件
- 04克隆仓库并管理 Git 状态
- 05安排命令或 Agent 提示词
- 06驱动无头 Chromium 浏览器
适用场景
- 01在隔离容器中运行 Agent 进行代码审查
- 02对克隆的仓库执行构建或测试
- 03使用 Chromium 自动化浏览器任务
- 04在容器中安排定期命令
MCP 相关
README 提到,启用浏览器后会自动将 chrome-devtools MCP 接入容器内的编码 Agent。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ upstash/ skills --skill upstash-box-js安装后在 Claude Code 中输入/upstash-box-js确认已加载。
详细介绍
@upstash/box SDK
沙盒化云容器,内置 AI Agent、shell、文件系统、git、cron 计划任务,以及可选的无头浏览器。
Python SDK(upstash-box)使用 snake_case 命名,与此 API 一一对应——有关下文所有内容的 Python 写法,请参阅 upstash-box-py 技能。
内置文档和源代码
编写或调试 SDK 代码之前,请查看 node_modules/@upstash/box/docs/ 中的内置文档,以及 node_modules/@upstash/box/src/ 中易于阅读的 TypeScript 源代码。若相关文件存在,请阅读并搜索;否则请使用此技能中的参考资料。
安装与设置
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")
Agent 运行
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 ..." } },
})
Agent 选项(按运行框架区分)
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 内运行自己的 Agent 二进制文件,而非使用托管运行框架。
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(Agent、命令或代码)都会返回一个 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
Shell 执行
// 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:身份验证通过握手标头进行,浏览器无法设置该标头(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、文件、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
//  and GitHub rewrites it to the uploaded asset.
await box.git.createPR({
title: "Fix the tower layout",
body: "\n",
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 任务——shell 命令或 Agent 提示词。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)
浏览器
创建 Box 时设置 browser: true,即可驱动无头 Chromium。标签页管理通过 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() — 自主多步骤浏览器 Agent — 已在 0.7.0 中移除,
BrowserRunOptions / BrowserRunResult / BrowserRunStep 类型也一并移除
(Stagehand v4 移除了底层 Agent 原语)。现在,支持 DOM 感知的浏览器仅
提供 observe、act 和 extract。以下是三种替代方案:
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. 将目标交给内置 Agent — browser: true 会自动将 chrome-devtools MCP
(Chromium 已在 127.0.0.1:9222 上预热)接入 box 的编码 Agent,因此
box.agent.run({ prompt }) 会自行驱动浏览器并持续迭代,直到完成。无需先调用
tab.create()。此方式会按编码 Agent 的模型 Token 计费,而非按浏览器 AI 用量
计费,并且需要 Agent 执行框架和密钥。
3. 通过 CDP 连接,在流程完全确定时,使用 Playwright / Puppeteer 和
box.browser.cdpUrl()。
EphemeralBox
轻量、短期的 box(最长 3 天)。支持 exec、files、schedule、cd、网络策略和快照。不支持 Agent、git、技能、labels 命名空间、浏览器或公共 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
将 box 端口公开为公共 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 注册表安装 Agent 技能。格式:owner/repo/skill-name。
已安装的技能会成为 box 的 Agent 指令,因此应像对待依赖项一样,只固定使用你信任的所有者提供的技能。技能会在创建 box 时从注册表解析,而非从任意 URL 获取,并且只会在 box 的容器内运行。
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 服务接入 box Agent。接入的服务会提供 Agent 可调用的工具,因此请使用由你控制或信任的服务;如果 Agent 还会处理不可信输入,请保持 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)
}
直接通过 Shell 连接到 box(Box API 密钥即 SSH 密码):
ssh <box-id>@us-east-1.box.upstash.com
注意事项
- 默认工作目录是
/workspace/home,不是/home或/ box.cd()用于客户端跟踪路径——它会验证路径是否存在,但不会更改 box 的 shell 当前工作目录。所有 SDK 方法都会自动使用它。- 必须设置
agent.harness;provider/runner仍可使用,但已弃用 - 没有
box.fork()——它已从 SDK 中移除。请创建 box 的快照,并改用Box.fromSnapshot()。 EphemeralBox不支持agent、git、skills、browser或公共 URL——这些功能请使用完整的Box(它支持schedule和快照)- 对于 Agent 运行,
run.exitCode为null;只有执行命令时才可用 - 成功时,
run.result是标准输出;失败时则是标准错误——如果命令以退出码 0 结束但只写入标准错误,结果会是"";请读取run.stderr maxRetries仅针对暂时性故障——如果运行因run.cancel()或自身的timeout而中止,会立即失败,不会再启动第二次仍会计费的运行。超时会抛出BoxError("Run timed out")/"Stream timed out",并将中止作为cause携带Box.delete({ boxIds: [] })和Box.deleteSnapshots({ snapshotIds: [] })会抛出异常——空列表绝不会被视为“删除全部”。如需删除全部快照,请不带 ID 调用Box.deleteSnapshots()files.download({ folder })接收 box 内部的路径;输出会保存到本地的./<basename>- 只有在提供
length时,files.read()才会进行切片——仅提供{ offset }会读取整个文件,而{ length: 0 }则不会读取任何内容 files.stat()默认使用 lstat:符号链接会报告为type: "symlink",除非传入{ follow: true }- 对目录调用
files.remove()需要设置{ recursive: true };创建嵌套路径时,files.mkdir()需要设置{ parents: true } exec.session()仅支持 Node(WebSocket 握手会携带身份验证标头),且句柄拥有该进程——调用close()或断开连接都会终止命令,且无法重新连接会话exec.session({ tty: true })会将标准错误合并到标准输出,因此 PTY 会话不会触发onStderrbox.browser要求创建 box 时设置browser: true- 没有
tab.run()——自主浏览器 Agent 已在 0.7.0 中移除。请自行循环调用observe+act(action)+extract,将目标交给 box 内的 Agent,或通过cdpUrl()使用 Playwright 操控浏览器 tab.act(action)(重放observe()的结果)不消耗 token,也不需要模型提供方密钥;只有使用字符串调用act(instruction)才会计量- 除非创建 box 时设置了
keepAlive: true,否则调用getInitCommand/setInitCommand/deleteInitCommand会抛出异常 box.delete()不可撤销——如果需要保留状态,请先创建快照- 对私有仓库和 PR 执行 Git 操作时,需要在
BoxConfig中设置git.token Box.fromSnapshot()会创建一个新 box,不会修改原 box,也不会从传入的配置转发browser、skills或mcpServersEphemeralBox没有updateNetworkPolicy——请在创建时设置networkPolicyresponseSchema和浏览器的schema需要安装zod(对等依赖,v3 或 v4)- 所有
timeout值的单位都是毫秒