# ネットワーク越しの Pod 協働(将来設計ノート) 本ドキュメントは将来の設計方針を記録するものであり、実装チケットではない。 --- ## 動機 ローカルの workspace が Pod 間の発見・通知・scope 会計を提供するが、 これは 1 台のマシンに閉じている。複数マシンにまたがるプロジェクト (モノレポの異なる部分を別マシンで触る、CI マシンの Pod と開発マシンの Pod が連携する等)では、マシン間のメッセージングが必要になる。 ## 設計原則 - **中央サーバーを作らない**。各マシンが主権を持ち、P2P でやり取りする - **ローカル workspace を歪めない**。ネットワーク層は workspace の上に 乗る「他の workspace への郵便」であり、workspace 自体を分散化しない - **ファイルシステムはマシンローカル**。cross-host の scope 分譲は 行わない。協働はメッセージベースのタスク委譲で完結する - **SSH を transport に使う**。開発者の手元に既にあるインフラで、 鍵管理・暗号化・認証が追加投資なしで手に入る ## アドレッシング Pod のネットワークアドレスは `yoi.pod-name@host` の形式を**論理的な 宛先表記**として使う。実際の SSH 接続がこの文字列そのままで行えるかは transport 方式に依存する(後述)。 - `yoi` = SSH ユーザー名(固定) - `host` = 相手マシンのホスト名 or IP - `pod-name` = 送信先の Pod 名(相手マシン上のローカル workspace 内で一意) 推奨構文は **`yoi@host:pod-name`**(git 方式)。 詳細は後述の「アドレッシング構文」を参照。 ## アドレッシング構文 論理的な宛先表記として **`yoi@host:pod-name`** を推奨する。 git の `git@github.com:user/repo` と同じ構文で: - SSH ユーザーは `yoi` 固定(動的ユーザー名が不要) - `:` 以降がルーティング情報(Pod 名) - クライアント側が `yoi@host:pod-name` をパースし、 `ssh yoi@host "yoi-route pod-name"` に変換する git がこの方式で `git-upload-pack user/repo` にルーティングしている のと同じ仕組み。OS レベルの設定(NSS モジュール等)が一切不要で、 ユーザー 1 つ + ForceCommand(またはシェルスクリプト)だけで動く。 ## SSH transport の選択肢 ### A. 単一ユーザー + コマンド引数 ``` ssh yoi@host send pod-name "message" ``` - 相手マシンにシステムユーザー `yoi` を 1 つ作る - `authorized_keys` に接続元 Pod の公開鍵を登録 - ForceCommand または shell スクリプトが第一引数 (`send`) と 第二引数 (`pod-name`) を解釈してローカル workspace のレジストリから Pod の socket を引き、メッセージをルーティング - **導入コスト最低**。ユーザー 1 つ + スクリプト 1 つで動く - 宛先が引数に入るので `yoi.pod-name@host` の見た目にはならない ### B. 鍵ベースルーティング(gitolite 方式) ``` ssh yoi@host # 使った鍵でどの Pod 宛か判別 ``` - `~yoi/.ssh/authorized_keys` に Pod ごとのエントリ: ``` command="yoi-route pod-a",no-port-forwarding,... ssh-ed25519 AAAA... pod-a@remote command="yoi-route pod-b",no-port-forwarding,... ssh-ed25519 AAAA... pod-b@remote ``` - SSH 接続時に使われた鍵が `command=` で指定されたルーティング先を決定 - gitolite / Gitea / Gogs で実証済みのパターン - 接続元は `ssh yoi@host` だけ。**鍵が宛先を決める** - クライアント側 SSH config で alias を作れば見た目を整えられる: ``` Host pod-a.host-b HostName host-b User yoi IdentityFile ~/.config/yoi/keys/pod-a ``` - 鍵の登録が相互に必要(Pod A が Pod B に送るなら、B のマシンの authorized_keys に A の公開鍵 + route 先を登録) ### C. 動的ユーザー名 ``` ssh yoi.pod-name@host ``` - `yoi.pod-name` を OS レベルで有効なユーザー名として解決する: - NSS (Name Service Switch) モジュールを書くか `libnss-extrausers` を利用 - PAM モジュールで認証をフック - `sshd_config` で `Match User yoi.*` → `ForceCommand` でルーティング - **最も直感的なアドレッシング**だが OS レベルの設定が必要 - yoi をインストールするだけでは動かない(管理者権限での設定が要る) - コンテナ環境ではやりやすい(ユーザー管理を自由にできる) ### 推奨 **MVP は A(単一ユーザー + コマンド引数)** から始める。設定が最小で 動くものが作れる。将来 B(鍵ベースルーティング)に進化させると セキュリティが強化される(Pod 単位での接続制御が可能)。 C は UX は最高だが導入コストが高く、必要が明確になるまで見送る。 ## ファイルシステムの境界 Pod はローカルのファイルシステムだけを操作する。ネットワーク越しの Pod 協働では: - **scope はマシンローカルに閉じる**。host_a の `/src` と host_b の `/src` は別物。cross-host の scope 分譲は行わない - **協働はタスク委譲**。「このリポジトリでこの変更をして結果を教えて」 というメッセージで、相手の Pod がローカルに実行する - **sshfs / NFS 等のネットワークファイルシステムは使わない**。 透過的なリモートファイルアクセスは壊れやすく、scope モデルと相性が悪い ## ローカル workspace との関係 | 層 | スコープ | 役割 | |---|---|---| | Workspace | 1 台のマシン | Pod 発見 / scope 会計 / 通知バス | | Network peers | マシン間 | メッセージング / タスク委譲 / 結果通知 | - Network peers は workspace の**上に**乗る。workspace を置き換えない - ローカルの Pod 間通信は引き続き workspace の Unix socket バスを使う - リモートの Pod への通信だけ SSH transport を経由する - Pod から見ると「ローカル peer に送る」と「リモート peer に送る」は 同じ API で、transport 層が切り替わるだけ(が理想) ## broadcast の扱い ローカル workspace は Unix socket で 1 本の bus を持てたが、 ネットワーク越しには共有 bus が無い。 - 各マシン(または各 Pod)が **known-peers リスト** を持つ - broadcast = known-peers を iterate して個別送信 - 規模が数十 Pod なら十分実用的 - 将来的に gossip protocol で peer 発見を自動化できるが、 MVP では手動登録(`yoi peer add pod-a@host-b`)で十分 ## 受信側のルーティング SSH 接続を受けた側が、宛先 Pod のローカル socket にルーティングする 仕組みが必要。 ``` [SSH 接続] → yoi-route ↓ workspace registry を参照 ↓ /run/yoi/.../pod-name.sock に転送 ↓ Pod が受信・処理 ``` `yoi-route` は: 1. workspace のレジストリを読んで pod-name の socket path を引く 2. socket に接続してメッセージを中継 3. 応答を SSH 接続に返す workspace が複数ある場合のルーティング(どの workspace の pod-name か)は追加の設計が必要。Pod 名がマシン上で globally unique であれば workspace を指定しなくて済む。 ## Daemon-less リモート Pod 生成(SSH-only モデル) リモートホスト上の Pod 生成は **daemon 無しで SSH だけで成立する**。 remote 側に必要なのは `yoi` バイナリと SSH アクセスのみ。 ### 前提 - yoi は環境再現(git clone, コンテナ構築等)を自身の責務としない。 作業対象のファイルがリモートに既にあるか、ユーザーが任意の手段で 用意する前提(git clone, rsync, 手動配置、CI の checkout 等) - yoi が転送するのは**セッション(会話履歴)と manifest overlay** だけ。コードベースの同期は外部に委ねる - コンテナ内で動かすか bare metal で動かすかも yoi は問わない。 `yoi` バイナリが動くホストの fs 上で活動する主体がある、 それだけが前提 ### フロー ``` host_a (spawner) host_b (remote) Pod A (pod binary + ssh のみ) │ ├── ssh: session データを転送 ────────→ ファイル書き込み ├── ssh: profile / one-file manifest 入力を転送 ─→ 必要ならファイル書き込み ├── ssh: `yoi pod --profile ... &` ───────→ Pod プロセス起動、socket 作成 ├── ssh -L: socket を tunnel ─────────→ Pod B の unix socket │ └── localhost:tunnel に接続 ──────────→ Method::Run / Event stream (以降はローカル Pod と同じ protocol) ``` ### コマンドイメージ ```bash # 1. session + profile/manifest input を転送 ssh yoi@host-b "mkdir -p ~/workspaces/task-123/store" tar cz session/ | ssh yoi@host-b "tar xz -C ~/workspaces/task-123/store" scp profile.lua yoi@host-b:~/workspaces/task-123/profile.lua # 2. Pod を起動(detach) ssh yoi@host-b "yoi pod --store ~/workspaces/task-123/store \ --profile ~/workspaces/task-123/profile.lua &" # 3. socket を tunnel で引っ張る ssh -L /tmp/pod-b.sock:/run/yoi/task-123/pod.sock yoi@host-b # 4. あとは /tmp/pod-b.sock にローカルと同じ protocol で繋ぐ ``` spawner の `SpawnPod` ツールがこの一連を内部で実行する。LLM から 見たら「ツールを呼んだら Pod ができた」だけ。 ### なぜこれで足りるか - **protocol は変わらない**: SSH tunnel の向こう側は普通の Pod socket。 ローカルの Pod と同じ `Method` / `Event` でやり取りする - **scope は host ごとに独立**: cross-host の scope 分譲はそもそも 成立しないので、workspace の scope 会計は remote には関係しない - **通知**: SSH tunnel が繋がっている限り `Event` stream がそのまま 流れる。tunnel が切れたら再接続する - **環境構築は yoi の責務外**: git clone するか rsync するかは Pod の instruction で指示するか、事前に用意されている前提 ### daemon が必要になるケース SSH-only モデルの制約が、daemon 導入の動機になる: - **Pod 一覧の取得**: remote の runtime_dir を SSH 越しに `ls` する 必要がある(daemon がいればレジストリ API で済む) - **Pod の生存監視**: tunnel が切れたら再接続するまで状態不明 (daemon がいれば health check を引き受ける) - **複数の spawner が同じ remote Pod に繋ぐ**: tunnel の共有が面倒 (daemon がいれば multiplexing できる) - **workspace サービス(registry / 通知バス)の remote 提供**: SSH-only モデルではリモート側に workspace サービスが無い これらは **MVP では問題にならず**、daemon は「便利にしたくなった ときの upgrade path」として位置づける。 ### リモート側のディレクトリ構成 ``` /home/yoi/ ← yoi システムユーザーの home ├── workspaces/ │ ├── / ← workspace ごとのルート │ │ ├── repo/ ← ユーザーが用意した作業ファイル群 │ │ └── store/ ← session store(spawner から転送) │ └── ... └── .ssh/ └── authorized_keys ← 接続元 Pod の公開鍵 ``` - `yoi` システムユーザーが SSH 接続先 + ファイル所有者 - `repo/` 配下の準備は yoi の責務外(git clone, rsync 等は ユーザーや instruction が指示) - `store/` は spawner がセッションデータを書き込む場所 ## セキュリティの考慮 - SSH の鍵認証がベースライン。パスワード認証は使わない - Pod 単位の鍵ペアにより、接続制御の粒度を Pod レベルにできる(B 方式) - `authorized_keys` の `command=` で実行可能な操作を制限できる (`no-port-forwarding`, `no-pty` 等) - 将来的に Pod 間の trust relationship を定義する仕組みが要るが、 それは本ドキュメントの範囲外 ## 未解決の論点 - SSH transport の具体的な message protocol(JSON-RPC? 独自? protocol crate の拡張?) - 非同期メッセージの扱い(相手が offline のとき queue するか、fail するか) - peer 登録の自動化(workspace 内の Pod が自動で peer list を共有する等) - workspace が複数ある環境での pod-name 解決 - Pod の migration(あるマシンから別のマシンへ Pod を移す)の可能性と scope の扱い