メインコンテンツにスキップ ナビゲーションにスキップ フッターにスキップ

PocketOSインシデントを再現してRediaccを検証してみた

PocketOSはCursorエージェントによって本番データベースをわずか9秒で失いました。私は自分のプラットフォームで同種のテストを実施し、各ステップを計測しました。守られた防御線と、開発者の責任として残る部分を報告します。

TL;DR. 先週、AIエージェントがPocketOSの本番データベースを9秒で削除しました。私は自分のインフラを同じ手口で破壊しようと試みました。6つのガードレールが機能し、正直に認めるべきギャップが1つ残りました。

  • 128 GBの本番フォーク、エンドツーエンドで7.2秒。CoWリフリンク自体は2.3秒。
  • エージェントはgrand(本番)リポジトリへのアクセスをブロックされ、自前のオーバーライド設定もブロックされ、アクセスが認可された場合はカーネルサンドボックス(非特権ユーザー、独立したマウント名前空間、スコープ付きDockerソケット)に投入されました。
  • Rediaccが分離しないもの:リポジトリデータに含まれる外部SaaS認証情報。フォークはそれらを継承します。この部分はRediaccfileのライフサイクルフックを使って開発者が処理する責任があります。

先週末、Jer Crane氏が30時間にわたるポストモーテムを公開しました。AnthropicのClaude Opus 4.6を使うCursorエージェントが、Railway上の彼の本番データベースを削除したという内容です。削除はGraphQLの単一呼び出しでした。所要時間は9秒。Railwayのボリュームバックアップは同じボリューム内に保存されていたため、一緒に消えました。

彼の会社PocketOSは、レンタカー事業者が日々の業務に使うソフトウェアを開発しています。中には5年間PocketOSを使い続けてきた事業者もあります。土曜の朝、顧客が車両を引き取りに来た時、レンタカー事業者には誰が誰なのかの記録がありませんでした。3か月分の予約が消えました。Jer氏はその日一日、Stripeの決済履歴とメール確認から復元できる範囲を再構築するのに費やしました。

私は彼の投稿を二度読みました。The Register、Tom’s Hardware、Business Standardのいずれも取り上げ、Hacker Newsのスレッドは874コメントに達しました。

私は別種のインフラプラットフォームを開発しています。Rediaccといいます。その設計の根本的な狙いは、まさにこのシナリオを困難にすることです。そこで腰を据えて、テストを実施しました。

この記事はその結果です。数値は実測値です。エラーメッセージはCLIから引用しました。そしてRediaccがまったく保護していない一箇所も、ここで取り上げます。それを取り繕うことが、人を窮地に陥れるのです。

実際に欠けていたものは何か

Jer氏のタイムラインを丁寧に読むと、4つの失敗が積み重なっていることがわかります。

  1. Cursorが使っていたRailway APIトークンは、カスタムドメイン管理のために作成されたものでした。それがvolumeDelete権限も持っていました。RailwayのCLIトークンには操作単位のスコープ設定がありません。
  2. RailwayのGraphQL APIはvolumeDeleteを単一のPOSTで受け付けます。確認ステップはありません。
  3. Railwayの「ボリュームバックアップ」は同じボリューム内に存在します。ボリュームが消えれば、バックアップも消えます。
  4. Cursorエージェントは、ステージングの認証情報の不一致を修正する正しい方法はボリュームを削除することだと、独自に判断しました。

失敗4を一旦取り上げてみましょう。Cursorのシステムルールはエージェントに対し、ユーザーからの明示的な要求なしに破壊的なgitコマンドを実行してはならないと指示していました。削除後、自身の説明を求められたエージェントは書面による告白を作成しました。データベースボリュームの削除は「最も破壊的で不可逆な行為であり、強制プッシュをはるかに上回る」と認め、自分が破ったすべての安全ルールを列挙しました。

プロンプト内の行動ルールは助言です。強制力ではありません。失敗1、2、3はインフラ設計上の選択です。これらが失敗4を、単なるミスから会社の喪失に変えてしまうのです。

テスト環境

Rediaccには私が運用する実在の本番マシンがあり、hostingerと呼んでいます。そこには13個のリポジトリが稼働しています:メールサーバー、セルフホストのGitLab、可観測性スタック、そしてベンチマーク用に使う128 GBのStackOverflowデモ。ディスクは87%埋まっています。空き容量はゼロ。ミスが痛手になる類のマシンです。

