AIに動画を作らせる 開けた口は3つだけ

Mac miniに常駐させたエージェントに、Windows機(RTX 5070 Ti)の動画生成を頼めるようにした。頼めるのは3つの操作だけで、動画の解像度もモデルもエージェントには選ばせない。
結果から書く。**MCP経由で軽量設定の動画が1本、約200秒で出た。**1.63秒、H.264、80KB。実機で確かめたのはWindows機の中(ループバック)までで、miniからLAN越しに届くかは、まだ確かめていない。
9月24日から25日にかけての作業で、コミットは #1733 になる。
生の口を、そのまま開けなかった
最初に考えたのは、動画生成アプリ(video-gen)のポートをLANへ出す案だ。やめた。
video-gen/next.config.mjsは/comfyui/:path*をhttp://localhost:8188/:path*へそのまま転送している。ポートを開けると、ComfyUI全体(任意のワークフローの投入を含む)が見えるopen-folder、pick-folder、upload-image、load-projectのような、Windows側のフォルダやGUIに触れる口がある- これらのどれにも認証が無い
考えた脅威は、LANの他人ではない。エージェント自身だ。外部の文章に仕込まれた指示を、ローカルの小型モデルが真に受ける。LANが自分だけでも、この経路は残る。
並べた案と判断は次のとおり。
| 案 | 判断 |
|---|---|
| ComfyUIをLANへ公開する | 却下。認証が無く、任意のワークフローが通る |
| video-genのポートをLANへ公開する | 却下。上の転送と、任意パス・GUIの口がある |
| 口を3本だけの中継にする | 却下。注文しかできず、工程を操作できない |
| video-genをopenclawのコンテナへ入れる | 却下。ComfyUIと同じPCのフォルダを読み書きしていて、別のマシンでは動かない |
| Windows側にMCPサーバーを置き、高レベルなツールだけ公開する | 採用 |
開けた口は3つ
ツールはget_status(状態を見る)、generate_shots(注文する)、get_job(結果を見る)の3つ。注文の入力は、次のように絞った。
shotsは1〜3件、各1〜1000文字、英語で書くpresetはlightかstandardseedは任意の整数
**解像度、長さ、ステップ数、モデル、負プロンプト、開始画像は受け取らない。**プリセットで固定してある。受け取らない項目を増やすほど、エージェントが間違った指示や仕込まれた指示に従っても、起きることが小さくなる。
ジョブは直列に処理する。待ちが3件を超えたら断り、空きメモリが閾値を下回っているときも断る。
受け取りで詰まった
動画は/filesから取る。最初は、ここにもBearerトークンを必須にしていた。すぐに気づいた。エージェントがcurlで動画を保存するには、**シェルにトークンが要る。**設計書の脅威モデルは、コンテナの中で動くものは環境変数を読める前提で書いてある。親トークンをシェルへ渡す形は、それと食い違う。
そこで、get_jobが「ジョブ・ショット・期限」に紐づく署名付きURLを返す形にした。有効期限は15分で、切れたらget_jobを呼び直す。シェルに見えるのはそのURLだけで、親トークンはMCPクライアント(ゲートウェイ)側にしか置かない。

作りは署名付きURLで鍵を隠すにまとめた。今日、テストを動かして確かめたことだけ書く。
- テスト51件が0.37秒で通った(GPU不要。生成器は偽物に差し替え。後述の修正で53件になる)
- 署名の書き換え、期限だけの延長、期限切れ、別ショットへの付け替え、別ジョブへの付け替え、署名なし。6通りとも401
- 期限の秒ちょうどは有効で、その1秒後から無効
- 署名付きURLで
/mcpには入れない
動いた設定は1つだけ
プリセットはSulphur2の量子化版(Q5)に固定した。ltx2は、非量子化のGemma(27GB)を読み込む段階でこのマシンのメモリが逼迫し、直接生成でも事前エンコード方式でも生成できなかった。ただし、「ltx2が不可」とは断定していない。過去に事前エンコード方式で動いた記録がある。
9月24日に、UIからSulphur2で768x512・153フレーム・20ステップが通った(エンコード約12秒、生成約376秒)。これをstandardとし、軽くした設定をlightにした。
| light | standard | |
|---|---|---|
| 解像度 | 512x320 | 768x512 |
| フレーム数 | 49(約1.6秒) | 153(約5.1秒) |
| 実測 | 約200秒(MCP経由、9/25) | 376秒(UI実行、9/24) |
lightの約200秒は、ComfyUIを再起動した直後の初回で、サンプリング約156秒を含む。実行中の空きメモリは約15GBまで、仮想メモリの空きは約31GBまで下がった。停止のしきい値は8GBなので、余裕はあった。
ComfyUIが消えて、ログが白紙だった
ltx2を試していた9月24日、ComfyUIが数分で消えた。自作のメモリ監視(仮想メモリの空きが8GB未満で止める)が働いた可能性が高い。「可能性が高い」としか言えないのは、その監視のログが出なかったからだ。
Microsoft Learnの文字コードのページに、こう書いてある。BOMなしのUTF-8のスクリプトは、Windows PowerShellが古い「ANSI」コードページとして読み違える。日本語のコメントを1行目に置いていたスクリプトが、これに当たった。決定ログの結論は、日本語コメントが次の行(ログの出力先を決める代入)を巻き込んだ、というものだ。スクリプトはASCIIだけに書き直した。

大事なのは、保護が働いたことより、働いた記録が無かったことだ。先日書いたガードが素通りする書き方と同じ型で、止める側が壊れていても、止まらないだけで表には何も出ない。
検証で見つけた抜け
記事の裏取りのため、MCPの仕様(Streamable HTTP)を原文で読み直した。セキュリティの節は、サーバーに次を求めている。
- すべての接続で
Originヘッダーを検証する(MUST) - ローカルで動かすときはlocalhostにだけ束縛する(
SHOULD) - 認証を実装する(
SHOULD)
作ったサーバーは、束縛(既定は127.0.0.1)と認証(Bearer)は満たしている。Originは、ソースを検索した限り見ていなかった。/mcpはBearer必須なので悪用の道は狭いが、仕様のMUSTは満たしていなかった。
その場で直した。Origin付きの要求は既定で403にし、許可する送信元だけ環境変数で足す。curlやMCPクライアントはOriginを付けないので、今までどおり通る。判定は認証より前に置いた。
直したあとに、修正を外して新しいテストが落ちることを確かめた。テストが素通りしていないかの確認だ。テストは53件が通り、修正を外すと、新しく足した結合テスト1件だけが落ちた。
今日の教訓
- **エージェントに渡す口は、先に「渡さない一覧」を書く。**渡せるものは、その残りから選ぶ
- **選ばせる項目を減らす。**解像度もモデルも固定して、入力は「何を撮るか」だけにした
- **受け取りの口が、鍵を渡す穴になりやすい。**期限と対象を絞った札を出す
- 止める側は、止まった記録が残ることまで確かめる
まだ確かめていないことも残る。miniのコンテナからWindows機のMCPへLAN越しに届くか、openclaw側のMCP登録、台本を書くモデルの選定(今は推定で選んだだけで、出来を比べていない)。次はここを実機で通す。
関連
- 署名付きURLで鍵を隠す — 署名の作り、検証の順番、悪用6通りの実測、残る弱点
- 鍵を渡さないpush設計 — 同じ考え方をGitHubの鍵に使った例
- 前の日はAIに鍵を渡さず、合図1枚でmainへ。このエージェントが実装を任される先は、ちょうぼっちやValscopeなどを含むこのリポジトリだ。