From e7a4b76c546f61ffc84a2aa0a856e58176c933f4 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 18 Apr 2026 19:26:23 +0900 Subject: [PATCH] =?UTF-8?q?scope-lock=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 - crates/pod/src/scope_lock.rs | 13 +++-- tickets/scope-lock.md | 94 -------------------------------- tickets/scope-lock.review.md | 100 ----------------------------------- 4 files changed, 6 insertions(+), 202 deletions(-) delete mode 100644 tickets/scope-lock.md delete mode 100644 tickets/scope-lock.review.md diff --git a/TODO.md b/TODO.md index 21518bfb..ecf33eb3 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,6 @@ - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) - [ ] Pod オーケストレーション - - [ ] Scope lock file: write 排他とスコープ分譲の記録基盤 → [tickets/scope-lock.md](tickets/scope-lock.md) - [ ] SpawnPod ツール: LLM から Pod を生成 → [tickets/spawn-pod-tool.md](tickets/spawn-pod-tool.md) - [ ] Pod 間通信ツール: SendToPod / ReadPodOutput / StopPod / ListPods → [tickets/pod-comm-tools.md](tickets/pod-comm-tools.md) - [ ] Pod 間コールバック通知 → [tickets/pod-callback.md](tickets/pod-callback.md) diff --git a/crates/pod/src/scope_lock.rs b/crates/pod/src/scope_lock.rs index 84cf8594..23702568 100644 --- a/crates/pod/src/scope_lock.rs +++ b/crates/pod/src/scope_lock.rs @@ -13,9 +13,9 @@ //! recovery rides on the next Pod that opens the file — no background //! reaper. -use std::fs::{File, OpenOptions}; +use std::fs::{DirBuilder, File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; -use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; use fs4::fs_std::FileExt; @@ -109,11 +109,10 @@ impl LockFileGuard { /// allocation table. Existing files/directories are left alone. pub fn open(path: &Path) -> io::Result { if let Some(parent) = path.parent() { - let existed = parent.exists(); - std::fs::create_dir_all(parent)?; - if !existed { - std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?; - } + DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(parent)?; } let file = OpenOptions::new() .read(true) diff --git a/tickets/scope-lock.md b/tickets/scope-lock.md deleted file mode 100644 index a7559b31..00000000 --- a/tickets/scope-lock.md +++ /dev/null @@ -1,94 +0,0 @@ -# Scope lock file: write 排他とスコープ分譲の記録基盤 - -## レビュー状態 - -初回レビュー実施済み。[scope-lock.review.md](scope-lock.review.md) を参照。 -指摘1件(ファイルパーミッション 0600 の明示設定)の修正を条件に受け入れ可。 - -## 背景 - -Pod オーケストレーションでは scope の分譲(spawner が自身の scope を spawned Pod に譲渡)が発生する。また、人間が独立に複数の Pod を起動した場合にも同一パスへの write 衝突を検出する必要がある。 - -これらを解決するため、マシン上の全 Pod の scope 割り当てを**単一の lock file**で一元管理する。 - -## 仕様 - -### lock file - -置き場: `$XDG_RUNTIME_DIR/insomnia/scope.lock` - -内容: -```json -{ - "allocations": [ - { - "name": "abc123", - "pid": 12345, - "socket": "/run/insomnia/.../pod-a.sock", - "scope_allow": ["/project/src:write:recursive"], - "delegated_from": null - }, - { - "name": "def456", - "pid": 12346, - "socket": "/run/insomnia/.../pod-b.sock", - "scope_allow": ["/project/src/core:write:recursive"], - "delegated_from": "abc123" - } - ] -} -``` - -アクセスは `flock(2)` による advisory lock で排他制御する。 - -### 操作 - -| タイミング | 動作 | -|---|---| -| **Pod 起動** | lock → stale 検出(PID 死活)→ 自動回収 → write 衝突チェック → 自分の scope を登録 → unlock | -| **scope 分譲** | lock → spawner の allocation に deny 追記 → 新 Pod の allocation を追加(`delegated_from` に spawner)→ unlock | -| **Pod 正常終了** | lock → 自分の allocation を削除 → `delegated_from` が自分の子が残っていなければ親の deny を解除 → unlock | -| **stale 検出** | `kill(pid, 0)` で生存確認。死んでいたら allocation を削除し scope を `delegated_from` の親に返却 | - -### stale の自動回収 - -Pod がクラッシュした場合、lock file にエントリが残る。次に lock file を開いた Pod が stale を検出し自動回収する: - -- 死亡 Pod の scope のうち、生存中の子 Pod が持つ分を除外 -- 残りを `delegated_from` の親に返却 -- 死亡 Pod のエントリを削除 -- 子 Pod の `delegated_from` を親に付け替え - -### effective scope の導出 - -``` -effective_scope = 自分の allocation - Σ(delegated_from が自分を指す子の allocation) -``` - -### セキュリティとアクセス - -- ファイルパーミッション `0600`(owner only)、ディレクトリは `0700`。他ユーザーからの読み取りを防ぐ -- owner(Pod を動かしているユーザー)は当然読める。JSON なので直接確認も可能 -- Pod による lock file 探索は排他制御の目的に限定する。Pod 発見のための lock file スキャンは行わない(Pod の発見は spawn 記録 + 明示的な紹介のみ) -- 衝突で Pod 起動が拒否されたとき、競合相手の name をエラーメッセージに含める - -## 実装 - -- 新規モジュール `crates/pod/src/scope_lock.rs`(または `crates/scope-lock/`) -- Pod 起動時(`Pod::from_manifest` / `Pod::from_manifest_toml`)に lock 取得 -- Pod 終了時(`Drop` または明示的 release)に lock 解放 -- Controller 層でのエラー伝搬 - -## 完了条件 - -- Pod 起動時に scope lock file に allocation が記録される -- 同一パスへの write 衝突が検出され、Pod 起動が拒否される(競合相手の name がエラーに含まれる) -- Pod 正常終了時に allocation が削除される -- stale エントリ(PID 死亡)が自動回収され、scope が親に戻る -- 分譲チェーン(A→B→D)の部分回収が正しく動作する -- 単体テストで衝突検出・stale 回収・分譲/返却が検証される - -## 範囲外 - -- SpawnPod ツール自体の実装(`tickets/spawn-pod-tool.md`) -- scope の分譲粒度(permission レベルでの分譲等)は当面パス単位のみ diff --git a/tickets/scope-lock.review.md b/tickets/scope-lock.review.md deleted file mode 100644 index 7d34bfdc..00000000 --- a/tickets/scope-lock.review.md +++ /dev/null @@ -1,100 +0,0 @@ -# レビュー: Scope lock file - -対象差分: `crates/pod/src/scope_lock.rs` (新規 992行), `crates/manifest/src/scope.rs`, `crates/pod/src/{pod,lib,runtime_dir}.rs`, `crates/pod/Cargo.toml`(未コミット) - -## 要件達成状況 - -| 要件 | 状態 | -|---|---| -| lock file に Pod の scope allocation を記録 | ✅ `register_pod` / `delegate_scope` で JSON に書き込み | -| flock による排他アクセス | ✅ `LockFileGuard::open` で `lock_exclusive`、Drop で `unlock` | -| write 衝突の検出 | ✅ `find_conflict_owner` が delegation tree を下降して真の所有者を特定 | -| stale エントリの自動回収 (PID 死活) | ✅ `reclaim_stale` が `kill(pid, 0)` で判定、dead を削除・子を reparent | -| scope 分譲の記録 (`delegated_from`) | ✅ `delegate_scope` が spawner の effective scope 内か検証してから登録 | -| 分譲チェーンの reparent (A→B→D、B 死亡時 D を A に付け替え) | ✅ `release_pod` / `reclaim_stale` ともに `delegated_from` を親に付け替え | -| Pod 正常終了時の allocation 解放 | ✅ `ScopeAllocationGuard` の Drop で `release_pod` を呼ぶ | -| Pod 起動時 (`from_manifest`) に自動登録 | ✅ `scope_lock::install_top_level` を `from_manifest` 内で呼出 | -| テストで衝突検出・stale 回収・分譲/返却を検証 | ✅ 16 テストケース | -| パーミッション制御 (0600) | — チケットに記載あるが、ファイル作成時の umask 制御は未実装。`OpenOptions` に mode 設定なし | - -## アーキテクチャ - -### 良い点 - -**`LockFileGuard` の RAII 設計**: `open` で flock 取得、`save` で書き込み、Drop で unlock。mutate-but-don't-save のパスでは変更が破棄される(エラー時に安全)。 - -**`ScopeAllocationGuard` で Pod ライフサイクルに紐付け**: Pod 構造体が `Option` を保持し、Drop で lock file から自動削除。`Pod::new` / `Pod::restore`(テスト用)は `None` でバイパス。 - -**`find_conflict_owner` が delegation tree を下降**: 衝突エラーに「真の所有者」(最深のノード)を表示。`conflict_detection_descends_to_real_owner` テストで lock-in。 - -**`reclaim_stale_with` のテストシーム**: PID 生存判定を引数で差し替え可能。テストで任意の PID を「dead」にできる。 - -**`is_within_effective_write`**: spawner の allow set から子に委譲済みの部分を差し引いた「実効 scope」を計算。`delegate_scope` のバリデーションに使用。 - -**`rules_overlap` の 4 パターン分岐** (recursive×recursive, recursive×non, non×recursive, non×non): 非再帰ルールの覆域(target 自身 + 直下の子)を正しく考慮。 - -### 指摘事項 - -#### 1. 🟡 ファイルパーミッションの明示設定 - -チケットの「セキュリティとアクセス」セクション: -> ファイルパーミッション `0600`(owner only)、ディレクトリは `0700` - -`LockFileGuard::open` は `OpenOptions::new().create(true)` で作成しているが、パーミッションの明示設定がない。umask がデフォルト (0022) なら `0644` になり、他ユーザーから読めてしまう。 - -```rust -// 現状 -let file = OpenOptions::new() - .read(true).write(true).create(true).truncate(false) - .open(path)?; - -// 修正案: Unix 拡張で mode を設定 -use std::os::unix::fs::OpenOptionsExt; -let file = OpenOptions::new() - .read(true).write(true).create(true).truncate(false) - .mode(0o600) - .open(path)?; -``` - -ディレクトリも `create_dir_all` の後に `std::fs::set_permissions` で `0700` に制限する。 - -**判断**: セキュリティ要件。修正すべき。 - -#### 2. 🟢 `socket` フィールドの予測パス - -`pod.rs` で socket path を `runtime_dir::default_base()?.join(&manifest.pod.name).join("sock")` と予測しているが、実際の `RuntimeDir` が作る socket path と一致するか保証がない(`RuntimeDir` のパス構築ロジックが変わったら乖離する)。 - -現時点では一致しているが、将来 `RuntimeDir` のパス規則が変わったとき scope_lock の socket 情報が嘘になる。`RuntimeDir` 側に `fn socket_path_for(pod_name: &str) -> PathBuf` のような static メソッドを置いて、両者が同じ関数を呼ぶようにするとより堅牢。 - -**判断**: 現時点では問題なし。リファクタ余地として認識。 - -#### 3. 🟢 `covers_fully` の permission 比較 - -```rust -fn covers_fully(cover: &ScopeRule, inner: &ScopeRule) -> bool { - if cover.permission < inner.permission { - return false; - } - ... -} -``` - -`Permission` の `Ord` は `Read < Write`。`cover.permission < inner.permission` = 「cover が Read で inner が Write なら不十分」。正しい。 - -#### 4. 🟢 テストの網羅性 - -16 ケース: -- ファイル操作: open creates / save-reopen roundtrip -- overlap 判定: prefix relation / non-recursive -- 登録: write conflict / duplicate name / read doesn't conflict -- 分譲: must be subset / succeeds within parent / rejects sibling overlap -- 解放: reparents children / reopens scope / returns to parent -- stale: reparents and removes dead entries -- guard: releases on drop -- conflict detection: descends to real owner - -delegation tree の主要シナリオ (A→B→D) がカバーされている。 - -## 結論 - -**指摘1 (ファイルパーミッション 0600) の修正を条件に受け入れ可**。他は問題なし。実装の核心(delegation tree walk, effective scope 計算, stale reclaim + reparent)が正確で、テストが充実している。