AIに自分自身を再起動させたら止まった。Development Runnerを分離した

連載「ChatGPTと育てる、自分専用AI基盤」第3回。ChatGPTとDevelopment MCPから、強い実行と長時間ジョブをDevelopment Runnerへ分離し、Docker操作を別境界へ移した構成を描いた水彩画調サムネイル。 AI

※「ChatGPTと育てる、自分専用AI基盤」第3回です。

前回は、ChatGPTが書いたコードを人間がSSHでVPSへ運び続ける状態をやめるため、Development MCPを作ったところまで書きました。

第2回:ChatGPTにコードを書かせるだけでは足りなかった。Development MCPを作った

Development MCPが動き始めると、ChatGPTからファイルを読み、修正し、テストし、Git差分を見るところまで一続きになりました。

かなり便利になりました。

すると、当然その次が欲しくなります。

「コードを直せるなら、そのサービスをbuildして再起動するところまでやってほしい」

ところが、ここで少し厄介な問題がありました。

Development MCP自身も、Dockerコンテナとして動いていたのです。

自分を更新するために、自分を再起動する。

実際にやってみると、処理をしている本人が途中で消えます。

さらに一度は、Development MCPを更新するためのDocker Compose操作が依存関係を巻き込み、再起動を担当していた側まで一緒に再作成されるという事故が起きました。

再起動作業を担当している人が、作業中に自分自身の電源を切ったようなものです。

この失敗から、Development RunnerをDevelopment MCPの外側へ分離することになりました。

Development MCPにDocker socketを渡せば簡単だった

一番簡単な方法はあります。

Development MCPへDocker socketを直接渡してしまえば、コンテナのbuild、起動、停止、再起動まで操作できます。

ただ、これはかなり強い権限です。

Development MCPはChatGPTから日常的に使います。ファイルを読む。コードを直す。テストする。Git差分を見る。

その道具に、そのままDocker全体を操作できる権限まで持たせるのは、便利ではあるものの、失敗したときの影響範囲が大きすぎると考えました。

第2回でKnowledge MCPとDevelopment MCPを分けたのと同じです。

必要な能力が強くなるなら、既存の道具を万能化するのではなく、責務をもう一段分ける。

そこで、強い実行権限だけを担当するDevelopment Runnerを作りました。

制御する側と、実際に強い操作をする側を分けた

構成は、概念的にはこうなりました。

ChatGPT
   ↓
Development MCP
   │
   ├─ ファイル操作
   ├─ Git差分
   ├─ テスト
   └─ Runnerへ限定された処理を依頼
             ↓
      Development Runner
             ↓
   Docker Compose / restart / 長時間job

Development MCPは、ChatGPTと会話しながら開発を進める制御面です。

Development Runnerは、Docker Composeや再起動、長時間処理といった、より強い操作を担当する実行面です。

Runnerへ追加した主な機能は、当時の記録では次のようになっています。

  • Docker Composeの状態確認・操作
  • 対象サービスの再起動
  • 長時間ジョブの投入
  • ジョブ状態の確認
  • ログの読取
  • キャンセル
  • rollback

これによって、ChatGPTからはDevelopment MCPという比較的限定された入口を使いながら、必要なときだけRunnerへ強い仕事を渡せるようになりました。

「長時間処理」も別問題だった

Runnerを分けた理由は、Docker権限だけではありません。

AIとサーバーをつないで開発していると、すぐ終わる処理ばかりではありません。

Docker imageのbuild、まとまったテスト、少し重い処理などは時間がかかります。

それをChatGPTとの一回のリクエストの中で最後まで待とうとすると、接続や実行時間の都合で扱いにくくなります。

そこで、処理をジョブとして投入し、あとから状態とログを確認できるようにしました。

処理を依頼
  ↓
job IDを受け取る
  ↓
処理は継続
  ↓
あとからstatusを見る
  ↓
必要ならlogを見る
  ↓
失敗なら原因を直して再実行

今では当たり前に使っている仕組みですが、当時はこれも大きな変化でした。

ChatGPTとの一回の会話が終わることと、サーバー上の仕事が終わることを分離できたからです。

7月30日の導入記録では、Development Runnerのhealth、Development MCPのhealth、Docker Compose状態確認、非同期pytest、Ruff、mypy、pytestまで確認しています。

その後、Runnerのジョブ履歴自体もSQLiteへ残すようになり、8月3日時点では110件のジョブ履歴が記録されていました。

サーバー操作が、その場限りのコマンドから後から追える仕事の履歴へ変わっていきました。

この「後から追える」ことは、のちに別の意味でも効きました。複数Chat・workspace・Workerを並列に動かすようになったあと、主開発Chatが別Chat所有のPublishing MCP workspaceまで進めそうになったIncidentでは、Runner / workspaceのログを見ていたことで責務越境に早く気づけました。最終結果だけでなく、今どのChat / owner / workspaceが何をしているかを人間が観測できること自体が安全機能になった例です。