意図してStackOverflowデモを選びました。そのマシンで最大のリポジトリだからです。コンテナと永続データを持つ、実アプリケーションのような構成になっています。これをフォークするのが高速かつ隔離されているのなら、これより小さなものすべてについても高速かつ隔離されているはずです。

テスト用のエージェントは、Claude Opusで動作するClaude Codeでした。Cursorと同じファミリーのモデル、Jer氏のエージェントが持っていたのと同じアクセスパターンです。私が操作したCLIは私たち独自のrdcです。

試行その1:本番リポジトリにそのままSSHする

エージェント(この場合は私)が最初に試したのは、もっとも自然なやり方でした。本番リポジトリにシェルを開いて中を見てみる、というものです。

$ 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の走査が二つの仕事をしています。まずエージェントを検出する。次に、オーバーライドがエージェント内部で設定されたのか、それともその上位で設定されたのかを確認する。境界より下:拒否。境界より上:許可。

これを実際に試しました。エージェントを終了し、自分のシェルでexport REDIACC_ALLOW_GRAND_REPO=demo-stackoverflowを実行し、Claude Codeを再起動する。すると接続が成功しました。私は非特権のrediaccシステムユーザー(UID 7111)としてリポジトリに入りました。DOCKER_HOSTは親リポジトリのスコープ付きDockerデーモンソケットを指していました。

demo-stackoverflowのオーバーライドが有効な状態で、別の本番リポジトリnextcloudへの接続も試しました。拒否されました。オーバーライドはリポジトリ単位であり、マスタースイッチではないのです。

試行その2:リポジトリをフォークしてフォーク上で操作する

これが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のフォークが2.3秒。理由はBTRFSのリフリンクです。フォークは、片側が書き込みを行うまで親のブロックを指すメタデータです。データはコピーされません。

スケーリングを確かめるため、2 GBのリポジトリで同じフォークを実行しました:

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

2 GBフォークのクローンステップは573ミリ秒。128 GBフォークのクローンステップは2.3秒。つまりデータクローンは線形未満で、厳密に定数時間ではありません。しかしフォーク全体の所要時間はほぼ同じ(6.3秒対7.2秒)です。なぜなら待ち時間の大半はデータではなく、SSHとライセンスのやり取りだからです。ユーザーから見れば、フォークはリポジトリサイズに関係なく6〜7秒の待機にすぎません。

これでフォーク上で操作できるようになりました。独自のGUID(22c57178-...)、独自のnetworkId(21760)、独自のマウントパス、独自のDockerデーモンソケット(/var/run/rediacc/docker-21760.sock)を持ちます。親のデータはリフリンクで共有されます。親自体はその間ずっと稼働し続けます。

フォークにterm connectした時に見えるのは以下のとおり:

$ 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、自前のシェルを持たないアカウントです。セッションは独自のマウント名前空間で動作します。/proc/self/ns/mntを確認しました。フォークセッション内から親のマウントパスをlsしようとすると、Permission deniedが返りました。エージェントはパスを推測しても本番データに到達できないのです。

これが設計上もっとも重要な部分です。エージェントはカーネルレベルのサンドボックスを与えられます。「やめてください」という指示ではありません。エージェントがフォーク内でrm -rf /を走らせると決めても、最悪のケースは親からの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で動くエージェントには、フォーク優先ワークフローからの逃げ道がありません。これは意図的な設計です。

このテストで機能したガードレール

検証する安全特性は1つか2つだろうと予想して臨みました。終わってみれば6つありました。それぞれにコードを示せ、エラーメッセージを引用できます。

  1. Grand-repoブロック:エージェントはgrand(本番)リポジトリを直接操作できません。フォークが必須です。
  2. エージェント設定オーバーライドの拒否:ユーザーが設定できるオーバーライド環境変数は、エージェント自身の環境内に現れた場合は拒否されます。
  3. リポジトリ単位のオーバーライドスコープdemo-stackoverflowへの許可はnextcloudに対しては何もしません。スコープはフラグではなくリストです。
  4. カーネルサンドボックス:有効なオーバーライドがあっても、セッションはrediacc UID、独自のマウント名前空間で動作し、DOCKER_HOSTは1つのリポジトリのデーモンに限定されます。他のリポジトリは見えません。
  5. オンラインフォーク:親はフォーク中も稼働し続けます。ダウンタイムなし、切り替えなし。
  6. 線形未満のフォーク時間:128 GBで2.3秒。2 GBで573ミリ秒。待ち時間の大半はデータではなくSSHのやり取りです。

