diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 464b7d72a2..c59d99e3bb 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2288,8 +2288,8 @@ dependencies = [ "async-trait", "base64 0.22.1", "codex-app-server-protocol", - "codex-exec-server", "codex-execpolicy", + "codex-executor-fs", "codex-features", "codex-git-utils", "codex-model-provider-info", @@ -2609,6 +2609,7 @@ dependencies = [ "bytes", "codex-app-server-protocol", "codex-client", + "codex-executor-fs", "codex-protocol", "codex-sandboxing", "codex-test-binary-support", @@ -2668,6 +2669,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "codex-executor-fs" +version = "0.0.0" +dependencies = [ + "async-trait", + "codex-protocol", + "codex-utils-absolute-path", + "serde", + "tokio", +] + [[package]] name = "codex-experimental-api-macros" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 85c947aea2..9437431617 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -36,6 +36,7 @@ members = [ "hooks", "secrets", "exec", + "executor-fs", "exec-server", "execpolicy", "execpolicy-legacy", @@ -142,6 +143,7 @@ codex-core-plugins = { path = "core-plugins" } codex-core-skills = { path = "core-skills" } codex-device-key = { path = "device-key" } codex-exec = { path = "exec" } +codex-executor-fs = { path = "executor-fs" } codex-exec-server = { path = "exec-server" } codex-execpolicy = { path = "execpolicy" } codex-experimental-api-macros = { path = "codex-experimental-api-macros" } diff --git a/codex-rs/config/Cargo.toml b/codex-rs/config/Cargo.toml index 3c7e5a8296..0a4cd05c34 100644 --- a/codex-rs/config/Cargo.toml +++ b/codex-rs/config/Cargo.toml @@ -16,7 +16,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } codex-app-server-protocol = { workspace = true } -codex-exec-server = { workspace = true } +codex-executor-fs = { workspace = true } codex-execpolicy = { workspace = true } codex-features = { workspace = true } codex-git-utils = { workspace = true } diff --git a/codex-rs/config/src/loader/layer_io.rs b/codex-rs/config/src/loader/layer_io.rs index 773a71f3bf..d941404f3b 100644 --- a/codex-rs/config/src/loader/layer_io.rs +++ b/codex-rs/config/src/loader/layer_io.rs @@ -5,7 +5,7 @@ use super::macos::load_managed_admin_config_layer; use crate::diagnostics::config_error_from_toml; use crate::diagnostics::io_error_from_config_error; use crate::state::LoaderOverrides; -use codex_exec_server::ExecutorFileSystem; +use codex_executor_fs::ExecutorFileSystem; use codex_utils_absolute_path::AbsolutePathBuf; use std::io; use std::path::Path; diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index 6375490354..4ca7a25c89 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -25,7 +25,7 @@ use crate::state::LoaderOverrides; use crate::thread_config::ThreadConfigContext; use crate::thread_config::ThreadConfigLoader; use codex_app_server_protocol::ConfigLayerSource; -use codex_exec_server::ExecutorFileSystem; +use codex_executor_fs::ExecutorFileSystem; use codex_git_utils::resolve_root_git_project_for_trust; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 21701d5188..14d0ad5398 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -17,6 +17,7 @@ base64 = { workspace = true } bytes = { workspace = true } codex-app-server-protocol = { workspace = true } codex-client = { workspace = true } +codex-executor-fs = { workspace = true } codex-protocol = { workspace = true } codex-sandboxing = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index fde69d6421..c668631601 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -1,186 +1,9 @@ -use async_trait::async_trait; -use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::PermissionProfile; -use codex_protocol::models::SandboxEnforcement; -use codex_protocol::permissions::FileSystemPath; -use codex_protocol::permissions::FileSystemSandboxKind; -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 std::path::Path; -use tokio::io; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct CreateDirectoryOptions { - pub recursive: bool, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct RemoveOptions { - pub recursive: bool, - pub force: bool, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct CopyOptions { - pub recursive: bool, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FileMetadata { - pub is_directory: bool, - pub is_file: bool, - pub is_symlink: bool, - pub created_at_ms: i64, - pub modified_at_ms: i64, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ReadDirectoryEntry { - pub file_name: String, - pub is_directory: bool, - pub is_file: bool, -} - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FileSystemSandboxContext { - pub permissions: PermissionProfile, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cwd: Option, - pub windows_sandbox_level: WindowsSandboxLevel, - #[serde(default)] - pub windows_sandbox_private_desktop: bool, - #[serde(default)] - pub use_legacy_landlock: bool, -} - -impl FileSystemSandboxContext { - pub fn from_legacy_sandbox_policy(sandbox_policy: SandboxPolicy, cwd: AbsolutePathBuf) -> Self { - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &cwd); - let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), - &file_system_sandbox_policy, - NetworkSandboxPolicy::from(&sandbox_policy), - ); - Self::from_permission_profile_with_cwd(permissions, cwd) - } - - 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: AbsolutePathBuf, - ) -> Self { - Self::from_permissions_and_cwd(permissions, Some(cwd)) - } - - fn from_permissions_and_cwd( - permissions: PermissionProfile, - cwd: Option, - ) -> Self { - Self { - permissions, - cwd, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - windows_sandbox_private_desktop: false, - use_legacy_landlock: false, - } - } - - pub fn should_run_in_sandbox(&self) -> bool { - let file_system_policy = self.permissions.file_system_sandbox_policy(); - matches!(file_system_policy.kind, FileSystemSandboxKind::Restricted) - && !file_system_policy.has_full_disk_write_access() - } - - pub(crate) fn drop_cwd_if_unused(mut self) -> Self { - let file_system_policy = self.permissions.file_system_sandbox_policy(); - if !file_system_policy_has_cwd_dependent_entries(&file_system_policy) { - self.cwd = None; - } - self - } -} - -pub(crate) 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; - -#[async_trait] -pub trait ExecutorFileSystem: Send + Sync { - async fn read_file( - &self, - path: &AbsolutePathBuf, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult>; - - /// Reads a file and decodes it as UTF-8 text. - async fn read_file_text( - &self, - path: &AbsolutePathBuf, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult { - let bytes = self.read_file(path, sandbox).await?; - String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) - } - - async fn write_file( - &self, - path: &AbsolutePathBuf, - contents: Vec, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; - - async fn create_directory( - &self, - path: &AbsolutePathBuf, - create_directory_options: CreateDirectoryOptions, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; - - async fn get_metadata( - &self, - path: &AbsolutePathBuf, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult; - - async fn read_directory( - &self, - path: &AbsolutePathBuf, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult>; - - async fn remove( - &self, - path: &AbsolutePathBuf, - remove_options: RemoveOptions, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; - - async fn copy( - &self, - source_path: &AbsolutePathBuf, - destination_path: &AbsolutePathBuf, - copy_options: CopyOptions, - sandbox: Option<&FileSystemSandboxContext>, - ) -> FileSystemResult<()>; -} +pub use codex_executor_fs::CopyOptions; +pub use codex_executor_fs::CreateDirectoryOptions; +pub use codex_executor_fs::ExecutorFileSystem; +pub use codex_executor_fs::FileMetadata; +pub use codex_executor_fs::FileSystemResult; +pub use codex_executor_fs::FileSystemSandboxContext; +pub use codex_executor_fs::ReadDirectoryEntry; +pub use codex_executor_fs::RemoveOptions; +pub(crate) use codex_executor_fs::file_system_policy_has_cwd_dependent_entries; diff --git a/codex-rs/executor-fs/BUILD.bazel b/codex-rs/executor-fs/BUILD.bazel new file mode 100644 index 0000000000..47ac04e00f --- /dev/null +++ b/codex-rs/executor-fs/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "executor-fs", + crate_name = "codex_executor_fs", +) diff --git a/codex-rs/executor-fs/Cargo.toml b/codex-rs/executor-fs/Cargo.toml new file mode 100644 index 0000000000..5d3958af23 --- /dev/null +++ b/codex-rs/executor-fs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "codex-executor-fs" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +async-trait = { workspace = true } +codex-protocol = { workspace = true } +codex-utils-absolute-path = { workspace = true } +serde = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["io-util"] } diff --git a/codex-rs/executor-fs/src/lib.rs b/codex-rs/executor-fs/src/lib.rs new file mode 100644 index 0000000000..cf89ee1dc9 --- /dev/null +++ b/codex-rs/executor-fs/src/lib.rs @@ -0,0 +1,188 @@ +use async_trait::async_trait; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxKind; +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 std::path::Path; +use tokio::io; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CreateDirectoryOptions { + pub recursive: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RemoveOptions { + pub recursive: bool, + pub force: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CopyOptions { + pub recursive: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FileMetadata { + pub is_directory: bool, + pub is_file: bool, + pub is_symlink: bool, + pub created_at_ms: i64, + pub modified_at_ms: i64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReadDirectoryEntry { + pub file_name: String, + pub is_directory: bool, + pub is_file: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileSystemSandboxContext { + pub permissions: PermissionProfile, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cwd: Option, + pub windows_sandbox_level: WindowsSandboxLevel, + #[serde(default)] + pub windows_sandbox_private_desktop: bool, + #[serde(default)] + pub use_legacy_landlock: bool, +} + +impl FileSystemSandboxContext { + pub fn from_legacy_sandbox_policy(sandbox_policy: SandboxPolicy, cwd: AbsolutePathBuf) -> Self { + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &cwd); + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(&sandbox_policy), + ); + Self::from_permission_profile_with_cwd(permissions, cwd) + } + + 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: AbsolutePathBuf, + ) -> Self { + Self::from_permissions_and_cwd(permissions, Some(cwd)) + } + + fn from_permissions_and_cwd( + permissions: PermissionProfile, + cwd: Option, + ) -> Self { + Self { + permissions, + cwd, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + use_legacy_landlock: false, + } + } + + pub fn should_run_in_sandbox(&self) -> bool { + let file_system_policy = self.permissions.file_system_sandbox_policy(); + matches!(file_system_policy.kind, FileSystemSandboxKind::Restricted) + && !file_system_policy.has_full_disk_write_access() + } + + pub fn drop_cwd_if_unused(mut self) -> Self { + let file_system_policy = self.permissions.file_system_sandbox_policy(); + if !file_system_policy_has_cwd_dependent_entries(&file_system_policy) { + self.cwd = None; + } + self + } +} + +pub 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; + +#[async_trait] +/// Async filesystem interface shared by config loading, exec-server, and other +/// workspace crates. Implementations are expected to honor the provided +/// sandbox context for each operation and return standard `tokio::io` errors. +pub trait ExecutorFileSystem: Send + Sync { + async fn read_file( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult>; + + async fn read_file_text( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult { + let bytes = self.read_file(path, sandbox).await?; + String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + } + + async fn write_file( + &self, + path: &AbsolutePathBuf, + contents: Vec, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult<()>; + + async fn create_directory( + &self, + path: &AbsolutePathBuf, + create_directory_options: CreateDirectoryOptions, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult<()>; + + async fn get_metadata( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult; + + async fn read_directory( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult>; + + async fn remove( + &self, + path: &AbsolutePathBuf, + remove_options: RemoveOptions, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult<()>; + + async fn copy( + &self, + source_path: &AbsolutePathBuf, + destination_path: &AbsolutePathBuf, + copy_options: CopyOptions, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult<()>; +}