首页
/ Vercel 部署 Claude 受管智能体:基于自托管 Sandbox 的 Webhook → 工作队列 → 每会话运行器实战指南

Vercel 部署 Claude 受管智能体:基于自托管 Sandbox 的 Webhook → 工作队列 → 每会话运行器实战指南

2026-09-07 22:43:02作者:魏献源Searcher

导读:本文完整拆解 claude-cookbooks 仓库中 Vercel 参考实现(managed_agents/self_hosted_sandboxes/vercel/),它是"自托管执行沙箱(Self-Hosted Sandboxes)"系列中基于 Vercel Functions + Vercel Sandbox 的一个变体。你将掌握:如何用同一个 webhook 调用链(签名校验 → 排空环境工作队列 → 每工作项拉起一个 Vercel Sandbox)把 Claude 受管智能体的工具执行环境从 Anthropic 托管侧迁到你自己可控的 Vercel 基础设施上,以及沙箱复用、冷启动控制与单一环境密钥凭证体系等关键实现细节。

背景:同一契约,五种计算提供方

要理解 Vercel 变体,首先要明白它在整个 self_hosted_sandboxes 体系中的位置。系列总览 managed_agents/self_hosted_sandboxes/README.md 明确定义了每个变体都要实现的统一契约:

  1. 接收 session.status_run_started webhook,并用 client.beta.webhooks.unwrap() 校验签名;
  2. 排空环境工作队列(drain),保证单次投递即可补上之前错过的条目;
  3. 对每个工作项,启动一个每会话(per-session)沙箱,运行 SDK/CLI 工具运行器(bash / read / write / edit / glob / grep),心跳续租,并把 tool_result 回传给会话。

各变体只是换了"计算层 + 运行器载体":

变体 计算层 运行器
docker/ 自控主机上的原生 Docker 每会话容器中跑 ant beta:worker run
cf/ Cloudflare Containers 每会话容器中跑 ant beta:worker run
cf-worker/ Cloudflare Workers(无容器) Durable Object 中的 TS SessionToolRunner + 隔离内假文件系统
modal/ Modal Sandbox + 每会话 Volume Python sandbox_runner.py
vercel/ Vercel Functions + Sandbox Node runner.mjs 运行于 Vercel Sandbox

本文主角的形态是:webhook → 排空队列 → 每会话运行器,与 Modal、Daytona、Cloudflare 各 demo 一致,但运行器是跑在 Vercel Sandbox 里的 TS EnvironmentWorker.handleItem(),并使用 SDK 的 betaAgentToolset20260401() 工具集,直接操作沙箱的真实文件系统(bash / read / write / edit / glob / grep)。

仓库文件结构

该变体代码量小但职责清晰,共四个文件 + 两份工程配置:

文件 职责
api/webhook.ts Vercel Function:校验签名 → 排空队列 → 每工作项拉起 Sandbox
runner/runner.mjs 在沙箱内运行:EnvironmentWorker.handleItem(),构建每会话的 AgentToolContext、下载智能体的 skills、心跳续租地跑 SessionToolRunner,退出时强制停止工作项
package.json 工程与依赖声明(vercel dev / vercel deploy / tsc --noEmit 脚本)
tsconfig.json ES2022 + Bundler 模块解析、strict 模式,include 覆盖 apirunner
vercel.json Functions 配置:maxDuration: 60includeFiles
README.md 部署与测试文档(本文对应的原文档)

关键依赖版本(见 package.json):@anthropic-ai/sdk ^0.97.0@vercel/sandbox ^1.10.0@vercel/kv ^3.0.0vercel ^52.0.0(CLI)、minimatch ^9.0.5。这些版本与仓库配套的自托管使用指南中钉定的 TS SDK 0.97.0 一致。

整体调用链:webhook 只是"唤醒信号"

api/webhook.ts 顶部注释就把设计意图讲得很透:The webhook is a wake-up signal only(webhook 仅作唤醒信号)。Vercel Functions 是无状态、有超时上限的 serverless 函数,不可能像常驻进程那样长轮询,因此设计上把所有"重量级工作"都转移到沙箱内的分离运行器身上。

