From 21bf009a3f95978007468005982903c8d7cae9e7 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 15:52:59 +0900 Subject: [PATCH 1/3] feat: move profile scope to launch policy --- crates/manifest/src/config.rs | 9 +- crates/manifest/src/profile.rs | 39 ++--- crates/pod/src/entrypoint.rs | 238 ++++++++++++++++++++++++++-- crates/pod/src/spawn/tool.rs | 40 +++++ resources/profiles/coder.lua | 1 - resources/profiles/companion.lua | 3 - resources/profiles/default.lua | 1 - resources/profiles/intake.lua | 1 - resources/profiles/orchestrator.lua | 2 - resources/profiles/reviewer.lua | 1 - 10 files changed, 279 insertions(+), 56 deletions(-) diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index fdef0f71..9185535b 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -729,9 +729,6 @@ impl TryFrom for PodManifest { }, }; - if cfg.scope.allow.is_empty() { - return Err(ResolveError::MissingField("scope.allow")); - } for rule in &cfg.scope.allow { ensure_absolute("scope.allow.target", &rule.target)?; } @@ -1028,11 +1025,11 @@ mod tests { } #[test] - fn resolve_rejects_empty_scope() { + fn resolve_accepts_empty_scope_for_profile_launch_policy() { let mut cfg = minimal_valid(); cfg.scope.allow.clear(); - let err = PodManifest::try_from(cfg).unwrap_err(); - assert!(matches!(err, ResolveError::MissingField("scope.allow"))); + let manifest = PodManifest::try_from(cfg).unwrap(); + assert!(manifest.scope.allow.is_empty()); } #[test] diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index 98f4ff6f..868e269c 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -1262,12 +1262,7 @@ fn profile_scope_to_config( scope: Option, workspace_base: &Path, ) -> Result { - profile_scope_intent_to_config( - scope, - workspace_base, - Some(ProfileScopeIntent::WorkspaceWrite), - "scope", - ) + profile_scope_intent_to_config(scope, workspace_base, None, "scope") } fn profile_delegation_scope_to_config( @@ -1594,10 +1589,9 @@ mod tests { assert!(companion.feature.task.enabled); assert!(companion.feature.pods.enabled); assert!(companion.feature.ticket.enabled); - assert_eq!(companion.scope.allow[0].permission, Permission::Write); - assert_eq!(companion.scope.deny.len(), 1); - assert_eq!(companion.scope.deny[0].permission, Permission::Write); - assert_eq!(companion.scope.deny[0].target, tmp.path().join(".worktree")); + assert!(companion.scope.allow.is_empty()); + assert!(companion.scope.deny.is_empty()); + assert!(companion.delegation_scope.allow.is_empty()); assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert!(companion.web.is_some()); @@ -1605,7 +1599,8 @@ mod tests { assert!(!intake.feature.task.enabled); assert!(!intake.feature.pods.enabled); assert!(intake.feature.ticket.enabled); - assert_eq!(intake.scope.allow[0].permission, Permission::Read); + assert!(intake.scope.allow.is_empty()); + assert!(intake.delegation_scope.allow.is_empty()); assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert!(intake.web.is_some()); assert!(!intake.feature.ticket_orchestration.enabled); @@ -1615,21 +1610,19 @@ mod tests { assert!(orchestrator.feature.pods.enabled); assert!(orchestrator.feature.ticket.enabled); assert!(orchestrator.feature.ticket_orchestration.enabled); - assert_eq!(orchestrator.scope.allow[0].permission, Permission::Read); + assert!(orchestrator.scope.allow.is_empty()); + assert!(orchestrator.delegation_scope.allow.is_empty()); assert_eq!( orchestrator.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5") ); assert!(orchestrator.web.is_some()); - assert_eq!( - orchestrator.delegation_scope.allow[0].permission, - Permission::Write - ); let coder = resolve("coder"); assert!(coder.feature.task.enabled); assert!(!coder.feature.pods.enabled); - assert_eq!(coder.scope.allow[0].permission, Permission::Write); + assert!(coder.scope.allow.is_empty()); + assert!(coder.delegation_scope.allow.is_empty()); assert_eq!(coder.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert!(coder.web.is_some()); @@ -1637,7 +1630,8 @@ mod tests { assert!(!reviewer.feature.task.enabled); assert!(!reviewer.feature.pods.enabled); assert!(!reviewer.feature.ticket.enabled); - assert_eq!(reviewer.scope.allow[0].permission, Permission::Read); + assert!(reviewer.scope.allow.is_empty()); + assert!(reviewer.delegation_scope.allow.is_empty()); assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert!(reviewer.web.is_some()); } @@ -1959,11 +1953,8 @@ return profile { resolved.manifest.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5") ); - assert_eq!(resolved.manifest.scope.allow[0].target, tmp.path()); - assert_eq!( - resolved.manifest.scope.allow[0].permission, - Permission::Write - ); + assert!(resolved.manifest.scope.allow.is_empty()); + assert!(resolved.manifest.delegation_scope.allow.is_empty()); assert!(resolved.manifest.session.record_event_trace); assert_eq!( resolved.profile.as_ref().unwrap().name.as_deref(), @@ -2016,7 +2007,7 @@ record_event_trace = false resolved.manifest.pod.prompt_pack.as_deref(), Some(yoi_dir.join("prompts.toml").as_path()) ); - assert_eq!(resolved.manifest.scope.allow[0].target, nested); + assert!(resolved.manifest.scope.allow.is_empty()); assert_eq!( resolved .manifest diff --git a/crates/pod/src/entrypoint.rs b/crates/pod/src/entrypoint.rs index 1e7a0c8c..a1e627ae 100644 --- a/crates/pod/src/entrypoint.rs +++ b/crates/pod/src/entrypoint.rs @@ -5,7 +5,8 @@ use std::process::ExitCode; use crate::{Pod, PodController, PromptLoader}; use clap::{CommandFactory, FromArgMatches, Parser}; use manifest::{ - PodManifest, PodManifestConfig, ProfileResolveOptions, ProfileResolver, ProfileSelector, paths, + Permission, PodManifest, PodManifestConfig, ProfileResolveOptions, ProfileResolver, + ProfileSelector, ScopeConfig, ScopeRule, paths, }; use pod_store::{CombinedStore, FsPodStore, PodMetadataStore}; use session_store::{FsStore, SegmentId, Store}; @@ -147,27 +148,43 @@ where { let workspace_root = runtime_workspace_root(cli)?; let runtime_pod_name = runtime_pod_name(cli, &workspace_root); - let mut manifest_and_loader = if let Some(config_json) = cli.spawn_config_json.as_deref() { - load_spawn_config_json(config_json)? - } else if let Some(profile) = &cli.profile { - let selector = ProfileSelector::parse_cli(profile); - load_profile_fn(&selector, &workspace_root, &runtime_pod_name)? - } else if let Some(path) = &cli.manifest { - load_single_manifest(path, cli.pod.as_deref(), &runtime_pod_name)? - } else { - if cli.project.is_some() { - return Err( + let ((mut manifest, loader), manifest_source) = + if let Some(config_json) = cli.spawn_config_json.as_deref() { + ( + load_spawn_config_json(config_json)?, + ManifestSource::SpawnConfig, + ) + } else if let Some(profile) = &cli.profile { + let selector = ProfileSelector::parse_cli(profile); + ( + load_profile_fn(&selector, &workspace_root, &runtime_pod_name)?, + ManifestSource::ProfileLaunch, + ) + } else if let Some(path) = &cli.manifest { + ( + load_single_manifest(path, cli.pod.as_deref(), &runtime_pod_name)?, + ManifestSource::ManifestFile, + ) + } else { + if cli.project.is_some() { + return Err( "--project is no longer supported; normal startup uses profile discovery/default, \ and --manifest is the only one-file manifest mode" .to_string(), ); - } - let selector = ProfileSelector::Default; - load_profile_fn(&selector, &workspace_root, &runtime_pod_name)? - }; + } + let selector = ProfileSelector::Default; + ( + load_profile_fn(&selector, &workspace_root, &runtime_pod_name)?, + ManifestSource::ProfileLaunch, + ) + }; - apply_session_restore_overrides(&mut manifest_and_loader.0, cli)?; - Ok(manifest_and_loader) + if manifest_source == ManifestSource::ProfileLaunch { + apply_profile_launch_policy(&mut manifest, &workspace_root, cli.ticket_role.as_deref())?; + } + apply_session_restore_overrides(&mut manifest, cli)?; + Ok((manifest, loader)) } fn apply_session_restore_overrides(manifest: &mut PodManifest, cli: &Cli) -> Result<(), String> { @@ -233,9 +250,106 @@ fn load_single_manifest( } let manifest = PodManifest::try_from(config) .map_err(|e| format!("failed to resolve manifest {}: {e}", path.display()))?; + if manifest.scope.allow.is_empty() { + return Err(format!( + "manifest {} must declare scope.allow; profile launches receive concrete scope from launch policy", + path.display() + )); + } Ok((manifest, PromptLoader::builtins_only())) } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ManifestSource { + ProfileLaunch, + ManifestFile, + SpawnConfig, +} + +fn read_rule(target: PathBuf) -> ScopeRule { + ScopeRule { + target, + permission: Permission::Read, + recursive: true, + } +} + +fn write_rule(target: PathBuf) -> ScopeRule { + ScopeRule { + target, + permission: Permission::Write, + recursive: true, + } +} + +fn workspace_scope( + workspace_root: &Path, + permission: Permission, + deny_write: &[PathBuf], +) -> ScopeConfig { + let allow_rule = ScopeRule { + target: workspace_root.to_path_buf(), + permission, + recursive: true, + }; + let deny = deny_write + .iter() + .cloned() + .map(write_rule) + .collect::>(); + ScopeConfig { + allow: vec![allow_rule], + deny, + } +} + +fn workspace_worktree_delegation(workspace_root: &Path) -> ScopeConfig { + ScopeConfig { + allow: vec![ + read_rule(workspace_root.to_path_buf()), + write_rule(workspace_root.join(".worktree")), + ], + deny: Vec::new(), + } +} + +fn apply_profile_launch_policy( + manifest: &mut PodManifest, + workspace_root: &Path, + ticket_role: Option<&str>, +) -> Result<(), String> { + let role = match ticket_role { + Some(raw) => { + Some(TicketRole::parse(raw).ok_or_else(|| format!("invalid ticket role `{raw}`"))?) + } + None => None, + }; + match role { + Some(TicketRole::Orchestrator) => { + manifest.scope = workspace_scope(workspace_root, Permission::Read, &[]); + manifest.delegation_scope = workspace_worktree_delegation(workspace_root); + } + Some(TicketRole::Intake) | Some(TicketRole::Reviewer) => { + manifest.scope = workspace_scope(workspace_root, Permission::Read, &[]); + manifest.delegation_scope = ScopeConfig::default(); + } + Some(TicketRole::Coder) => { + manifest.scope = workspace_scope(workspace_root, Permission::Write, &[]); + manifest.delegation_scope = ScopeConfig::default(); + } + None => { + let worktree_root = workspace_root.join(".worktree"); + manifest.scope = workspace_scope( + workspace_root, + Permission::Write, + std::slice::from_ref(&worktree_root), + ); + manifest.delegation_scope = workspace_worktree_delegation(workspace_root); + } + } + Ok(()) +} + pub async fn run_cli() -> ExitCode { run_cli_from("yoi pod", std::env::args_os().skip(1)).await } @@ -762,6 +876,96 @@ permission = "write" assert!(called); assert_eq!(manifest.pod.name, "runtime-workspace"); + assert_eq!(manifest.scope.allow.len(), 1); + assert_eq!(manifest.scope.allow[0].target, workspace); + assert_eq!(manifest.scope.allow[0].permission, Permission::Write); + assert_eq!(manifest.scope.deny.len(), 1); + assert_eq!( + manifest.scope.deny[0].target, + tmp.path().join("runtime-workspace/.worktree") + ); + assert_eq!(manifest.scope.deny[0].permission, Permission::Write); + assert_eq!(manifest.delegation_scope.allow.len(), 2); + assert_eq!( + manifest.delegation_scope.allow[0].target, + tmp.path().join("runtime-workspace") + ); + assert_eq!( + manifest.delegation_scope.allow[0].permission, + Permission::Read + ); + assert_eq!( + manifest.delegation_scope.allow[1].target, + tmp.path().join("runtime-workspace/.worktree") + ); + assert_eq!( + manifest.delegation_scope.allow[1].permission, + Permission::Write + ); + } + + #[test] + fn orchestrator_profile_launch_gets_read_root_and_worktree_delegation_from_launch_policy() { + let tmp = TempDir::new().unwrap(); + let workspace = tmp.path().join("original-workspace"); + std::fs::create_dir(&workspace).unwrap(); + let cli = Cli::try_parse_from([ + "yoi pod", + "--workspace", + workspace.to_str().unwrap(), + "--profile", + "builtin:orchestrator", + "--ticket-role", + "orchestrator", + ]) + .unwrap(); + + let (manifest, _loader) = + resolve_manifest_with_profile_loader(&cli, |selector, _workspace_root, pod_name| { + assert_eq!( + selector, + &ProfileSelector::source_named( + manifest::ProfileRegistrySource::Builtin, + "orchestrator" + ) + ); + let mut manifest = + PodManifest::from_toml(&manifest_toml("from-orchestrator-profile", tmp.path())) + .unwrap(); + manifest.pod.name = pod_name.to_string(); + Ok((manifest, PromptLoader::builtins_only())) + }) + .unwrap(); + + assert_eq!(manifest.scope.allow.len(), 1); + assert_eq!(manifest.scope.allow[0].target, workspace); + assert_eq!(manifest.scope.allow[0].permission, Permission::Read); + assert!(manifest.scope.deny.is_empty()); + assert_eq!(manifest.delegation_scope.allow.len(), 2); + assert_eq!( + manifest.delegation_scope.allow[0].target, + tmp.path().join("original-workspace") + ); + assert_eq!( + manifest.delegation_scope.allow[0].permission, + Permission::Read + ); + assert_eq!( + manifest.delegation_scope.allow[1].target, + tmp.path().join("original-workspace/.worktree") + ); + assert_eq!( + manifest.delegation_scope.allow[1].permission, + Permission::Write + ); + assert!( + !manifest + .delegation_scope + .allow + .iter() + .any(|rule| rule.target == tmp.path().join("original-workspace") + && rule.permission == Permission::Write) + ); } #[test] diff --git a/crates/pod/src/spawn/tool.rs b/crates/pod/src/spawn/tool.rs index d4d0464b..42edd8be 100644 --- a/crates/pod/src/spawn/tool.rs +++ b/crates/pod/src/spawn/tool.rs @@ -1054,6 +1054,46 @@ mod tests { } } + #[test] + fn orchestration_delegation_allows_root_read_and_worktree_writes_not_root_writes() { + let tmp = TempDir::new().unwrap(); + let workspace_root = tmp.path().join("original"); + let implementation_worktree = workspace_root.join(".worktree/ticket-1"); + std::fs::create_dir_all(&implementation_worktree).unwrap(); + let delegation = DelegationScope::from_config(&ScopeConfig { + allow: vec![ + abs_rule(&workspace_root, Permission::Read), + abs_rule(&workspace_root.join(".worktree"), Permission::Write), + ], + deny: Vec::new(), + }) + .unwrap(); + + let coder_scope = vec![ + abs_rule(&workspace_root, Permission::Read), + abs_rule(&implementation_worktree, Permission::Write), + ]; + assert!( + coder_scope + .iter() + .all(|rule| delegation.allows_rule(rule).unwrap()) + ); + + let reviewer_scope = vec![abs_rule(&workspace_root, Permission::Read)]; + assert!( + reviewer_scope + .iter() + .all(|rule| delegation.allows_rule(rule).unwrap()) + ); + + let root_writer_scope = vec![abs_rule(&workspace_root, Permission::Write)]; + assert!( + root_writer_scope + .iter() + .any(|rule| !delegation.allows_rule(rule).unwrap()) + ); + } + fn parent_manifest(root: &Path, deny: Option<&Path>) -> PodManifest { PodManifestConfig { pod: PodMetaConfig { diff --git a/resources/profiles/coder.lua b/resources/profiles/coder.lua index fe93ffb3..1cc02fb7 100644 --- a/resources/profiles/coder.lua +++ b/resources/profiles/coder.lua @@ -2,7 +2,6 @@ local p = yoi.profile.import("builtin:default") p.slug = "coder" p.description = "Coder role profile with bundled reusable policy" -p.scope = yoi.scope.workspace_write() p.worker.instruction = "$yoi/role/coder" p.feature = { task = { enabled = true }, diff --git a/resources/profiles/companion.lua b/resources/profiles/companion.lua index dc9c0abf..7d1530bc 100644 --- a/resources/profiles/companion.lua +++ b/resources/profiles/companion.lua @@ -2,9 +2,6 @@ local p = yoi.profile.import("builtin:default") p.slug = "companion" p.description = "Companion role profile with bundled reusable policy" -p.scope = yoi.scope.workspace_write({ - deny_write = { ".worktree" }, -}) p.feature = { task = { enabled = true }, memory = { enabled = true }, diff --git a/resources/profiles/default.lua b/resources/profiles/default.lua index 97354f27..e5d54f5f 100644 --- a/resources/profiles/default.lua +++ b/resources/profiles/default.lua @@ -2,7 +2,6 @@ return yoi.profile { slug = "default", description = "Default Yoi coding profile", - scope = yoi.scope.workspace_write(), session = { record_event_trace = true, diff --git a/resources/profiles/intake.lua b/resources/profiles/intake.lua index 087a7813..b01cc5d3 100644 --- a/resources/profiles/intake.lua +++ b/resources/profiles/intake.lua @@ -2,7 +2,6 @@ local p = yoi.profile.import("builtin:default") p.slug = "intake" p.description = "Intake role profile with bundled reusable policy" -p.scope = yoi.scope.workspace_read() p.worker.instruction = "$yoi/role/intake" p.feature = { task = { enabled = false }, diff --git a/resources/profiles/orchestrator.lua b/resources/profiles/orchestrator.lua index 71cd3f64..90ad4831 100644 --- a/resources/profiles/orchestrator.lua +++ b/resources/profiles/orchestrator.lua @@ -2,7 +2,6 @@ local p = yoi.profile.import("builtin:default") p.slug = "orchestrator" p.description = "Orchestrator role profile with bundled reusable policy" -p.scope = "workspace_read" p.worker.instruction = "$yoi/role/orchestrator" p.feature = { task = { enabled = false }, @@ -12,6 +11,5 @@ p.feature = { ticket = { enabled = true, access = "lifecycle" }, ticket_orchestration = { enabled = true }, } -p.delegation_scope = "workspace_write" return p diff --git a/resources/profiles/reviewer.lua b/resources/profiles/reviewer.lua index 2c16a6d3..d5aa0378 100644 --- a/resources/profiles/reviewer.lua +++ b/resources/profiles/reviewer.lua @@ -2,7 +2,6 @@ local p = yoi.profile.import("builtin:default") p.slug = "reviewer" p.description = "Reviewer role profile with bundled reusable policy" -p.scope = yoi.scope.workspace_read() p.worker.instruction = "$yoi/role/reviewer" p.feature = { task = { enabled = false }, From 77892b94f2c7420ba24fce46d61fc97efab515f2 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 15:53:42 +0900 Subject: [PATCH 2/3] ticket: report 00001KV11DHGZ implementation --- .../implementation-report-21bf009a.md | 48 ++++++++++++++++ .yoi/tickets/00001KV11DHGZ/item.md | 2 +- .yoi/tickets/00001KV11DHGZ/thread.md | 56 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 .yoi/tickets/00001KV11DHGZ/artifacts/implementation-report-21bf009a.md diff --git a/.yoi/tickets/00001KV11DHGZ/artifacts/implementation-report-21bf009a.md b/.yoi/tickets/00001KV11DHGZ/artifacts/implementation-report-21bf009a.md new file mode 100644 index 00000000..e291592a --- /dev/null +++ b/.yoi/tickets/00001KV11DHGZ/artifacts/implementation-report-21bf009a.md @@ -0,0 +1,48 @@ +実装報告(Coder) + +Commit: +- implementation: `21bf009a3f95978007468005982903c8d7cae9e7` (`feat: move profile scope to launch policy`) + +変更ファイル: +- `resources/profiles/default.lua` +- `resources/profiles/companion.lua` +- `resources/profiles/intake.lua` +- `resources/profiles/orchestrator.lua` +- `resources/profiles/coder.lua` +- `resources/profiles/reviewer.lua` +- `crates/manifest/src/profile.rs` +- `crates/manifest/src/config.rs` +- `crates/pod/src/entrypoint.rs` +- `crates/pod/src/spawn/tool.rs` + +実装内容: +- Builtin reusable Profiles から concrete filesystem `scope` / `delegation_scope` を削除した。 +- Profile resolution の implicit default workspace-write scope を廃止し、Profiles without scope を empty scope として解決可能にした。 +- Fresh profile launch の effective authority を `pod` entrypoint の launch policy で付与するようにした。 + - normal Companion/TUI/default profile launch: workspace write direct scope(`.worktree` write deny)+ workspace read / `.worktree` write delegation。 + - Ticket Orchestrator role launch: original workspace read direct scope + original workspace read / `/.worktree` write delegation。root workspace write delegation は付与しない。 + - Ticket Intake/Reviewer role launch: workspace read direct scope。 + - Ticket Coder role launch: workspace write direct scope。 +- Single-file `--manifest` mode は従来どおり concrete `scope.allow` を要求し、Profile launch policy と混同しないようにした。 +- SpawnPod child scope replacement path は維持し、narrow Orchestrator delegation で Coder/Reviewer 用の root-read + implementation-worktree-write validation が成立することをテストした。 +- Existing user Profile `scope` / `delegation_scope` compatibility は deprecated-compatible path として残した(Ticket の選択肢に沿って、builtin role launch authority は Profile scope に依存しない)。 + +Validation: +- `cargo test -p manifest` ✅ +- Focused tests ✅ + - `cargo test -p manifest builtin_role_profiles_are_registered_and_resolve --quiet` + - `cargo test -p manifest builtin_default_resolves_without_external_evaluator --quiet` + - `cargo test -p manifest resolve_accepts_empty_scope_for_profile_launch_policy --quiet` + - `cargo test -p pod --lib entrypoint::tests::normal_startup_uses_default_profile --quiet` + - `cargo test -p pod --lib entrypoint::tests::orchestrator_profile_launch_gets_read_root_and_worktree_delegation_from_launch_policy --quiet` + - `cargo test -p pod --lib spawn::tool::tests::orchestration_delegation_allows_root_read_and_worktree_writes_not_root_writes --quiet` + - `cargo test -p pod --lib pod::pod_metadata_restore_manifest_tests::snapshot_preserves_saved_scope_over_current_manifest --quiet` + - `cargo test -p client ticket_role --quiet` +- `cargo build -p yoi` ✅ +- `cargo fmt --check` ✅ +- `git diff --check` ✅ +- `nix build .#yoi` not run: no Cargo.lock, packaging, or resource inclusion pattern changed. + +Residual risks / notes: +- Full `cargo test -p pod --lib` was attempted and still has two prompt-text assertion failures (`worktree status, diff, and test results`) in prompt catalog/system tests; this diff did not touch prompt resources or those assertions. Focused scope/profile/spawn/restore tests passed. +- User Profile `scope` compatibility remains supported for now; future schema cleanup can remove or deprecate it explicitly if desired. diff --git a/.yoi/tickets/00001KV11DHGZ/item.md b/.yoi/tickets/00001KV11DHGZ/item.md index 8acde168..662f1613 100644 --- a/.yoi/tickets/00001KV11DHGZ/item.md +++ b/.yoi/tickets/00001KV11DHGZ/item.md @@ -2,7 +2,7 @@ title: 'Profile から concrete scope を外して launch policy で付与する' state: 'inprogress' created_at: '2026-06-13T17:45:32Z' -updated_at: '2026-06-14T06:35:44Z' +updated_at: '2026-06-14T06:53:30Z' assignee: null readiness: 'implementation_ready' risk_flags: ['scope', 'delegation-scope', 'profiles', 'launch-policy', 'orchestrator', 'spawnpod', 'restore'] diff --git a/.yoi/tickets/00001KV11DHGZ/thread.md b/.yoi/tickets/00001KV11DHGZ/thread.md index 875b39f9..c3e388e9 100644 --- a/.yoi/tickets/00001KV11DHGZ/thread.md +++ b/.yoi/tickets/00001KV11DHGZ/thread.md @@ -82,4 +82,60 @@ Validation: Profile extend removal prerequisite/conflict has been resolved and merged. Ticket evidence, related records, orchestration plan, and clean workspace state were checked. No blockers remain; implementation can proceed in a dedicated child worktree. +--- + + + +## Implementation report + +実装報告(Coder) + +Commit: +- implementation: `21bf009a3f95978007468005982903c8d7cae9e7` (`feat: move profile scope to launch policy`) + +変更ファイル: +- `resources/profiles/default.lua` +- `resources/profiles/companion.lua` +- `resources/profiles/intake.lua` +- `resources/profiles/orchestrator.lua` +- `resources/profiles/coder.lua` +- `resources/profiles/reviewer.lua` +- `crates/manifest/src/profile.rs` +- `crates/manifest/src/config.rs` +- `crates/pod/src/entrypoint.rs` +- `crates/pod/src/spawn/tool.rs` + +実装内容: +- Builtin reusable Profiles から concrete filesystem `scope` / `delegation_scope` を削除した。 +- Profile resolution の implicit default workspace-write scope を廃止し、Profiles without scope を empty scope として解決可能にした。 +- Fresh profile launch の effective authority を `pod` entrypoint の launch policy で付与するようにした。 + - normal Companion/TUI/default profile launch: workspace write direct scope(`.worktree` write deny)+ workspace read / `.worktree` write delegation。 + - Ticket Orchestrator role launch: original workspace read direct scope + original workspace read / `/.worktree` write delegation。root workspace write delegation は付与しない。 + - Ticket Intake/Reviewer role launch: workspace read direct scope。 + - Ticket Coder role launch: workspace write direct scope。 +- Single-file `--manifest` mode は従来どおり concrete `scope.allow` を要求し、Profile launch policy と混同しないようにした。 +- SpawnPod child scope replacement path は維持し、narrow Orchestrator delegation で Coder/Reviewer 用の root-read + implementation-worktree-write validation が成立することをテストした。 +- Existing user Profile `scope` / `delegation_scope` compatibility は deprecated-compatible path として残した(Ticket の選択肢に沿って、builtin role launch authority は Profile scope に依存しない)。 + +Validation: +- `cargo test -p manifest` ✅ +- Focused tests ✅ + - `cargo test -p manifest builtin_role_profiles_are_registered_and_resolve --quiet` + - `cargo test -p manifest builtin_default_resolves_without_external_evaluator --quiet` + - `cargo test -p manifest resolve_accepts_empty_scope_for_profile_launch_policy --quiet` + - `cargo test -p pod --lib entrypoint::tests::normal_startup_uses_default_profile --quiet` + - `cargo test -p pod --lib entrypoint::tests::orchestrator_profile_launch_gets_read_root_and_worktree_delegation_from_launch_policy --quiet` + - `cargo test -p pod --lib spawn::tool::tests::orchestration_delegation_allows_root_read_and_worktree_writes_not_root_writes --quiet` + - `cargo test -p pod --lib pod::pod_metadata_restore_manifest_tests::snapshot_preserves_saved_scope_over_current_manifest --quiet` + - `cargo test -p client ticket_role --quiet` +- `cargo build -p yoi` ✅ +- `cargo fmt --check` ✅ +- `git diff --check` ✅ +- `nix build .#yoi` not run: no Cargo.lock, packaging, or resource inclusion pattern changed. + +Residual risks / notes: +- Full `cargo test -p pod --lib` was attempted and still has two prompt-text assertion failures (`worktree status, diff, and test results`) in prompt catalog/system tests; this diff did not touch prompt resources or those assertions. Focused scope/profile/spawn/restore tests passed. +- User Profile `scope` compatibility remains supported for now; future schema cleanup can remove or deprecate it explicitly if desired. + + --- From dcbdf251d77fc8847696802d0b8deadaff55014f Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 16:01:14 +0900 Subject: [PATCH 3/3] ticket: approve profile launch policy scope --- .../artifacts/review-approve-21bf009a.md | 20 +++++++++++++ .yoi/tickets/00001KV11DHGZ/item.md | 2 +- .yoi/tickets/00001KV11DHGZ/thread.md | 28 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .yoi/tickets/00001KV11DHGZ/artifacts/review-approve-21bf009a.md diff --git a/.yoi/tickets/00001KV11DHGZ/artifacts/review-approve-21bf009a.md b/.yoi/tickets/00001KV11DHGZ/artifacts/review-approve-21bf009a.md new file mode 100644 index 00000000..859e897c --- /dev/null +++ b/.yoi/tickets/00001KV11DHGZ/artifacts/review-approve-21bf009a.md @@ -0,0 +1,20 @@ +Approve implementation review for Ticket 00001KV11DHGZ. + +Scope reviewed: implementation commit 21bf009a plus ticket report commit 77892b94 against base cdb12af9. + +Evidence: +- Builtin role profile resources no longer contain `scope` or `delegation_scope`; reusable profile data retains role/model/prompt/feature/tool policy only. +- Fresh profile launch scope is applied in `crates/pod/src/entrypoint.rs` by launch policy after profile resolution. Default/Companion launches receive direct workspace write scope with `.worktree` write denied and delegation gets workspace read plus `.worktree` write. Orchestrator ticket-role launches receive direct root read and delegation root read plus `.worktree` write, with no root workspace write delegation. +- `SpawnPod` profile/inherit handling continues to replace child direct scope with the explicit delegated child scope and resets child delegation unless explicitly provided; profile/default scope does not leak into child direct authority. +- Pod metadata restore uses saved manifest snapshots when present, so saved scope/delegation are preserved instead of being overwritten by current profile/default launch policy. +- One-file manifest loading still rejects missing/empty concrete `scope.allow`; the retained user-profile scope compatibility path is separated from builtin role authority and is overwritten by launch/delegation policy on fresh role launches. + +Validation performed: +- `cargo test -p manifest --quiet` +- Focused pod tests for normal startup launch policy, orchestrator launch policy, SpawnPod delegation scoping, and metadata snapshot restore. +- `cargo test -p client ticket_role --quiet` +- `cargo build -p yoi` +- `cargo fmt --check` +- `git diff --check cdb12af9..HEAD` + +Result: approve. No blocking requirement or design-boundary concern found. diff --git a/.yoi/tickets/00001KV11DHGZ/item.md b/.yoi/tickets/00001KV11DHGZ/item.md index 662f1613..84ace02c 100644 --- a/.yoi/tickets/00001KV11DHGZ/item.md +++ b/.yoi/tickets/00001KV11DHGZ/item.md @@ -2,7 +2,7 @@ title: 'Profile から concrete scope を外して launch policy で付与する' state: 'inprogress' created_at: '2026-06-13T17:45:32Z' -updated_at: '2026-06-14T06:53:30Z' +updated_at: '2026-06-14T07:00:13Z' assignee: null readiness: 'implementation_ready' risk_flags: ['scope', 'delegation-scope', 'profiles', 'launch-policy', 'orchestrator', 'spawnpod', 'restore'] diff --git a/.yoi/tickets/00001KV11DHGZ/thread.md b/.yoi/tickets/00001KV11DHGZ/thread.md index c3e388e9..dad8d605 100644 --- a/.yoi/tickets/00001KV11DHGZ/thread.md +++ b/.yoi/tickets/00001KV11DHGZ/thread.md @@ -138,4 +138,32 @@ Residual risks / notes: - User Profile `scope` compatibility remains supported for now; future schema cleanup can remove or deprecate it explicitly if desired. +--- + + + +## Review: approve + +Approve implementation review for Ticket 00001KV11DHGZ. + +Scope reviewed: implementation commit 21bf009a plus ticket report commit 77892b94 against base cdb12af9. + +Evidence: +- Builtin role profile resources no longer contain `scope` or `delegation_scope`; reusable profile data retains role/model/prompt/feature/tool policy only. +- Fresh profile launch scope is applied in `crates/pod/src/entrypoint.rs` by launch policy after profile resolution. Default/Companion launches receive direct workspace write scope with `.worktree` write denied and delegation gets workspace read plus `.worktree` write. Orchestrator ticket-role launches receive direct root read and delegation root read plus `.worktree` write, with no root workspace write delegation. +- `SpawnPod` profile/inherit handling continues to replace child direct scope with the explicit delegated child scope and resets child delegation unless explicitly provided; profile/default scope does not leak into child direct authority. +- Pod metadata restore uses saved manifest snapshots when present, so saved scope/delegation are preserved instead of being overwritten by current profile/default launch policy. +- One-file manifest loading still rejects missing/empty concrete `scope.allow`; the retained user-profile scope compatibility path is separated from builtin role authority and is overwritten by launch/delegation policy on fresh role launches. + +Validation performed: +- `cargo test -p manifest --quiet` +- Focused pod tests for normal startup launch policy, orchestrator launch policy, SpawnPod delegation scoping, and metadata snapshot restore. +- `cargo test -p client ticket_role --quiet` +- `cargo build -p yoi` +- `cargo fmt --check` +- `git diff --check cdb12af9..HEAD` + +Result: approve. No blocking requirement or design-boundary concern found. + + ---