惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
阮一峰的网络日志
阮一峰的网络日志
A
About on SuperTechFans
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
V
Visual Studio Blog
Martin Fowler
Martin Fowler
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 叶小钗
I
InfoQ
B
Blog RSS Feed
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
P
Proofpoint News Feed
WordPress大学
WordPress大学
小众软件
小众软件
B
Blog
MongoDB | Blog
MongoDB | Blog
人人都是产品经理
人人都是产品经理
量子位
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客

阁子

LEANN 的重算式向量索引 · 阁子 lamarck 按真实调用进化全部 skill · 阁子 FrontierAgent 的上下文压缩与重复抑制 · 阁子 magpie 本地语义搜索 · 阁子 OpenViking 的上下文组织与检索 · 阁子 munder-difflin 的多 agent 协调实现 · 阁子 ai-memory:换个 CLI 接着干的 agent 记忆层 · 阁子 TimesFM:预测之前的脏活都在模型外面 · 阁子 DarwinX:模型不动,只进化外壳 · 阁子 Final2x:一个刻意做薄的超分外壳 · 阁子 Cordis:把卸载做完备的插件框架 · 阁子 DeepSeek Harness:一切皆插件的开源 agent 运行时 · 阁子 Agency Agents:一座 AI 角色库的分发工程 · 阁子 Harvey LAB:法律 Agent 基准的架构与评测方法 · 阁子 DeepTutor:开源的 AI 私人家教工作台 · 阁子 Semantica:面向可审计 AI 的图原生基础设施 · 阁子 PromptCache:LLM 语义缓存网关的架构与实现 · 阁子 Prime Agent,只给模型一个工具的长跑 agent · 阁子 Skill Shelf: 给 agent 的 skill 仓库,兼做服务的配置中心 · 阁子 QM,给全公司用的 agent 平台 · 阁子 PersonaLive: 把肖像扩散模型压到能直播 · 阁子 Neon: 把本地视频点亮成系统虚拟摄像头 · 阁子 LiteReality-Agent笔记: 从一次扫描到可交互的房间 · 阁子 LiteReality-Agent笔记: 从一次扫描到可交互的房间 蝉与梦 · 阁子 小工具(三) 小工具(三) · 阁子 相机小述 相机小述 · 阁子 四元数与旋转矩阵
Cloudflare Computer,装在 Durable Object 里的文件系统 · 阁子
2026-08-10 · via 阁子

Cloudflare 最近放出了一个叫 Cloudflare Computer 的预览仓库,把「给 agent 一台电脑」拆成了两半:文件系统和执行环境。
文件系统住进 Durable Object 的 SQLite,执行环境退化成可插拔的外设,容器只是三种外设之一。
这个拆法跟我对沙箱的直觉正好相反,翻完二十篇规格文档,值得记一篇。

它是什么

@cloudflare/computer 是一个跑在 Durable Object 里的虚拟文件系统:整棵树落在 DO 自带的 SQLite 里,
对外是 workspace.fs(用起来像 node:fs/promises)加 workspace.runtime.exec(source, { backend }) 一个执行入口。
今天有三种后端:container(真容器加 FUSE 挂载)、isolate shell(Dynamic Worker 里跑 just-bash)、isolate JavaScript(Dynamic Worker 里求值 ES module)。
也可以一个后端都不配,纯当持久文件系统用——fs 照常工作,exec 才抛错。

丑话在前:README 顶上就是 PREVIEW ONLY,API 不稳定,不适合生产;docs/ 下二十篇规格自称 forward-looking,要当设计意图读,不能当代码现状读。
下文尽量把两者分开标:比如 R2 只读挂载在 schema 里留好了座位(_vfs_mounts 表、stub_size 列),但运行时还没接线,就属于「规格意图」。

为什么不是沙箱

给 agent 配环境,常规做法是发一个容器:文件在容器盘上,跑完要么整包丢弃,要么自己往对象存储打包搬运。
状态的权威在容器里,而容器天生要被回收,这两件事是拧着的。isolate 倒是轻,但 Workers 里压根没有像样的持久文件系统。

这个项目把关系倒过来:权威态只有一份,住在 DO 的 SQLite 里(DO 本来就是带事务的有状态单点),
执行环境全部变成无状态外设,注册在稳定 ID 下、首次使用才懒连接,用完随便死。容器重启、isolate 回收,树还在。

Cloudflare Computer 三后端架构

权威态怎么存

schema 是经典 inode 设计,三个机制点值得说:

