Add sandbox support to filesystem APIs (#16751)

## Summary
- add optional `sandboxPolicy` support to the app-server filesystem
request surface
- thread sandbox-aware filesystem options through app-server and
exec-server adapters
- enforce sandboxed read/write access in the filesystem abstraction with
focused local and remote coverage

## Validation
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-exec-server file_system`
- `cargo test -p codex-app-server suite::v2::fs`

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-04-08 12:10:48 -07:00
committed by GitHub
co-authored by Codex
parent e003f84e1e
commit f383cc980d
12 changed files with 1271 additions and 96 deletions
+168 -10
View File
@@ -1,13 +1,7 @@
use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::FsCopyParams;
use codex_app_server_protocol::FsCreateDirectoryParams;
use codex_app_server_protocol::FsGetMetadataParams;
use codex_app_server_protocol::FsReadDirectoryParams;
use codex_app_server_protocol::FsReadFileParams;
use codex_app_server_protocol::FsRemoveParams;
use codex_app_server_protocol::FsWriteFileParams;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use tokio::io;
use tracing::trace;
@@ -21,6 +15,13 @@ use crate::FileMetadata;
use crate::FileSystemResult;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
use crate::protocol::FsReadDirectoryParams;
use crate::protocol::FsReadFileParams;
use crate::protocol::FsRemoveParams;
use crate::protocol::FsWriteFileParams;
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
const NOT_FOUND_ERROR_CODE: i64 = -32004;
@@ -43,7 +44,32 @@ impl ExecutorFileSystem for RemoteFileSystem {
trace!("remote fs read_file");
let response = self
.client
.fs_read_file(FsReadFileParams { path: path.clone() })
.fs_read_file(FsReadFileParams {
path: path.clone(),
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
STANDARD.decode(response.data_base64).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("remote fs/readFile returned invalid base64 dataBase64: {err}"),
)
})
}
async fn read_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<Vec<u8>> {
trace!("remote fs read_file_with_sandbox_policy");
let response = self
.client
.fs_read_file(FsReadFileParams {
path: path.clone(),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
STANDARD.decode(response.data_base64).map_err(|err| {
@@ -60,6 +86,25 @@ impl ExecutorFileSystem for RemoteFileSystem {
.fs_write_file(FsWriteFileParams {
path: path.clone(),
data_base64: STANDARD.encode(contents),
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn write_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs write_file_with_sandbox_policy");
self.client
.fs_write_file(FsWriteFileParams {
path: path.clone(),
data_base64: STANDARD.encode(contents),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
@@ -76,6 +121,25 @@ impl ExecutorFileSystem for RemoteFileSystem {
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(options.recursive),
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn create_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
create_directory_options: CreateDirectoryOptions,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs create_directory_with_sandbox_policy");
self.client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(create_directory_options.recursive),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
@@ -86,7 +150,32 @@ impl ExecutorFileSystem for RemoteFileSystem {
trace!("remote fs get_metadata");
let response = self
.client
.fs_get_metadata(FsGetMetadataParams { path: path.clone() })
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
Ok(FileMetadata {
is_directory: response.is_directory,
is_file: response.is_file,
created_at_ms: response.created_at_ms,
modified_at_ms: response.modified_at_ms,
})
}
async fn get_metadata_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<FileMetadata> {
trace!("remote fs get_metadata_with_sandbox_policy");
let response = self
.client
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
Ok(FileMetadata {
@@ -104,7 +193,35 @@ impl ExecutorFileSystem for RemoteFileSystem {
trace!("remote fs read_directory");
let response = self
.client
.fs_read_directory(FsReadDirectoryParams { path: path.clone() })
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
Ok(response
.entries
.into_iter()
.map(|entry| ReadDirectoryEntry {
file_name: entry.file_name,
is_directory: entry.is_directory,
is_file: entry.is_file,
})
.collect())
}
async fn read_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
trace!("remote fs read_directory_with_sandbox_policy");
let response = self
.client
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
Ok(response
@@ -125,6 +242,26 @@ impl ExecutorFileSystem for RemoteFileSystem {
path: path.clone(),
recursive: Some(options.recursive),
force: Some(options.force),
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn remove_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
remove_options: RemoveOptions,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs remove_with_sandbox_policy");
self.client
.fs_remove(FsRemoveParams {
path: path.clone(),
recursive: Some(remove_options.recursive),
force: Some(remove_options.force),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
@@ -143,6 +280,27 @@ impl ExecutorFileSystem for RemoteFileSystem {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: options.recursive,
sandbox_policy: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn copy_with_sandbox_policy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
copy_options: CopyOptions,
sandbox_policy: Option<&SandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs copy_with_sandbox_policy");
self.client
.fs_copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: copy_options.recursive,
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;