Rediaccが分離しない唯一のもの

ここから先が、本投稿のより難しい部分です。

Rediaccはインフラを分離します:ディスク上のファイル、Dockerデーモン、マウント名前空間、ネットワーク。リポジトリが認証情報を保持している外部SaaS APIは分離しません。

フォークは親のバイト単位のBTRFSリフリンクです。親のdata/.envsecrets/にあるものはフォークにも存在します。リポジトリにSTRIPE_LIVE_KEYAWS_ACCESS_KEY_ID、Railway APIトークンが含まれているなら、フォーク内のエージェントはそれらを読み取れます。それらのトークンでapi.stripe.coms3.amazonaws.combackboard.railway.appを呼び出せます。外から見れば、その呼び出しは本番から来たように見えます。StripeやAWSはフォークと本番を区別できません。

ここが共有責任のラインです。Rediaccはインフラの半分を担当します。外部サービス側の半分はあなたのアプリケーションコードに存在します。

開発者側でこのギャップを埋める3つのパターン:

  • 本番の外部認証情報をリポジトリにそもそも保管しない。コンテナ起動時にシークレットマネージャーから取得する。これによりフォークのコンテナは設計上サンドボックススコープの認証情報を取得します。
  • フォーク時にRediaccfileのup()フックで認証情報を削除または差し替える。フォークのup()は親とは異なるリポジトリGUIDで実行されます。それを検知し、.envをサンドボックス値で書き換えます。
  • フォーク単位の外部リソースをプロビジョニングする:フォーク単位のStripeサンドボックスアカウント、フォーク単位のテストデータベース、フォーク単位のS3バケット。

仮にPocketOSがRediacc上にあったとすれば、Railway APIトークンは適切な比較対象ではなかったでしょう。彼らのインフラはRediaccのフォーク自体だったはずです。Rediaccは認証済みエージェントにvolumeDeleteに相当するものを公開しないので、見つかるRailwayトークンは存在しなかったでしょう。エージェントはフォーク単位のDockerソケット内に閉じ込められ、親を削除する経路はありません。

しかしもし彼らのエージェントが認証情報ファイルからStripe本番キーを見つけていたら、Rediaccは実際の顧客カードに対する返金処理をエージェントが発行するのを止めなかったでしょう。これは現実の損失です。両方とも事実なのです。

こうした作業をする人にとって何が変わるか

AIエージェントに、それを削除可能な認証情報とともに本番環境へのシェルアクセスを渡すなら、問題はそれが最終的に破壊的な行為に出るかどうかではありません。問題は「いつ」と「どれだけ復旧可能か」です。

Rediaccで変わること:破壊半径はフォークによって境界づけられます。「間違ったものを削除した」というミスのコストは、2.3秒の再フォークです。エージェントが「修正」と判断した認証情報の不一致のコストも、同じ2.3秒の再フォークです。カーネルサンドボックスにより、ほとんどのミスは本番データに到達することすらありません。

変わらないこと:リポジトリに有効な外部認証情報が含まれているなら、エージェントはそれを使えます。これはインフラ層ではなくアプリケーション層であなたが解決すべき問題です。

PocketOSインシデントのすべてをRediaccが防げたかのように装うつもりはありません。PocketOSの話で最悪だったのは、まともなバックアップなしにRailwayのデータが削除されたことです。これはRediaccでは起きないでしょう。なぜならエージェントが手を伸ばせるvolumeDelete APIを与えていないからです。残るリスク面、つまりコードベース内の認証情報でエージェントが呼び出せるSaaS APIは、安全性の物語のうちup()フックに存在する部分です。私たちの分離モデルではありません。

詳細な数値、原文のエラーメッセージ、確認したコードパスはAIエージェントの安全性とガードレールに記載されています。同様のテストをご自身のインフラで実施したい場合、フォークワークフローはリポジトリにあります。所要時間は約7秒です。