Skip to content

画面なき「関所」の死の沈黙:ヘッドレスAIサーバーが『ユーザーの許可』を待ち続けてフリーズする罠

1. 概要

CLI(対話型インターフェース)のツールとして開発されたシステムを、WebのREST APIやバックグラウンドプロセスとして「サーバー化(ヘッドレス化)」する際、UI(画面表示)とサーバーロジックの結合度に重大なバグ(死角)が潜んでいることがある。 セキュリティの観点から「危険な操作を検知した際にユーザーにポップアップや対話で許可(Yes/No)を求める」という対話型ガードレールは、画面が存在しないヘッドレスサーバー環境においては、誰にも表示されない「許可の返答」をシステムが永久に待ち続けるという、イベントループの「死の沈黙(フリーズ)」を引き起こす。 本稿では、AIエージェントサーバーであるOpenCodeで発生した、ヘッドレス時のパーミッション待ちスタックバグ(anomalyco/opencode#36804)を事例に、対話型UIと非対話型APIサーバーの設計のねじれが招く可用性の罠について分析する。


2. インシデントの分析:REST APIが永久に「busy」になるゾンビセッション

2.1. 現象:複数コマンド送信時の不可解なフリーズ

OpenCodeをバックグラウンドで動かすための opencode serve(REST APIサーバーモード)において、API経由でエージェントに命令を投げた際、特定のコマンド(例: /tmp 以下にディレクトリを作成する mkdir -p /tmp/zt-1/a)が含まれていると、セッションのステータスが永久に busy(処理中)のままフリーズするバグが発生した。

この時、サーバーの背後で動くLLM(AI)の生成処理はとっくに終わっており、CPUやネットワークのソケット、子プロセス(mkdir)すら物理的に一切起動していない状態だった。 外から見る限りでは、「非同期処理のイベントループのどこかで、コマンド送信のイベントがサイレントにドロップし、システムがバグで応答待ちのままハングアップした」ように見えた。

2.2. 真相の発見:誰も答えない「Yes/No」のパーミッションダイアログ

当初、開発者は「ローカルLLMの出力速度が速すぎるために発生した非同期スレッドの競合(Race Condition)」を疑ったが、調査を進めるうちに衝撃の真相に辿り着いた。

  1. セキュリティポリシーのトリガー: OpenCodeには、AIがプロジェクトフォルダの外(例: /tmp などの外部ディレクトリ)にファイルを書き出そうとした際、処理を一時停止し、ユーザーに「このフォルダへのアクセスを許可しますか? [Yes/No]」と問いかける安全装置(Ask Permission)が備わっている。
  2. 画面なきサーバーでの「ブロック」: CLIやデスクトップGUIとして動かしている時は、画面上に確認ダイアログが表示され、人間がキーボードを叩いて答えれば処理が再開する。 しかし、opencode serve は画面のない「ヘッドレスなAPIサーバー」として動いているため、確認ダイアログを表示する端末(TUI)がどこにも存在しなかった
  3. 未回答リクエストの渋滞: システム内部の処理は、画面のない暗闇の中で「ユーザーがYes/Noを押してくれる」ことをいつまでも忠実に待ち続け、処理をサスペンド(一時停止)したまま座り込んでいた。 APIで GET /permission という隠されたエンドポイントを調査した結果、裏側で 誰にも見えない「パーミッション承認待ちリクエスト」が何重にも渋滞を起こして溜まっている ことが確認された。

3. 実体験:デスクトップ用アプリをサーバー化した際に入る「死の無限待ち」

CLI/TUI用ユーティリティやデスクトップ用のライブラリを、Dockerコンテナに詰め込んでAPIサーバーやバッチプログラムとして動かそうとした際、この「画面入力を待ち続けてハングする」現象はバックエンドエンジニアが頻繁に踏み抜く地雷である。

  • 実体験エピソード: PDFファイルや画像を自動で変換・加工して保存するサーバーサイドの連携バッチを開発していた。 変換処理のエンジンとして、Linux用のオープンソースのグラフィックツールをコマンドライン(subprocess)経由で実行させていた。 通常は順調にバッチが回っていたが、ある日、壊れた(破損した)画像ファイルがバッチに流れてきた瞬間、処理が永久に完了しなくなってサーバーがハングアップした。 原因を特定するためにローカル環境でそのツールを画面(X11)付きで動かしたところ、破損した画像を読み込んだ瞬間にツールが 「このファイルは破損しています。修復を試みますか? [OK/キャンセル]」というグラフィカルなポップアップ画面を出して停止していた。 サーバー環境ではX11(画面)がないため、このポップアップが「透明」な状態でメモリ上に生成され、誰もクリックできない [OK] ボタンが押されるのを、プログラムが無限に待ち続けていたのである。 「対話的確認」を前提とするプログラムは、非対話型環境(サーバー)に持ち込んだ瞬間、最悪のフリーズバグに変貌する。

4. 解決策と「ヘッドレス・ポリシー」の設計原則

この「死の沈黙」を防ぐためには、システムが「自分が今、対話可能な環境(Interactive)にいるか、それとも画面なきサーバー環境(Headless)にいるか」を識別し、以下の設計を徹底しなければならない。

4.1. ヘッドレス時の「即時拒否(Fail-Closed)」へのポリシー切り替え

システムが API サーバーやバッチモード(Headless)で起動された場合、「ユーザーに確認する(Ask)」という中間状態(Suspended)そのものを無効化する。 確認が必要な危険な操作を検知した際は、確認画面を出そうとせず、即座に「エラー(拒否 / 403 Forbidden)」を返して処理を中断させる(Fail-Closedの原則)

4.2. API駆動型パーミッション・フロー(疎結合な認可)

もしサーバーモードでもユーザーの動的許可を必要とする場合は、システムをフリーズ(同期ブロック)させて待つのではなく、

  • 「リクエストを一度 202 (Accepted/Pending) などの非同期ステータスで即座に応答を完了させて切り離す」
  • クライアントはポーリングやWebhookでその認可要求を受け取り、Web UI等を介して非同期で許可APIを叩いてリクエストを「再開」させる。

という、HTTPのセマンティクスに則った疎結合な設計に切り替えなければならない。


5. 結論

AIエージェントの安全を守るための「許可(パーミッション)ゲート」は、CLIにおいてはスマートな防御壁となるが、ヘッドレスサーバーにおいてはシステムを窒息させる凶器となる。 アプリケーションとUIの文脈は常に分離されなければならず、サーバーインフラの設計者は、「画面がない環境では、ユーザー対話(Ask)を一切排除し、エラーとして即時拒否するか非同期で処理を完了させる」 という 「ヘッドレス・Fail-Closedポリシー」 をシステム全体に徹底させなければならない。


6. 参考文献 / 一次情報

Shiori pointer-RAG automation experiment.