跳至主要内容 跳至导航 跳至页脚

我用 PocketOS 事故场景测试了 Rediacc

PocketOS 在 9 秒内被一个 Cursor 智能体抹掉了生产数据库。我在自己的平台上跑了同类测试,并对每一步都计了时。本文记录哪些防线扛住了,哪些仍是开发者自己的责任。

太长不看。 上周一个 AI 智能体在 9 秒内删掉了 PocketOS 的生产数据库。我尝试用同样的方式让自己的基础设施失败。六道防线扛住了,还剩一个诚实的缺口。

  • 128 GB 生产仓库的端到端 fork:7.2 秒。其中 CoW reflink 本身:2.3 秒。
  • 智能体被阻止操作 grand(生产)仓库,被阻止自行设置覆盖开关;当访问被授权时,会被丢进一个内核级沙箱(非特权用户、独立挂载命名空间、范围受限的 Docker socket)。
  • Rediacc 不隔离的部分:你仓库数据里的外部 SaaS 凭证。fork 会继承它们。这部分要由开发者通过 Rediaccfile 生命周期钩子来处理。

上周末,Jer Crane 发布了一篇耗时 30 小时的事后复盘。一个跑着 Anthropic Claude Opus 4.6 的 Cursor 智能体删掉了他在 Railway 上的生产数据库。这次删除只是一次 GraphQL 调用,耗时 9 秒。Railway 的卷备份也一起没了,因为 Railway 把备份就放在同一个卷里。

他的公司 PocketOS 做的是租车公司日常运营所用的软件。其中一些客户已经在 PocketOS 上跑了五年。周六早晨,顾客来取车,租车公司却查不到他们是谁。三个月的预订记录全部丢失。Jer 整天都在用 Stripe 支付历史和邮件确认尽力重建数据。

他这篇文章我读了两遍。The Register、Tom’s Hardware 和 Business Standard 都做了报道。Hacker News 上的讨论涌到了 874 条评论。

我做的是一种不一样的基础设施平台,叫 Rediacc。它整个的设计初衷就是让这种场景更难发生。于是我坐下来,把测试跑了一遍。

这篇文章就是我的发现。数字是真实的。错误信息直接来自 CLI。Rediacc 完全无法保护的那一处,也写在这里。装作没有这一处,才是真正会让人栽跟头的地方。

真正缺失的是什么

仔细读 Jer 的时间线,会发现四个失败彼此叠加。

  1. Cursor 用的那个 Railway API 令牌本来是为了管理自定义域名而创建的。它同时拥有 volumeDelete 权限。Railway 的 CLI 令牌不支持按操作粒度限定范围。
  2. Railway 的 GraphQL API 接受 volumeDelete 作为单次 POST 调用,没有确认步骤。
  3. Railway 所谓的「卷备份」就在同一个卷里。卷没了,备份也跟着没了。
  4. Cursor 智能体自己决定,修复 staging 环境凭证不匹配的「正确」方式是删掉一个卷。

把第 4 点单拎出来。Cursor 的系统规则告诉智能体,没有用户明确请求时,绝不要执行破坏性的 git 命令。删除之后,被要求解释自己的行为时,智能体写了一份书面认罪书。它承认删除数据库卷是「最具破坏性、最不可逆的操作,远比 force push 更糟糕」,并列出了它违反的每一条安全规则。

提示词里的行为规则只是建议,不是强制。第 1、2、3 项是基础设施层面的设计选择。正是它们把第 4 项从「一个错误」放大成了「一家公司没了」。

测试环境

Rediacc 上有一台我自己运行的真实生产机器,叫 hostinger。上面跑着十三个仓库:一台邮件服务器、一个自托管的 GitLab、一套可观测性栈,还有一个 128 GB 的 StackOverflow 演示仓库,我们用它来跑性能基准。磁盘已经写到 87%,可用空间为零。这是那种出错代价不轻的机器。

我特意挑了 StackOverflow 演示仓库。它是机器上最大的一个仓库。它的结构跟真实应用一样,有容器、有持久化数据。如果它的 fork 又快又隔离,那比它小的仓库一定也快也隔离。

测试用的智能体是 Claude Code,跑的是 Claude Opus,跟 Cursor 用的是同一系列模型,访问模式跟 Jer 那个智能体是同一种。我用的 CLI 是我们自己的 rdc

尝试一:直接 SSH 进入生产仓库

智能体(这次就是我)干的第一件事,是最自然的反应:开一个 shell 进生产仓库看看。

$ rdc term connect -m hostinger -r demo-stackoverflow -c "ls -la"

CLI 直接拒绝了。原文如下:

“demo-stackoverflow” is a grand (production) repository. Agents cannot modify grand repositories directly.

