首页
/ 在 Cloudflare Workers 上自托管 Claude Agent 沙箱:基于 Durable Object 与 SessionToolRunner 的纯 Worker 实现指南

在 Cloudflare Workers 上自托管 Claude Agent 沙箱:基于 Durable Object 与 SessionToolRunner 的纯 Worker 实现指南

2026-09-07 19:48:48作者:郦嵘贵Just

本文档完整解析 claude-cookbooks 仓库中 managed_agents/self_hosted_sandboxes/cf-worker/README.md 所描述的纯 Worker 变体:如何不启动任何真实容器,仅凭 Cloudflare Worker + Durable Object,就用 TypeScript SDK 的 client.beta.sessions.events.toolRunner() 为一个自托管环境逐 session 拉起"沙箱执行器",并通过内存中的假文件系统完成 read/write/edit/glob/grep 工具调用。读完本文,你可以完整理解 webhook → 队列排空 → 每会话 runner 的链路设计、Durable Object 如何自行持有租约心跳与强制停止、为何单个环境密钥即可贯穿控制面与会话面,并掌握本地部署到 Cloudflare 的完整步骤及其与 Container / Vercel / Modal 变体的取舍。

一、这个变体在整套方案中的定位

仓库 managed_agents/self_hosted_sandboxes/README.md 把"Claude 托管 Agent 的自托管执行沙箱"定义为一个在多个计算平台上实现的统一契约,每个变体都做同样的三件事:

  1. 接收并校验 session.status_run_started webhook(用 client.beta.webhooks.unwrap());
  2. 排空环境工作队列(environment work queue),使任何单次 webhook 投递都能补捞此前错过的所有积压任务
  3. 对每个工作项拉起一个"每会话沙箱",运行 SDK/CLI 的工具 runner(bash/read/write/edit/glob/grep),维持租约心跳,并把 tool_result 回投给会话。

各变体的计算载体与 runner 对比如下(详见 variants 对比表):

变体 计算载体 Runner 形态
docker/ 自管主机上的普通 Docker 每会话容器内跑 ant beta:worker run
cf/ Cloudflare Containers 每会话 Cloudflare Container 内跑 ant beta:worker run
cf-worker/(本文) Cloudflare Workers(无容器) Durable Object 内跑 TS SessionToolRunner + 隔离内假文件系统
modal/ Modal Modal Sandbox 中运行 Python sandbox_runner.py
daytona/ Daytona 上传到 Daytona Sandbox 的同一份 sandbox_runner.py
vercel/ Vercel Functions + Sandbox Vercel Sandbox 内的 Node runner.mjs

本文的 cf-worker 变体走的是同一套 webhook → 队列排空 → 每会话 runner 的形状,但把执行载体换成 Durable Object:它运行 TS 库形态的会话侧调度循环,配一个"隔离内的假文件系统"来替代真实容器。它的价值在于作为 SessionToolRunner 的 TypeScript 库用法参考——展示如何把自定义工具传入 toolRunner()、以及如何由 DO 自行负责工作项的生命周期(心跳 + 强制停止)。而 cf/ 变体的定位与它互补:若需要真实 shell、或需要 EnvironmentWorker(它组合了整个工作项生命周期,但会引入 Worker isolate 跑不动的 Node 专用模块 agent-toolset/node),应选用 Container 或 Vercel/Modal 演示(见 cf/README.md)。

二、整体数据流:一次 webhook 投递如何拉起一个会话 runner

cf-worker 只有两个逻辑部件,都在 src/index.ts 中可见:

POST /(webhook)
   │  client.beta.webhooks.unwrap()  校验签名
   ▼
仅接受 session.status_run_started
   ▼
drainWork(): 轮询 /work/poll → ack → (若该 session 尚无活实例) RUNNER DO.start({...})
   ▼
SandboxRunner Durable Object(src/runner.ts)
   ├─ heartbeatLoop(): 对 work 项持续心跳,维护租约
   └─ client.beta.sessions.events.toolRunner(sessionId, {tools: fakeTools(this.fs), signal})
        每来一次工具调用 → 在内存 Map 上执行 → 回投 tool_result
   ▼