每次 webhook 投递的处理流程(对应 POST 处理器):

  1. 校验签名:用 ANTHROPIC_ENVIRONMENT_KEY 构造 Anthropic 客户端(webhookKey 传入 ANTHROPIC_WEBHOOK_SECRET),对原始 body 调用 anthropic.beta.webhooks.unwrap(body, { headers });签名不过则返回 401 signature verification failed。注释特别说明 whsec_ 密钥原样传入即可,SDK 内部会做 URL-safe base64 解码。
  2. 事件类型过滤:只有 data.type === "session.status_run_started" 才继续处理,其余事件返回 { status: "ignored" }
  3. 排空工作队列:调用 drainWork() 轮询并处理工作项。

drainWork:为什么是"排空"而不是"取一条"

drainWork() 的实现值得细看。SDK 的 TS WorkPoller 是长轮询、空队列时永不返回的,而 serverless handler 必须及时响应,所以这里手动 poll → ack 直到队列为空:

  • 上限 MAX_DRAIN = 25常量定义),防止单次函数调用被无限排空拖垮;
  • 每次 poll 传入 reclaim_older_than_ms: 2000,即工作项认领后超过 2 秒未续租就允许被重新认领;
  • 单次投递排空全部待处理工作项(而不只是触发它的那一条),因此一条迟到或错过的 webhook 也能通过下一次投递补齐
  • poll 返回 404 时的处理值得注意:/work/poll 在取出一个 Redis 中 session 已消失的过期条目时会 404,该过期条目已被服务端消费,所以重试即可越过;其余错误视为配置/瞬时故障,终止本次排空、等下一个 webhook 恢复。

代码注释还提示 SDK 会自动发送 anthropic-beta: managed-agents-2026-04-01 请求头,无需手动附加 beta 参数。

processWorkItem:认领后拉起沙箱

对每条已认领的工作项,processWorkItem() 依次完成:

  1. ack 认领:客户端已用 environment key 鉴权,ack 无需任何逐调用鉴权头;
  2. 沙箱复用探测(见下文 KV 小节);
  3. 创建 SandboxSandbox.create() 指定 runtime: "node24"、超时 30 分钟,并设置 networkPolicy——出网白名单只放行 Anthropic API 域名与 registry.npmjs.org(供 npm install),若你的工具还需要访问 github.com 等域名需自行追加;
  4. 写入运行器writeFilesrunner.mjs 与其内联 package.json 写入沙箱内 /vercel/sandbox/runner/。注意 runner.mjs 源码是在 webhook 模块加载时用 readFileSync 读入内存的(L44-L47),这样既能被 Vercel 文件追踪器打进产物,又省去沙箱二次读取;
  5. 准备目录:以 root(sudo: true)执行 mkdir -p /mnt/session /workspacechown 给沙箱用户,确保非特权运行的 runner 有权限写智能体的工作区;
  6. 沙箱内 npm install:安装 @anthropic-ai/sdkminimatch,结果回显到函数日志(而不是只有沙箱终端可见)以便排查;
  7. detached 启动 runnerrunCommand({ cmd: "node", args: ["runner.mjs"], cwd: RUNNER_DIR, detached: true, env: {...} })detached: true 是关键——运行器的心跳与工具执行必须活过本次函数调用

detached 进程的环境变量由 webhook 注入,与 ant beta:worker poll --on-work 的环境契约一致(见 webhook.ts L216-L232):

ANTHROPIC_BASE_URL        # 默认 https://api.anthropic.com,可经 ANTHROPIC_BASE_URL 覆盖
ANTHROPIC_ENVIRONMENT_KEY # 运行器的单一凭证
ANTHROPIC_SESSION_ID      # 会话 id
ANTHROPIC_ENVIRONMENT_ID  # 自托管环境 id
ANTHROPIC_WORK_ID         # 工作项 id
WORKDIR                   # 智能体工作目录,默认 /workspace

日志镜像与脱敏