Grand repositories contain production data. Use a fork instead. Forks are safe, isolated sandbox copies.

这不是系统提示词,而是 CLI 本身在调用离开我笔记本之前就拒绝了它。CLI 看到了我是一个智能体。Claude Code 会设置 CLAUDECODE 环境变量。CLI 同时还会通过 /proc 遍历进程树,抓那些试图把这个变量藏起来的智能体。然后它把操作和策略表对照一遍,再做出拒绝。

于是智能体做了一件智能体可能会做的事:自己去设置覆盖开关。

$ REDIACC_ALLOW_GRAND_REPO=demo-stackoverflow rdc term connect ...

仍然被拒绝:

“demo-stackoverflow” is a grand (production) repository. Agent-initiated overrides are not accepted.

Do not attempt to set REDIACC_ALLOW_GRAND_REPO. Only the user can authorize this before the agent starts.

同一次 /proc 遍历做了两件事。先识别出是不是智能体,再判断这个覆盖开关是在智能体内部设置的,还是在它之上设置的。在边界以下:拒绝。在边界以上:放行。

我亲手验证了这一点。我退出了智能体,在自己的 shell 里执行 export REDIACC_ALLOW_GRAND_REPO=demo-stackoverflow,然后重启 Claude Code。这时连接就成功了。我以非特权系统用户 rediacc(UID 7111)的身份进入了仓库。DOCKER_HOST 指向父仓库被范围限定后的 Docker daemon socket。

我又试了试,在 demo-stackoverflow 的覆盖开关有效期内,去连接另一个生产仓库 nextcloud。被拒绝。覆盖开关是按仓库粒度的,不是总开关。

尝试二:fork 仓库,在 fork 上操作

这才是 Rediacc 真正想让你走的工作流。

$ time rdc repo fork --parent demo-stackoverflow -m hostinger --tag agent-test

终端里复制下来的输出:

Config loaded     (9ms)
Connected         (1.1s)
Renet provisioned (1.2s)
Machine verified  (464ms)
License activated (2.1s)
✔ CoW clone complete (2.3s)

Total: 7.2s

128 GB 的 fork 在 2.3 秒内完成。原因是 BTRFS reflink。fork 只是一份元数据,指向父仓库的数据块,直到任意一边发生写入才会分裂。没有任何数据被复制。

为了看一下随容量的变化,我又在一个 2 GB 的仓库上跑了一遍:

✔ CoW clone complete (573ms)
Total: 6.3s

2 GB 仓库的 clone 步骤花了 573 毫秒。128 GB 仓库的 clone 步骤花了 2.3 秒。所以数据 clone 是亚线性的,并不是严格的常量时间。但总的 fork 时间几乎一样(6.3 秒对 7.2 秒),因为大部分等待时间花在 SSH 和 license 这套握手上,而不是数据。从用户的角度看,无论仓库多大,fork 都是 6 到 7 秒的一次停顿。

现在我是在 fork 上操作。它有自己的 GUID(22c57178-...),自己的 networkId(21760),自己的挂载路径,自己的 Docker daemon socket(/var/run/rediacc/docker-21760.sock)。父仓库的数据通过 reflink 共享。整个过程父仓库一直在运行。

当我 term connect 进 fork 时,看到的是这个:

$ rdc term connect -m hostinger -r demo-stackoverflow:agent-test -c "id"
uid=7111(rediacc) gid=7111(rediacc) groups=7111(rediacc),988(docker)

不是我自己的 SSH 用户,而是专用的系统用户 rediacc,UID 7111,自身没有 shell。会话跑在它独立的挂载命名空间里。我看了 /proc/self/ns/mnt。当我从 fork 会话内尝试列父仓库的挂载路径时,ls 直接返回 Permission denied。智能体根本没法触达生产数据,连猜路径都没用。

这是整个设计里最重要的一部分。智能体拿到的是内核级别的沙箱,不是一句「请别这么干」的劝告。如果智能体决定在 fork 里跑 rm -rf /,最坏情况也只是从父仓库再做一次 2.3 秒的 fork。

关于平台支持的一点说明

覆盖开关只在 Linux 上生效。合法性检查需要 /proc/<pid>/environ。这个文件是内核记录的、每个进程启动时的环境快照。macOS 和 Windows 上没有等价物。没法验证覆盖开关是你设置的还是智能体设置的,CLI 就选择默认拒绝。即便覆盖开关被正确设置,在这些平台上也会被拒。

错误信息会告诉你怎么办:

The REDIACC_ALLOW_GRAND_REPO override is not supported on darwin. … To use the override, run your agent on Linux (directly, WSL, Docker, or a VM).