任务结束 → 强制 stop work 项(force: true)

2.1 webhook 只是"唤醒信号"

index.ts 的 fetch 处理器中,每个投递都会:

  1. anthropic.beta.webhooks.unwrap(body, { headers }) 验证签名,失败直接返回 401 signature verification failed#L132-L137);
  2. 若非 session.status_run_started,直接返回 { status: "ignored", event_type }#L139-L141);
  3. 命中则调用 drainWork(env, anthropic)

关键设计在文件头注释中已点明:webhook 只是唤醒信号——每次投递会把所有待处理工作项全部排空,因此"单次到达的 webhook 就能恢复任何更早错过的投递"。这消除了对 webhook 可靠性的强依赖,也符合上层 契约第 2 步 的约定。

2.2 队列排空:poll → ack → spawn,而不是长轮询

worker 控制面的 WorkPoller 会对空队列长轮询且永不返回,但 Worker 的 fetch handler 必须及时返回响应。因此 drainWork() 采用"轮询直到空"的同步排空策略,并设置了 MAX_DRAIN = 25 的循环上限(#L22)以防无界空转。

每次循环内的处理要点:

  • anthropic.beta.environments.work.poll(env.ANTHROPIC_ENVIRONMENT_ID, { reclaim_older_than_ms: 2000 }) 领取一个工作项(#L62-L64)。注释说明 SDK 会自动带上 anthropic-beta: managed-agents-2026-04-01,无需额外手动声明 beta 头。
  • 404("EnvironmentInstance not found for session …")做跳过而非终止处理:当队列取出 Redis 条目而其 session 已消失时,服务端已消费该过期条目,继续 continue 即可越过它;其余错误则 break,等下一个 webhook 再来恢复(#L65-L76)。
  • 日志采用显式白名单:只记录 idenvironment_iddata.typedata.id、时间戳与心跳时间等字段,刻意排除 actor(可能是 PII)和 metadata(用户提供内容),见 #L80-L89
  • 只处理 work.data.type === "session" 的工作项(#L90)。
  • ack 之后,以 session id 为名取 DO:env.RUNNER.get(env.RUNNER.idFromName(sessionId))#L100)——一个 session 恰好对应一个 Durable Object 实例,天然实现"每会话沙箱"。
  • 先查 stub.isLive():只有该 DO 当前没有活任务时才调用 stub.start(...),避免重复拉起(#L101-L109);已在跑的则记入 created: false 返回。

错误回显统一走 errDetail():它把 SDK 错误里的 statusrequestID 保留下来(便于与服务端 trace 关联),而消息正文经 redact() 处理后才会落到日志——sk-ant-*whsec_*Bearer <token> 三种凭证形态都会被抹掉。

三、核心 Runner:Durable Object 内的 SessionToolRunner

真正执行"会话侧调度"的是 SandboxRunner 这个 Durable Object。它持有两个实例状态:一个假文件系统 private fs: FakeFS = new Map()#L37)和一个用于中止整条链路的 AbortController#L38)。

3.1 分派只做会话侧,租约生命周期由 DO 自持

代码注释明确区分了两层职责(#L4-L10):

  • SessionToolRunner 只管分派循环:reconcile + 事件流 + 工具执行 + 结果回投;
  • 工作项生命周期(心跳 + 强制停止)由 DO 持有,因为 DO 才是工作项的租约人(lessee);
  • 仓库中没有用 EnvironmentWorker,因为它会引入 Node 专用模块 agent-toolset/node,Workers isolate 无法运行——这正是这个变体要"手写"心跳循环的根本原因。

这解释了为什么 start() 里要把任务 detach 出去跑:start() 由 webhook 调用,this.ctx.waitUntil(...) 让心跳与 runner 循环在 webhook 返回后继续执行(runner.ts#L44-L61)。同时 start() 开头有 isLive() 幂等保护,配合 index.ts 中的重复检查构成双层防重入。

3.2 调度主循环:toolRunner()

runWorkItem() 的骨架是:

for await (const call of client.beta.sessions.events.toolRunner(opts.sessionId, {
  tools: fakeTools(this.fs),
  signal: ctrl.signal, // 缺省:end_turn 空闲 60s 后退出;传 0 则跑到 session 结束
})) {
  console.log(`[runner] dispatched tool=${call.name} tool_use_id=${call.toolUseId} is_error=${call.isError} posted=${call.posted}`);
}

每条日志都带 tool_use_idis_errorposted,方便用 wrangler tail 观察"工具是否被成功分派与回投"。

3.3 心跳循环:动态跟随服务端 TTL

heartbeatLoop() 默认每 30 秒(HEARTBEAT_DEFAULT_MS = 30_000#L22)向 client.beta.environments.work.heartbeat() 报告一次:

  • 携带 expected_last_heartbeat: last 做并发安全的条件更新(#L104-L109);
  • 若服务端返回 ttl_seconds > 0,把间隔收敛到 [1s, ttl/2] ∩ 30s 区间(#L110)——即按服务端租约时长自适应频率;
  • 若响应显示 statestopping/stopped,或 lease_extended 为假,说明控制面要回收该工作项,立即 ctrl.abort() 退出(#L111-L115);
  • 错误分两类处理(isFatal4xx):永久性 4xx(4xx 且非 408/429)直接放弃,临时性错误只告警并继续下次心跳(#L116-L124)。408/429 被视为可重试,这符合常见的限流/超时语义。

3.4 结束清理:强制停止工作项

调度循环无论正常退出还是异常退出,都进入 finally#L87-L96):先 ctrl.abort() 停掉心跳,await heartbeat 收尾,再调 work.stop(opts.workId, { force: true }) 释放租约;409(已被停止等竞态)被吞掉,其余错误才记日志。这与 ant beta:worker run 在退出时的行为一致——runner 是 work 项的租约人,退出前必须显式 force-stop。

四、"假沙箱"工具集:Durable Object 内存里的 Map 文件系统

tools 模块在 src/tools.ts 中。所谓"隔离内假文件系统"就是一个 Map<string, string>(类型别名 FakeFS#L12),key 是路径、value 是文件内容,常驻在 Durable Object 里。它展示的核心 API 是:如何把自定义工具传给 client.beta.sessions.events.toolRunner——入参形态与 client.beta.messages.toolRunner 接受的完全一致

五个工具的 schema 与语义如下(均由 fakeTools() 返回):

工具 入参 行为
read file_path fs.get(file_path),缺失返回 error: <path>: no such file
write file_path, content fs.set(...),返回写入字节数
edit file_path, old_string, new_string 替换首个匹配;文件缺失或 old_string 不在时返回明确错误
glob pattern 把 glob 转正则后在 key 上过滤,命中按字典序 \n 拼接
grep pattern, path? 逐行正则搜索,输出 path:line:textpath 可选用于限定范围
bash command 不可用桩:直接返回错误并提示改用上面五个工具

其中 globToRegex()** 翻译成 .** 翻译成 [^/]*? 翻译成 [^/],让 glob 的"目录通配"语义在路径匹配上成立;grep 复用同一转换来限定扫描范围。

值得注意的边界行为都写进了返回字符串而非抛异常:例如 edit 在 old_string 找不到时返回可读错误、bash 明确告诉模型"此环境不可用 bash"。这种"用工具结果说话"的做法让 Agent 能自然地切换策略(改走文件类工具),而不是撞上不可恢复的运行错误。

fakeTools 最后返回 [bash, read, write, edit, glob, grep]#L87),与真实 toolset 同形。相比之下,容器类变体(如 docker/cf/)里这些工具由 CLI/SDK 在真实 shell 上实现;而在纯 Worker isolate 里没有进程模型,所以 bash 只能是被禁用的占位。

五、单一凭证设计:环境密钥贯穿控制面与会话面

cf-worker 变体与整套方案共享一条安全设计(cf/README.md 也有同样表述):组织级 API key(org API key)从不进入任何 runner

  • webhook 侧drainWork 用的 Anthropic client 以 ANTHROPIC_ENVIRONMENT_KEYauthToken,负责 poll / ackwork.stop 也走同一密钥),因此这些调用不需要任何额外的 Authorization 头(见 index.ts#L94-L98 注释);
  • runner 侧:DO 内 new Anthropic({ authToken: opts.environmentKey }) 用同一个环境密钥,支撑会话事件流、租约心跳与强制停止三类调用(runner.ts#L49-L53#L73-L74);
  • webhook 签名则由独立的 ANTHROPIC_WEBHOOK_SECRETwhsec_*)负责,SDK 会内部解码其 URL-safe base64,所以代码中把密钥原样传入 webhookKeyindex.ts#L126-L128)。

这种"单密钥 + 签名密钥"的最小凭证面,意味着丢失或轮换 runner 凭证只需处理环境密钥一处;生产上还叠加了前文 2.2 节的日志脱敏,双保险。如何生成环境密钥、把密钥导出为宿主环境变量,见 docs/usage-guide.md 的步骤 1–2

六、空闲策略:60 秒 end_turn 空闲即退出

两个层面都有空闲控制,且语义一致:

  • SDK 缺省空闲策略(本变体所用):toolRunner()session.status_idlestop_reason: end_turnmaxIdleMs(缺省 60 秒)退出;任何其他事件——包括 requires_action 的空闲(Agent 正被 DO 阻塞等待工具)——都会重置计时器runner.ts#L12-L15)。想要"一直跑到会话结束",可把该参数传 0(#L79-L80 注释)。
  • CLI 变体的对照:容器类变体由 ant beta:worker run--max-idle 承载同样的"end_turn 空闲后退出、其他事件重置时钟"策略(usage-guide.md Flags 表)。

也就是说,空闲回收的责任不在 webhook、也不在控制面队列,而在会话侧 runner 自身:一次 end_turn 后若 60 秒内没有新事件,runner 自然结束 → finally 里 force-stop work 项 → 控制面随即回收该 session 的租约。这正是"Durable Object 租约人"模型的闭环:没有显式的全局清理器,靠的是 runner 自主退出 + 租约自然过期

七、部署配置与本地运行

7.1 wrangler.toml 关键配置

wrangler.toml 的内容决定了整个部署形态:

name = "cma-self-hosted-sandbox-worker"
main = "src/index.ts"
compatibility_date = "2026-03-01"
compatibility_flags = ["nodejs_compat"]

[observability]
enabled = true

[[durable_objects.bindings]]
name = "RUNNER"
class_name = "SandboxRunner"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SandboxRunner"]

[vars]
ANTHROPIC_BASE_URL = "https://api.anthropic.com"
# 替换为你的自托管环境 id(Console → Environments)
ANTHROPIC_ENVIRONMENT_ID = "env_01..."

逐项说明:

  • main 指向 src/index.tsindex.tsSandboxRunner 一并 re-export(#L12),供 DO 绑定解析;
  • compatibility_flags = ["nodejs_compat"] 是 SDK 得以在 Workers isolate 里运行的前提;compatibility_date 需为支持所依赖 Durable Object SQLite 与 Worker 特性的较新日期;
  • [[durable_objects.bindings]] 声明绑定名 RUNNER 与类 SandboxRunner,与 Env 接口 中的 RUNNER: DurableObjectNamespace<SandboxRunner> 一一对应;
  • [[migrations]]new_sqlite_classesSandboxRunner 创建 SQLite-backed DO 存储(FakeFS 的持久化基础);
  • [vars]ANTHROPIC_BASE_URL 默认指向官方 API,需要时可改成你的网关;ANTHROPIC_ENVIRONMENT_ID 占位 env_01...部署前必须替换为 Console → Environments 中实际的环境 id
  • ANTHROPIC_WEBHOOK_SECRETANTHROPIC_ENVIRONMENT_KEY 是两类敏感凭证,只能走 secret 注入,wrangler.toml 里只留注释(#L22-L24)。

7.2 部署步骤(原文命令 + 注释补充)

原文档给出四步,此处原样继承并补充说明:

npm i                 # 安装 @anthropic-ai/sdk、zod 等依赖(见 package.json)
wrangler secret put ANTHROPIC_WEBHOOK_SECRET    # 注册 webhook 时的 whsec_ 签名密钥
wrangler secret put ANTHROPIC_ENVIRONMENT_KEY   # 环境的 sk-ant-oat01-... 密钥
# 先编辑 wrangler.toml 里的 ANTHROPIC_ENVIRONMENT_ID,然后:
wrangler deploy

package.json 中 依赖与脚本 还提供了三组常用命令:

  • npm run devwrangler dev:本地起 Worker,配合 wrangler tail(runner 里 logLevel: "info" 即为此服务)可观察 runner 生命周期;
  • npm run deploywrangler deploy:生产发布;
  • npm run typechecktsc --noEmit:零输出类型检查(tsconfig.json 全程 strict: true,面向 ES2022 + Workers types)。

7.3 自托管环境前置条件

本变体依赖的公共 API 需要以下请求头,且要求你的账号已被授权使用自托管环境能力(创建方法及代码级创建示例见 usage-guide.md 的步骤 1):

anthropic-version: 2023-06-01
anthropic-beta: managed-agents-2026-04-01

环境密钥在 Console → Workspace → Environments 中生成,作为 ANTHROPIC_ENVIRONMENT_KEY 使用;它认证该环境的整条 worker 流程——poll、ack、stop、heartbeat、session 事件流、技能下载(usage-guide 原文语),是 runner 唯一的所需凭证。

八、适用范围与局限:何时换用容器类变体

原文档明确划定了这个变体的边界,引用如下要点:

  • 它是 SessionToolRunner 的 TS 库用法参考,聚焦自定义工具 + DO 自持心跳/强制停止;
  • 由于 Workers isolate 里没有真实 shell 与进程模型bash 只能是不可用桩,工具能力局限在内存文件系统;
  • EnvironmentWorker(组合整个工作项生命周期的高层封装)不能在这里使用,因为它引入 Node 专用模块 agent-toolset/node,Workers isolate 无法加载;
  • 因此,需要真实 shell、或希望使用 EnvironmentWorker 时,请改用 Container 变体 或 Vercel/Modal 演示

在实践中这意味着一个清晰的分级决策:

  • 演示与库用法学、希望部署面最小、无需持久真实文件系统、Agent 只需读改写文本/搜文件 → 选 cf-worker;
  • 需要真实命令行执行、装包、编译、运行进程 → 走 docker/cf/ 容器路线(runner 为 ant beta:worker run,工具集在真实 shell 上生效);
  • 需要开箱即用的 EnvironmentWorker 高层编排 → 参考 vercel/modal/

无论选哪种,上层契约相同(webhook 唤醒 → 排空队列 → 每会话 runner → 心跳 → 强制停止),因此控制面(webhook 注册、环境密钥)可以复用,只需要替换计算载体。

九、小结:从这份参考实现可以带走什么

纵观 README 与其 index.tsrunner.tstools.ts 三个实现文件,能提炼出五条可复用的工程结论:

  1. 用 Durable Object 实现"每会话沙箱":以 session id 命名 DO 实例即得天然的路由与隔离,isLive() + 幂等 start() 防止重复拉起;
  2. webhook 只做唤醒:每次投递排空整个队列(上限 MAX_DRAIN),使系统对单次投递丢失天然免疫;
  3. 租约生命周期属于执行器自身:DO 自行心跳、按服务端 TTL 自适应间隔、观测到 stopping/lease 未续 即自中止,退出时 force: true 释放工作项;
  4. 工具即函数betaZodTool + Zod schema 即可把任意内存操作包装成与 toolRunner 兼容的工具,bash 这类不可行操作应返回可读错误而非抛异常;
  5. 最小凭证面:环境密钥唯一贯穿 poll/ack/stop/事件流/心跳,webhook 签名密钥独立分离,日志侧再做正则脱敏,三层防护形成闭环。

对想"零容器"跑通 Claude 托管 Agent 自托管沙箱、或想在自有 TS 代码里复用 SessionToolRunner 的开发者而言,这份实现是目前仓库中最直接、最完整的 TS 库用法范本;需要完整 shell 能力时,再在同一契约下平滑切换到容器变体即可。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 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
531
594
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.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388