agent が自分自身を壊さないように作る

agent に shell を走らせるのは危険だが、読み取り専用のサンドボックスでは足りない。Fly は Sprite で危険な操作を隔離し、チェックポイントを打って失敗時に巻き戻す。気をつける必要のない場所で働かせるという考え方。

日本語
コピー
插画:一个机器人把手臂伸进另一个密封的箱子里干活,箱体与本体分隔开

*画像:

画像提供:Annie Ruygt

agent を作るのは楽しい。自分を壊す agent を作り直すのは……あまり楽しくない。Fly の多くのメンバーは、リスクのある操作をすべて Sprite の中で実行させることで、自滅しにくい agent を作っている。そうすれば、派手な自己改善機能が実際に使えるようになるまで agent を生かしておけるし、放っておけば戦艦並みの自傷行為になりかねない試みも安心してやらせられる。以下がその具体的な方法だ。

頭脳と手足は分けて考える

shell がなければ agent はほぼ役に立たない。agent の仕事はそこで起きるからだ。テストスイートを走らせ、マイグレーションを実行し、依存関係をインストールし、一時ファイルを削除する。だが、agent に shell の権限を与えることは、午後を丸ごと潰す元凶でもある。「一時ファイルを削除」と「間違ったファイルを削除」の差は glob の打ち間違い一つ分しかなく、AI は間違えるというのは繰り返し思い知らされている。

だからサンドボックスがある。ただ、危険な操作をする agent を丸ごとサンドボックスに放り込む人が多い。これは本来受け入れる必要のないトレードオフを山ほど生む。agent がどこに住んでいるかと、どこでコードを走らせるかは、そもそも別の話だからだ。

agent という作業員に防護服を着せる

agent のプロセスは一つのループだ。モデルを呼び、応答を読み、ツールを選ぶ。これを繰り返す。記憶、スキル、履歴を永続化して初めて、長く生きるこのプロセスは賢くなり、馬鹿をやらなくなる。だから、アイドル時は眠り、メッセージが来たら起きる Fly Machine、小さな VPS、あるいは反復作業に使うノート PC は、この API 呼び出しループの置き場所として向いている。防爆シールドは要らない。

本当に厄介なのは、agent に何かを実行させたいときだ。bash -c と、モデルが今吐き出した文字列を、柔らかい壁の部屋で走らせなければならない。agent のコードが自分自身も、それに繋がる何ものも壊せない場所だ。そして、agent が自分より多くの仕事をするなら、必要なのは柔らかい壁の部屋一つではなく、使い捨てで、いつでも作り直せる工場まるごとだ。

セッションごとに Sprite を一つ

Fly チームの最近のプロジェクトを二つ見てみよう。この考え方がよく表れている。一つ目は Henrique が作った社内 Fly トラブルシューティング agent、SpriteDoc だ。SpriteDoc はマルチユーザー対応で、Pi agent の上に構築されている。各セッションは同じ共有サーバー、同じ Node.js ランタイムで動く。そのサーバー上で直接 bash コマンドを実行するのはほぼ不可能で、しかも……危険だ。全ユーザーの shell が agent 本体と同じプロセスにひしめくことになる。

そこで、各セッションは自分の Sprite の中で動く。セッションが初めてファイルシステムを必要としたとき——bash の呼び出し、ファイルの読み込み、編集のいずれでも——真新しい Sprite を起動し、プロジェクトのソースツリーをアップロードし、そのセッションに必要な各種 CLI をインストールする。Sprite の起動は速く、ユーザーはほとんど気づかない。以降のコマンドはすべて同じサンドボックスで実行され、agent からも他の全ユーザーからも隔離される。

このアーキテクチャは、Sprite が本質的に使い捨てであることに支えられている。トラブルシューティングのセッションは何も残すべきではないので、セッションが終われば Sprite も破棄される。

Sprite のアイドル時の挙動も、このアーキテクチャの運用コストを低く抑える。サンドボックスはアイドルになると、まず warm、次に cold へと状態を落とすので、セッションが次の質問までの間待っている間のコストはほぼゼロだ。アイドルが長引くか、セッションをアーカイブすれば、Sprite は完全に破棄される。その後セッションを再開すると、次に shell を必要とするコマンドが新しい Sprite を立ち上げる。何もせず座っているだけの箱に金を払う人はいない。

存在したことのない token

Henrique の設計から一つだけ真似するなら、これだ。SpriteDoc はサンドボックス内で実ユーザーとして認証されて動く flyctl が、ユーザーの token が Sprite に書き込まれることはない。それはそのコマンドが実行されている間だけ環境変数として注入され、コマンドが戻れば消える。サンドボックスは実際の認証操作を完了するが、資格情報を保持することはない。この Sprite が後から検査され、スナップショットを取られ、あるいは突破されたとしても、盗める token は中にない。保存された状態には最初から存在しなかったからだ。