webhook 启动运行器后会监听其日志流约 30 秒(或累计 8 KB)并把开头内容镜像到函数日志(L238-L251),这样启动即崩(坏 import、401、缺环境变量)或首次工具分发都能仅靠 vercel logs 排查,不必依赖 Sandboxes 观测面板。出于安全考虑,输出前经 redact()sk-ant-…whsec_…Bearer … 等凭据形态打码。

运行器内部:EnvironmentWorker.handleItem()

runner/runner.mjs 全部逻辑只有约 60 行——因为它是对 Modal 变体 sandbox_runner.py 的 TS 类比,真正的工作由 SDK 内部完成:

const ctrl = new AbortController();
process.once("SIGTERM", () => ctrl.abort());
process.once("SIGINT", () => ctrl.abort());

const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY;
const client = new Anthropic({
  authToken: environmentKey,
  logLevel: "info", // 把 worker 生命周期(start/idle/heartbeat shutdown)写进沙箱日志
});

await client.beta.environments.work
  .worker({
    environmentKey,
    workdir: WORKDIR,           // 默认 /workspace
    unrestrictedPaths: true,    // 放开 bash/read/write/edit/glob/grep 的路径限制
    signal: ctrl.signal,
  })
  .handleItem();

client.beta.environments.work.worker(...).handleItem() 就是整个运行器:它会在 workdir 构建每会话的 AgentToolContext、把智能体的 skills 下载到 {workdir}/skills/<name>/,然后为该会话跑一个 SessionToolRunner(心跳 + 事件流 + 工具分发 + 结果回帖,完整实现可对照 use-guide 的 Library usage 说明),退出时强制停止工作项(force-stop)

这里有个值得注意的设计取舍:runner 是 npm install 出来的 SDK 依赖,运行器代码不需要每个沙箱里都有 tsc 编译产物runner.mjs 本身是 ESM JavaScript,直接 node runner.mjs 即可运行。

关于 unrestrictedPaths: true:对应 CLI 的 --unrestricted-paths 标志,放开后工具可读写任意路径(沙箱本身就是隔离边界,无需像宿主机那样收紧)。与 usage-guide 中"worker 直接在宿主机执行 shell 与文件操作时必须放进隔离边界"的告诫互为印证。

空闲策略(Idle Policy)

README 强调该 demo 使用 SDK 默认空闲策略:运行器在 session.status_idlestop_reason: end_turn 之后 60 秒退出;而任何其它事件——包括 requires_action 空闲(智能体正阻塞在沙箱工具上)——都会重置时钟。也就是说,只要会话仍有活动迹象,运行器就保持存活;只有真正结束一轮(end_turn)后才开始倒计时退出。这与 usage-guide--max-idle 默认 1m after end_turn idle 的语义完全一致。

单一凭证设计

整个系列(不只 Vercel)在认证上有一个统一原则:环境密钥是唯一凭证ANTHROPIC_ENVIRONMENT_KEY 同时用于控制面(poll / ack / stop / 心跳)与每会话调用(事件流、工具执行、skills 下载、force-stop),没有任何一处出现 org API key。这在架构上是刻意的:沙箱内运行器只拿到作用域受限的环境密钥,即使沙箱被攻破,也无法滥用组织级凭据。

对升级路径感兴趣的读者可对照 upgrade-guide:旧版 SDK 曾用 "service key 只授权 poll + 每工作项 secret 解码出 sessions_token" 的双凭证模型,新模型已合并为单一环境密钥,decodeWorkSecretsessions_token 均已移除。

前提条件

  • npmVercel CLInpm i -g vercel);
  • 一个已关联的 Vercel 项目(vercel link);
  • 一个已注册的自托管环境及其环境密钥

创建自托管环境与生成环境密钥的完整流程见 managed_agents/self_hosted_sandboxes/docs/usage-guide.md:既可以在 Console 中 Workspace → Environments → New → Self-hosted 创建,也可以用 client.beta.environments.create({ name, config: { type: "self_hosted" } }) 在代码中创建,随后在 Console 中 Generate environment key 并将其导出为 ANTHROPIC_ENVIRONMENT_KEY。所有公开 API 调用都需要请求头 anthropic-version: 2023-06-01anthropic-beta: managed-agents-2026-04-01(SDK helper 会自动附加)。