AIが別のChatの仕事まで進めそうになった話――能力向上で生まれた新しい失敗モード

そして、本当に自分自身を止めた

設計を分けたからといって、最初から完全だったわけではありません。

実運用で、Development MCPをDocker Composeのupで更新した際、Composeの依存関係によりDevelopment Runnerまで再作成されることがありました。

つまり、Runnerが処理を担当している最中に、その処理自身がRunnerを更新対象へ含めてしまったわけです。

結果として、処理主体が自分自身を止める状態になりました。

「Runnerを別サービスにしたから安全」と思っていましたが、サービスを分けただけでは不十分でした。

依存関係を通じて自分自身を操作できるなら、まだ同じ問題が残っています。

--no-depsをルールにした

そこで、Development MCPだけを更新するときは、依存サービスを巻き込まないようにしました。

docker compose up -d --no-deps development-mcp

重要なのは、このコマンドそのものより、一度起きた事故を運用ルールではなく仕組みへ入れたことです。

「次から気をつける」では、また起きます。

そこで、

  • Development MCP更新時は依存サービスを巻き込まない
  • Runner自身のrestart / stopをRunner APIから拒否する
  • Docker socketはRunnerだけに持たせる
  • 操作対象のrepositoryとCompose projectを固定する

という制約へ変えました。

このあたりから、「AIへ何を許可するか」だけでなく、AIが自分自身や制御系を変更できるかを意識するようになりました。

権限の強さだけではなく、位置が重要だった

Development Runnerを作ったときに面白かったのは、単純な「権限を弱くする」という話ではなかったことです。

Docker操作は必要です。

サービスの再起動も必要です。

長時間処理も必要です。

全部禁止してしまえば安全ですが、開発は進みません。

必要なのは、強い権限をなくすことではなく、強い権限をどこに置くかを決めることでした。

日常的にChatGPTから触るDevelopment MCPは比較的狭い権限にする。

本当に強い操作はRunnerへ集める。

そしてRunnerも、何でも自由に実行するのではなく、対象と操作方法を限定する。

こうすると、ChatGPTから見ると使いやすいまま、事故が起きたときの範囲を狭くできます。

この考え方は、その後のPersonal AI Platformでも何度も出てきます。

権限を一か所へ集めない。

制御面と実行面を分ける。

自己変更できる場所を意識する。

いま振り返ると、Development Runnerは単なるDocker操作用サービスというより、AIに強い権限を渡すための境界線だったと思います。

「AIが自分を壊さない」はお願いではなく設計にする

AIにサーバーを触らせるとき、最初はつい、

「既存サービスを壊さないで」

「必要なところだけ変更して」

と指示したくなります。

もちろん、それも必要です。

でも、Development Runnerの失敗で分かったのは、それだけでは足りないということでした。

AIが間違えないことを期待するより、

  • そもそも触れない
  • 対象を固定する
  • 自己停止を拒否する
  • 依存サービスを巻き込まない
  • 長い処理はジョブとして隔離する
  • 失敗したらrollbackできる

という構造にした方が安定します。

これはAI特有の話というより、普通のシステム設計でも同じなのだと思います。

ただ、自然言語で気軽に「更新して」と頼めるAIだからこそ、境界を曖昧にすると強い操作まで簡単に届いてしまう。

そのため、便利になればなるほど、外側の構造が重要になりました。

振り返ると、Runnerは「強い手」を別の場所に置いた仕組みだった

第1回では長期記憶をKnowledge MCPへ分けました。

第2回では開発操作をDevelopment MCPへ分けました。

第3回では、そのDevelopment MCPからさらに、強い実行権限をDevelopment Runnerへ分けました。

Knowledge MCP
  → 知識を扱う

Development MCP
  → 開発を考え、変更し、確認する

Development Runner
  → 強い実行処理を担当する

便利な機能を一つのAIへ足していったのではなく、能力が増えるたびに役割を分ける方向へ進んだことになります。

この構造のおかげで、ChatGPTから開発を続けながらも、Docker操作や長時間処理を別の境界として扱えるようになりました。

そして、開発作業がかなり自動化されると、次に残ったのは「変更をどう確定するか」という問題でした。

コードを書き換え、テストできても、その変更をそのままmainへ入れてよいわけではありません。

そこで次は、GitHubのPull Requestを使って、AIが作った変更を安全に確定する流れを整えていくことになります。

次回:ChatGPTからPRを作り、安全にマージする

Development MCPとRunnerで変更・テストまで進められるようになったあと、GitHubをどう「変更を確定する境界」にしたかを第4回にまとめました。

連載ナビ

関連記事

コメント

タイトルとURLをコピーしました