diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 547a05774a71..ec8a4ad5c471 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2589,6 +2589,7 @@ dependencies = [ "codex-extension-api", "codex-features", "codex-feedback", + "codex-file-system", "codex-git-utils", "codex-home", "codex-hooks", @@ -3001,6 +3002,7 @@ name = "codex-file-system" version = "0.0.0" dependencies = [ "codex-protocol", + "codex-utils-absolute-path", "codex-utils-path-uri", "serde", ] diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 3273379f9461..d16e8d9cbe7e 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -38,6 +38,7 @@ codex-exec-server = { workspace = true } codex-extension-api = { workspace = true } codex-features = { workspace = true } codex-feedback = { workspace = true } +codex-file-system = { workspace = true } codex-login = { workspace = true } codex-memories-read = { workspace = true } codex-mcp = { workspace = true } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index f22b1fb628df..504ff3e17d58 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -54,7 +54,6 @@ use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; use codex_exec_server::Environment; use codex_exec_server::EnvironmentManager; -use codex_exec_server::FileSystemSandboxContext; use codex_extension_api::ExtensionDataInit; use codex_extension_api::LoadedUserInstructions; use codex_extension_api::PromptSlot; diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 70b085fc5db5..b0242cf8484a 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -4,6 +4,7 @@ use crate::agents_md::LoadedAgentsMd; use crate::config::GhostSnapshotConfig; use crate::environment_selection::ResolvedTurnEnvironments; use codex_core_skills::HostLoadedSkills; +use codex_file_system::FileSystemSandboxContext; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::SessionId; @@ -341,7 +342,7 @@ impl TurnContext { network_sandbox_policy, ); FileSystemSandboxContext { - permissions, + permissions: permissions.into(), cwd: Some(cwd.clone()), windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index c9a5e316e4dd..9ad9d284c786 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -97,7 +97,7 @@ impl ApplyPatchRuntime { let permissions = effective_permission_profile(attempt.permissions, req.additional_permissions.as_ref()); Some(FileSystemSandboxContext { - permissions, + permissions: permissions.into(), cwd: Some(attempt.sandbox_cwd.clone()), windows_sandbox_level: attempt.windows_sandbox_level, windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 044002f44653..4100b662ee25 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -233,7 +233,12 @@ async fn file_system_sandbox_context_uses_active_attempt() { ); let expected_permissions = PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy); - assert_eq!(sandbox.permissions, expected_permissions); + let native_permissions: PermissionProfile = sandbox + .permissions + .clone() + .try_into() + .expect("native sandbox permissions"); + assert_eq!(native_permissions, expected_permissions); assert_eq!( sandbox.cwd, Some(codex_utils_path_uri::PathUri::from_abs_path(&path)) diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index a99a9496babf..cd2cc38cb3b9 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -66,7 +66,11 @@ impl FileSystemSandboxRunner { request: FsHelperRequest, ) -> Result { let cwd = sandbox_cwd(sandbox)?; - let mut file_system_policy = sandbox.permissions.file_system_sandbox_policy(); + let native_permissions: PermissionProfile = + sandbox.permissions.clone().try_into().map_err(|err| { + invalid_request(format!("invalid sandbox permission path URI: {err}")) + })?; + let mut file_system_policy = native_permissions.file_system_sandbox_policy(); let helper_read_roots = if sandbox.use_legacy_landlock { Vec::new() } else { @@ -80,7 +84,7 @@ impl FileSystemSandboxRunner { normalize_file_system_policy_root_aliases(&mut file_system_policy); let network_policy = NetworkSandboxPolicy::Restricted; let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( - sandbox.permissions.enforcement(), + native_permissions.enforcement(), &file_system_policy, network_policy, ); @@ -578,7 +582,7 @@ mod tests { }, access: FileSystemAccessMode::Write, }]); - let sandbox_context = crate::FileSystemSandboxContext::from_permission_profile( + let sandbox_context = codex_file_system::FileSystemSandboxContext::from_permission_profile( PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), ); @@ -650,7 +654,7 @@ mod tests { policy: &FileSystemSandboxPolicy, cwd: PathUri, ) -> crate::FileSystemSandboxContext { - crate::FileSystemSandboxContext::from_permission_profile_with_cwd( + codex_file_system::FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::from_runtime_permissions(policy, NetworkSandboxPolicy::Restricted), cwd, ) diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index ee116ef08e2b..49b96b9e5013 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use crate::FileSystemSandboxContext; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use codex_file_system::FileSystemSandboxContext; use codex_protocol::config_types::ShellEnvironmentPolicyInherit; use codex_utils_path_uri::PathUri; use serde::Deserialize; @@ -454,7 +454,7 @@ mod base64_bytes { mod tests { use super::FsReadFileParams; use super::HttpRequestParams; - use crate::FileSystemSandboxContext; + use codex_file_system::FileSystemSandboxContext; use codex_protocol::models::PermissionProfile; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; @@ -465,18 +465,19 @@ mod tests { .expect("current directory") .join("legacy-file.txt"); let legacy_cwd = std::env::current_dir().expect("current directory"); - let expected_sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + let native_sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::default(), PathUri::from_path(&legacy_cwd).expect("cwd URI"), ); let mut legacy_sandbox = - serde_json::to_value(&expected_sandbox).expect("sandbox should serialize"); + serde_json::to_value(&native_sandbox).expect("sandbox should serialize"); legacy_sandbox["cwd"] = serde_json::json!(legacy_cwd.to_string_lossy()); let params: FsReadFileParams = serde_json::from_value(serde_json::json!({ "path": legacy_path.to_string_lossy(), "sandbox": legacy_sandbox, })) .expect("legacy absolute path should deserialize"); + let expected_sandbox = native_sandbox; let expected = FsReadFileParams { path: PathUri::from_path(legacy_path).expect("path URI"), sandbox: Some(expected_sandbox.clone()), diff --git a/codex-rs/exec-server/tests/file_system/shared.rs b/codex-rs/exec-server/tests/file_system/shared.rs index 27bdb22d85dc..37870812e68d 100644 --- a/codex-rs/exec-server/tests/file_system/shared.rs +++ b/codex-rs/exec-server/tests/file_system/shared.rs @@ -30,7 +30,8 @@ fn sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths() - std::fs::create_dir_all(&git_dir)?; let sandbox = workspace_write_sandbox(writable_dir.clone()); - let policy = sandbox.permissions.file_system_sandbox_policy(); + let permissions: PermissionProfile = sandbox.permissions.try_into()?; + let policy = permissions.file_system_sandbox_policy(); let cwd = absolute_path(writable_dir.clone()); let writable_roots = policy.get_writable_roots_with_cwd(cwd.as_path()); let writable_dir = absolute_path(std::fs::canonicalize(writable_dir)?); @@ -534,19 +535,21 @@ async fn file_system_sandboxed_write_allows_additional_write_root( Some(vec![absolute_path(writable_dir)]), )), }; + let native_permissions: PermissionProfile = sandbox.permissions.clone().try_into()?; let file_system_policy = effective_file_system_sandbox_policy( - &sandbox.permissions.file_system_sandbox_policy(), + &native_permissions.file_system_sandbox_policy(), Some(&additional_permissions), ); let network_policy = effective_network_sandbox_policy( - sandbox.permissions.network_sandbox_policy(), + native_permissions.network_sandbox_policy(), Some(&additional_permissions), ); sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement( - sandbox.permissions.enforcement(), + native_permissions.enforcement(), &file_system_policy, network_policy, - ); + ) + .into(); file_system .write_file( diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index 66bb9ec71c79..591c699f5241 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -342,9 +342,10 @@ async fn image_url( environment: &ToolEnvironment, ) -> Result { let path_uri = PathUri::from_abs_path(path); + let sandbox = environment.file_system_sandbox_context.clone(); let bytes = environment .file_system - .read_file(&path_uri, Some(&environment.file_system_sandbox_context)) + .read_file(&path_uri, Some(&sandbox)) .await .map_err(|error| { FunctionCallError::RespondToModel(format!( diff --git a/codex-rs/file-system/Cargo.toml b/codex-rs/file-system/Cargo.toml index d1beefbc8b08..087e5da6c0a1 100644 --- a/codex-rs/file-system/Cargo.toml +++ b/codex-rs/file-system/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] codex-protocol = { workspace = true } +codex-utils-absolute-path = { workspace = true } codex-utils-path-uri = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index 429c2ead50a1..a3acdda230a9 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -1,4 +1,5 @@ use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemPath; @@ -7,6 +8,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use std::future::Future; use std::io; @@ -50,7 +52,7 @@ pub struct ReadDirectoryEntry { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct FileSystemSandboxContext { - pub permissions: PermissionProfile, + pub permissions: PermissionProfile, #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option, pub windows_sandbox_level: WindowsSandboxLevel, @@ -73,25 +75,32 @@ impl FileSystemSandboxContext { &sandbox_policy, &native_cwd, ); - let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), - &file_system_sandbox_policy, - NetworkSandboxPolicy::from(&sandbox_policy), - ); + let permissions = + PermissionProfile::::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(&sandbox_policy), + ); Ok(Self::from_permission_profile_with_cwd(permissions, cwd)) } - pub fn from_permission_profile(permissions: PermissionProfile) -> Self { + pub fn from_permission_profile(permissions: PermissionProfile) -> Self { Self::from_permissions_and_cwd(permissions, /*cwd*/ None) } - pub fn from_permission_profile_with_cwd(permissions: PermissionProfile, cwd: PathUri) -> Self { + pub fn from_permission_profile_with_cwd( + permissions: PermissionProfile, + cwd: PathUri, + ) -> Self { Self::from_permissions_and_cwd(permissions, Some(cwd)) } - fn from_permissions_and_cwd(permissions: PermissionProfile, cwd: Option) -> Self { + fn from_permissions_and_cwd( + permissions: PermissionProfile, + cwd: Option, + ) -> Self { Self { - permissions, + permissions: permissions.into(), cwd, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -100,14 +109,36 @@ impl FileSystemSandboxContext { } pub fn should_run_in_sandbox(&self) -> bool { - let file_system_policy = self.permissions.file_system_sandbox_policy(); + let Ok(permissions) = + PermissionProfile::::try_from(self.permissions.clone()) + else { + // A sandbox context for another host must not select the unsandboxed filesystem. + return true; + }; + let file_system_policy = permissions.file_system_sandbox_policy(); matches!(file_system_policy.kind, FileSystemSandboxKind::Restricted) && !file_system_policy.has_full_disk_write_access() } pub fn has_cwd_dependent_permissions(&self) -> bool { - let file_system_policy = self.permissions.file_system_sandbox_policy(); - file_system_policy_has_cwd_dependent_entries(&file_system_policy) + match &self.permissions { + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { entries, .. }, + .. + } => entries.iter().any(|entry| match &entry.path { + FileSystemPath::GlobPattern { pattern } => !Path::new(pattern).is_absolute(), + FileSystemPath::Special { + value: FileSystemSpecialPath::ProjectRoots { .. }, + } => true, + FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => false, + }), + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + .. + } + | PermissionProfile::Disabled + | PermissionProfile::External { .. } => false, + } } pub fn drop_cwd_if_unused(mut self) -> Self { @@ -118,21 +149,6 @@ impl FileSystemSandboxContext { } } -fn file_system_policy_has_cwd_dependent_entries( - file_system_policy: &FileSystemSandboxPolicy, -) -> bool { - file_system_policy - .entries - .iter() - .any(|entry| match &entry.path { - FileSystemPath::GlobPattern { pattern } => !Path::new(pattern).is_absolute(), - FileSystemPath::Special { - value: FileSystemSpecialPath::ProjectRoots { .. }, - } => true, - FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => false, - }) -} - pub type FileSystemResult = io::Result; /// Future returned by [`ExecutorFileSystem`] operations. diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 9d4647b30b21..c39a2ac3803b 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -23,6 +23,7 @@ use crate::protocol::SandboxPolicy; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_image::ImageProcessingError; +use codex_utils_path_uri::PathUri; use schemars::JsonSchema; use crate::mcp::CallToolResult; @@ -62,22 +63,59 @@ impl SandboxPermissions { } } -#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)] -pub struct FileSystemPermissions { - pub entries: Vec, +#[derive(Debug, Clone, Eq, Hash, PartialEq, JsonSchema, TS)] +pub struct FileSystemPermissions { + pub entries: Vec>, pub glob_scan_max_depth: Option, } -pub type LegacyReadWriteRoots = (Option>, Option>); +impl From> for FileSystemPermissions { + fn from(value: FileSystemPermissions) -> Self { + FileSystemPermissions { + entries: value + .entries + .into_iter() + .map(FileSystemSandboxEntry::::from) + .collect(), + glob_scan_max_depth: value.glob_scan_max_depth, + } + } +} + +impl TryFrom> for FileSystemPermissions { + type Error = io::Error; + + fn try_from(value: FileSystemPermissions) -> Result { + Ok(FileSystemPermissions { + entries: value + .entries + .into_iter() + .map(FileSystemSandboxEntry::::try_from) + .collect::>()?, + glob_scan_max_depth: value.glob_scan_max_depth, + }) + } +} -impl FileSystemPermissions { +impl Default for FileSystemPermissions { + fn default() -> Self { + Self { + entries: Vec::new(), + glob_scan_max_depth: None, + } + } +} + +pub type LegacyReadWriteRoots = + (Option>, Option>); +impl FileSystemPermissions { pub fn is_empty(&self) -> bool { self.entries.is_empty() } pub fn from_read_write_roots( - read: Option>, - write: Option>, + read: Option>, + write: Option>, ) -> Self { let mut entries = Vec::new(); if let Some(read) = read { @@ -98,21 +136,25 @@ impl FileSystemPermissions { } } - pub fn explicit_path_entries( - &self, - ) -> impl Iterator { + pub fn explicit_path_entries(&self) -> impl Iterator { self.entries.iter().filter_map(|entry| match &entry.path { FileSystemPath::Path { path } => Some((path, entry.access)), FileSystemPath::GlobPattern { .. } | FileSystemPath::Special { .. } => None, }) } - pub fn legacy_read_write_roots(&self) -> Option { + pub fn legacy_read_write_roots(&self) -> Option> + where + PathType: Clone, + { self.as_legacy_permissions() .map(|legacy| (legacy.read, legacy.write)) } - fn as_legacy_permissions(&self) -> Option { + fn as_legacy_permissions(&self) -> Option> + where + PathType: Clone, + { if self.glob_scan_max_depth.is_some() { return None; } @@ -140,30 +182,35 @@ impl FileSystemPermissions { #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -struct LegacyFileSystemPermissions { +#[serde(bound(deserialize = "PathType: Deserialize<'de>"))] +struct LegacyFileSystemPermissions { #[serde(default, skip_serializing_if = "Option::is_none")] - read: Option>, + read: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - write: Option>, + write: Option>, } #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -struct CanonicalFileSystemPermissions { +#[serde(bound(deserialize = "PathType: Deserialize<'de>"))] +struct CanonicalFileSystemPermissions { #[serde(default, skip_serializing_if = "Vec::is_empty")] - entries: Vec, + entries: Vec>, #[serde(default, skip_serializing_if = "Option::is_none")] glob_scan_max_depth: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] -enum FileSystemPermissionsDe { - Canonical(CanonicalFileSystemPermissions), - Legacy(LegacyFileSystemPermissions), +enum FileSystemPermissionsDe { + Canonical(CanonicalFileSystemPermissions), + Legacy(LegacyFileSystemPermissions), } -impl Serialize for FileSystemPermissions { +impl Serialize for FileSystemPermissions +where + PathType: Clone + Serialize, +{ fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -180,7 +227,10 @@ impl Serialize for FileSystemPermissions { } } -impl<'de> Deserialize<'de> for FileSystemPermissions { +impl<'de, PathType> Deserialize<'de> for FileSystemPermissions +where + PathType: Deserialize<'de>, +{ fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -253,12 +303,12 @@ impl SandboxEnforcement { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] #[ts(tag = "type")] -pub enum ManagedFileSystemPermissions { +pub enum ManagedFileSystemPermissions { /// Apply a managed filesystem sandbox from the listed entries. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] Restricted { - entries: Vec, + entries: Vec>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] glob_scan_max_depth: Option, @@ -267,6 +317,50 @@ pub enum ManagedFileSystemPermissions { Unrestricted, } +impl From> for ManagedFileSystemPermissions { + fn from(value: ManagedFileSystemPermissions) -> Self { + match value { + ManagedFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => ManagedFileSystemPermissions::Restricted { + entries: entries + .into_iter() + .map(FileSystemSandboxEntry::::from) + .collect(), + glob_scan_max_depth, + }, + ManagedFileSystemPermissions::Unrestricted => { + ManagedFileSystemPermissions::Unrestricted + } + } + } +} + +impl TryFrom> + for ManagedFileSystemPermissions +{ + type Error = io::Error; + + fn try_from(value: ManagedFileSystemPermissions) -> Result { + Ok(match value { + ManagedFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => ManagedFileSystemPermissions::Restricted { + entries: entries + .into_iter() + .map(FileSystemSandboxEntry::::try_from) + .collect::>()?, + glob_scan_max_depth, + }, + ManagedFileSystemPermissions::Unrestricted => { + ManagedFileSystemPermissions::Unrestricted + } + }) + } +} + impl ManagedFileSystemPermissions { fn from_sandbox_policy(file_system_sandbox_policy: &FileSystemSandboxPolicy) -> Self { match file_system_sandbox_policy.kind { @@ -311,12 +405,12 @@ pub const BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS: &str = ":danger-full-a #[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] #[ts(tag = "type")] -pub enum PermissionProfile { +pub enum PermissionProfile { /// Codex owns sandbox construction for this profile. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] Managed { - file_system: ManagedFileSystemPermissions, + file_system: ManagedFileSystemPermissions, network: NetworkSandboxPolicy, }, /// Do not apply an outer sandbox. @@ -327,6 +421,40 @@ pub enum PermissionProfile { External { network: NetworkSandboxPolicy }, } +impl From> for PermissionProfile { + fn from(value: PermissionProfile) -> Self { + match value { + PermissionProfile::Managed { + file_system, + network, + } => PermissionProfile::Managed { + file_system: file_system.into(), + network, + }, + PermissionProfile::Disabled => PermissionProfile::Disabled, + PermissionProfile::External { network } => PermissionProfile::External { network }, + } + } +} + +impl TryFrom> for PermissionProfile { + type Error = io::Error; + + fn try_from(value: PermissionProfile) -> Result { + Ok(match value { + PermissionProfile::Managed { + file_system, + network, + } => PermissionProfile::Managed { + file_system: file_system.try_into()?, + network, + }, + PermissionProfile::Disabled => PermissionProfile::Disabled, + PermissionProfile::External { network } => PermissionProfile::External { network }, + }) + } +} + /// Metadata for the named or implicit built-in permissions profile that /// produced the active `PermissionProfile`. /// @@ -360,7 +488,7 @@ impl ActivePermissionProfile { } } -impl Default for PermissionProfile { +impl Default for PermissionProfile { fn default() -> Self { Self::Managed { file_system: ManagedFileSystemPermissions::Restricted { @@ -548,10 +676,10 @@ impl PermissionProfile { #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] -enum TaggedPermissionProfile { +enum TaggedPermissionProfile { #[serde(rename_all = "snake_case")] Managed { - file_system: ManagedFileSystemPermissions, + file_system: ManagedFileSystemPermissions, network: NetworkSandboxPolicy, }, Disabled, @@ -561,8 +689,8 @@ enum TaggedPermissionProfile { }, } -impl From for PermissionProfile { - fn from(value: TaggedPermissionProfile) -> Self { +impl From> for PermissionProfile { + fn from(value: TaggedPermissionProfile) -> Self { match value { TaggedPermissionProfile::Managed { file_system, @@ -581,16 +709,22 @@ impl From for PermissionProfile { /// represented enforcement explicitly. #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields)] -struct LegacyPermissionProfile { +struct LegacyPermissionProfile { network: Option, - file_system: Option, + file_system: Option>, } -impl From for PermissionProfile { - fn from(value: LegacyPermissionProfile) -> Self { - let file_system_sandbox_policy = value.file_system.as_ref().map_or_else( - || FileSystemSandboxPolicy::restricted(Vec::new()), - FileSystemSandboxPolicy::from, +impl From> for PermissionProfile { + fn from(value: LegacyPermissionProfile) -> Self { + let file_system = value.file_system.map_or_else( + || ManagedFileSystemPermissions::Restricted { + entries: Vec::new(), + glob_scan_max_depth: None, + }, + |permissions| ManagedFileSystemPermissions::Restricted { + entries: permissions.entries, + glob_scan_max_depth: permissions.glob_scan_max_depth, + }, ); let network_sandbox_policy = if value .network @@ -602,18 +736,24 @@ impl From for PermissionProfile { } else { NetworkSandboxPolicy::Restricted }; - Self::from_runtime_permissions(&file_system_sandbox_policy, network_sandbox_policy) + Self::Managed { + file_system, + network: network_sandbox_policy, + } } } #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] -enum PermissionProfileDe { - Tagged(TaggedPermissionProfile), - Legacy(LegacyPermissionProfile), +enum PermissionProfileDe { + Tagged(TaggedPermissionProfile), + Legacy(LegacyPermissionProfile), } -impl<'de> Deserialize<'de> for PermissionProfile { +impl<'de, PathType> Deserialize<'de> for PermissionProfile +where + PathType: Deserialize<'de>, +{ fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index e5fbc5a0bba3..5573f488e803 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; +use codex_utils_path_uri::PathUri; use globset::GlobBuilder; use globset::GlobMatcher; use schemars::JsonSchema; @@ -176,11 +177,31 @@ impl FileSystemSpecialPath { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] -pub struct FileSystemSandboxEntry { - pub path: FileSystemPath, +pub struct FileSystemSandboxEntry { + pub path: FileSystemPath, pub access: FileSystemAccessMode, } +impl From> for FileSystemSandboxEntry { + fn from(value: FileSystemSandboxEntry) -> Self { + FileSystemSandboxEntry { + path: value.path.into(), + access: value.access, + } + } +} + +impl TryFrom> for FileSystemSandboxEntry { + type Error = io::Error; + + fn try_from(value: FileSystemSandboxEntry) -> Result { + Ok(FileSystemSandboxEntry { + path: value.path.try_into()?, + access: value.access, + }) + } +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS, )] @@ -338,9 +359,9 @@ enum InvalidDenyReadGlobBehavior { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] #[ts(tag = "type")] -pub enum FileSystemPath { +pub enum FileSystemPath { Path { - path: AbsolutePathBuf, + path: PathType, }, /// A git-style glob pattern. Pattern entries currently support /// FileSystemAccessMode::Deny only. @@ -352,6 +373,32 @@ pub enum FileSystemPath { }, } +impl From> for FileSystemPath { + fn from(value: FileSystemPath) -> Self { + match value { + FileSystemPath::Path { path } => FileSystemPath::Path { + path: PathUri::from_abs_path(&path), + }, + FileSystemPath::GlobPattern { pattern } => FileSystemPath::GlobPattern { pattern }, + FileSystemPath::Special { value } => FileSystemPath::Special { value }, + } + } +} + +impl TryFrom> for FileSystemPath { + type Error = io::Error; + + fn try_from(value: FileSystemPath) -> Result { + Ok(match value { + FileSystemPath::Path { path } => FileSystemPath::Path { + path: path.to_abs_path()?, + }, + FileSystemPath::GlobPattern { pattern } => FileSystemPath::GlobPattern { pattern }, + FileSystemPath::Special { value } => FileSystemPath::Special { value }, + }) + } +} + const PROJECT_ROOTS_GLOB_PATTERN_PREFIX: &str = "codex-project-roots://"; pub fn project_roots_glob_pattern(subpath: &Path) -> String { diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index fbb63dceae5e..3b8d9db7cf97 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -370,10 +370,10 @@ mod tests { ); assert_eq!(metadata.cwd, PathBuf::from("/child/worktree")); + let permission_profile: PermissionProfile = PermissionProfile::Disabled; assert_eq!( metadata.sandbox_policy, - serde_json::to_string(&PermissionProfile::Disabled) - .expect("serialize permission profile") + serde_json::to_string(&permission_profile).expect("serialize permission profile") ); assert_eq!(metadata.approval_mode, "never"); } diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index 24bc32993cdf..4778dd69d9c4 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -731,8 +731,9 @@ mod tests { builder.model_provider = Some(config.default_model_provider_id.clone()); builder.cwd = home.path().to_path_buf(); let mut metadata = builder.build(config.default_model_provider_id.as_str()); + let permission_profile: PermissionProfile = PermissionProfile::Disabled; metadata.sandbox_policy = - serde_json::to_string(&PermissionProfile::Disabled).expect("serialize profile"); + serde_json::to_string(&permission_profile).expect("serialize profile"); runtime .upsert_thread(&metadata) .await diff --git a/codex-rs/thread-store/src/local/update_thread_metadata.rs b/codex-rs/thread-store/src/local/update_thread_metadata.rs index 9bde79916763..820cb68918f7 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -886,9 +886,10 @@ mod tests { .await .expect("sqlite metadata read") .expect("sqlite metadata"); + let permission_profile: PermissionProfile = PermissionProfile::Disabled; assert_eq!( metadata.sandbox_policy, - serde_json::to_string(&PermissionProfile::Disabled).expect("serialize profile") + serde_json::to_string(&permission_profile).expect("serialize profile") ); }