配置环境变量

在项目根目录依次执行:

npm install
vercel link
vercel env add ANTHROPIC_WEBHOOK_SECRET   # 先填占位符
vercel env add ANTHROPIC_ENVIRONMENT_ID
vercel env add ANTHROPIC_ENVIRONMENT_KEY

三个环境变量的语义如下(对照 webhook.ts 源码注释):

环境变量 用途 说明
ANTHROPIC_WEBHOOK_SECRET webhook 签名校验 注册 webhook 时由 Anthropic 签发的 whsec_… 密钥,第一阶段先用占位符
ANTHROPIC_ENVIRONMENT_ID 定位自托管环境 形如 env_01…
ANTHROPIC_ENVIRONMENT_KEY 单一凭证 控制面(poll/ack/stop)+ 每会话调用的唯一凭据
ANTHROPIC_BASE_URL(可选) API 网关地址 默认 https://api.anthropic.com,自建网关/代理时可覆盖

此外,若启用沙箱复用还需 Vercel KV 的 KV_REST_API_URL / KV_REST_API_TOKEN(见下文"沙箱复用")。

部署

vercel deploy --prod

命令会打印 https://<project>.vercel.app 形式的线上地址。随后把 https://<project>.vercel.app/api/webhook 注册为 session.status_run_started 事件的 webhook(在 Console 或通过 API 完成)。注册时 Anthropic 会签发一个 secret,用它覆盖占位符并重新部署:

vercel env rm ANTHROPIC_WEBHOOK_SECRET production
vercel env add ANTHROPIC_WEBHOOK_SECRET production
vercel deploy --prod

这一步的"先部署占位符、再回填真值、再部署"流程是必须的:你只有先拿到线上 URL 才能完成 webhook 注册,而注册之后才知道真正要用的 secret。

本地联调可用 npm run dev(即 vercel dev),类型检查用 npm run typechecktsc --noEmit)。

测试

创建一个指向该自托管环境 id 的会话并向其发送消息:

session = client.beta.sessions.create(agent=agent_id, environment_id=ENVIRONMENT_ID)
client.beta.sessions.events.send(session.id, events=[{"type": "user.message", "content": "ls -la"}])

然后观察 Vercel 函数日志,应当依次看到:

vercel logs <project>.vercel.app
# [webhook] event=session.status_run_started session_id=...
# [webhook] acked work=... session=... sandbox=sbx_... (created)

沙箱的 stdout/stderr([runner] 前缀行)显示在 Vercel 控制台项目的 Observability → Sandboxes 标签页下;同时在函数日志里也能看到被镜像过来的运行器头 30 秒输出,双重可观测。

生产化要点(Notes 深度解读)

冷启动预算与 maxDuration

Sandbox.create() + writeFiles() + 沙箱内 npm install 合计约 15–25 秒,之后函数才响应;因此 vercel.json 给该函数设置了 "maxDuration": 60,否则函数会在沙箱就绪前被 Vercel 平台超时杀掉。运行器本身是 detached 的,不受函数超时影响。

vercel.json 中另一处容易被忽略的配置是 "includeFiles": "runner/**":它把 runner/runner.mjs 打进函数产物,作为文件追踪器漏掉 readFileSync 时的双保险(对应 webhook.ts 的注释说明)。

沙箱复用:Vercel 无原生标签 API,需要外部状态

