merge: profile override scope

This commit is contained in:
Keisuke Hirata 2026-06-20 21:12:19 +09:00
commit a13868818c
No known key found for this signature in database

View File

@ -320,6 +320,21 @@ fn workspace_worktree_delegation(workspace_root: &Path) -> ScopeConfig {
} }
} }
fn append_missing_rules(target: &mut Vec<ScopeRule>, defaults: Vec<ScopeRule>) {
for rule in defaults {
if !target.contains(&rule) {
target.push(rule);
}
}
}
fn apply_scope_launch_defaults(scope: &mut ScopeConfig, defaults: ScopeConfig) {
// Profile resolution has already applied explicit profile/workspace override scope rules.
// Launch policy contributes runtime defaults on top rather than replacing those grants.
append_missing_rules(&mut scope.allow, defaults.allow);
append_missing_rules(&mut scope.deny, defaults.deny);
}
fn apply_profile_launch_policy( fn apply_profile_launch_policy(
manifest: &mut PodManifest, manifest: &mut PodManifest,
workspace_root: &Path, workspace_root: &Path,
@ -333,24 +348,28 @@ fn apply_profile_launch_policy(
}; };
match role { match role {
Some(TicketRole::Orchestrator) => { Some(TicketRole::Orchestrator) => {
manifest.scope = workspace_scope(workspace_root, Permission::Read, &[]); let default_scope = workspace_scope(workspace_root, Permission::Read, &[]);
apply_scope_launch_defaults(&mut manifest.scope, default_scope);
manifest.delegation_scope = workspace_worktree_delegation(workspace_root); manifest.delegation_scope = workspace_worktree_delegation(workspace_root);
} }
Some(TicketRole::Intake) | Some(TicketRole::Reviewer) => { Some(TicketRole::Intake) | Some(TicketRole::Reviewer) => {
manifest.scope = workspace_scope(workspace_root, Permission::Read, &[]); let default_scope = workspace_scope(workspace_root, Permission::Read, &[]);
apply_scope_launch_defaults(&mut manifest.scope, default_scope);
manifest.delegation_scope = ScopeConfig::default(); manifest.delegation_scope = ScopeConfig::default();
} }
Some(TicketRole::Coder) => { Some(TicketRole::Coder) => {
manifest.scope = workspace_scope(workspace_root, Permission::Write, &[]); let default_scope = workspace_scope(workspace_root, Permission::Write, &[]);
apply_scope_launch_defaults(&mut manifest.scope, default_scope);
manifest.delegation_scope = ScopeConfig::default(); manifest.delegation_scope = ScopeConfig::default();
} }
None => { None => {
let worktree_root = workspace_root.join(".worktree"); let worktree_root = workspace_root.join(".worktree");
manifest.scope = workspace_scope( let default_scope = workspace_scope(
workspace_root, workspace_root,
Permission::Write, Permission::Write,
std::slice::from_ref(&worktree_root), std::slice::from_ref(&worktree_root),
); );
apply_scope_launch_defaults(&mut manifest.scope, default_scope);
manifest.delegation_scope = workspace_worktree_delegation(workspace_root); manifest.delegation_scope = workspace_worktree_delegation(workspace_root);
} }
} }
@ -665,6 +684,22 @@ permission = "write"
) )
} }
fn scope_rule(target: &Path, permission: Permission) -> ScopeRule {
ScopeRule {
target: target.to_path_buf(),
permission,
recursive: true,
}
}
fn assert_scope_contains(rules: &[ScopeRule], target: &Path, permission: Permission) {
let expected = scope_rule(target, permission);
assert!(
rules.contains(&expected),
"expected scope rules to contain {expected:?}; got {rules:?}"
);
}
#[test] #[test]
fn user_manifest_flag_is_not_accepted() { fn user_manifest_flag_is_not_accepted() {
let err = Cli::try_parse_from(["yoi pod", "--user-manifest", "manifest.toml"]).unwrap_err(); let err = Cli::try_parse_from(["yoi pod", "--user-manifest", "manifest.toml"]).unwrap_err();
@ -754,6 +789,68 @@ permission = "write"
assert_eq!(manifest.worker.language, "manifest"); assert_eq!(manifest.worker.language, "manifest");
} }
#[test]
fn profile_launch_preserves_workspace_override_scope_allow_in_final_manifest() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("runtime-workspace");
let external = tmp.path().join("external-readable");
let yoi_dir = workspace.join(".yoi");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(&external).unwrap();
std::fs::create_dir_all(&yoi_dir).unwrap();
write(
&yoi_dir.join("override.local.toml"),
&format!(
r#"
[[scope.allow]]
target = "{}"
permission = "read"
recursive = true
"#,
external.display()
),
);
let profile = tmp.path().join("profile.lua");
write(
&profile,
r#"
local yoi = require("yoi")
return yoi.profile {
slug = "override-scope",
model = { scheme = "anthropic", model_id = "test-model" },
}
"#,
);
let cli = Cli::try_parse_from([
"yoi pod",
"--workspace",
workspace.to_str().unwrap(),
"--profile",
profile.to_str().unwrap(),
])
.unwrap();
let (manifest, _loader) = resolve_manifest(&cli).unwrap();
let snapshot = serde_json::to_value(&manifest).unwrap();
let snapshot_scope: ScopeConfig =
serde_json::from_value(snapshot["scope"].clone()).unwrap();
assert_scope_contains(&manifest.scope.allow, &external, Permission::Read);
assert_scope_contains(&manifest.scope.allow, &workspace, Permission::Write);
assert_scope_contains(
&manifest.scope.deny,
&workspace.join(".worktree"),
Permission::Write,
);
assert_scope_contains(&snapshot_scope.allow, &external, Permission::Read);
assert_scope_contains(&snapshot_scope.allow, &workspace, Permission::Write);
assert_scope_contains(
&snapshot_scope.deny,
&workspace.join(".worktree"),
Permission::Write,
);
}
#[test] #[test]
fn profile_uses_selected_profile() { fn profile_uses_selected_profile() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
@ -883,15 +980,15 @@ permission = "write"
assert!(called); assert!(called);
assert_eq!(manifest.pod.name, "runtime-workspace"); assert_eq!(manifest.pod.name, "runtime-workspace");
assert_eq!(manifest.scope.allow.len(), 1); assert_eq!(manifest.scope.allow.len(), 2);
assert_eq!(manifest.scope.allow[0].target, workspace); assert_scope_contains(&manifest.scope.allow, tmp.path(), Permission::Write);
assert_eq!(manifest.scope.allow[0].permission, Permission::Write); assert_scope_contains(&manifest.scope.allow, &workspace, Permission::Write);
assert_eq!(manifest.scope.deny.len(), 1); assert_eq!(manifest.scope.deny.len(), 1);
assert_eq!( assert_scope_contains(
manifest.scope.deny[0].target, &manifest.scope.deny,
tmp.path().join("runtime-workspace/.worktree") &tmp.path().join("runtime-workspace/.worktree"),
Permission::Write,
); );
assert_eq!(manifest.scope.deny[0].permission, Permission::Write);
assert_eq!(manifest.delegation_scope.allow.len(), 2); assert_eq!(manifest.delegation_scope.allow.len(), 2);
assert_eq!( assert_eq!(
manifest.delegation_scope.allow[0].target, manifest.delegation_scope.allow[0].target,
@ -944,9 +1041,9 @@ permission = "write"
}) })
.unwrap(); .unwrap();
assert_eq!(manifest.scope.allow.len(), 1); assert_eq!(manifest.scope.allow.len(), 2);
assert_eq!(manifest.scope.allow[0].target, workspace); assert_scope_contains(&manifest.scope.allow, tmp.path(), Permission::Write);
assert_eq!(manifest.scope.allow[0].permission, Permission::Read); assert_scope_contains(&manifest.scope.allow, &workspace, Permission::Read);
assert!(manifest.scope.deny.is_empty()); assert!(manifest.scope.deny.is_empty());
assert_eq!(manifest.delegation_scope.allow.len(), 2); assert_eq!(manifest.delegation_scope.allow.len(), 2);
assert_eq!( assert_eq!(