vfs_dirents (parent_inode, name) → child_inode 加一张 vfs_nodes 节点表,路径解析走间接层:
本地 rename 是一条 UPDATE,O(1);硬链接就是两条 dirent 指同一个 inode,白送。
文件内容切成 512 KiB 的 chunk,按哈希落进内容寻址的 vfs_blobs;节点行上冗余一个 size 列,stat 不用每次去 SUM
vfs_meta 里一行单调递增的 rev 计数器,每次变更原子自增并盖到节点上——整套增量同步就悬在这一个数上。

计数器是刻意的单写者设计:所有变更先过 Workspace 的 FIFO 串行化,单行永不争用;哪天要加并发写者,得先动它。
vfs_nodes 上按 rev 建了索引,同步端枚举「上次游标之后动过的东西」就是一次索引扫描,不用全表比对。

同步协议

只有 container 后端需要同步——另外两个后端根本没有第二份存储。同步双向增量,各自带单调游标。

push(DO 到容器)发生在每次 exec 之前,把容器没见过的 rev 全推过去。同一路径中间改写五次,线上只有终态一条;
条目不带字节,只带 chunk 哈希,发送方先 hasObjects 探测,缺的块才 pushObjects

pull(容器回 DO)发生在 exec 返回之后,fetchChanges({ after: 游标 }),游标是 (rev, path) 二元组。
DO 按 256 条一批收流:查本地缺哪些块、fetchObjects 补齐、applyChanges 落库,再把游标推进到这一批最后一条。
中途崩溃从上个批次续传,重复工作的上界是一批而不是整条流;接收侧 alreadyApplied 把重复条目原地丢弃,重放廉价且幂等。

线协议里没有 rename opcode:只有新路径的活条目加旧路径的墓碑,apply 因此不需要按操作顺序重放。
代价是目录改名要给整棵子树逐个盖新 rev,O(子树) 条条上线;类型冲突走 last-writer-wins,上游节点直接顶掉本地整棵子树。

还有两处取舍写得很直白:硬链接跨线不保身份,一个 inode 挂几个名字就每个名字发一条,落到对端是内容相同的独立文件;
目录条目的幂等判定只看 mode 不看 mtime,两边目录 mtime 有漂移,不会变成同步流量。

container 后端一次 exec 的同步回路

容器后端

容器里跑的守护进程叫 computerd,是个 Node SEA 单二进制:Node 运行时、fuse-native 预编译件、libfuse 全部内嵌,
宿主镜像不需要装 Node,Debian slim 加个 fuse3 就能起。

它把容器内 VFS 用 FUSE 挂到 /workspace,容器里任何工具——shell、npm、编译器——看到的就是 DO 里那棵树,路径一致。
建连是反向拨号:后端调 computerdPOST /connect,让它主动拨一条 WebSocket 出来,capnweb 会话的 bootstrap stub 是 WorkspaceRPCsyncshell 两个子 stub)。
容器侧 VFS 放在内存里,容器重启就丢,下一次 push 重新基线——权威态从来不在这一边。

命令执行的括号是固定的:push、spawn、流事件或 result、pull,把 result() 或事件流耗尽,收尾的 pull 才算完成。
computerd 自己的默认端口是 45678,Cloudflare 后端把镜像内监听钉在 8080;
它保留进程日志,断线后 getExec 能重新挂回去看回放、补信号——三个后端里生命周期最全的一个。

isolate shell

第二种后端不要容器:just-bash 解释器跑在 env.LOADER 拉起的 Dynamic Worker 里,
shell 发出的每个文件系统调用走 Workers RPC 打回宿主 DO,直接读写权威态。零第二存储、零同步回路,result 里 pushedpulled 恒为 0。

有个实现细节挺有意思:DurableObjectNamespace 过不了 Worker Loader env 的 structured clone,
所以塞进 isolate 的是一个叫 WorkspaceServiceProxy 的回环入口,每次 env.HOST.getWorkspace() 由宿主侧代查命名空间。
isolate 的 globalOutbound 置 null,fetchconnect 全封死,唯一出口就是这个回环;
gitassets publish 是仅有的两个内建转发命令,R2 桶绑定和签名密钥永远不进 isolate。

命令集是 just-bash 的子集:catgrepawksedjq 这类文本活够用;要编译、装包、跑浏览器,回容器。

隔离粒度是每个 workspace 一个 isolate,Worker Loader 按 workspace-shell:${workspace.id} 缓存:
同一工作区的并发 exec 共享热 isolate,一段跑飞的脚本最多把自己工作区的 isolate 撑爆,宿主 DO 和别的工作区不受影响。
这一版是一次调用、缓冲结果的语义,不保留执行记录供事后重挂;timeoutMs 和并发的 killExec 只在语句边界协作式中止。

