scope再設計の完了

This commit is contained in:
Keisuke Hirata 2026-04-14 12:10:00 +09:00
parent 2db2c1611c
commit cbf728d66a
2 changed files with 0 additions and 247 deletions

View File

@ -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)

View File

@ -1,246 +0,0 @@
# Scope の再設計
## 背景
Scope は Pod がどのファイルにどの権限でアクセスできるかを宣言するもの。
現状の問題:
1. `Pod.scope``Option<Scope>` — 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<ScopeRule>,
pub deny: Vec<ScopeRule>,
}
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<ScopeRule>,
deny: Vec<ScopeRule>,
}
impl Scope {
/// Effective permission for `path`. `None` means out-of-scope.
pub fn permission_at(&self, path: &Path) -> Option<Permission>;
/// 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<C, St> {
pwd: PathBuf,
scope: Scope, // Option ではない
// ...
}
impl<C: LlmClient, St: Store> Pod<C, St> {
pub async fn new(
manifest: PodManifest,
worker: Worker<C>,
store: St,
pwd: PathBuf,
scope: Scope,
) -> Result<Self, PodError> { /* ... */ }
pub async fn restore(
session_id: SessionId,
manifest: PodManifest,
client: C,
store: St,
pwd: PathBuf,
scope: Scope,
) -> Result<Self, PodError> { /* ... */ }
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<Vec<u8>, 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<WriteOutcome, ToolsError> {
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<ScopeRule>, deny: Vec<ScopeRule> }` に置き換え(旧 `root` フィールド削除)
- `Permission` enum、`ScopeRule` 構造体を追加
- `PodManifest::scope``Option<ScopeConfig>``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: 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 値。