scope-lock完了
This commit is contained in:
parent
a7b9b6fa4b
commit
e7a4b76c54
1
TODO.md
1
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)
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 レベルでの分譲等)は当面パス単位のみ
|
||||
|
|
@ -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<ScopeAllocationGuard>` を保持し、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)が正確で、テストが充実している。
|
||||
Loading…
Reference in New Issue
Block a user