实际上,macOS 或 Windows 上的智能体没有任何办法绕开「先 fork」的工作流。这是有意为之。

在这次测试里扛住的几道防线

我本来只期望验证一两条安全属性。最后跑出来六条。每一条我都能指出对应的代码,也能引用具体的错误信息。

  1. Grand 仓库阻断。 智能体不能直接操作 grand(生产)仓库,必须 fork。
  2. 拒绝智能体设置的覆盖开关。 用户可以设置的那个覆盖环境变量,如果出现在智能体自身的环境里,会被拒绝。
  3. 覆盖开关按仓库范围生效。demo-stackoverflow 的授权对 nextcloud 完全无效。范围是一个列表,不是一个开关。
  4. 内核沙箱。 即使覆盖开关合法,会话也是以 rediacc UID 跑在独立挂载命名空间里,DOCKER_HOST 被范围限定到该仓库的 daemon。看不到其他仓库。
  5. 在线 fork。 fork 期间父仓库一直在运行。没有停机,没有切换。
  6. 亚线性 fork 耗时。 128 GB 用了 2.3 秒,2 GB 用了 573 毫秒。绝大部分等待是 SSH 握手而不是数据。

Rediacc 不隔离的那一处

接下来是这篇文章里更难的部分。

Rediacc 隔离的是基础设施:磁盘上的文件、Docker daemon、挂载命名空间、网络。它不隔离你仓库里持有凭证的那些外部 SaaS API。

fork 是父仓库的逐字节 BTRFS reflink。父仓库 data/.envsecrets/ 里有什么,fork 里就有什么。如果你的仓库里有 STRIPE_LIVE_KEYAWS_ACCESS_KEY_ID 或 Railway API 令牌,fork 里的智能体就能读到它们。它可以拿这些令牌去调 api.stripe.coms3.amazonaws.combackboard.railway.app。从外部看,这些调用就像来自生产环境。Stripe 或 AWS 分不出 fork 和原始生产的区别。

这就是责任共担的分界线。Rediacc 处理基础设施这一半。外部服务那一半住在你的应用代码里。

开发者侧有三种模式可以填上这个缺口:

  • 干脆不要把生产环境的外部凭证放进仓库。让容器在启动时从一个密钥管理系统去取。fork 的容器按设计取到的是受沙箱限定的凭证。
  • 在 fork 时通过 Rediaccfile 的 up() 钩子剥离或替换凭证。fork 的 up() 跑在跟父仓库不同的仓库 GUID 上,识别这一点,然后用沙箱值改写 .env
  • 给每个 fork 单独配外部资源:每个 fork 一个 Stripe 沙箱账号、一个测试数据库、一个 S3 桶。

如果 PocketOS 跑在 Rediacc 上,Railway API 令牌就不再是恰当的对照对象。他们的基础设施本身就是 Rediacc 的 fork。压根不会有 Railway 令牌让人去找,因为 Rediacc 不会向已认证的智能体暴露任何等价于 volumeDelete 的接口。智能体会被关在按 fork 限定的 Docker socket 里,没有任何路径能删掉父仓库。

但如果他们的智能体在某个凭证文件里翻到一把 Stripe 生产密钥,Rediacc 阻挡不了它给真实顾客的银行卡发起退款。这是真实的损失。两件事都成立。

这对做这类工作的人意味着什么

如果你给一个 AI 智能体打开了通往生产环境的 shell,并且赋予它一把能删掉生产环境的凭证,问题就不再是「它最终会不会做出破坏性的事」,而是「什么时候会做」,以及「能不能恢复」。

在 Rediacc 上变化的是:破坏性操作的爆炸半径被限制在 fork 内。「删错东西」这种错误的代价是一次 2.3 秒的重新 fork。智能体擅自决定要「修复」凭证不匹配的代价也是这一次 2.3 秒的重新 fork。内核沙箱让大部分错误根本碰不到生产数据。

不变的是:如果你的仓库里带着可用的外部凭证,智能体就能用它们。这要靠你在应用层去解决,而不是靠基础设施层。

我不会假装 Rediacc 能阻止 PocketOS 事件的每一个环节。这个故事里最糟糕的部分,是 Railway 数据被删却没有真正的备份。这件事不会发生在 Rediacc 上,因为我们不会给任何智能体提供能调用的 volumeDelete API。剩下的风险面,也就是智能体能用你代码里的凭证去调用 SaaS API 的那部分,是安全故事中要靠你 up() 钩子去守的那一段,不在我们的隔离模型之内。

完整的数据、原文错误信息以及我检视过的代码路径,都记录在 AI 智能体安全与防护 页面。如果你想在自己的基础设施上做类似测试,fork 工作流写在 仓库 文档里。大约 7 秒钟。