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

我用 PocketOS 事故场景测试了 Rediacc

PocketOS lost their production database to a Cursor agent in 9 seconds. I ran the same test, timed every step, and noted what held and what's yours to fix.

TL;DR. 上周,一个 AI 智能体在 9 秒内删除了 PocketOS 的生产数据库。我尝试让自己的基础设施也以同样的方式失败。六道防线扛住了;一个诚实的缺口仍然存在。

  • 128 GB 生产仓库 fork,端到端:7.2 秒。 CoW reflink 本身:2.3 秒。
  • 该智能体被阻止访问 grand(生产)仓库,被阻止自行设置覆盖开关,并在获得授权时被放入内核级沙箱(非特权用户、独立挂载命名空间、范围受限的 Docker socket)。
  • Rediacc 无法隔离的部分:仓库数据中的外部 SaaS 凭证。fork 会继承它们。(更新。2026 年 5 月:rdc repo secret 现在能让你把凭证完全放在仓库镜像之外。详见新增的结尾章节“更新:这个缺口现在可以补上了”。) 这部分需要开发者通过 Rediaccfile 生命周期钩子来处理。

更新。2026 年 5 月。 自本文发布以来,Rediacc 已推出原生的按仓库密钥rdc repo secret)。前文所说的那个“诚实的缺口”(烘焙在仓库里的外部 SaaS 凭证)现在有了内置答案:可以把凭证完全放在 LUKS 镜像之外,因此 fork 默认不会继承任何密钥。直接跳到新增的结尾章节 更新:这个缺口现在可以补上了,了解已交付的内容、改变了什么,以及还残留哪些风险。

上周末,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 仓库上跑了同样的 fork:

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

2 GB fork 的 clone 步骤耗时 573 毫秒。128 GB fork 的 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 /,最坏情况也只是从父仓库重新 fork 一次,耗时 2.3 秒。

平台支持说明

覆盖开关只在 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 上的智能体没有任何 escape hatch 能绕开“先 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 阻挡不了它给真实顾客的银行卡发起退款。这是真实的损失。两件事都成立。

更新:这个缺口现在可以补上了

我保留了上面那节的原文。当时那份诚实是站得住脚的。自发布以来,我们交付了缺失的那一块,缺口现在可以补上了:不是靠改变 fork 的行为,而是靠新增一处放凭证的地方,让它住在加密的仓库镜像之外。

机制就是 rdc repo secret。它的设计参照了 GitHub Actions secrets。有两点很关键。

密钥不住在 LUKS 镜像里面。 它们住在磁盘上的另一个独立平面:env 模式的密钥以 ${REDIACC_SECRET_<KEY>} 插值方式在你的 compose 文件里到达容器;file 模式的密钥则通过 Docker compose 的 secrets: 块以 tmpfs 文件 /run/secrets/<key> 的形式到达容器。无论哪种方式,值都不会接触仓库的加密镜像。fork 的 BTRFS reflink 复制的是镜像。它不会复制从未在镜像里的东西。一份新鲜 fork 的密钥列表是空的。它的容器启动时没有任何生产凭证,连发起一次生产调用都不可能。

这个模型是只写的。 rdc repo secret get 返回的是 SHA-256 摘要,不是值。CLI 设计上就没有任何方式可以把密钥读回来。这就关上了终端录像、shell 历史和意外重定向带来的泄露面。轮换的方式是 --current <previous-value> 来校验前一个值,或者在你不知道前一个值的时候用 --rotate-secret 跳过前置条件(会作为一次轮换被审计)。对人类和智能体是对称的。两道关卡同样适用。

这对 PocketOS 那个场景意味着什么:如果他们用的是 Rediacc 并采纳了密钥机制,fork 里的智能体就不会在某个凭证文件里翻到 STRIPE_LIVE_KEY。fork 的密钥映射会是空的。沙箱内根本没有任何东西能用来调用 Stripe。上面原文里“真实的损失”这一保留语,会缩小为“对那些把凭证烘焙进镜像的仓库来说是真实的损失”。而那些仓库的修复方式,是一次性把它们迁移到新机制里。

诚实的残余缺口:在采用 rdc repo secret 之前你已经写进仓库镜像的密钥。提交到某个卷里的 .env、被持久化进数据库的凭证、带 token 的 YAML 文件。仍然是镜像数据的一部分。CoW reflink 会照旧把它们传播下去。这套机制给了你一个安全的存放位置;它不会回溯式地把已经在加密卷磁盘上的东西清掉。今天的修复方式是手动迁移一次(读出值,用 rdc repo secret 写入,再从镜像源里清除)。一个一等公民式的 rdc repo secret lift --from .env 在路线图上。

针对智能体威胁模型,这个设计有两个值得专门指出来的副作用。第一,repo secret listrepo secret get 作为 MCP 工具暴露出来(读取安全:只给名字和摘要,绝不给值),所以一个帮你搭应用的智能体可以发现配置了什么,而不会看到具体是什么。第二,当一次写操作未通过前置条件时,JSON 错误信封里包含一个结构化的 next.options[].run 字段,列出智能体应当原样转交给人类的具体命令。包括针对“我忘了前一个值”的轮换逃生口。完整模型见 AI 智能体安全

完整的操作指引见 仓库 § 密钥。分三步:首先播种值,运行 rdc repo secret set --name <repo> --key <KEY> --value <val> --current "";然后在 compose 文件里引用 ${REDIACC_SECRET_<KEY>};最后运行 rdc repo fork,确认 fork 的密钥列表是空的。

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

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

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

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

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

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