diff --git a/TODO.md b/TODO.md index dd3c5d1b..8c64ae24 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ - [ ] テスト設計 → [tickets/test-design.md](tickets/test-design.md) - [ ] ツール設計 - [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md) -- [ ] Scope の再設計 (pwd 分離、allow/deny、必須化) → [tickets/scope-redesign.md](tickets/scope-redesign.md) - [ ] 複数 Pod 間の Scope 排他制御 → [tickets/scope-exclusion.md](tickets/scope-exclusion.md) - [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) diff --git a/tickets/scope-redesign.md b/tickets/scope-redesign.md deleted file mode 100644 index fe783d3d..00000000 --- a/tickets/scope-redesign.md +++ /dev/null @@ -1,246 +0,0 @@ -# Scope の再設計 - -## 背景 - -Scope は Pod がどのファイルにどの権限でアクセスできるかを宣言するもの。 -現状の問題: - -1. `Pod.scope` が `Option` — scope なし Pod が成立しうる(ツール未登録) -2. `Scope` は `root: PathBuf` のみ — 書き込み許可/禁止の概念がない -3. 単一ディレクトリしか宣言できない — 「src は書ける、docs は読むだけ」が表現できない -4. pwd(作業ディレクトリ)と「アクセス可能領域」が同じ概念に押し込まれている -5. 将来の複数 Pod 排他制御に必要な「権限つき領域宣言」のデータ構造がない - -## 方針 - -### pwd と scope を分離 - -- **pwd** は Pod の作業ディレクトリ。`[pod]` テーブルで指定。Glob/Grep のデフォルト検索パスにもなる。 -- **scope** は「許可された領域」と「禁止された領域」のリスト。`[scope]` テーブルで指定。 -- pwd は scope の allow に **read 以上で含まれていなければならない**。違反は manifest ロード時エラー。 - -### permission のレベル格子モデル - -`permission` は単一文字列で能力レベルを表す: - -| 値 | レベル | 意味 | -|---|---|---| -| `"read"` | 1 | 読める | -| `"write"` | 2 | 読み書きできる | - -- **allow**: 「このレベル**以上**を許可」。`permission = "write"` は読み書き両方を grant。 -- **deny**: 「このレベル**未満**まで引き下げる」。`permission = "write"` は write だけ落として read は残す。`permission = "read"` は read を落とすので write も連鎖して落ちる(write は read を要求するため)= 完全アクセス不可。 -- **有効権限** = `min( max(allow), min(deny) )`。allow にマッチしない path は scope 外(アクセス不可)。 -- 順序非依存。 - -### マニフェスト - -```toml -[pod] -name = "agent-foo" -pwd = "./src" - -[scope] - -[[scope.allow]] -target = "./src" -permission = "write" -recursive = true # 省略可、デフォルト true - -[[scope.allow]] -target = "./docs" -permission = "read" - -[[scope.deny]] -target = "./src/secrets.rs" -permission = "write" # → ./src/secrets.rs は read-only に格下げ - -[[scope.deny]] -target = "./src/.env" -permission = "read" # → アクセス不可 -``` - -ルール: -- `[scope]` は **省略不可**。なければ manifest エラー。 -- `[[scope.allow]]` は **最低 1 件必須**。空 scope は無意味。 -- `[[scope.deny]]` は省略可。 -- `permission` は allow / deny のどちらでも必須。 -- `recursive` 省略時は `true`。`false` のとき、target ディレクトリ直下のエントリのみが対象(サブディレクトリは降りない)。 -- pwd は allow のいずれかで read 以上にマッチしなければならない。 -- pwd が deny で read を落とされていたらエラー。 - -### Rust 型 - -```rust -// crates/manifest/src/lib.rs -pub struct PodMeta { - pub name: String, - pub pwd: PathBuf, -} - -pub struct ScopeConfig { - pub allow: Vec, - pub deny: Vec, -} - -pub struct ScopeRule { - pub target: PathBuf, - pub permission: Permission, - #[serde(default = "default_recursive")] - pub recursive: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "lowercase")] -pub enum Permission { - Read = 1, - Write = 2, -} -``` - -```rust -// crates/manifest/src/scope.rs -pub struct Scope { - allow: Vec, - deny: Vec, -} - -impl Scope { - /// Effective permission for `path`. `None` means out-of-scope. - pub fn permission_at(&self, path: &Path) -> Option; - - /// Convenience: writable iff effective permission ≥ Write. - pub fn is_writable(&self, path: &Path) -> bool; - - /// Convenience: readable iff effective permission ≥ Read. - pub fn is_readable(&self, path: &Path) -> bool; -} -``` - -`permission_at` 計算: -1. allow 群のうち path にマッチするもの(recursive 考慮)から最大 permission を取る。なければ `None` を返す。 -2. deny 群のうち path にマッチするものから最小 permission を取る(あれば、その値**未満**にキャップ)。 -3. キャップ後のレベルが 0 なら `None`、`Read` なら `Some(Read)`、`Write` なら `Some(Write)`。 - -### Pod で必須化 - -```rust -pub struct Pod { - pwd: PathBuf, - scope: Scope, // Option ではない - // ... -} - -impl Pod { - pub async fn new( - manifest: PodManifest, - worker: Worker, - store: St, - pwd: PathBuf, - scope: Scope, - ) -> Result { /* ... */ } - - pub async fn restore( - session_id: SessionId, - manifest: PodManifest, - client: C, - store: St, - pwd: PathBuf, - scope: Scope, - ) -> Result { /* ... */ } - - pub fn pwd(&self) -> &Path { &self.pwd } - pub fn scope(&self) -> &Scope { &self.scope } -} -``` - -`from_manifest` 系は manifest の `[pod].pwd` と `[scope]` から pwd / scope を構築する。整合性チェック(pwd が allow に含まれる、deny で潰されていない)はここで行う。 - -### ScopedFs の変更 - -```rust -pub fn read(&self, path: &Path) -> Result, ToolsError> { - if !self.inner.scope.is_readable(path) { - return Err(ToolsError::OutOfScope(path.to_path_buf())); - } - // ... -} - -pub fn write(&self, path: &Path, content: &[u8]) -> Result { - if !self.inner.scope.is_writable(path) { - // 読めるが書けない場合と、そもそも見えない場合を区別 - return Err(if self.inner.scope.is_readable(path) { - ToolsError::ReadOnly(path.to_path_buf()) - } else { - ToolsError::OutOfScope(path.to_path_buf()) - }); - } - // ... -} -``` - -Glob/Grep のデフォルト検索パスは Pod の `pwd`。targets 全走査はしない。 -絶対パスでの検索結果は `is_readable` でフィルタする。 - -### Controller 統合 - -```rust -// 今: scope が Some のときだけツール登録 -if let Some(scope) = scope_for_tools { ... } - -// 後: 常にツール登録(scope は必須) -let fs = tools::ScopedFs::new(pod.scope().clone(), pod.pwd().to_path_buf()); -let tracker = tools::Tracker::new(); -worker.register_tools(tools::builtin_tools(fs, tracker)); -``` - -## 影響範囲 - -- `crates/manifest/src/lib.rs` - - `PodMeta` に `pwd: PathBuf` を追加 - - `ScopeConfig` を `{ allow: Vec, deny: Vec }` に置き換え(旧 `root` フィールド削除) - - `Permission` enum、`ScopeRule` 構造体を追加 - - `PodManifest::scope` を `Option` → `ScopeConfig` に(省略不可) - - manifest ロード時の整合性チェック(scope 必須、allow 1 件以上、pwd と allow/deny の関係) - -- `crates/manifest/src/scope.rs` - - `Scope` 構造体を allow/deny ベースに作り直し - - `permission_at` / `is_readable` / `is_writable` - - 旧 `Scope::contains` は削除(呼び出し元は `is_readable` / `is_writable` に置換) - -- `crates/pod/src/pod.rs` - - `scope: Option` → `scope: Scope` - - `pwd: PathBuf` フィールド追加 - - `Pod::new` / `Pod::restore` シグネチャ変更 - - `Pod::scope()` の戻り値を `Option<&Scope>` → `&Scope` に - - `from_manifest` 系は manifest から pwd / scope を取り出して新シグネチャに渡す - -- `crates/tools/src/scoped_fs.rs` - - 内部の scope 参照を新 API(`is_readable` / `is_writable`)に置換 - - `pwd` を別途保持(Glob/Grep のデフォルト検索パス用) - - 既存の境界チェックを置換 - -- `crates/tools/src/error.rs` - - `ReadOnly(PathBuf)` variant を追加 - - 既存の `OutOfScope` は維持 - -- `crates/pod/src/controller.rs` - - `scope` の Optional 分岐を削除し、常時ツール登録 - -- `crates/pod/src/prune.rs` 等の scope 参照箇所 - - `Option<&Scope>` 前提の処理を `&Scope` 前提に書き換え - -- 既存テスト - - `manifest` のパースケース更新(新 TOML 構文) - - `tools::scoped_fs` のテスト更新(権限レベル別、deny 部分化のケース追加) - -## 確認済み論点 - -- **target の解決基準**: `[scope]` 内の `target` は manifest の `[pod].pwd` からの相対パスとして解決する。manifest ロード時に pwd と合わせて絶対パスへ正規化。 -- **deny 合成**: マッチする deny が複数あれば min(permission) を取り、その値未満にキャップ。順序非依存・冪等。 -- **Glob/Grep の検索起点**: 既存の `path` パラメータを保持し、省略時のデフォルトを旧 `scope.root()` から **pwd** に切り替えるだけ。pwd 外に絶対パスで検索された場合、結果は `is_readable` でフィルタする。越境検索禁止の特別ロジックは入れない。 - -## 非ゴール - -- **複数 Pod 間の scope 排他制御** は本 ticket では扱わない。別 ticket [scope-exclusion.md] に切り出す。本 ticket では「将来 read 共有 / write 排他の単位として `Scope` を使えるようにデータ構造を整える」までで止める。 -- **bash ツールへの execute 権限** は本 ticket では扱わない。bash-tool ticket / permission-extension-point ticket で扱う。`Permission` は当面 `Read` / `Write` の 2 値。