computer 项目 Mount Interface 深度解析:为 Agent 工作区注入外部数据源

发布时间:2026/9/16 21:14:10
computer 项目 Mount Interface 深度解析:为 Agent 工作区注入外部数据源 computer 项目 Mount Interface 深度解析为 Agent 工作区注入外部数据源【免费下载链接】computerGive your agent a computer 项目地址: https://gitcode.com/GitHub_Trending/computer1/computer本文以 docs/06_mount_interface.md 为骨架结合packages/computer/src/mounts/下的类型定义、注册表、索引器与 R2 提供者源码系统讲解cloudflare/computer中挂载Mount机制的设计目标、配置方式、策略分类、读写语义与边界约束。读完本文你将掌握如何把 R2 Bucket、GitHub 仓库或自定义数据源挂载进 Agent 的 VFS 工作区理解懒加载 / 急切物化两种策略的取舍并能够基于MountWriteAPI编写自己的挂载提供者。[!IMPORTANT] 本仓库文档与实现之间存在已知偏差docs/06_mount_interface.md描述的是目标设计intended design而当前main分支实现仅落地了eager急切策略与 R2 提供者。文档中提到的 lazy 策略、GitHubRepo内置提供者、debounce 写回write-back等属于规划目标尚未随主分支发布。引用时以源码为准、以文档为方向两者冲突时以代码为准仓库中docs/02_sync_protocol.md也明确注明此规则。一、Mount 是什么把外部内容源接入 VFS 子树在cloudflare/computer的架构中Workspace拥有一棵以 SQLite 为后端的虚拟文件系统VFS。Mount 的职责是用外部源的内容填充 VFS 的某个子树——外部源可以是 R2 Bucket、GitHub 仓库、artifact 压缩包或任何自定义数据源。根据 docs/06_mount_interface.md 的定义Mount 在Workspace构造时一次性配置存活期与Workspace相同Mounts are configured once at construction and live for the lifetime of theWorkspace每个挂载对应一个绝对挂载根mount root即 VFS 内的一个绝对路径挂载根不允许互相嵌套——/workspace/a与/workspace/a/b同时出现会在构造期被直接拒绝。从源码看这一构造期校验实现在 packages/computer/src/mounts/registry.ts 的buildMountRegistry中validateRoot要求挂载根必须以/开头、不允许尾随斜杠root.endsWith(/)且长度大于 1 时报错rejectNesting通过b.startsWith(${a}/)判定祖先关系用 O(n²) 遍历注释说明期望每个工作区只有少量挂载。packages/computer/src/mounts/registry.test.ts 的测试用例分别验证了嵌套根被拒绝并报出两个路径相对路径被拒绝尾随斜杠被拒绝等规则。二、配置 Mount一个对象、三种挂载示例文档给出的配置形态是在Workspace构造选项中传入mounts字段键为绝对挂载根值为挂载提供者new Workspace({ // ... mounts: { /workspace/.agents/skills: R2Bucket(env.SHARED_FILES, { prefix: .agents/skills }), /workspace/project: GitHubRepo(cloudflare/agents, { env }), /workspace/scratch: R2Bucket(env.SCRATCH, { mode: read-write }), }, });三个示例对应三类典型场景只读共享资产把共享桶中的技能/模板目录只读挂到.agents/skills代码仓库克隆把 GitHub 仓库的工作树急切克隆进工作区可写临时区以可写模式挂载一个专属 scratch 桶。WorkspaceOptions.mounts的类型在 packages/computer/src/workspace.ts第 154-158 行中定义为Recordstring, MountValueMountValue即Mount | MountFactory见 packages/computer/src/mounts/registry.ts。构造Workspace时buildMountRegistry会立即对全部根做校验并解析工厂见 packages/computer/src/workspace.ts 第 399-402 行。需要说明的是当前源码中的Mount类型只包含EagerMount一种策略见 packages/computer/src/mounts/types.ts 第 52 行export type Mount EagerMount;注释明确写着initial cut 仅支持 eagerlazy 分支留给后续里程碑。三、两种策略Lazy 与 Eager文档定义了挂载的两种策略核心区别在于内容何时、以何种粒度进入 VFS。Lazy懒加载——按需枚举 按需取字节interface LazyMount { readonly kind: string; readonly strategy?: lazy; readonly writable: boolean; list(): PromiseMountEntry[]; fetch(relPath: string): PromiseUint8Array; put?(relPath: string, bytes: Uint8Array): Promisevoid; // writable only delete?(relPath: string): Promisevoid; // writable only }工作流首次使用时调用一次list()枚举整棵树把**存根stub**插入vfs_nodes当某条路径第一次被读取时再调用fetch(relPath)取回该文件的字节。适用场景底层存储支持单文件随机访问与按地址取回R2、S3、HTTP。代价是存在懒惰的边界见下文Limits of laziness小节。Eager急切物化——一次性灌入interface EagerMount { readonly kind: string; readonly strategy: eager; readonly writable: boolean; materialize(api: MountWriteApi): Promisevoid; put?(relPath: string, bytes: Uint8Array): Promisevoid; delete?(relPath: string): Promisevoid; } interface MountWriteApi { writeFile(absPath: string, bytes: Uint8Array, mode?: number): void; mkdir(absPath: string, mode?: number): void; }materialize(api)通过一个受限的写 API 一次性把全部内容写入 VFS每个已索引挂载在每个 DO 生命周期内只调用一次。适用场景底层存储以单次事务形式产出内容git clone 一次性给出整个工作树。源码中的 Eager 实现细节当前实际落地当前仓库实现的MountWriteAPI与文档略有差异更贴合真实运行形态见 packages/computer/src/mounts/types.ts 第 82-86 行export interface MountWriteAPI { readonly root: string; writeFile(absPath: string, source: ReadableStreamUint8Array, mode?: number): Promisevoid; mkdir(absPath: string, mode?: number): Promisevoid; }两个值得注意的实现事实流式写入writeFile接收的是ReadableStreamUint8Array而非字节数组这样数 GB 的 blob 可以经由分块 blob 写入器chunked blob writer流过无需整块驻留内存根作用域API 携带绝对挂载根root以相对 key 工作的提供者R2、GitHub用它拼接绝对路径无需另行传参。此外索引器在 packages/computer/src/mounts/index.ts 的createWriteAPI中对writeFile/mkdir做了三重约束checkPath拒绝任何越出挂载根的路径maxEntries超限直接抛错maxBytes通过source.tee()分流统计字节数超限时取消转发流以短路写入——配额执行时机是写入过程中实时拦截而非事后清算。索引器的可靠性设计源码级packages/computer/src/mounts/index.ts 的runIndex揭示了恰好一次物化的完整机制先在_vfs_mounts表插入(root, kind, moderead-write, indexed0)——物化期间临时置为 read-write否则索引器自己的mkdir/writeFile会被 dofs 的只读挂载守卫read-only mount guard拦截预创建挂载根fs.mkdir(root, { recursive: true })——即使materialize()产出 0 个条目空挂载也有可解析的 inodereaddir(root)返回[]而非ENOENT调用mount.materialize(api)成功后通过 BFS 遍历vfs_dirents给子树内每个 inode 打上mount_root root溯源标记stampMountProvenance失败回滚任何异常都会用fs.rm(root, { recursive: true, force: true })清掉半成品子树_vfs_mounts保持indexed0下次索引从头重试成功后才在单条UPDATE中把 mode 翻转为注册值并置indexed1随后失效 dofs 的守卫缓存。MountIndex.ensureIndexed()通过缓存 in-flight promise 保证并发调用共享同一次物化Workspace.ensureMountsIndexed()packages/computer/src/workspace.ts 第 419-421 行把它暴露为幂等入口并在ready()流程中调用同文件第 547-560 行失败不会毒化后续 ready 调用。四、工厂Factory从会话上下文派生挂载WorkspaceOptions.mounts中的值可以是工厂函数而非挂载对象type MountFactory (ctx: MountContext) Mount; interface MountContext { sessionId: string; // agents DO name root: string; // absolute mount root, no trailing slash vfs: VFS; // direct VFS handle for fs-shaped consumers }工厂在首次索引时调用一次好处是per-session 挂载按会话隔离的 R2 前缀、按会话 fork 的 git 仓库可以直接从会话上下文推导身份调用方无需手动把sessionId穿进提供者裸Mount对象同样被接受用于向后兼容。源码事实印证工厂解析逻辑在 packages/computer/src/mounts/registry.ts 第 66-74 行typeof value function时构造{ sessionId, root, vfs }上下文并调用一次sessionId默认vfs句柄是惰性构造的第 60-64 行的缓存闭包没有工厂挂载的调用方不会白白构建SQLiteWorkspaceProvider测试 packages/computer/src/mounts/registry.test.ts 验证了工厂恰好被调用一次ctx.sessionId透传构造时的sessionId: session-42裸 Mount 不被当作工厂调用等行为。五、只读 vs 读写EROFS 与写回默认只读挂载默认mode: read-only。行为契约挂载根之下的任何写入抛EROFSexec()期间容器侧container-side产生的写入在 post-exec pull收到字节之后、落入vfs_nodes之前被丢弃。源码验证只读守卫实现在数据层cloudflare/dofs——Workspace.fs的writeFile/mkdir/rm会查询已注册的挂载根并拒绝EROFS同样的检查也作用于pullOnce的 apply 路径因此容器侧对只读挂载的写入同样被拒绝并通过Workspace.pull的skipped[]暴露见 packages/computer/src/workspace.ts 第 447-452 行注释。测试 packages/computer/src/mounts/providers/r2.test.ts 第 152-166 行专门断言了ws.fs.writeFile(/workspace/ro/new.txt, ...)以{ code: EROFS }拒绝且不会触发任何 R2 调用。读写与 Write-back gating目标设计传入mode: read-write开启写穿write-through容器侧写入在 post-exec pull 之后以有界并发镜像回挂载提供者。为防止突发写入放大为大量上游请求DO 侧写入fs.writeFile、fs.rm在到达提供者之前会被debounce某路径在最后一次 DO 侧变更后被持有writeBackMs默认500 ms窗口窗口内只镜像最终状态——构建脚本把 manifest 重写十几次、编辑器每次按键都保存最终坍缩为一次put。两个逃生舱口escape hatchworkspace.flushMounts(root?)强制立即镜像所有挂起的 debounced 写入挂载选项{ writeBack: manual }完全禁用 debounce写入只在 VFS 累积仅当调用flushMounts()时才落到提供者。单路径最终状态的镜像顺序文档原文操作顺序fs.writeFiledebounced先写 VFS 行debounce 触发后再调 mountput()。put失败时 VFS 行保留并通过冲突钩子暴露fs.rm文件debounced先写 VFS 行再调 mountdelete()fs.mkdir仅写 VFSR2 类存储没有目录概念exec写入先 pull 进 VFS再以有界并发镜像回挂载注flushMounts、writeBack、refreshMount、onMountConflict等 API 属于文档描述的目标设计当前main分支的挂载模块尚未实现r2.ts注释明确写put / delete proxies join later when the write-back path lands。六、内置提供者R2Bucket(binding, options?)文档设计为 lazy 挂载当前源码实现为eager、流式、只读的挂载见 packages/computer/src/mounts/providers/r2.ts。R2Bucket(env.SHARED_FILES, { prefix: .agents/skills, // strip from R2 keys when computing relPaths mode: read-only, // or read-write ignore: [.cache], // mount-scoped; composed with the global ignore maxBytes: 1 30, // optional quota; throws at index time if exceeded });源码中实际支持的选项R2BucketOptions比文档更细选项默认值说明以源码为准prefix从 R2 key 计算挂载内相对路径时剥离的前缀尾随斜杠可选skills 与 skills/ 等价前缀之外的对象不呈现moderead-only当前里程碑只支持只读listLimit1000R2list()的分页大小R2 文档上限主要用于测试分页concurrency8逐对象get()扇出的有界并发避免对数千文件的桶一次性打爆请求maxBytes/maxEntries未设转发给EagerMount的配额由索引器强制执行物化流程源码第 122-143 行两阶段处理先用paginate()把list()的所有 key 收集齐truncatedfalse前持续翻页再逐 key 发起get()——注释解释这样做的理由materialize()只在冷启动跑一次list 相对 get 便宜两阶段让代码更直白流式直通get()返回的ReadableStream直接pipe进MountWriteAPI.writeFile任何对象体都不会被缓冲对象在 list 与 get 之间消失时抛disappeared mid-materialize错误测试 r2.test.ts 第 246-277 行验证索引失败后indexed0、子树被回滚、readdir返回ENOENT。值得强调的测试证据同文件流式大文件16 MiB 对象写入后vfs_chunks中每个 chunk 大小 ≤ 512 KiB 且总和等于文件大小证明没有中间缓冲把整个文件重组第 187-221 行前缀剥离prefix: skills/时skills/a.txt挂到/workspace/skills/a.txt前缀外的other.txt不可见第 134-150 行空桶materialize后挂载根可解析、readdir返回[]、_vfs_mounts记录indexed: 1第 223-244 行。GitHubRepo(slug, options)目标设计GitHubRepo(cloudflare/agents, { env, // for the GITHUB_TOKEN secret ref: main, // optional, default main prefix: /src/content/docs/, // optional; only this subtree is materialized });通过isomorphic-git克隆仓库并物化工作树materialize()只运行一次克隆当前只读无put/delete当前仓库中尚未实现此提供者属于文档目标isomorphic-git已在依赖树中出现docs/02_sync_protocol.md也提到它在依赖树中用于GitHubRepo。七、自定义挂载实现你的提供者文档给出一个 artifact 挂载的工厂示例——实现LazyMount或EagerMount可选包在工厂里const ArtifactBundle (id: string): MountFactory ({ sessionId, root, vfs }: { sessionId: string; root: string; vfs: VFS }) ({ kind: artifact, strategy: eager, writable: false, async materialize(api) { const bundle await fetchArtifact(id); for (const file of bundle.files) { api.writeFile(${root}/${file.path}, file.bytes, file.mode); } }, });结合当前源码编写自定义提供者时请遵循以下约束MountBase见 types.ts 第 16-33 行kind: string仅用于诊断和_vfs_mounts表不会被解释执行mode: read-only | read-write决定 EROFS 行为maxBytes/maxEntries为可选硬上限超限由索引器在写入过程中抛错并整体回滚子树r2.ts与index.ts均对此做了实现级配合strategy: eager是当前唯一合法值materialize(api)的writeFile签名是(absPath, ReadableStream, mode?)不是字节数组——即便你的数据源是内存字节也请用new Blob([bytes]).stream()或类似方式转为流以便复用分块写入路径。挂载值既可以是裸对象也可以是工厂MountValue Mount | MountFactory二者等价注册表在构造期统一解析。八、索引与持久化恰好一次物化触发时机对任意fs、shell、prefetch方法的首次调用会并行索引所有挂载持久化索引状态持久化到 SQLite 的_vfs_mounts表DO 重启不会触发重新 listprefetchworkspace.prefetch(root?)急切水合指定挂载根下的 lazy 存根不传参数则覆盖所有挂载适合在onStart/waitUntil中调用避免首次grep时的冷启动拉取扇出并发去重对同一存根的并发读共享同一个 in-flightfetch()——按绝对路径去重。源码印证恰好一次由两部分构成索引器在成功物化后把_vfs_mounts.indexed置 1重启跳过MountIndex在进程内缓存 in-flight promise并发去重。索引失败时indexed0保留下一次运行从头重试。九、Per-mount 通用选项除提供者专属配置外每个挂载都接受以下通用选项文档原表选项默认含义moderead-onlyread-only或read-writeignore[]从该挂载的同步 pull 中省略的路径段。路径仍通过底层Workspace.fs可见该选项与顶层ignore取并集组合。参见 02. Sync Protocol → Ignore listswriteBackdebouncedebounce默认或manualwriteBackMs500去抖窗口毫秒数writeBack: manual时忽略maxBytes无上限该挂载索引总字节数硬上限超限在索引时抛出任何数据落入vfs_nodes之前maxEntries无上限条目数硬上限执行时机与maxBytes相同工作区级ignore选项即更名前的pullIgnore同时作用于所有挂载与顶层路径挂载级ignore仅对该挂载做扩展。默认不忽略任何路径。同步层面的 ignore 语义细节过滤只作用于变更流、文件仍可通过Workspace.fs读写、被过滤条目仍推进 fetch cursor、ignore 配置应保持稳定见 docs/02_sync_protocol.md 的 Ignore lists 小节。十、挂载冲突容器侧优先两个写入者可能命中同一路径DO 侧的fs.writeFile与容器侧exec触碰同一文件。post-exec pull 先把容器侧状态应用到 VFS再镜像回挂载。策略容器侧状态获胜——它最后运行、且是代理行为agentically的结果镜像时挂载被覆盖。需要记录或否决该裁决的调用方可提供钩子new Workspace({ onMountConflict: ({ root, relPath, doRev, containerRev }) { // Return accept (default) or keep-do to retain the // DO-side write and skip the mount mirror for this path. return accept; }, });只读挂载上的冲突会被报告但从不镜像容器侧字节仍在 VFS 内获胜只读挂载保持不动从docs/02_sync_protocol.md的冲突语义看这与整个系统的同步粒度上的 last-write-wins一脉相承Container always wins是有意设计的层级见下节。十一、懒惰的边界stub 与同步协议的张力Laziness 仅限 DO 侧。lazy 挂载的list()把存根行插入vfs_nodes时manifest_hash为NULLfetch(relPath)在 DO 侧首次读取该存根时被调用字节经由 blob / chunk 路径流动并把存根升级为普通文件行。由于同步协议按 hash 运送 blob而存根行没有 blob 可运送pushOnce无法把存根交付给容器。具体后果文档原文容器侧FUSE读取一个在 DO 上仅以存根形式存在的路径会看到ENOENT——存根未被解析没有可推送的内容workspace.prefetch(root?)正是为需要在交给容器之前完整解析子树的调用方准备的应放在onStart/waitUntil中运行避免首次exec的意外存根一旦在 DO 侧被解析经readFile、prefetch或任何其他读字节路径就与普通文件一样通过同步协议运送。Eager 挂载没有这种不对称性——它们在索引时即物化首次 push 之后容器立即可见。文档还展望了两种候选扩展——容器侧按需拉取FUSE miss → DO 抓取存根 → push与 push 时物化遍历 push 队列时解析存根——但两者都需要新的 wire 形态不在当前范围内。十二、挂载不是同步对等方非目标这是一个刻意设计而非疏漏。挂载是带可选refresh()钩子的内容源不是同步协议的第三参与方协议始终是 DO 与容器之间的双端two-peer事务。文档给出三点理由R2 与 GitHub 没有单调 rev 时钟——把它们当作对等方将被迫做每 tick 轮询 与记忆快照做 diff协议不变量appliedPushCursor、水位线对账、tombstone都假设单对等方——没有真正的 CRDT / LWW 故事就无法泛化到 N 个对等方容器总是赢的冲突层级是有意设计的——把它压平、让挂载成为第三个平级对等方会与这一层级相矛盾。当挂载需要拾取上游变更时调用workspace.refreshMount(root)——这才是接缝所在而不是新增一条同步腿。十三、未决问题Open questions以下行为尚未完全定稿如果你的用例依赖某一具体解法请提交 issue单文件挂载 → 挂载目录内的文件。挂载总是覆盖一个子树更难的版本不是如何挂载单个 R2 对象而是挂载根本身嵌套在另一个挂载内时会发生什么。构造期嵌套已被拒绝但一个可写挂载的put()落到的路径同时被另一挂载认领例如 per-session GitHub 挂载的树里有一个 config.json另一挂载也想拥有它需要定义明确的裁决。可能的答案挂载根是路径最长前缀者获胜但契约尚未成文。拆卸钩子tear-down。挂载有materialize或list/fetch但没有拆卸钩子。刷新已被覆盖见上节与workspace.refreshMount但一个长出后台任务的提供者没有清理的位置。目前没有调用方需要它等到出现时再重新审视。十四、给开发者的实践建议综合文档与当前源码使用挂载时的几条落地建议以源码为准当前main只支持EagerMount与R2Bucket只读流式挂载GitHubRepo、lazy 策略、writeBack/flushMounts属目标设计接入前请先核对 packages/computer/src/mounts/types.ts 与 packages/computer/src/mounts/providers/r2.ts 的实际导出配置校验发生在构造期挂载根必须是绝对路径、无尾随斜杠、互不嵌套错误会在new Workspace(...)时立刻抛出见 registry.test.ts物化是恰好一次 失败回滚_vfs_mounts表驱动跨重启幂等物化中途失败会清空子树并从零重试因此不要在materialize()里做有副作用的外部调用R2 提供者的get()中途消失会被明确报错并回滚配额是流式强制的maxBytes/maxEntries在写入过程中实时拦截超限即抛错适合防御异常桶或异常仓库容器侧可见性eager 挂载在首次 push 后容器立即可见需要预热的子树务必在onStart/waitUntil中显式预热文档中的prefetch语义避免首次exec的 ENOENT 意外写冲突按容器赢收敛DO 侧与容器侧同时写同一路径时以容器侧为准需要干预时接入冲突钩子目标设计多容器共享工作区时请参考 docs/02_sync_protocol.md 的冲突语义章节采用单活跃写入者 路径分区的实践模式。相关文档02. Sync Protocol同步协议、水位线、Ignore 列表与冲突语义03. Filesystem Schemavfs_nodes/vfs_dirents/_vfs_mounts等表结构04. Filesystem InterfaceWorkspace.fs文件系统接口10. Project Layout仓库模块布局挂载源码类型 types.ts、注册表 registry.ts、索引器 index.ts、R2 提供者 providers/r2.ts 及对应测试 r2.test.ts、registry.test.ts【免费下载链接】computerGive your agent a computer 项目地址: https://gitcode.com/GitHub_Trending/computer1/computer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询