与 Modal(Sandbox.from_name + tags)不同,Vercel Sandbox 没有原生的 tag / label / get-by-name API,且 Sandbox.list() 不返回任何每沙箱元数据,所以复用必须借助外部状态。本 demo 的做法是:

  • 若检测到 KV_REST_API_URL / KV_REST_API_TOKEN(即项目挂载了 Vercel KV),webhook 就把 session_id → sandbox_id 映射写入 KV(L184),key 形如 cma:sandbox:<sessionId>,TTL 与沙箱超时一致(30 分钟)以便自清理;
  • 下一次 run_started 到达时,findLiveSandbox() 先查 KV 映射,再用 Sandbox.get({ sandboxId }) 确认沙箱仍 running(否则删掉映射重新创建),并调用 extendTimeout() 续期沙箱 VM——沙箱 VM 有自己的时钟、独立于运行器,因此每个新 run 都要续租,让活跃会话的 VM 能跨轮次存活;一旦运行器在 end_turn 空闲 60 秒后退出,续租停止,沙箱在下一次超时边界自然消亡;
  • 未配置 KV 时,每次投递都创建全新沙箱。此时靠 SessionToolRunnerseen / answered 去重逻辑兜底——重复运行器只是浪费,而不会出错(不会重复回帖结果)。

KV 写入是 best-effort 的:写失败只意味着下次投递会新建一个重复沙箱,不会破坏正确性。

工作目录:纯临时目录,非持久卷

webhook 在沙箱启动时创建 /mnt/session/workspace 两个目录,并把 /workspace 作为工具分发的工作目录(skills 下载到 /workspace/skills/<name>/),与 Modal/CF 各 demo 的布局对齐(对应注释:Modal 在 /mnt/session 挂卷、CF 容器用 --workdir /workspace)。但需要特别强调:

Vercel Sandbox 没有 volume API,这两个目录只是普通的一次性临时目录,不是持久卷。智能体的工作树在沙箱停止时即消失。

这是与 Modal 变体(每个会话挂载独立 cma-session-<session_id> Volume,工作树与 skills 可跨沙箱生命周期存活,见 modal_sandbox_webhook.py)最本质的差异:Vercel 上如果你想在会话跨多轮后保留文件,必须自己做外部持久化(如把产物上传到对象存储),无法靠沙箱磁盘。

npm install 与冷启动优化

npm install 在沙箱启动时(spawn 时刻)执行,所以本 demo 没有构建步骤——runner.mjs 原样上传即可。若想再省约 10 秒冷启动,可改用 esbuild 预打包 runner/runner.mjs 为单文件 bundle 再上传,跳过沙箱内安装依赖的环节。代价是构建与上传逻辑前移,适合对 P50 冷启动敏感的线上场景。

常见问题定位

  • 签名 401:检查 ANTHROPIC_WEBHOOK_SECRET 是否已用注册后签发的真值覆盖(而非占位符),并确认已重新 vercel deploy --prod
  • webhook 收到但无沙箱:先看 vercel logs 是否有 [webhook] FAILED work=...WorkError 携带可控诊断文本,其余异常只打印类型名(构造器名),避免 SDK/Vercel 异常把请求上下文泄露进日志——这是代码注释中刻意的安全取舍。
  • 沙箱内 401/网络失败:核对注入 runner 的 ANTHROPIC_ENVIRONMENT_KEY 是否属于该环境,以及 networkPolicy.allow 是否覆盖了你工具要访问的域名(默认只放行 Anthropic API 主机与 registry.npmjs.org)。
  • 跨轮工作树丢失:这是 Vercel 无卷 API 的固有限制,需自行做外部持久化,或接受每会话临时文件语义。

小结

这套 Vercel 参考实现展示了一种优雅的分层:serverless 函数只做"信号处理 + 资源编排"(校验、排空、拉起),真正有状态、需要长连接的工作全部下沉到 detached 的沙箱运行器。它绕开了 serverless 的三大天然限制(执行时长、无状态、出网白名单),同时通过 KV 复用、日志镜像、单一环境密钥等手段把成本与安全风险都控制在合理范围。若你的执行环境恰好落在 Vercel 生态内,可在此基础上按需扩展 networkPolicy 出网白名单、追加自定义工具(参考 usage-guide 的 Customising the tool list 一节betaAgentToolset20260401(ctx) 的组合用法),并结合 upgrade-guide 在 SDK 升级时保持 API 面一致。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388