【第2部】サーバーにCLIを上げるAI実行基盤の設計

# 【第2部】サーバーにCLIを上げるAI実行基盤の設計
> 連載「Slack AIエージェント基盤を作る」全8部の第2部。
第1部では、Slackを単なるチャットではなく業務OSとして見る話をした。では、その裏側で実際に仕事をする実行環境はどこに置くのか。第2部の主役は、ローカルで使っていたCLIエージェントをサーバー上の共通実行基 盤へ上げる設計だ。ここができると、Slackの依頼が個人の端末に依存しなくなる。
前後で読む
- 前回: 第1部 Slackを業務OSにする
- 次回: 第3部 並行実装を回す
AI開発ツールは、最初はローカルのCLIとして使われることが多い。手元のターミナルでコードを読ませ、修正させ、テストを回す。個人の作業ではそれで十分だ。
ところがSlackからチーム全員が依頼できる形にしようとすると、ローカルCLIのままでは限界が見える。誰の端末で動かすのか。認証情報はどこに置くのか。実行中に端末を閉じたらどうするのか。成果物を誰が受け取るのか。
この設計の面白いところは、CLIエージェントをサーバー上の実行基盤として扱っている点にある。Slackで受けた依頼をジョブに変換し、サーバー側でCLIを起動し、実行結果をSlackへ戻す。人間がターミナルを開いていた作業を、共有インフラへ移している。
CLIエージェント実行基盤が必要になる理由
CLIエージェント実行基盤の役割は、AIにコードを書かせることだけでは ない。むしろ中心は、実行を安全に継続できる状態にすることだ。
ローカルCLIは、作業者のコンテキストに強く依存する。SSH設定、認証情報、作業ディレクトリ、エディタの状態、Gitの差分、環境変数。これらが個人の端末に散らばると、Slackから依頼されたジョブを再現できない。
サーバー実行にすると、次のような利点が出る。
- 実行環境をチームで揃えられる
- 長時間タスクを端末に依存せず継続できる
- Slackのスレッドとジョブ状態を対応づけられる
- ワーカー数を制限して負荷を管理できる
- 成果物やログを一定のルールで保存できる
ここで注意したいのは、CLIをサーバーに置けば終わりではないという点だ。CLIは人間向けの対話ツールとして作られていることが多い。サーバーで動かすなら、標準入力、標準出力、終了コード、タイムアウト、再試行、キャンセルを扱う薄い制御層が必要になる。
サーバー側で守るべき境界
AIエージェントに作業を任せると、便利さと同時に権限の問題が出てくる。Slackから「調べて」と頼んだだけで、本番データや秘密情報へ触れてよいわけではない。
サーバー側では、少なくとも次の境界を持たせる。
- プロジェ クト単位の作業領域
- 実行してよいコマンドの範囲
- 読み込んでよいファイルの範囲
- 書き込んでよい成果物の保存先
- 外部通信を許可する条件
- 認証情報を参照できるプロセスの範囲
共有する設計情報には、環境変数名や保存先パスを直接書かない。代わりに「認証情報はサーバー側の安全な設定として注入する」「ジョブごとに作業領域を分ける」と抽象化する。技術の肝は伝えつつ、攻撃者がそのまま使える情報を渡さないためだ。
OpenAIのコード生成ガイドでも、コード作業ではモデルに渡すコンテキストと実行環境の設計が結果に影響する。CLIエージェント実行基盤でも同じで、モデル選定より先に、どの情報を渡し、どの操作を許すかを決める必要がある。
SlackからCLIへ渡すジョブモデル
Slackからの1メッセージを、そのままCLIへ投げると運用が荒れる。実行基盤では、Slackイベントを次のようなジョブモデルへ変換する。
- 依頼者
- Slackのチャンネルとスレッド
- 対象プロジェクト
- タスク本文
- 希望する出力形式
- 実行モード
- タイムアウト
- 成果物の保存先
この形にすると、CLIの種類が変わっても 上位のSlack連携を保ちやすい。あるタスクはコードに強いCLIへ、別のタスクは文章生成に強いCLIへ渡す。Slack側は同じ操作感のまま、裏側の実行エンジンを差し替えられる。
Slack Bolt for JavaScriptは、イベント受信と返信処理を組み立てる土台になる。ただし、Boltが提供するのは入り口だ。ジョブ管理、ワーカー管理、成果物管理はアプリケーション側で持つ必要がある。
長時間タスクの扱い方
CLIエージェントは、短い質問より長い作業で価値を出す。コード調査、複数ファイルの修正、テスト、資料生成、差分レビューは数分から数十分かかる。Slackの返信待ちだけで処理すると、途中経過が見えず、ユーザーは不安になる。
そこで実行基盤は、ジョブの状態をSlackへ段階的に返す。
- 受理した
- 実行キューに入った
- ワーカーに割り当てた
- 調査中
- 生成中
- 確認中
- 完了した
- 失敗した
状態を細かく出しすぎるとノイズになる。逆に何も出さないと使われなくなる。実務では、ユーザーが「止まっていない」と分かる粒度にするのがちょうどよい。定期的なハートビート通知と、完了時の成果物リンクを分けると扱いやすい。
実行基盤で詰まるのはモデルではなく周辺依存
CLI agentをサーバーに載せる話では、ついモデルやCLIの性能に目が行く。だが実運用で先に詰まるのは周辺依存だ。ブラウザを表示できない。PDFを画像化できない。日本語フォントが崩れる。CLI認証がローテーションで壊れる。長時間タスクのログを追えない。
サーバー上のAI実行環境は、アプリケーションサーバーというより業務用の小さなワークステーションに近い。ファイル操作、レンダリング、ブラウザ、GitHub操作、成果物保存、認証状態の保全まで含めて初めて、Slackから任せられる実行基盤になる。
設計判断としては、依存が無いまま起動しない、認証ファイルを起動時に上書きしない、workerごとにhomeを分ける、外部送信を含む処理は承認境界を通す。この4つが最低ラインになる。
サーバー上のCLI agentは「プロセス」ではなく「実行単位」として扱う
CLIをサーバーで動かすとき、単に`child_process.spawn()`で起動するだけでは足りない。エンタープライズで運用するなら、CLI agentはOSプロセスである前に、監査可能な実行単位として扱う必要がある。
最低限、ジョブには次の情報を持たせる。
{
"jobId": "job_...",
"slack": {
"channel": "masked-channel",
"threadTs": "masked-thread",
"replyTo": "thread"
},
"actor": {
"userKey": "hashed-user",
"profile": "corporate"
},
"runtime": {
"backend": "codex-or-claude-compatible-cli",
"mode": "normal-agent",
"timeoutMs": 1800000,
"allowNetwork": true
},
"workspace": {
"projectKey": "masked-project",
"workdir": "isolated-session-dir"
},
"policy": {
"writeScope": "workspace-only",
"requiresApprovalForExternalSend": true
}
}この形にしておくと、Slackの1メッセージ、CLIプロセス、成果物、ログ、キャンセル要求を同じIDで追える。後から「誰が何を頼んだのか」「どのCLIがどこまで実行したのか」「外部送信の手前で止まったのか」を確認できる。
spawn、stream、cancelを分けて設計する
サーバー上のCLI agentで一番事故りやすいのは、長時間実行中の状態管理だ。CLIは数十秒で終わることもあれば、資料生成やコード修正で数十分動くこともある。その間、Slackには進捗を返し、ユーザーがキャンセルしたら実際のOSプロセスも止めなければならない。
ここで「LLMにキャンセルしてと伝える」だけでは弱い。実際には、子プロセスのPID、終了シグナル、タイムアウト、終了後のロック解放まで持つ必要がある。コンテナ再起動やプロセスkillが起きても、残った状態を復旧できるようにしておく。
互換CLIに寄せる理由
既存スキルが特定CLIを前提にしている場合、全部を書き換えるのは現実的ではない。そこで、`claude`相当の呼び出しを互換ラッパで受け、裏側でCodex系CLIにも流せるようにする。入力引数、stream形式、終了コード、成果物の扱いを揃えることで、既存スキル資産を壊さずにバックエンドを差し替えられ る。
これは単なる移行テクニックではない。AIエージェントの実行基盤を作るなら、モデルやCLIは変わる前提で抽象化しておくべきだ。プロダクトの寿命より、CLIの流行のほうが短いからだ。
起動時のfail-fastとruntime同期
サーバー上でCLI agentを動かすなら、起動できたことと、実務に耐えることは別物だ。実装では、Dockerfileとentrypointの責務を分けて考える。
Dockerfile側では、LLM CLI、ブラウザ自動化、LibreOffice、PDF変換、日本語フォント、GitHub CLI、検索系CLIを入れる。entrypoint側では、永続volumeの初期化、profile別ファイルの同期、Bot専用CLI homeの準備、共有スキルの反映、必要なバイナリのhealth checkを行う。
特に重要なのは、失敗を遅らせないことだ。PPTX生成を受けてからLibreOfficeが無いと分かる、PDF確認の段階でPopplerが無いと分かる、ブラウザ操作時にXvfbが起きていないと分かる。この手の失敗は、ユーザーから見るとAIが途中で壊れたように見える。だから起動時に落とす。
実運用では、volumeの絶対パスや機密値の名前を共有ドキュメントへ出さない。それでも設計上のポイントは明確だ。CLI認証は上書きしない。workerごとにhomeを分ける。レンダリングやブラウザの依存は起動時に検査する。これ で、Slackからの長時間タスクを途中で落としにくくなる。
関連記事
連載「Slack AIエージェント基盤を作る」
- 第1部 Slackを業務OSにする
- 第2部 CLIをサーバーに上げる
- 第3部 並行実装を回す
- 第4部 Slack発言をジョブ化する
- 第5部 意図理解で振り分ける
- 第6部 記憶を設計する
- 第7部 定期実行で運用する
- 第8部 安全性と観測性を固める
- まとめ Slack AIエージェント基盤の全体設計



