我们在三个云沙箱上运行同一个有 bug 的应用:Mainbrella、E2B 与 Daytona 对比
We ran the same broken app on three cloud sandboxes
为验证修复补丁是否真的有效,作者把同一个植入 bug 的编辑器应用分别放到 Mainbrella、E2B 和 Daytona 三个云沙箱上运行,三家都成功拒绝了那个"看起来能通过"的误导性补丁。测试还发现 E2B 的三个分支在启动前就继承了源进程的 PID 和 nonce,而 Mainbrella 和 Daytona 的副本则是全新进程。这是 Repo Rodeo 修复验证框架的首个工作负载。
2026年10月11日 · Andrew Arrow · 更新于10月12日
把“加入等候名单”移到我们小编辑器的顶部,点击保存,一切看起来都正常。按钮还在那里。编辑器显示“已保存”。重新加载页面,按钮消失了。放进去三个区块;回来两个。
我们植入了这个 bug,然后写了一个正确的补丁和一个以假乱真的假补丁。为了独立检查它们,我们需要三份准备好的应用副本,并且依赖已经安装好。这就引出了 Git checkout 无法回答的问题:我们复制的是文件,还是正在运行的应用程序?
我们试了 Mainbrella、E2B 和 Daytona。Mainbrella 是我们自己构建的,所以其中之一是我们的。三者都抓住了假的编辑器修复。第二个 bug 让它们的副本之间的差异变得重要:其准备好的工作只存在于一个正在运行的 worker 的内存中。没有编码代理尝试这些修复。
保存后
重新加载后
修复必须能在重新加载后存活
你可以无需账户试用这个损坏的编辑器。它的序列化器根据数组索引分配位置,然后过滤它们:
blocks
.map((block, position) => ({ ...block, position }))
.filter(block => block.position);零是假值。再见,第一个区块。存储写入成功,所以 UI 宣布“已保存”,并继续显示未受影响的草稿。JavaScript 完全按照我们的要求做了,这很无礼但公平。
正确的补丁移除了过滤器。误导性的补丁在保存后调用 render()。它重绘了同样看起来健康的草稿,而没有读回存储的文档。一个在保存处结束的视频用任一补丁看起来都很棒。重新加载才能让修复证明自己的价值。
我们的浏览器测试在保存前、保存后和重新加载后检查有序的区块 ID 和文本。在页面某处搜索“加入等候名单”会接受帮助文本,而文档已经丢失了按钮。
验证器报告 passed、reproduced 或 unverified。复现这个 bug 需要重新加载前完整的文档,以及之后恰好是那个文档减去其第一个区块,且剩余顺序和文本不变。HTTP 错误、超时、跳过的测试或不相关的断言都算未验证。如果准备好的基线不能复现丢失,我们在尝试补丁之前就停止。
| 复制 | 浏览器检查 | 重新加载后的文档 |
|---|---|---|
| 未更改 | 5 失败,1 通过 | 第一个区块丢失 |
| 正确补丁 | 6 通过 | 所有区块保留 |
| 误导性补丁 | 5 失败,1 通过 | 第一个区块仍然丢失 |
通过的基线检查测试的是重置:它必须在不删除无关浏览器存储键的情况下恢复演示。其他检查覆盖重新排序的区块、原始顺序、编辑过的文本、两条重新加载路径,以及从记录的种子生成的六个打乱文档。每个保留文档都要经过两次重新加载。这些是已知答案的对照,不是代理基准。
验证器在可写的应用副本之外运行。提议的修复不能通过编辑测试来提高分数。这是Repo Rodeo的第一个工作负载,我们的测试框架,用于为修复尝试提供独立环境。在把它交给代理之前,我们想知道它能否拒绝一个我们已经知道是错误的补丁。
副本中出来的结果
编辑器运行使用了相同的固定应用归档、补丁哈希、浏览器检查和种子。每个提供商准备了一个源并创建了三个分支。检出内容和已安装依赖在所有九个分支中都保留了下来;没有一个需要再次 npm ci。在控制项完成后,每个分支都保留了自己的标记和预期的源哈希,而源没有分支标记。证据索引链接了较新的匹配运行以及下面的工作器控制项。
走到那一步暴露了我们设置中的错误。Mainbrella 将客户机 Host 头转发为 container,Vite 以 403 拒绝,直到启动器允许那个确切的 host。E2B 的基础模板通过其登录 shell 选择了一个较旧的 Node;我们的启动器需要显式 PATH 指向固定的二进制文件。然后我们的浏览器辅助程序错误处理了经过身份验证的重定向。一个就绪的服务器和一个可用的浏览器预览是两件需要分别检查的事情。
我们将这些失败保留在设置说明中。禁止页面上的六个红色测试并不能告诉我们关于按钮的任何信息。一旦预览正常工作,更有趣的问题是复制的服务器是否已经在监听。
仅凭 PID 无法确定这一点:一个新进程可以重用编号。我们的启动器在启动时生成一次随机 nonce,并从无缓存端点提供它。它从不从磁盘加载 nonce。一个在我们启动任何东西之前就用源的 nonce 和 PID 响应的分支已经继承了该进程状态。
所有三个提供商都通过不同的启动路径为我们提供了监听副本。下面 Mainbrella 的行来自对同一固定应用的后续生产检查,带有保存的启动配方。我们恢复了三个未打补丁的副本,而没有重新运行浏览器修复控制项。E2B 和 Daytona 使用原始编辑器运行。
| 提供商 | 监听 | 如何启动 | 进程 |
|---|---|---|---|
| Mainbrella | 是,3/3 | 保存的配方(自动) | 全新 |
| E2B | 是,3/3 | 实时 fork | 继承 |
| Daytona | 是,3/3 | 运行器命令 | 全新 |
Mainbrella 的原始运行需要我们的运行器来启动每个恢复的服务器。使用保存的配方,所有三个副本都监听了,并通过其受保护的预览提供编辑器,而无需客户端启动命令。每个都有一个全新的 nonce;源继续运行。所有 417 个跟踪的文件哈希在每个副本中都匹配,连同依赖项、准备好的标记、权限和一个符号链接。原始进程记录保留了较早的 PID、nonce 和文件观察结果。
E2B 在我们启动任何东西之前,将源的监听器、PID 和 nonce 带入了所有三个 fork。其fork 文档还说捕获会暂停源并断开活动连接。一个进程记住其工作并不意味着监视它的终端保持连接。
控制台同时显示了源和三个分支:四个并发沙箱,其中 htop 在源中打开。
Daytona 的容器快照要求停止源。捕获后,源和所有三个分支都需要新的服务器。其成功重跑保留了文件、依赖项和分支隔离,并产生了与其他两个相同的修复结果。这测试了 Daytona 的容器快照;其 VM 内存快照是我们尚未使用过的单独模式。
历史设置面板将沙箱标识为类 Container,其预览为私有。
对于编辑器来说,重启没问题。文档存在于外部浏览器的 localStorage 中,位于服务器容器之外。每次浏览器测试都会重建它。我们甚至在检查补丁之前就停止了 E2B 继承的服务器。那个实验展示了进程保留,但修复从未使用它。
一个你无法重启到正确状态的 worker
为了让进程状态变得重要,我们添加了一个小型 worker,其 JavaScript 堆中仅保存五个已接受的任务。两个任务代表免费物品:它们的单价为零,但仍需要收据。worker 在处理批次之前暂停。那个暂停的实例,包括其队列,就是我们所复制的。
有缺陷的处理器在计算总数之前按 unitCents 过滤任务。它丢弃了两个免费物品,并返回三张带有完成标志的收据。正确的补丁移除了过滤器并返回全部五张收据。一个误导性的补丁返回空输出列表并声称完成。如果我们让那个徽章决定结果,每个版本都会通过。
外部验证器提交一次任务,并保留自己的 ID 和预期金额账本。它从不再次将它们发送到分支,worker 也从不将它们写入磁盘。在恢复之前,我们检查准备 ID、队列指纹、待处理计数、PID 和 nonce。之后,每张预期收据必须恰好出现一次且金额正确,并且同一进程必须处理了同一准备好的队列。
这里有一个刻意的约束。worker 在我们释放检查点时导入 processor.mjs,因此替换该文件不需要重启持有队列的进程。我们不是在执行中途热补丁一个函数。如果没有显式的重新加载点,复制的进程可能会忠实地继续执行旧代码。
E2B 的三个分支继承了五个待处理任务。未更改的处理器丢失了两个免费物品,正确的补丁返回了全部五张收据,而假的则返回零。Mainbrella 和 Daytona 保留了处理器文件,但启动了空的 worker。即使将正确的补丁应用于这些副本,也无法测试已接受的批次:复现已经消失。
该表显示每个提供商一次运行:五张预期收据中的收据数,随后是验证器的判定。选定的运行使用相同的种子、12346、Node 22.22.2,以及相同的 worker、补丁、运行器和验证器字节。每个提供商都有自己的准备 ID 和源 nonce。
| 对照 | Mainbrella 已保存工作区 | E2B 实时分支 | Daytona 容器快照 |
|---|---|---|---|
| 恢复前的待处理任务 | 0 | 每个分支 5 个 | 0 |
| 未更改的处理器 | 0/5 · 复现丢失 | 3/5 · 被拒绝 | 0/5 · 复现丢失 |
| 正确补丁 | 0/5 · 复现丢失 | 5/5 · 通过 | 0/5 · 复现丢失 |
| 误导性补丁 | 0/5 · 复现丢失 | 0/5 · 被拒绝 | 0/5 · 复现丢失 |
| 冷重启,正确补丁 | 0/5 · 复现丢失 | 0/5 · 复现丢失 | 0/5 · 复现丢失 |
重启源提供了最后一个对照。使用新的 nonce、没有准备 ID、没有待处理任务,正确的处理器在空队列上报告完成。验证器称其为 reproduction-lost。它无法使用不再存在的工作来区分好的补丁和坏的补丁。
我们还给每个分支设置了不同的内存标记,并检查了其他分支和源。每个分支都保留了自己的标记。在 E2B 中,当三个分支修复都完成后,源中仍然保留着最初的五个待处理作业。复制的队列可以被独立消费,而不会耗尽它们的源。E2B、Mainbrella 和 Daytona 观察结果 包含了完整的 nonce、指纹、输入账本和返回的回执。
早先一次 Mainbrella 重复在检查冷重启时因 HTTP 429 而停止。其保留的命令历史已填满。我们提高了该限制,同时保持独立的输出和事件预算,然后针对生产环境重新运行了相同的种子。运行完成,清理确认所有四个容器和工作区均已不存在。运行历史 也保留了那次未完成的尝试;成功的重跑不会将其抹去。
一项单独的自动启动检查恢复了一个监听工作进程,其待处理作业为零,而其源仍保留着五个。我们的底层 Cloudflare 快照不包含内存和运行中的进程。启动配方无法恢复该工作进程已接受的作业。
这是一个带有便利检查点的构造工作负载。持久队列或可重放的输入日志可以在重启后重建该批次;我们有意省略了那条恢复路径。实时克隆之所以有帮助,是因为三个候选者可以消费已接受到内存中的工作的独立副本。对于编辑器,外部浏览器可以重建文档,因此文件系统恢复就足够了。
在失去连接后找到工作
一个被保留的进程仍然需要一种方式让新客户端找到它。在匹配的编辑器后续测试中,我们启动了一个八秒的作业,在其首次输出后分离,然后用新客户端连接。所有三个提供商都恢复了相同的作业标识、退出状态零和剩余输出。机制各不相同:Mainbrella 重放了 SSE 事件游标,E2B 使用字节偏移恢复命令输出,Daytona 恢复了保留的会话输出。Daytona 的检查分离了一个轮询客户端;它没有测试中断的实时流。
丢失捕获回复会留下一个不同的问题。云可能已经保存了工作区,而客户端从未得知其 ID。盲目重试可能会创建另一个副本,并让第一个无人认领。
在 Mainbrella 上,我们故意丢弃了一个成功捕获响应的主体。在发送之前,运行器保存了确切的请求主体和幂等键。使用两者重试恢复了同一个工作区;再次重放又返回了它。保留的生产结果 记录了三项请求产生一个工作区和一个捕获预留。
丢失的回复发生在成功的 HTTP 头之后。这证明了已完成保存的恢复,而不是捕获在更早阶段被中断时会发生什么。我们没有在 E2B 或 Daytona 上运行带键重放:那些适配器没有实现它。这是本次比较中的一个缺口,而不是其服务已证实的失败。我们在所有三个匹配运行中都禁用了带键重放,以便它们共享的恢复检查覆盖同一个被中断的作业。
检查删除实际做了什么
我们的第一个 Daytona 快照超过了测试框架的三分钟截止时间,而提供商仍在继续工作。后来的一次删除返回了 403:我们的密钥可以管理沙箱,但无法删除持久快照。我们有权制造混乱,却没有足够的权限来清理它。
第一次尝试的截图显示,快照仍被标记为Snapshotting。
适配器现在会在预配前检查两组权限,在提交前保存请求的快照名称,并以十分钟为期限单独轮询。重新运行完成了快照和所有修复控制,然后删除了其沙箱和快照,同时保留了十三个预先存在的快照。
然而其总体结果却显示为failed。API 确认了删除,但几个旧预览返回了 401,而不是适配器所要求的 404 或 410。与此同时,E2B 通过了一条特殊规则,允许在 API 确认不存在后重复出现 502。它还获得了更多探测。这些标签反映了我们代码中不同的接受规则。用它们来对提供商进行排名是个错误。
我们用共享的清理协议重新运行了编辑器。每个当前预览都必须在删除前凭其凭据返回 200。清理后,我们检查了确切的 API 资源不存在情况,并对每个旧 URL 进行了三轮请求,分别带和不带预览标头,轮次之间暂停两秒。我们还在删除编辑器资源后预配了一个小型实时控制服务器。它的预览必须在随后三轮的每一轮中都保持 200,这样我们就能检查网关是否仍在为一个已知存活的沙箱提供服务。
| 提供商 | 确切的 API 资源 | 带凭据的旧 URL | HTTP 解读 |
|---|---|---|---|
| Mainbrella | 不存在 | 所有四个 URL 均返回 404 | 路由不存在 |
| E2B | 不存在 | 所有四个 URL 均返回 502 | 网关不可用;访问转换无定论 |
| Daytona | 不存在;保留了 13 个原始快照 | 源返回 401;三个分支返回 404 | 访问被拒绝或路由不存在 |
三者都完成了预期的修复结果,并恢复了其原始 API 清单。三者的实时控制都保持在 200。E2B 的网关在旧 URL 出现 502 期间能够为一个实时沙箱提供服务,但这些响应仍不能证明旧令牌被独立作废。一个已删除且 URL 不可用的沙箱与一个已撤销的凭据是不同的观察结果。E2B、Mainbrella 和 Daytona 记录保留了每一个响应及其控制。
移除标头移除了 E2B 和 Daytona 的令牌。Mainbrella 的凭据存在于主机名中,因此其无标头请求仍携带相同的持有者凭据。Daytona 在重启源后单独演示了实时 URL 上的令牌轮换:旧令牌返回 401,而其替代令牌返回 200。该检查确立了仅靠删除探测无法确立的东西;协议说明解释了凭据传输和保留的源。
这些检查都没有确立物理存储回收。我们早先的本地 Mainbrella 运行在 API 删除后留下了未附加的 Docker 快照镜像,回收时间未知。生产捕获大小、磁盘预留、月度记账计数器和实际费用也需要单独测量。记账说明保留了这些数字;API 不存在并不能证明字节或账单已经消失。
工作流需要哪种能力
| 观察到的行为 | Mainbrella | E2B | Daytona |
|---|---|---|---|
| 编辑器控制 | 正确补丁通过;伪造被拒绝 | 正确补丁通过;伪造被拒绝 | 正确补丁通过;伪造被拒绝 |
| 状态已复制 | 文件;全新进程,可选保存的启动配方 | 文件和活动进程 | 文件;在测试模式下全新进程 |
| 准备好的工作队列 | 在全新进程中丢失 | 已保留;正确的补丁返回 5/5 回执 | 在全新进程中丢失 |
| 新客户端恢复作业输出 | 是,SSE 游标 | 是,保留的命令输出 | 是,保留的会话输出 |
| 丢失的捕获回复通过键控重试恢复 | 单独演示 | 未尝试 | 未尝试 |
较新的匹配编辑器和 worker 运行固定使用 Node 22.22.2;历史编辑器运行使用了不同的 guest 版本。分配仍然不同,而且少量对照无法支持可靠性、速度或价格排名。修复以补丁形式提供。我们尚未确立能自主修复任意仓库的智能体。
公开证据包括文档观察、worker 输入和回执、校验和以及来源出处。原始浏览器跟踪和 API 回执保持私密,因为它们可能包含 bearer 预览凭据。编辑器包无法独立重放完整的原始验证决策。保留的 worker 源代码和本地对照让你无需云账户即可检查检查点并复现重启丢失。
对于我们的无人值守工作流,我倾向于使用持久化的请求键,它能在回复消失后恢复已完成的捕获。这就是我们单独演示的 Mainbrella 行为。对于准备好的 worker 批次,我会使用 E2B 的实时 fork:它保留了修复必须处理的工作。Daytona 的容器快照满足了编辑器的文件系统要求。编辑器的正确补丁必须经受重新加载;worker 的正确补丁需要留下五个被接受的作业才能运行。
来源:Hacker News · mainbrella.com