mod environment_accessor; mod exec_permission_profile_serde; mod find_up; mod local_logs; mod nfs; use bytes::Bytes; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::config_types::WindowsSandboxProxySettingsMode; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxEntryMissingPathBehavior; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicyContext; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::sandbox::SandboxOverride; use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; pub use environment_accessor::EnvironmentAccess; pub use environment_accessor::EnvironmentAccessExt; pub use environment_accessor::EnvironmentAccessKey; pub use environment_accessor::FileSystemEnvironmentAccessor; pub use find_up::FindUpErrorPolicy; pub use find_up::find_nearest_ancestor_with_markers; pub use find_up::find_nearest_native_ancestor_with_markers; use futures::Stream; use futures::stream::BoxStream; pub use local_logs::LocalLogsDir; pub use nfs::is_nfs; use serde::Deserialize; use serde::Serialize; use std::future::Future; use std::io; use std::num::NonZeroUsize; use std::path::Path; use std::pin::Pin; use std::task::Context; use std::task::Poll; /// Maximum chunk size returned by [`ExecutorFileSystem::read_file_stream`]. pub const FILE_READ_CHUNK_SIZE: usize = 1024 * 1024; /// Maximum decoded chunk size accepted by a streamed filesystem write. pub const FILE_WRITE_CHUNK_SIZE: usize = 1024 * 1024; /// Maximum accepted directory depth for a filesystem walk. pub const MAX_WALK_DEPTH: usize = 64; /// Maximum accepted directory count, including the walk root. pub const MAX_WALK_DIRECTORIES: usize = 10_000; /// Maximum accepted number of directory entries to examine. pub const MAX_WALK_ENTRIES: usize = 50_000; /// Maximum estimated size of a walk response. pub const MAX_WALK_RESPONSE_BYTES: usize = 4 * 1024 * 1024; /// Per-entry or per-error overhead charged to the walk response budget. pub const WALK_RESPONSE_ITEM_OVERHEAD_BYTES: usize = 64; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ReadFileOptions { pub follow_symlinks: bool, } impl Default for ReadFileOptions { fn default() -> Self { Self { follow_symlinks: true, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WriteFileOptions { pub follow_symlinks: bool, } impl Default for WriteFileOptions { fn default() -> Self { Self { follow_symlinks: true, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct GetMetadataOptions { pub follow_symlinks: bool, } impl Default for GetMetadataOptions { fn default() -> Self { Self { follow_symlinks: true, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CreateDirectoryOptions { pub recursive: bool, pub follow_symlinks: bool, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct RemoveOptions { pub recursive: bool, pub force: bool, pub follow_symlinks: 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, /// Size in bytes. pub size: u64, 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, } /// Bounds for a recursive filesystem walk. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct WalkOptions { /// Maximum directory depth below the root that may be traversed. pub max_depth: usize, /// Maximum number of directories that may be traversed, including the root. pub max_directories: usize, /// Maximum number of directory entries that may be examined. pub max_entries: usize, /// Whether directory symlinks should be followed. pub follow_directory_symlinks: bool, /// Whether directories whose names start with `.` should be returned but not traversed. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub prune_hidden_directories: bool, } /// Type of a filesystem entry returned by a walk. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub enum WalkEntryKind { Directory, File, } /// One entry returned by a walk. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct WalkEntry { pub path: PathUri, pub kind: WalkEntryKind, } /// A descendant that could not be inspected during a walk. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct WalkError { pub path: PathUri, pub message: String, } /// Entries and recoverable errors collected by a bounded walk. #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct WalkOutcome { pub entries: Vec, pub errors: Vec, pub truncated: bool, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ExecFileSystemPath { Path { path: PathUri }, GlobPattern { pattern: String }, Special { value: FileSystemSpecialPath }, } impl From for ExecFileSystemPath { fn from(value: FileSystemPath) -> Self { match value { FileSystemPath::Path { path } => Self::Path { path }, FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, FileSystemPath::Special { value } => Self::Special { value }, } } } impl From for FileSystemPath { fn from(value: ExecFileSystemPath) -> Self { match value { ExecFileSystemPath::Path { path } => Self::Path { path }, ExecFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, ExecFileSystemPath::Special { value } => Self::Special { value }, } } } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct ExecFileSystemSandboxEntry { pub path: ExecFileSystemPath, pub access: FileSystemAccessMode, #[serde(default, skip_serializing_if = "Option::is_none")] pub missing_path_behavior: Option, } impl From for ExecFileSystemSandboxEntry { fn from(value: FileSystemSandboxEntry) -> Self { Self { path: value.path.into(), access: value.access, missing_path_behavior: value.missing_path_behavior, } } } impl From for FileSystemSandboxEntry { fn from(value: ExecFileSystemSandboxEntry) -> Self { Self { path: value.path.into(), access: value.access, missing_path_behavior: value.missing_path_behavior, } } } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ExecManagedFileSystemPermissions { Restricted { entries: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] glob_scan_max_depth: Option, }, Unrestricted, } impl From for ExecManagedFileSystemPermissions { fn from(value: ManagedFileSystemPermissions) -> Self { match value { ManagedFileSystemPermissions::Restricted { entries, glob_scan_max_depth, } => Self::Restricted { entries: entries.into_iter().map(Into::into).collect(), glob_scan_max_depth, }, ManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, } } } impl From for ManagedFileSystemPermissions { fn from(value: ExecManagedFileSystemPermissions) -> Self { match value { ExecManagedFileSystemPermissions::Restricted { entries, glob_scan_max_depth, } => Self::Restricted { entries: entries.into_iter().map(Into::into).collect(), glob_scan_max_depth, }, ExecManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, } } } /// Executor permission profile whose explicit filesystem paths serialize as file URIs. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ExecPermissionProfile { Managed { file_system: ExecManagedFileSystemPermissions, network: NetworkSandboxPolicy, }, Disabled, External { network: NetworkSandboxPolicy, }, } impl From for ExecPermissionProfile { fn from(value: PermissionProfile) -> Self { match value { PermissionProfile::Managed { file_system, network, } => Self::Managed { file_system: file_system.into(), network, }, PermissionProfile::Disabled => Self::Disabled, PermissionProfile::External { network } => Self::External { network }, } } } impl From for PermissionProfile { fn from(value: ExecPermissionProfile) -> Self { match value { ExecPermissionProfile::Managed { file_system, network, } => Self::Managed { file_system: file_system.into(), network, }, ExecPermissionProfile::Disabled => Self::Disabled, ExecPermissionProfile::External { network } => Self::External { network }, } } } /// Windows sandbox choice encoded in executor RPCs. /// /// The serialized field retains its legacy `windowsSandboxLevel` name for compatibility, but MXC /// is a sandbox implementation rather than a RestrictedToken level. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum WindowsSandboxSelection { #[default] Disabled, RestrictedToken, Elevated, Mxc, } impl From for WindowsSandboxSelection { fn from(level: WindowsSandboxLevel) -> Self { match level { WindowsSandboxLevel::Disabled => Self::Disabled, WindowsSandboxLevel::RestrictedToken => Self::RestrictedToken, WindowsSandboxLevel::Elevated => Self::Elevated, } } } /// Filesystem sandbox policy and the selected executor paths needed to interpret it. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FileSystemSandboxContext { /// Serializes paths as executor file URIs instead of the profile's default native paths. #[serde(with = "exec_permission_profile_serde")] pub permissions: PermissionProfile, /// Controller-selected override for observations, never an authorization to widen permissions. #[serde(default, skip_serializing_if = "SandboxOverride::is_no_override")] pub sandbox_override: SandboxOverride, /// Working directory on the selected executor used to interpret sandbox permissions. /// Required even for absolute permissions; a process may use a different working directory. pub cwd: PathUri, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub workspace_roots: Vec, /// Executor-local user home used to resolve home-relative policy paths. #[serde(default, skip_serializing_if = "Option::is_none")] pub user_home_dir: Option, /// Executor-local default directories used to resolve `:tmpdir` policy entries. #[serde(default, skip_serializing_if = "Option::is_none")] pub temporary_directories: Option>, #[serde(rename = "windowsSandboxLevel")] pub windows_sandbox_selection: WindowsSandboxSelection, #[serde(default, skip_serializing_if = "Option::is_none")] pub windows_sandbox_proxy_settings_mode: Option, #[serde(default)] pub use_legacy_landlock: bool, } impl FileSystemSandboxContext { pub fn from_legacy_sandbox_policy( sandbox_policy: SandboxPolicy, cwd: PathUri, ) -> io::Result { // Legacy policy projection materializes native roots, so convert at the receiving-host // boundary while retaining the URI in the resulting sandbox context. let native_cwd = cwd.to_abs_path()?; let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &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), ); Ok(Self::from_permission_profile(permissions, cwd)) } pub fn from_permission_profile(permissions: PermissionProfile, cwd: PathUri) -> Self { Self { permissions, sandbox_override: SandboxOverride::NoOverride, workspace_roots: vec![cwd.clone()], cwd, user_home_dir: None, temporary_directories: None, windows_sandbox_selection: WindowsSandboxSelection::Disabled, windows_sandbox_proxy_settings_mode: None, use_legacy_landlock: false, } } /// Whether filesystem reads need a platform sandbox on the selected executor. pub fn should_read_from_sandbox(&self) -> bool { !self .permissions .file_system_sandbox_policy() .has_full_disk_read_access_for_convention(self.cwd.infer_path_convention()) } /// Whether filesystem writes need a platform sandbox on the selected executor. pub fn should_write_into_sandbox(&self) -> bool { !self .permissions .file_system_sandbox_policy() .has_full_disk_write_access_for_convention(self.cwd.infer_path_convention()) } /// Whether this context selects either supported Windows sandbox implementation. pub fn windows_sandbox_is_requested(&self) -> bool { self.windows_sandbox_selection != WindowsSandboxSelection::Disabled } /// Checks that explicit permission paths can be enforced by the current host. An /// orchestrator can still construct this context with paths belonging to another executor. pub fn validate_file_system_paths_for_current_host(&self) -> io::Result<()> { if let PermissionProfile::Managed { file_system: ManagedFileSystemPermissions::Restricted { entries, .. }, .. } = &self.permissions { for entry in entries { if let FileSystemPath::Path { path } = &entry.path { path.to_abs_path().map_err(|error| { io::Error::new( error.kind(), format!("invalid sandbox permission path URI: {error}"), ) })?; } } } Ok(()) } /// Borrows the executor-owned paths needed to interpret filesystem policy entries. pub fn policy_context(&self) -> FileSystemSandboxPolicyContext<'_> { FileSystemSandboxPolicyContext { cwd: &self.cwd, workspace_roots: &self.workspace_roots, user_home_dir: self.user_home_dir.as_ref(), temporary_directories: self.temporary_directories.as_deref(), } } } /// Filesystem RPC wire context; older clients can omit the policy cwd before executor resolution. #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WireFileSystemSandboxContext { permissions: ExecPermissionProfile, #[serde(default, skip_serializing_if = "SandboxOverride::is_no_override")] sandbox_override: SandboxOverride, #[serde(default, skip_serializing_if = "Option::is_none")] cwd: Option, #[serde(default, skip_serializing_if = "Option::is_none")] policy_context: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] workspace_roots: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] user_home_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] temporary_directories: Option>, #[serde(rename = "windowsSandboxLevel")] windows_sandbox_selection: WindowsSandboxSelection, #[serde(default, skip_serializing_if = "Option::is_none")] windows_sandbox_proxy_settings_mode: Option, #[serde(default)] use_legacy_landlock: bool, } /// New filesystem clients provide these paths independently of the legacy helper launch cwd. #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct WireFileSystemPolicyContext { #[serde(default, skip_serializing_if = "Option::is_none")] cwd: Option, #[serde(default)] workspace_roots: Vec, } impl From for WireFileSystemSandboxContext { fn from(sandbox: FileSystemSandboxContext) -> Self { let FileSystemSandboxContext { permissions, sandbox_override, cwd, workspace_roots, user_home_dir, temporary_directories, windows_sandbox_selection, windows_sandbox_proxy_settings_mode, use_legacy_landlock, } = sandbox; let permissions = ExecPermissionProfile::from(permissions); // Older filesystem clients sent cwd and roots only when permissions needed them; old // executors also use that cwd to launch their helper. The explicit context is independent. let legacy_needs_cwd = match &permissions { ExecPermissionProfile::Managed { file_system: ExecManagedFileSystemPermissions::Restricted { entries, .. }, .. } => entries.iter().any(|entry| match &entry.path { ExecFileSystemPath::GlobPattern { pattern } => match cwd.infer_path_convention() { Some(convention) => LegacyAppPathString::from_string(pattern) .to_path_uri(convention) .is_err(), None => true, }, ExecFileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { .. }, } => true, ExecFileSystemPath::Path { .. } | ExecFileSystemPath::Special { .. } => false, }), ExecPermissionProfile::Managed { file_system: ExecManagedFileSystemPermissions::Unrestricted, .. } | ExecPermissionProfile::Disabled | ExecPermissionProfile::External { .. } => false, }; Self { permissions, sandbox_override, cwd: legacy_needs_cwd.then(|| cwd.clone()), workspace_roots: if legacy_needs_cwd { workspace_roots.clone() } else { Vec::new() }, policy_context: Some(WireFileSystemPolicyContext { cwd: Some(cwd), workspace_roots, }), user_home_dir, temporary_directories, windows_sandbox_selection, windows_sandbox_proxy_settings_mode, use_legacy_landlock, } } } impl WireFileSystemSandboxContext { /// Returns the policy cwd supplied by the client, falling back to the legacy cwd field. pub fn cwd(&self) -> Option<&PathUri> { match &self.policy_context { Some(policy_context) => policy_context.cwd.as_ref().or(self.cwd.as_ref()), None => self.cwd.as_ref(), } } /// Returns whether a legacy filesystem policy needs the client's cwd to be interpreted. pub fn requires_cwd(&self) -> bool { let ExecPermissionProfile::Managed { file_system: ExecManagedFileSystemPermissions::Restricted { entries, .. }, .. } = &self.permissions else { return false; }; entries.iter().any(|entry| match &entry.path { ExecFileSystemPath::GlobPattern { pattern } => !Path::new(pattern).is_absolute(), ExecFileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { .. }, } => true, ExecFileSystemPath::Path { .. } | ExecFileSystemPath::Special { .. } => false, }) } /// Constructs the strict context after executor ingress has resolved the policy cwd. pub fn into_context(self, cwd: PathUri) -> FileSystemSandboxContext { FileSystemSandboxContext { permissions: self.permissions.into(), sandbox_override: self.sandbox_override, cwd, workspace_roots: match self.policy_context { Some(policy_context) => policy_context.workspace_roots, None => self.workspace_roots, }, user_home_dir: self.user_home_dir, temporary_directories: self.temporary_directories, windows_sandbox_selection: self.windows_sandbox_selection, windows_sandbox_proxy_settings_mode: self.windows_sandbox_proxy_settings_mode, use_legacy_landlock: self.use_legacy_landlock, } } } pub type FileSystemResult = io::Result; /// Future returned by [`ExecutorFileSystem`] operations. pub type ExecutorFileSystemFuture<'a, T> = Pin> + Send + 'a>>; /// Stream of immutable chunks read from an [`ExecutorFileSystem`]. pub struct FileSystemReadStream { inner: Pin> + Send + 'static>>, } impl FileSystemReadStream { /// Wraps a filesystem byte stream. pub fn new(stream: impl Stream> + Send + 'static) -> Self { Self { inner: Box::pin(stream), } } } impl Stream for FileSystemReadStream { type Item = FileSystemResult; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.inner.as_mut().poll_next(cx) } } /// Abstract filesystem access used by components that may operate locally or via /// a remote environment. pub trait ExecutorFileSystem: Send + Sync { /// Resolves a path within this filesystem. fn canonicalize<'a>( &'a self, path: &'a PathUri, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, PathUri>; fn read_file<'a>( &'a self, path: &'a PathUri, options: ReadFileOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, Vec>; /// Reads a file as a stream of chunks no larger than [`FILE_READ_CHUNK_SIZE`]. fn read_file_stream<'a>( &'a self, path: &'a PathUri, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, FileSystemReadStream>; /// Replaces a regular file from a stream of bounded chunks. /// /// Accepts any sendable byte stream, boxed to keep this trait object-safe. /// Input chunks must be nonempty and no larger than [`FILE_WRITE_CHUNK_SIZE`]. /// Returns the number of bytes written after closing the target. Parents must already exist. /// Failure or cancellation may leave a truncated or partial target. /// /// Cancellation is best effort: dropping the future stops consuming input and issuing new /// blocks, but an in-flight open or write may still truncate or modify the target. Dropping /// does not wait for that I/O or remote handle cleanup, so a subsequent replacement may overlap /// with the abandoned operation. /// /// Local and streaming-capable remote filesystems write one chunk at a time. For compatibility, /// a remote filesystem may buffer the entire input and send one write to an older executor. /// The default implementation returns `Unsupported` without consuming the input. fn write_file_stream<'a>( &'a self, path: &'a PathUri, contents: BoxStream<'a, FileSystemResult>, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, u64> { Box::pin(async move { let _ = (path, contents, sandbox); Err(io::Error::new( io::ErrorKind::Unsupported, "filesystem does not support streaming writes", )) }) } /// Reads a file and decodes it as UTF-8 text. fn read_file_text<'a>( &'a self, path: &'a PathUri, options: ReadFileOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, String> { Box::pin(async move { let bytes = self.read_file(path, options, sandbox).await?; String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) }) } fn write_file<'a>( &'a self, path: &'a PathUri, contents: Vec, options: WriteFileOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, ()>; fn create_directory<'a>( &'a self, path: &'a PathUri, create_directory_options: CreateDirectoryOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, ()>; fn get_metadata<'a>( &'a self, path: &'a PathUri, options: GetMetadataOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, FileMetadata>; fn read_directory<'a>( &'a self, path: &'a PathUri, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, Vec>; /// Recursively lists descendants, optionally following directory symlinks. fn walk<'a>( &'a self, path: &'a PathUri, options: WalkOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, WalkOutcome>; fn remove<'a>( &'a self, path: &'a PathUri, remove_options: RemoveOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, ()>; fn copy<'a>( &'a self, source_path: &'a PathUri, destination_path: &'a PathUri, copy_options: CopyOptions, sandbox: Option<&'a FileSystemSandboxContext>, ) -> ExecutorFileSystemFuture<'a, ()>; }