mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
chore: remove SkillMetadata.permissions and derive skill sandboxing from permission_profile (#13061)
## Summary
This change removes the compiled permissions field from skill metadata
and keeps permission_profile as the single source of truth.
Skill loading no longer compiles skill permissions eagerly. Instead, the
zsh-fork skill escalation path compiles `skill.permission_profile` when
it needs to determine the sandbox to apply for a skill script.
## Behavior change
For skills that declare:
```
permissions: {}
```
we now treat that the same as having no skill permissions override,
instead of creating and using a default readonly sandbox. This change
makes the behavior more intuitive:
- only non-empty skill permission profiles affect sandboxing
- omitting permissions and writing permissions: {} now mean the same
thing
- skill metadata keeps a single permissions representation instead of
storing derived state too
Overall, this makes skill sandbox behavior easier to understand and more
predictable.
This commit is contained in:
committed by
GitHub
Unverified
parent
9965bf31fa
commit
0bb152b01d
@@ -442,7 +442,6 @@ mod tests {
|
||||
dependencies: Some(SkillDependencies { tools }),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: PathBuf::from("skill"),
|
||||
scope: SkillScope::User,
|
||||
}
|
||||
|
||||
@@ -483,7 +483,6 @@ mod tests {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: PathBuf::from(path),
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
}
|
||||
|
||||
@@ -253,7 +253,6 @@ mod tests {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: skill_doc_path,
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::config::Permissions;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::default_project_root_markers;
|
||||
@@ -12,7 +11,6 @@ use crate::skills::model::SkillLoadOutcome;
|
||||
use crate::skills::model::SkillMetadata;
|
||||
use crate::skills::model::SkillPolicy;
|
||||
use crate::skills::model::SkillToolDependency;
|
||||
use crate::skills::permissions::compile_permission_profile;
|
||||
use crate::skills::system::system_cache_root_dir;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -69,7 +67,6 @@ struct LoadedSkillMetadata {
|
||||
dependencies: Option<SkillDependencies>,
|
||||
policy: Option<SkillPolicy>,
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
permissions: Option<Permissions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -528,7 +525,6 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
dependencies,
|
||||
policy,
|
||||
permission_profile,
|
||||
permissions,
|
||||
} = load_skill_metadata(path);
|
||||
|
||||
validate_len(&name, MAX_NAME_LEN, "name")?;
|
||||
@@ -551,7 +547,6 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
dependencies,
|
||||
policy,
|
||||
permission_profile,
|
||||
permissions,
|
||||
path_to_skills_md: resolved_path,
|
||||
scope,
|
||||
})
|
||||
@@ -617,14 +612,11 @@ fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata {
|
||||
policy,
|
||||
permissions,
|
||||
} = parsed;
|
||||
let permission_profile = permissions.clone().filter(|profile| !profile.is_empty());
|
||||
|
||||
LoadedSkillMetadata {
|
||||
interface: resolve_interface(interface, skill_dir),
|
||||
dependencies: resolve_dependencies(dependencies),
|
||||
policy: resolve_policy(policy),
|
||||
permission_profile,
|
||||
permissions: compile_permission_profile(skill_dir, permissions),
|
||||
permission_profile: permissions.filter(|profile| !profile.is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,9 +856,7 @@ mod tests {
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config::Constrained;
|
||||
use crate::config::ProjectConfig;
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::config_loader::ConfigLayerEntry;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
@@ -1100,7 +1090,6 @@ mod tests {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1258,7 +1247,6 @@ mod tests {
|
||||
}),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1315,7 +1303,6 @@ interface:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(skill_path.as_path()),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1435,44 +1422,6 @@ permissions:
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
let macos_seatbelt_profile_extensions =
|
||||
Some(crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions::default());
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let macos_seatbelt_profile_extensions = None;
|
||||
assert_eq!(
|
||||
outcome.skills[0].permissions,
|
||||
Some(Permissions {
|
||||
approval_policy: Constrained::allow_any(crate::protocol::AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(
|
||||
crate::protocol::SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![
|
||||
AbsolutePathBuf::try_from(normalized(
|
||||
skill_dir.join("output").as_path(),
|
||||
))
|
||||
.expect("absolute output path")
|
||||
],
|
||||
read_only_access: crate::protocol::ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![
|
||||
AbsolutePathBuf::try_from(normalized(
|
||||
skill_dir.join("data").as_path(),
|
||||
))
|
||||
.expect("absolute data path")
|
||||
],
|
||||
},
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
}
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1497,34 +1446,7 @@ permissions: {}
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(outcome.skills.len(), 1);
|
||||
#[cfg(target_os = "macos")]
|
||||
let expected = Some(Permissions {
|
||||
approval_policy: Constrained::allow_any(crate::protocol::AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(
|
||||
crate::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: Some(
|
||||
crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions::default(),
|
||||
),
|
||||
});
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let expected = Some(Permissions {
|
||||
approval_policy: Constrained::allow_any(crate::protocol::AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(
|
||||
crate::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
});
|
||||
assert_eq!(outcome.skills[0].permission_profile, None);
|
||||
assert_eq!(outcome.skills[0].permissions, expected);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1556,24 +1478,21 @@ permissions:
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(outcome.skills.len(), 1);
|
||||
let profile = outcome.skills[0]
|
||||
.permissions
|
||||
.as_ref()
|
||||
.expect("permission profile");
|
||||
assert_eq!(
|
||||
profile.macos_seatbelt_profile_extensions,
|
||||
Some(
|
||||
crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences:
|
||||
crate::seatbelt_permissions::MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation:
|
||||
crate::seatbelt_permissions::MacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string()
|
||||
],),
|
||||
macos_accessibility: true,
|
||||
macos_calendar: true,
|
||||
}
|
||||
)
|
||||
outcome.skills[0].permission_profile,
|
||||
Some(PermissionProfile {
|
||||
macos: Some(codex_protocol::models::MacOsPermissions {
|
||||
preferences: Some(codex_protocol::models::MacOsPreferencesValue::Mode(
|
||||
"readwrite".to_string(),
|
||||
),),
|
||||
automations: Some(codex_protocol::models::MacOsAutomationValue::BundleIds(
|
||||
vec!["com.apple.Notes".to_string()],
|
||||
)),
|
||||
accessibility: Some(true),
|
||||
calendar: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1607,17 +1526,19 @@ permissions:
|
||||
);
|
||||
assert_eq!(outcome.skills.len(), 1);
|
||||
assert_eq!(
|
||||
outcome.skills[0].permissions,
|
||||
Some(Permissions {
|
||||
approval_policy: Constrained::allow_any(crate::protocol::AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(
|
||||
crate::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
outcome.skills[0].permission_profile,
|
||||
Some(PermissionProfile {
|
||||
macos: Some(codex_protocol::models::MacOsPermissions {
|
||||
preferences: Some(codex_protocol::models::MacOsPreferencesValue::Mode(
|
||||
"readwrite".to_string(),
|
||||
)),
|
||||
automations: Some(codex_protocol::models::MacOsAutomationValue::BundleIds(
|
||||
vec!["com.apple.Notes".to_string()],
|
||||
)),
|
||||
accessibility: Some(true),
|
||||
calendar: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1667,7 +1588,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1709,7 +1629,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1764,7 +1683,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1807,7 +1725,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1853,7 +1770,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&shared_skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1915,7 +1831,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1953,7 +1868,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&shared_skill_path),
|
||||
scope: SkillScope::Admin,
|
||||
}]
|
||||
@@ -1995,7 +1909,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&linked_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -2064,7 +1977,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&within_depth_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -2093,7 +2005,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -2127,7 +2038,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -2170,7 +2080,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -2203,7 +2112,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -2317,7 +2225,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -2354,7 +2261,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -2409,7 +2315,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&nested_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -2421,7 +2326,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&root_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -2462,7 +2366,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -2501,7 +2404,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -2544,7 +2446,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&repo_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -2556,7 +2457,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&user_skill_path),
|
||||
scope: SkillScope::User,
|
||||
},
|
||||
@@ -2622,7 +2522,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: first_path,
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -2634,7 +2533,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: second_path,
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -2707,7 +2605,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -2767,7 +2664,6 @@ permissions:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::System,
|
||||
}]
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::Permissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
|
||||
@@ -16,8 +15,6 @@ pub struct SkillMetadata {
|
||||
pub dependencies: Option<SkillDependencies>,
|
||||
pub policy: Option<SkillPolicy>,
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
// This is an experimental field.
|
||||
pub permissions: Option<Permissions>,
|
||||
/// Path to the SKILLS.md file that declares this skill.
|
||||
pub path_to_skills_md: PathBuf,
|
||||
pub scope: SkillScope,
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
#[cfg(any(unix, test))]
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsAutomationValue;
|
||||
#[cfg(any(unix, test))]
|
||||
use codex_protocol::models::MacOsPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsPreferencesValue;
|
||||
#[cfg(any(unix, test))]
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
#[cfg(any(unix, test))]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(any(unix, test))]
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
#[cfg(any(unix, test))]
|
||||
use dunce::canonicalize as canonicalize_path;
|
||||
#[cfg(any(unix, test))]
|
||||
use tracing::warn;
|
||||
|
||||
#[cfg(any(unix, test))]
|
||||
use crate::config::Constrained;
|
||||
#[cfg(any(unix, test))]
|
||||
use crate::config::Permissions;
|
||||
#[cfg(any(unix, test))]
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
#[cfg(any(unix, test))]
|
||||
use crate::protocol::AskForApproval;
|
||||
#[cfg(any(unix, test))]
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
#[cfg(any(unix, test))]
|
||||
use crate::protocol::SandboxPolicy;
|
||||
|
||||
/// Compiles a skill `PermissionProfile` for the Unix shell escalation path.
|
||||
///
|
||||
/// Normal Windows builds do not currently call this helper, so it is only
|
||||
/// compiled on Unix and in tests.
|
||||
#[cfg(any(unix, test))]
|
||||
pub(crate) fn compile_permission_profile(
|
||||
_skill_dir: &Path,
|
||||
permissions: Option<PermissionProfile>,
|
||||
) -> Option<Permissions> {
|
||||
let PermissionProfile {
|
||||
@@ -78,6 +94,7 @@ pub(crate) fn compile_permission_profile(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(unix, test))]
|
||||
fn normalize_permission_paths(values: &[AbsolutePathBuf], field: &str) -> Vec<AbsolutePathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
@@ -94,6 +111,7 @@ fn normalize_permission_paths(values: &[AbsolutePathBuf], field: &str) -> Vec<Ab
|
||||
paths
|
||||
}
|
||||
|
||||
#[cfg(any(unix, test))]
|
||||
fn normalize_permission_path(value: &AbsolutePathBuf, field: &str) -> Option<AbsolutePathBuf> {
|
||||
let canonicalized = canonicalize_path(value.as_path()).unwrap_or_else(|_| value.to_path_buf());
|
||||
match AbsolutePathBuf::from_absolute_path(&canonicalized) {
|
||||
@@ -184,7 +202,7 @@ fn resolve_macos_automation_permission(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(all(not(target_os = "macos"), any(unix, test)))]
|
||||
fn build_macos_seatbelt_profile_extensions(
|
||||
_: &MacOsPermissions,
|
||||
) -> Option<MacOsSeatbeltProfileExtensions> {
|
||||
@@ -225,21 +243,18 @@ mod tests {
|
||||
let read_dir = skill_dir.join("data");
|
||||
fs::create_dir_all(&read_dir).expect("read dir");
|
||||
|
||||
let profile = compile_permission_profile(
|
||||
&skill_dir,
|
||||
Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![
|
||||
absolute_path(&skill_dir.join("data")),
|
||||
absolute_path(&skill_dir.join("data")),
|
||||
absolute_path(&skill_dir.join("scripts/../data")),
|
||||
]),
|
||||
write: Some(vec![absolute_path(&skill_dir.join("output"))]),
|
||||
}),
|
||||
..Default::default()
|
||||
let profile = compile_permission_profile(Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![
|
||||
absolute_path(&skill_dir.join("data")),
|
||||
absolute_path(&skill_dir.join("data")),
|
||||
absolute_path(&skill_dir.join("scripts/../data")),
|
||||
]),
|
||||
write: Some(vec![absolute_path(&skill_dir.join("output"))]),
|
||||
}),
|
||||
)
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("profile");
|
||||
|
||||
assert_eq!(
|
||||
@@ -284,7 +299,7 @@ mod tests {
|
||||
let skill_dir = tempdir.path().join("skill");
|
||||
fs::create_dir_all(&skill_dir).expect("skill dir");
|
||||
|
||||
let profile = compile_permission_profile(&skill_dir, None);
|
||||
let profile = compile_permission_profile(None);
|
||||
|
||||
assert_eq!(profile, None);
|
||||
}
|
||||
@@ -295,13 +310,10 @@ mod tests {
|
||||
let skill_dir = tempdir.path().join("skill");
|
||||
fs::create_dir_all(&skill_dir).expect("skill dir");
|
||||
|
||||
let profile = compile_permission_profile(
|
||||
&skill_dir,
|
||||
Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
let profile = compile_permission_profile(Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("profile");
|
||||
|
||||
assert_eq!(
|
||||
@@ -330,17 +342,14 @@ mod tests {
|
||||
let read_dir = skill_dir.join("data");
|
||||
fs::create_dir_all(&read_dir).expect("read dir");
|
||||
|
||||
let profile = compile_permission_profile(
|
||||
&skill_dir,
|
||||
Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![absolute_path(&skill_dir.join("data"))]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
..Default::default()
|
||||
let profile = compile_permission_profile(Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![absolute_path(&skill_dir.join("data"))]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
)
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("profile");
|
||||
|
||||
assert_eq!(
|
||||
@@ -379,20 +388,17 @@ mod tests {
|
||||
let skill_dir = tempdir.path().join("skill");
|
||||
fs::create_dir_all(&skill_dir).expect("skill dir");
|
||||
|
||||
let profile = compile_permission_profile(
|
||||
&skill_dir,
|
||||
Some(PermissionProfile {
|
||||
macos: Some(MacOsPermissions {
|
||||
preferences: Some(MacOsPreferencesValue::Mode("readwrite".to_string())),
|
||||
automations: Some(MacOsAutomationValue::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
])),
|
||||
accessibility: Some(true),
|
||||
calendar: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
let profile = compile_permission_profile(Some(PermissionProfile {
|
||||
macos: Some(MacOsPermissions {
|
||||
preferences: Some(MacOsPreferencesValue::Mode("readwrite".to_string())),
|
||||
automations: Some(MacOsAutomationValue::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
])),
|
||||
accessibility: Some(true),
|
||||
calendar: Some(true),
|
||||
}),
|
||||
)
|
||||
..Default::default()
|
||||
}))
|
||||
.expect("profile");
|
||||
|
||||
assert_eq!(
|
||||
@@ -419,8 +425,8 @@ mod tests {
|
||||
let skill_dir = tempdir.path().join("skill");
|
||||
fs::create_dir_all(&skill_dir).expect("skill dir");
|
||||
|
||||
let profile = compile_permission_profile(&skill_dir, Some(PermissionProfile::default()))
|
||||
.expect("profile");
|
||||
let profile =
|
||||
compile_permission_profile(Some(PermissionProfile::default())).expect("profile");
|
||||
|
||||
assert_eq!(
|
||||
profile.macos_seatbelt_profile_extensions,
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::features::Feature;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::permissions::compile_permission_profile;
|
||||
use crate::tools::runtimes::ExecveSessionApproval;
|
||||
use crate::tools::runtimes::build_command_spec;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
@@ -227,9 +228,7 @@ impl CoreShellActionProvider {
|
||||
}
|
||||
|
||||
fn skill_escalation_execution(skill: &SkillMetadata) -> EscalationExecution {
|
||||
skill
|
||||
.permissions
|
||||
.as_ref()
|
||||
compile_permission_profile(skill.permission_profile.clone())
|
||||
.map(|permissions| {
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(
|
||||
EscalatedPermissions {
|
||||
@@ -240,13 +239,6 @@ impl CoreShellActionProvider {
|
||||
},
|
||||
))
|
||||
})
|
||||
.or_else(|| {
|
||||
skill
|
||||
.permission_profile
|
||||
.clone()
|
||||
.map(EscalationPermissions::PermissionProfile)
|
||||
.map(EscalationExecution::Permissions)
|
||||
})
|
||||
.unwrap_or(EscalationExecution::TurnDefault)
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +404,140 @@ async fn shell_zsh_fork_skill_without_permissions_inherits_turn_sandbox() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Empty skill permissions should behave like no skill override and inherit the
|
||||
/// turn sandbox instead of forcing an explicit read-only skill sandbox.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn shell_zsh_fork_skill_with_empty_permissions_inherits_turn_sandbox() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let Some(runtime) = zsh_fork_runtime("zsh-fork empty skill permissions test")? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
|
||||
let outside_path = outside_dir
|
||||
.path()
|
||||
.join("zsh-fork-skill-empty-permissions.txt");
|
||||
let outside_path_quoted = shlex::try_join([outside_path.to_string_lossy().as_ref()])?;
|
||||
let script_contents = format!(
|
||||
"#!/bin/sh\nprintf '%s' allowed > {outside_path_quoted}\ncat {outside_path_quoted}\n"
|
||||
);
|
||||
let outside_path_for_hook = outside_path.clone();
|
||||
let script_contents_for_hook = script_contents.clone();
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = build_zsh_fork_test(
|
||||
&server,
|
||||
runtime,
|
||||
AskForApproval::OnRequest,
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
move |home| {
|
||||
let _ = fs::remove_file(&outside_path_for_hook);
|
||||
write_skill_with_shell_script_contents(
|
||||
home,
|
||||
"mbolin-test-skill",
|
||||
"sandboxed.sh",
|
||||
&script_contents_for_hook,
|
||||
)
|
||||
.unwrap();
|
||||
write_skill_metadata(home, "mbolin-test-skill", "permissions: {}\n").unwrap();
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (script_path_str, command) = skill_script_command(&test, "sandboxed.sh")?;
|
||||
|
||||
let first_call_id = "zsh-fork-skill-empty-permissions-1";
|
||||
let first_arguments = shell_command_arguments(&command)?;
|
||||
let first_mocks = mount_function_call_agent_response(
|
||||
&server,
|
||||
first_call_id,
|
||||
&first_arguments,
|
||||
"shell_command",
|
||||
)
|
||||
.await;
|
||||
|
||||
submit_turn_with_policies(
|
||||
&test,
|
||||
"use $mbolin-test-skill",
|
||||
AskForApproval::OnRequest,
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let approval = wait_for_exec_approval_request(&test)
|
||||
.await
|
||||
.expect("expected exec approval request before completion");
|
||||
assert_eq!(approval.call_id, first_call_id);
|
||||
assert_eq!(approval.command, vec![script_path_str.clone()]);
|
||||
assert_eq!(approval.additional_permissions, None);
|
||||
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_turn_complete(&test).await;
|
||||
|
||||
let first_output = first_mocks
|
||||
.completion
|
||||
.single_request()
|
||||
.function_call_output(first_call_id)["output"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
assert!(
|
||||
first_output.contains("allowed"),
|
||||
"expected empty skill permissions to inherit full-access turn sandbox, got output: {first_output:?}"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&outside_path)?, "allowed");
|
||||
|
||||
let second_call_id = "zsh-fork-skill-empty-permissions-2";
|
||||
let second_arguments = shell_command_arguments(&command)?;
|
||||
let second_mocks = mount_function_call_agent_response(
|
||||
&server,
|
||||
second_call_id,
|
||||
&second_arguments,
|
||||
"shell_command",
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = fs::remove_file(&outside_path);
|
||||
|
||||
submit_turn_with_policies(
|
||||
&test,
|
||||
"use $mbolin-test-skill",
|
||||
AskForApproval::OnRequest,
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cached_approval = wait_for_exec_approval_request(&test).await;
|
||||
assert!(
|
||||
cached_approval.is_none(),
|
||||
"expected second run to reuse the cached session approval"
|
||||
);
|
||||
|
||||
let second_output = second_mocks
|
||||
.completion
|
||||
.single_request()
|
||||
.function_call_output(second_call_id)["output"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
assert!(
|
||||
second_output.contains("allowed"),
|
||||
"expected cached empty-permissions skill approval to inherit the turn sandbox, got output: {second_output:?}"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&outside_path)?, "allowed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The validation to focus on is: writes to the skill-approved folder succeed,
|
||||
/// and writes to an unrelated folder fail, both before and after cached approval.
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -1539,7 +1539,6 @@ mod tests {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: PathBuf::from("test-skill"),
|
||||
scope: SkillScope::User,
|
||||
}]),
|
||||
|
||||
@@ -191,7 +191,6 @@ fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> SkillMetadata {
|
||||
}),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
}
|
||||
|
||||
@@ -936,7 +936,6 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: repo_skill_path,
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -948,7 +947,6 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: user_skill_path.clone(),
|
||||
scope: SkillScope::User,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user