AIエージェントの「暴走」を防ぐ:RustとNode.jsによる特権分離と心中プロトコル
本稿は、Qiita 記事「型システムを信じるな、規約を信じろ。独立プローブによる「泥臭い」境界防御のすすめ」の続編です。
このシリーズでは、古典的な技術を現在の AI 開発などに適用する「古くて新しい技術」について触れていきます。
本稿では、堅牢な MCP サーバーの設計において、あえて Node.js (Control Plane) と Rust (Execution Plane) という「異言語 二層構造」を選択し、心中(Shinju)プロトコルという極めて泥臭い境界防御に至った設計思想を紐解きます。
TL;DR
- 信頼性の分離: Node.js (Control Plane) と Rust (Execution Plane) を物理プロセスで分離。一方が窒息しても全体が沈没しない**「特権分離 (PrivSep)」**の断行
- 心中(Shinju)プロトコル: 「親が死んだら、子も道連れに自爆する」。フェイト・シェアリング (Fate Sharing / 運命共有) を強制し、ゾンビプロセスを 1 ミリも許さない冷酷な設計
- 境界防御の保険料: 長時間ジョブの巻き添え死や再起動ループといった「不都合な真実」を受け入れ、観測(Prometheus)という泥臭い運用でカバーする覚悟
第1章:設計仮定の破綻 — 1プロセス構成が招く「窒息」
MCP サーバーは、AI の指示を物理的な OS コマンドに変える「劇薬の門番」です。Node.js 単一で実装した場合、その「効率」を求めた 1 プロセス構成は、ある時、冷酷な現実によって窒息するリスクを抱えます。
AI が数万行のログを吐き出すコマンドを実行した瞬間、Node.js のイベントループはパース処理に占有され、ホスト(IDE)からの生存確認に応答できなくなることがあります。ホストはサーバーを「死んだ」とみなして通信を切断しますが、OS 上には制御不能な子プロセスが「ゾンビ」として取り残される事態が発生します。
1. 「脳」と「手」を同じ船に乗せてはいけない
「賢く振る舞いたい層(Control Plane)」と「泥臭く働く層(Execution Plane)」を一心同体にしてはいけない。 これが、実戦から得られた重要な設計原則です。
2. セキュリティ:汚染された実行環境からの保護と特権分離
執行を司る Execution Plane (Guardian) は、AI が生成した「信頼できない入力」を具現化するリスクの最前線です。たとえ執行プロセスがセグメンテーション違反や OOM(メモリ不足)でクラッシュしても、あるいは未知の脆弱性によって一時的に制御を奪われたとしても、物理的に分離された Control Plane (Face) のメモリ空間やポリシー評価ロジックまで汚染することは極めて困難です。
一蓮托生で沈没することを防ぎ、常に「執行部での異常」を IDE 側に正確に報告できる状態を維持する。审議と執行を分けることは、単なる安定性のためだけでなく、システムの「脳」を「汚れた現場」から隔離するための、文字通りの**特権分離(Privilege Separation / PrivSep)**なのです。
これは、OpenSSH などのセキュアなソフトウェアや、Google Chrome のサンドボックス構造(Sandbox Architecture) ―― UI を司るブラウザプロセスと、危険なコンテンツを処理するレンダラプロセスを隔離する設計 ―― と根本を同じくする、枯れた、しかし強力な防御手法であり、実行プロキシ (Execution Proxy) としての堅牢性を担保します。
第2章:Control Plane / Execution Plane の cold な契約
第3章:実装の深淵 — 心中(Shinju)プロトコルの要諦
一方が倒れれば共倒れすることで、世界を汚さない。これが計算機科学においてフェイト・シェアリング(Fate Sharing / 運命共有) と呼ばれる、心中(Shinju)の美学です。
依存するリソースを道連れに自決させることで、孤児プロセス(Orphan Process) の発生を物理的に防ぎます。
1. Node.js (Control Plane) の「執拗な」引導
Node.js 側の terminate 実装は、相手が死ぬまで見届ける執拗さが必要です。
async terminate(): Promise<void> {
if (!this.process) return;
this.process.kill('SIGTERM');
const exitPromise = new Promise(resolve => this.process!.on('exit', resolve));
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve('timeout'), 5000));
const result = await Promise.race([exitPromise, timeoutPromise]);
if (result === 'timeout') {
this.process.kill('SIGKILL');
}
}
2. Rust (Execution Plane) の自爆装置と LSP 方式の管理
Rust 側では、親との通信路(Stdin)が断たれた瞬間を決定論的に検知します。これは VS Code と Language Server 間の通信でも採用されている LSP 方式のライフサイクル管理(Stdin EOF Monitoring) という手法です。
// Stdin の EOF を監視
let mut reader = BufReader::new(tokio::io::stdin()).lines();
loop {
match tokio::time::timeout(Duration::from_secs(3600), reader.next_line()).await {
Ok(Ok(None)) | Ok(Err(_)) => {
// 親とのパイプが切れた。心中プロトコル(運命共有)発動
eprintln!("Control Plane disconnected. Shinju...");
let _ = killpg(pgid, Signal::SIGTERM); // 子プロセス群を一掃
std::process::exit(0);
}
}
}
第4章:観測性とトレードオフ — 境界防御の保険料
[!NOTE]
Kubernetes 注意: Kubernetes や systemd の自動再起動ポリシーは心中プロトコルと衝突する可能性があります。クラスタ運用時はrestartPolicyを見直す、Pod 起動時に randomized backoff を入れる、あるいは外部の Leader Election を検討してください。
心中プロトコルは、ゾンビプロセスを防ぐための「強すぎる正義」です。この「泥臭さ」を選択するには、不都合な真実を受け入れる覚悟が求められます。
- 長時間ジョブの強制死: ホストが瞬断すれば、99% 終わっていたビルドも道連れに死にます。
- デバッグの迷宮: 双方が消滅するため、死因の特定には完璧なロギング分離が必要です。
- オーケストレーターとの衝突: K8s や systemd の下では、心中が無限再起動ループを招くことがあるため、バックオフ設定等の「運用の手間」が保険料となります。
推奨メトリクス(プロセスライフサイクル中心)
-
process_start_total: 起動回数 -
process_stop_total: 終了回数 -
signal_sent_total{signal="SIGTERM"}: 優雅な終了 -
signal_sent_total{signal="SIGKILL"}: 強制的死 -
cleanup_failure_total: 掃除失敗回数 -
last_killed_pid: 最後に排除したPID
[!WARNING]
SIGKILL 副作用注意: SIGKILL は書き込み中のファイル破損や DB トランザクションの不整合を招く可能性がある劇薬です。可能な限り SIGTERM による猶予期間を設け、SIGKILL は最終手段として扱ってください。
第5章:泥臭い「故障注入」こそが、真の堅牢性を証明する
型システムだけではこの境界は守れません。物理的に叩いて壊す「不便な検証」が必要です。
-
Stdin 遮断:
tail -f /dev/null | binとして起動し、tailを kill して Guardian が心中するか。 -
OOM 試験:
cgcreate / cgexecでメモリを 100M に絞り、執行層の死が顔レイヤーにどう伝播するかを観測する。
DAD (Design-Action-Decision) 要約
| 観点 | 泥臭い決断 |
|---|---|
| Design (設計) | 審議 (Node.js) と執行 (Rust) を特権分離 (PrivSep) する。型の共有という「甘え」を捨てる。 |
| Action (行動) | パイプの EOF を決定論的自爆スイッチ(LSP 方式)として扱い、運命共有 (Fate Sharing) を強制する。 |
| Decision (決定) | ゾンビプロセスの生存を一切許容しない「一蓮托生の破滅(心中)」を選択し、状態復旧はホスト側の規約による自動再試行に委ねる。 |
まとめ
「完璧なコード」など存在しません。あるのは「確実な後始末」だけです。
この二層化と心中プロトコルは、不確実な AI の出力を現実の世界で安全に動かすための、実用的な「答え」の一つです。システム設計において、効率という甘い罠を捨て、あえて不便で泥臭い「境界防御」を検討する価値は十分にあります。
予告:二層化が招いた「唯一性」の危機
「二層に分ける」ことで手に入れた堅牢性。しかし、これによって新たな、そして極めて厄介な問題が浮上しました。
AIエージェントとの対話において、裏側で動く執行プロセスは常に「唯一無二(シングルトン)」でなければなりません。プロセスが分裂すれば、AIは「さっき作ったファイルが見つからない」と混乱し、ハルシネーション(幻覚)の深淵へと沈んでいきます。
異言語・異プロセス間で、どうやって「絶対に一人しかいない」ことを保証するか?
続編MCPサーバーにおける『唯一性』の再考 —— 状態を安全に継承する「Highlander Protocol」の設計
を公開しました。
