Files
codex/codex-rs/exec-server/src/file_system.rs
T
Michael Bolin 36f8bb4ffa exec-server: carry filesystem sandbox profiles (#18276)
## Why

The exec-server still needs platform sandbox inputs, but the migration
should preserve the `PermissionProfile` that produced them. Keeping only
the derived legacy sandbox map would keep `SandboxPolicy` as the
effective abstraction and would make full-disk vs. restricted profiles
harder to preserve as the permissions stack starts round-tripping
profiles.

`PermissionProfile` entries can also be cwd-sensitive (`:cwd`,
`:project_roots`, relative globs), so the exec-server must carry the
request sandbox cwd instead of resolving those entries against the
long-lived exec-server process cwd.

## What changed

`FileSystemSandboxContext` now carries `permissions: PermissionProfile`
plus an optional `cwd`:

- removed `sandboxPolicy`, `sandboxPolicyCwd`,
`fileSystemSandboxPolicy`, and `additionalPermissions`
- added `permissions` and `cwd`
- kept the platform knobs `windowsSandboxLevel`,
`windowsSandboxPrivateDesktop`, and `useLegacyLandlock`

Core turn and apply-patch paths populate the context from the active
runtime permissions and request cwd. Exec-server derives platform
`SandboxPolicy`/`FileSystemSandboxPolicy` at the filesystem boundary,
adds helper runtime reads there, and rejects cwd-dependent profiles that
arrive without a cwd.

The legacy `FileSystemSandboxContext::new(SandboxPolicy)` constructor
now preserves the old workspace-write conversion semantics for
compatibility tests/callers.

## Verification

- `cargo test -p codex-exec-server`
- `cargo test -p codex-exec-server sandbox_cwd -- --nocapture`
- `cargo test -p codex-exec-server
sandbox_context_new_preserves_legacy_workspace_write_read_only_subpaths
-- --nocapture`
- `cargo test -p codex-core --lib
file_system_sandbox_context_uses_active_attempt -- --nocapture`
2026-04-21 20:22:28 -07:00

169 lines
5.2 KiB
Rust

use async_trait::async_trait;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
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<AbsolutePathBuf>,
pub windows_sandbox_level: WindowsSandboxLevel,
#[serde(default)]
pub windows_sandbox_private_desktop: bool,
#[serde(default)]
pub use_legacy_landlock: bool,
}
impl FileSystemSandboxContext {
pub fn new(sandbox_policy: SandboxPolicy) -> Self {
if let Ok(cwd) = AbsolutePathBuf::current_dir() {
Self::from_legacy_sandbox_policy(sandbox_policy, cwd)
} else {
let permissions = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::from(&sandbox_policy),
NetworkSandboxPolicy::from(&sandbox_policy),
);
Self::from_permission_profile(permissions)
}
}
pub fn from_legacy_sandbox_policy(sandbox_policy: SandboxPolicy, cwd: AbsolutePathBuf) -> Self {
let permissions = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()),
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<AbsolutePathBuf>,
) -> 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 type FileSystemResult<T> = io::Result<T>;
#[async_trait]
pub trait ExecutorFileSystem: Send + Sync {
async fn read_file(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>>;
/// Reads a file and decodes it as UTF-8 text.
async fn read_file_text(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<String> {
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<u8>,
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<FileMetadata>;
async fn read_directory(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
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<()>;
}