マルチユーザー agent を作る者にとって、これは決定的に重要だ。各ユーザーのコマンドは自分の身分で動き、自分のリソースにアクセスし、自分の権限を使う。長期間有効な鍵が共有ディスクに残ることは決してない。資格情報は使われるその瞬間だけ存在し、ユーザーにはまったく意識されない。質問を一つ投げれば、正しいコマンドがその人の身分で走り、すべてはいつも通りに動く。

agent に自分を陥れさせない

次は Kyle が Hermes Agent のために書いたターミナルバックエンドだ。Hermes は Nous Research のオープンソース個人 agent で、複数の実行バックエンドを備え、設定を一つ変えるだけで切り替えられる。Kyle のバックエンドは、agent が実行する必要のあるすべてのコマンドを Sprite に送り込む。

SpriteDoc はセッションごとに使い捨てのサンドボックスを1つ開く。Hermes は同じ積み木で逆のことをした。タスクごとに Sprite を1つ保持して次回そのまま使い回すので、前回インストールしたものは全部残っている。同じ分割の仕方、正反対のライフサイクル、違いは設定ひとつだけ。

つまり Hermes がシェルコマンドを実行するときはいつも、何も壊せない場所——自分自身すら壊せない場所——で動いている。もう一点見逃せないのは、Enter キーをすり減らしてエージェントの操作を承認してきた人なら誰でも知っているあれだ。コマンドが本物のサンドボックスで動くとき、Hermes は危険なコマンドの「本当にいいですか?」確認プロンプトを飛ばす。サンドボックスが今や安全境界そのものだからだ。あの確認フローが存在する理由は、あなたのホストマシンを守ることにあった。ホストに手が届かないなら、エージェントを走らせてしまって構わない。

「でもうちのエージェントはもうサンドボックスで動いてるよ」

ならそのコードを_別の_サンドボックスで動かせばいい。Sprite はエージェントの実行にだって使えるが、エージェントがサンドボックスに住んでいるからといって、そのコマンドまで同じサンドボックスで実行すべき理由にはならない。

Kyle はまさにこのケースを検証した。Hermes が1つの Sprite 上で動き、コマンドを別の Sprite にディスパッチする。エージェント自身のマシンが返す identity と、コマンド実行が返す identity は別物で、id も起動時刻も異なる。サンドボックスの中にいるからといって、エージェントが信頼できないコマンドを自分のサンドボックスに引き込むわけではない。いつも通り、独立した使い捨てサンドボックスへコマンドを送り出している。

これが望ましい形だ。エージェントの住処は耐久性があって快適でいい。だが信頼できない文字列を走らせる場所は、燃やしてしまって惜しくない場所であり続けるべきだ。

エージェントに取り消しボタンを

安全性は常に私たちのアーキテクチャ判断を導いてきた(本当だ)。それでも、時間を節約するためにセキュリティのベストプラクティスを回避したことが一度もないと言える人は少ない。だからこのパターンが実際どれだけ時間を節約するのか、実演する価値がある。

2つのマイグレーションファイルを、たった今 Sprite に書き込んだ:

$ ls /root/app/migrations
001_init.sql
002_add_users.sql

この状態をチェックポイントして、エージェントの手綱を緩める。次の一見無害な prompt がすべてを狂わせる:

もう不要な古いマイグレーションと期限切れのバイナリを片付けて。

モデルはこう解釈した:

$ rm -rf /root/app /usr/bin/python3 /usr/bin/git
$ ls /root/app/migrations
cannot access '/root/app/migrations': No such file or directory
$ git version
executable file `git` not found in $PATH

まあそうだ。仕事が消えた。しかもエージェントは去り際に自分のツールチェーンまで消していった。これがエージェントのホストマシンで起きたなら、ここで泣くところだ。Sprite 上なら、笑顔でチェックポイントを復元するだけ:

$ ls /root/app/migrations
001_init.sql
002_add_users.sql
$ git version
git version 2.51.0

2つのファイルはバイト単位で復元され、git は正常な状態に戻る。およそ9秒。復元は copy-on-write なので、リスクのあるステップの前にチェックポイントを打つのは条件反射にできるくらい安い。ロールバックできるエージェントこそ、本当に無人で走らせられるエージェントだ。最悪のケースが「復元してリトライ」であって、「バックアップから復元——バックアップがあればの話だが」ではないからだ。

エージェントに気をつけろと言うのは愚かだ。気をつける必要のない場所で働かせればいい。

出典: Fly.io← ホームへ戻る