isolate JavaScript

第三种后端把 source 当真正的 ES module 求值,也在 Dynamic Worker 里:

const handle = await workspace.runtime.exec(
  `
    import fs from "node:fs/promises";
    export default async (input) => {
      await fs.writeFile("/workspace/result.txt", String(input.value));
      return { persisted: await fs.readFile("/workspace/result.txt", "utf8") };
    }
  `,
  { backend: "worker-javascript", input: { value: 21 } },
);
const result = await handle.result();

默认导出函数收到 options.input,返回值原样回到 result().value——这是结构化的值通道,不是 stdout 文本协议。
相对导入从 cwd 出发经权威文件系统解析,而且是先静态解析完整个模块图再起 Worker:拒绝符号链接穿越,
深度、模块数、总字节都有上限,动态 import 只认字符串字面量。
node:fs/promises 是 Workspace 后端的,isolate 里写文件就是写 DO 的 SQLite;ws:gitws:artifacts 两个受信模块管提交与发布。

process 是个 shim:env 只是调用方传入的快照,DO 自己的绑定和密钥永不合并进来;stdin 是一次性异步迭代;argvplatform 给的是占位假值。
限额给得很足:默认并发 24,超了直接 EEXEC_BUSY;执行记录保留 100 条或 60 分钟,可回放;
取消先停新的宿主能力调用、等已受理的调用排空,然后才发布 exit 130——正常完成同样排空,不存在 exit 0 之后还有未落账的写。

有个容易踩的点:runtime.exec() 在 Dynamic Worker 跑完之前就返回,靠悬着的宿主调用把 DO 顶在内存里;
handle 拿了不读,DO 一闲置执行就可能被驱逐——要么把事件流排干,要么给必须活过驱逐的工作配 ctx.storage.setAlarm()

isolate JavaScript 执行面数据流

怎么用

npm install @cloudflare/computer
import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";

const workspace = new Workspace({
  storage: ctx.storage,
  backends: [
    new WorkerJavaScriptBackend({ loader: env.LOADER, root: "/workspace" }),
  ],
});

后端按子路径导入,不用的后端连带 just-bash 载荷一起被 tree-shake 掉。能做的事:

workspace.fs:mkdir、readFile、writeFile、rm、symlink、stat,全异步、绝对路径、跨 DO 重启持久;
workspace.runtime:exec、getExec、killExec、disposeExec,handle 既是事件流又能 result()
命令后端的 result 带 pushedpulledsync 状态,命令成功而 pull 失败时可配 SyncRetryScheduler 只重试同步、不重跑命令;
@cloudflare/computer/tools:现成的 AI SDK 工具面(read、write、edit、ls,可选 exec 与 publish);
R2 只读挂载:规格已写、schema 留座,运行时未接线(规格意图,非今日代码)。

省略 backend 就用第一个配置项。文档特意提醒:路由不是鉴权,公网网关要自己校验 backend 参数。
仓库的 8 个 examples 里,think-compare-runtimes 把同一个 agent 任务在容器和 worker 两种运行时上并排跑,最能看出取舍。

边界与限制

工作区约 10 GB 上限,跟 DO 存储共享;容器侧整棵树在内存里,放 agent 级工作区合适,放 monorepo 不行。
FUSE 在大文件顺序 IO 上吃亏。官方 fs-bench 对 cloudflare/sandbox-sdk 做全量 npm install(854 个包、36675 个文件):computerd 124.7 秒,容器 ext4 63.9 秒,tmpfs 34.3 秒,比真盘慢约一倍。
反直觉的是元数据操作赢真盘:stat 0.91x、rm 0.66x、mkdir 树 0.74x、find 0.72x、git init 加提交 0.72x——内存 inode 表的功劳。
大文件才是重灾区:纯读 64 MiB 比真盘慢 30 倍、copy 慢 40 倍;但 npm init 加小安装 0.95x 追平真盘,日常负载的大头本来就是元数据。
慢的根源在写路径每 512 KiB 就哈希一次进内容寻址库,换来的是按块增量同步和内容去重。
PREVIEW ONLY,不收 unsolicited PR,反馈走 issue 和 discussion。

最后说一个印象:4900 行规格配 5.7 万行实现,连「考虑过又放弃的编码方案」都写进了文档,这个密度在预览期项目里少见。

写完有点手痒,想把自己博客的构建态也塞进 DO 里,转念一想静态站要什么权威态,睡了。