exec-server: sandbox filesystem helper ops

This commit is contained in:
starr-openai
2026-04-03 18:22:30 -07:00
parent 8d19646861
commit 17bb4317da
13 changed files with 1130 additions and 24 deletions
@@ -9,10 +9,27 @@ struct ExecServerArgs {
default_value = codex_exec_server::DEFAULT_LISTEN_URL
)]
listen: String,
// Hidden entrypoint used by LocalFileSystem to re-run the same fs logic
// in a one-shot helper process after sandbox transformation.
#[arg(long = "internal-fs-op", hide = true)]
internal_fs_op: bool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args = ExecServerArgs::parse();
codex_exec_server::run_main_with_listen_url(&args.listen).await
fn main() -> anyhow::Result<()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
runtime.block_on(async move {
let args = ExecServerArgs::parse();
if args.internal_fs_op {
// The helper mode reads a single structured fs request from stdin,
// executes it, writes the response to stdout, and exits.
codex_exec_server::run_internal_fs_op().await?;
} else {
codex_exec_server::run_main_with_listen_url(&args.listen)
.await
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
}
Ok(())
})
}
+66 -2
View File
@@ -1,5 +1,8 @@
use async_trait::async_trait;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use tokio::io;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -18,7 +21,7 @@ pub struct CopyOptions {
pub recursive: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FileMetadata {
pub is_directory: bool,
pub is_file: bool,
@@ -26,7 +29,7 @@ pub struct FileMetadata {
pub modified_at_ms: i64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReadDirectoryEntry {
pub file_name: String,
pub is_directory: bool,
@@ -39,27 +42,88 @@ pub type FileSystemResult<T> = io::Result<T>;
pub trait ExecutorFileSystem: Send + Sync {
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>>;
async fn read_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<Vec<u8>> {
self.read_file(path).await
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()>;
async fn write_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
self.write_file(path, contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
) -> FileSystemResult<()>;
async fn create_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
self.create_directory(path, options).await
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata>;
async fn get_metadata_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<FileMetadata> {
self.get_metadata(path).await
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
async fn read_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
self.read_directory(path).await
}
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()>;
async fn remove_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
options: RemoveOptions,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
self.remove(path, options).await
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
) -> FileSystemResult<()>;
async fn copy_with_sandbox_policy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
_sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
self.copy(source_path, destination_path, options).await
}
}
+2
View File
@@ -11,6 +11,7 @@ mod protocol;
mod remote_file_system;
mod remote_process;
mod rpc;
mod sandboxed_file_system;
mod server;
pub use client::ExecServerClient;
@@ -61,6 +62,7 @@ pub use protocol::TerminateResponse;
pub use protocol::WriteParams;
pub use protocol::WriteResponse;
pub use protocol::WriteStatus;
pub use sandboxed_file_system::run_internal_fs_op;
pub use server::DEFAULT_LISTEN_URL;
pub use server::ExecServerListenUrlParseError;
pub use server::run_main;
@@ -1,4 +1,5 @@
use async_trait::async_trait;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Component;
use std::path::Path;
@@ -14,6 +15,7 @@ use crate::FileMetadata;
use crate::FileSystemResult;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use crate::sandboxed_file_system::SandboxedFileSystem;
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
@@ -33,10 +35,37 @@ impl ExecutorFileSystem for LocalFileSystem {
tokio::fs::read(path.as_path()).await
}
async fn read_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<Vec<u8>> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.read_file(path)
.await;
}
self.read_file(path).await
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
tokio::fs::write(path.as_path(), contents).await
}
async fn write_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.write_file(path, contents)
.await;
}
self.write_file(path, contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
@@ -50,6 +79,20 @@ impl ExecutorFileSystem for LocalFileSystem {
Ok(())
}
async fn create_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.create_directory(path, options)
.await;
}
self.create_directory(path, options).await
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
let metadata = tokio::fs::metadata(path.as_path()).await?;
Ok(FileMetadata {
@@ -60,6 +103,19 @@ impl ExecutorFileSystem for LocalFileSystem {
})
}
async fn get_metadata_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<FileMetadata> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.get_metadata(path)
.await;
}
self.get_metadata(path).await
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
@@ -77,6 +133,19 @@ impl ExecutorFileSystem for LocalFileSystem {
Ok(entries)
}
async fn read_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.read_directory(path)
.await;
}
self.read_directory(path).await
}
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
match tokio::fs::symlink_metadata(path.as_path()).await {
Ok(metadata) => {
@@ -97,6 +166,20 @@ impl ExecutorFileSystem for LocalFileSystem {
}
}
async fn remove_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
options: RemoveOptions,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.remove(path, options)
.await;
}
self.remove(path, options).await
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
@@ -147,6 +230,21 @@ impl ExecutorFileSystem for LocalFileSystem {
.await
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
}
async fn copy_with_sandbox_policy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
if let Some(sandbox_policy) = sandbox_policy {
return SandboxedFileSystem::new(sandbox_policy.clone())
.copy(source_path, destination_path, options)
.await;
}
self.copy(source_path, destination_path, options).await
}
}
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
+90 -3
View File
@@ -8,6 +8,7 @@ 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::permissions::FileSystemSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use tokio::io;
use tracing::trace;
@@ -39,10 +40,22 @@ impl RemoteFileSystem {
#[async_trait]
impl ExecutorFileSystem for RemoteFileSystem {
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>> {
self.read_file_with_sandbox_policy(path, /*sandbox_policy*/ None)
.await
}
async fn read_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<Vec<u8>> {
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: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
STANDARD.decode(response.data_base64).map_err(|err| {
@@ -54,11 +67,22 @@ impl ExecutorFileSystem for RemoteFileSystem {
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
self.write_file_with_sandbox_policy(path, contents, /*sandbox_policy*/ None)
.await
}
async fn write_file_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs write_file");
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)?;
@@ -69,12 +93,23 @@ impl ExecutorFileSystem for RemoteFileSystem {
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
) -> FileSystemResult<()> {
self.create_directory_with_sandbox_policy(path, options, /*sandbox_policy*/ None)
.await
}
async fn create_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs create_directory");
self.client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(options.recursive),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
@@ -82,10 +117,22 @@ impl ExecutorFileSystem for RemoteFileSystem {
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
self.get_metadata_with_sandbox_policy(path, /*sandbox_policy*/ None)
.await
}
async fn get_metadata_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<FileMetadata> {
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: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
Ok(FileMetadata {
@@ -99,11 +146,23 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
self.read_directory_with_sandbox_policy(path, /*sandbox_policy*/ None)
.await
}
async fn read_directory_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
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: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
Ok(response
@@ -118,12 +177,23 @@ impl ExecutorFileSystem for RemoteFileSystem {
}
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
self.remove_with_sandbox_policy(path, options, /*sandbox_policy*/ None)
.await
}
async fn remove_with_sandbox_policy(
&self,
path: &AbsolutePathBuf,
options: RemoveOptions,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs remove");
self.client
.fs_remove(FsRemoveParams {
path: path.clone(),
recursive: Some(options.recursive),
force: Some(options.force),
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
@@ -135,6 +205,22 @@ impl ExecutorFileSystem for RemoteFileSystem {
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
) -> FileSystemResult<()> {
self.copy_with_sandbox_policy(
source_path,
destination_path,
options,
/*sandbox_policy*/ None,
)
.await
}
async fn copy_with_sandbox_policy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
sandbox_policy: Option<&FileSystemSandboxPolicy>,
) -> FileSystemResult<()> {
trace!("remote fs copy");
self.client
@@ -142,6 +228,7 @@ impl ExecutorFileSystem for RemoteFileSystem {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: options.recursive,
sandbox_policy: sandbox_policy.cloned(),
})
.await
.map_err(map_remote_error)?;
@@ -0,0 +1,555 @@
use std::collections::HashMap;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxCommand;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecutorFileSystem;
use crate::FileMetadata;
use crate::FileSystemResult;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use crate::local_file_system::LocalFileSystem;
const INTERNAL_FS_OP_FLAG: &str = "--internal-fs-op";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum SandboxedFileSystemRequest {
ReadFile {
path: AbsolutePathBuf,
},
WriteFile {
path: AbsolutePathBuf,
data_base64: String,
},
CreateDirectory {
path: AbsolutePathBuf,
recursive: bool,
},
GetMetadata {
path: AbsolutePathBuf,
},
ReadDirectory {
path: AbsolutePathBuf,
},
Remove {
path: AbsolutePathBuf,
recursive: bool,
force: bool,
},
Copy {
source_path: AbsolutePathBuf,
destination_path: AbsolutePathBuf,
recursive: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum SandboxedFileSystemResponse {
Unit,
ReadFile { data_base64: String },
GetMetadata { metadata: FileMetadata },
ReadDirectory { entries: Vec<ReadDirectoryEntry> },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum SandboxedFileSystemEnvelope {
Ok(SandboxedFileSystemResponse),
Error(SandboxedFileSystemError),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SandboxedFileSystemError {
kind: SandboxedFileSystemErrorKind,
message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum SandboxedFileSystemErrorKind {
NotFound,
PermissionDenied,
AlreadyExists,
InvalidInput,
Unsupported,
Other,
}
impl SandboxedFileSystemErrorKind {
fn from_io_kind(kind: io::ErrorKind) -> Self {
match kind {
io::ErrorKind::NotFound => Self::NotFound,
io::ErrorKind::PermissionDenied => Self::PermissionDenied,
io::ErrorKind::AlreadyExists => Self::AlreadyExists,
io::ErrorKind::InvalidInput => Self::InvalidInput,
io::ErrorKind::Unsupported => Self::Unsupported,
_ => Self::Other,
}
}
fn to_io_kind(self) -> io::ErrorKind {
match self {
Self::NotFound => io::ErrorKind::NotFound,
Self::PermissionDenied => io::ErrorKind::PermissionDenied,
Self::AlreadyExists => io::ErrorKind::AlreadyExists,
Self::InvalidInput => io::ErrorKind::InvalidInput,
Self::Unsupported => io::ErrorKind::Unsupported,
Self::Other => io::ErrorKind::Other,
}
}
}
#[derive(Clone)]
pub(crate) struct SandboxedFileSystem {
sandbox_policy: FileSystemSandboxPolicy,
}
impl SandboxedFileSystem {
pub(crate) fn new(sandbox_policy: FileSystemSandboxPolicy) -> Self {
Self { sandbox_policy }
}
async fn execute_request_via_sandbox(
&self,
request: SandboxedFileSystemRequest,
) -> FileSystemResult<SandboxedFileSystemResponse> {
let prepared = prepare_helper_request(&self.sandbox_policy)?;
let request_bytes = serde_json::to_vec(&request).map_err(io::Error::other)?;
let (program, args) = prepared.command.split_first().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "helper command is empty")
})?;
let mut command = Command::new(program);
command.args(args);
command.current_dir(&prepared.cwd);
command.envs(&prepared.env);
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
#[cfg(unix)]
{
if let Some(arg0) = prepared.arg0.as_ref() {
command.arg0(arg0);
}
}
let mut child = command.spawn()?;
let Some(mut stdin) = child.stdin.take() else {
return Err(io::Error::other("sandbox helper stdin was not piped"));
};
stdin.write_all(&request_bytes).await?;
drop(stdin);
let output = child.wait_with_output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let message = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
"sandbox helper exited without error output".to_string()
};
return Err(io::Error::other(message));
}
let envelope: SandboxedFileSystemEnvelope =
serde_json::from_slice(&output.stdout).map_err(io::Error::other)?;
match envelope {
SandboxedFileSystemEnvelope::Ok(response) => Ok(response),
SandboxedFileSystemEnvelope::Error(error) => {
Err(io::Error::new(error.kind.to_io_kind(), error.message))
}
}
}
}
#[async_trait]
impl ExecutorFileSystem for SandboxedFileSystem {
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::ReadFile {
path: path.clone(),
})
.await?;
let SandboxedFileSystemResponse::ReadFile { data_base64 } = response else {
return Err(io::Error::other(
"sandbox helper returned unexpected response for read_file",
));
};
STANDARD.decode(data_base64).map_err(|err| {
io::Error::other(format!("sandbox helper returned invalid base64: {err}"))
})
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::WriteFile {
path: path.clone(),
data_base64: STANDARD.encode(contents),
})
.await?;
if !matches!(response, SandboxedFileSystemResponse::Unit) {
return Err(io::Error::other(
"sandbox helper returned unexpected response for write_file",
));
}
Ok(())
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
) -> FileSystemResult<()> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::CreateDirectory {
path: path.clone(),
recursive: options.recursive,
})
.await?;
if !matches!(response, SandboxedFileSystemResponse::Unit) {
return Err(io::Error::other(
"sandbox helper returned unexpected response for create_directory",
));
}
Ok(())
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::GetMetadata {
path: path.clone(),
})
.await?;
let SandboxedFileSystemResponse::GetMetadata { metadata } = response else {
return Err(io::Error::other(
"sandbox helper returned unexpected response for get_metadata",
));
};
Ok(metadata)
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::ReadDirectory {
path: path.clone(),
})
.await?;
let SandboxedFileSystemResponse::ReadDirectory { entries } = response else {
return Err(io::Error::other(
"sandbox helper returned unexpected response for read_directory",
));
};
Ok(entries)
}
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::Remove {
path: path.clone(),
recursive: options.recursive,
force: options.force,
})
.await?;
if !matches!(response, SandboxedFileSystemResponse::Unit) {
return Err(io::Error::other(
"sandbox helper returned unexpected response for remove",
));
}
Ok(())
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
) -> FileSystemResult<()> {
let response = self
.execute_request_via_sandbox(SandboxedFileSystemRequest::Copy {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: options.recursive,
})
.await?;
if !matches!(response, SandboxedFileSystemResponse::Unit) {
return Err(io::Error::other(
"sandbox helper returned unexpected response for copy",
));
}
Ok(())
}
}
pub async fn run_internal_fs_op() -> io::Result<()> {
let mut input = Vec::new();
tokio::io::stdin().read_to_end(&mut input).await?;
let request: SandboxedFileSystemRequest =
serde_json::from_slice(&input).map_err(io::Error::other)?;
let response = match execute_internal_request(request).await {
Ok(response) => SandboxedFileSystemEnvelope::Ok(response),
Err(err) => SandboxedFileSystemEnvelope::Error(SandboxedFileSystemError {
kind: SandboxedFileSystemErrorKind::from_io_kind(err.kind()),
message: err.to_string(),
}),
};
let bytes = serde_json::to_vec(&response).map_err(io::Error::other)?;
tokio::io::stdout().write_all(&bytes).await?;
Ok(())
}
async fn execute_internal_request(
request: SandboxedFileSystemRequest,
) -> FileSystemResult<SandboxedFileSystemResponse> {
let file_system = LocalFileSystem;
match request {
SandboxedFileSystemRequest::ReadFile { path } => {
let bytes = file_system.read_file(&path).await?;
Ok(SandboxedFileSystemResponse::ReadFile {
data_base64: STANDARD.encode(bytes),
})
}
SandboxedFileSystemRequest::WriteFile { path, data_base64 } => {
let bytes = STANDARD.decode(data_base64).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid writeFile base64 payload: {err}"),
)
})?;
file_system.write_file(&path, bytes).await?;
Ok(SandboxedFileSystemResponse::Unit)
}
SandboxedFileSystemRequest::CreateDirectory { path, recursive } => {
file_system
.create_directory(&path, CreateDirectoryOptions { recursive })
.await?;
Ok(SandboxedFileSystemResponse::Unit)
}
SandboxedFileSystemRequest::GetMetadata { path } => {
let metadata = file_system.get_metadata(&path).await?;
Ok(SandboxedFileSystemResponse::GetMetadata { metadata })
}
SandboxedFileSystemRequest::ReadDirectory { path } => {
let entries = file_system.read_directory(&path).await?;
Ok(SandboxedFileSystemResponse::ReadDirectory { entries })
}
SandboxedFileSystemRequest::Remove {
path,
recursive,
force,
} => {
file_system
.remove(&path, RemoveOptions { recursive, force })
.await?;
Ok(SandboxedFileSystemResponse::Unit)
}
SandboxedFileSystemRequest::Copy {
source_path,
destination_path,
recursive,
} => {
file_system
.copy(&source_path, &destination_path, CopyOptions { recursive })
.await?;
Ok(SandboxedFileSystemResponse::Unit)
}
}
}
fn prepare_helper_request(
sandbox_policy: &FileSystemSandboxPolicy,
) -> io::Result<SandboxExecRequest> {
let helper_exe = helper_executable_path()?;
let codex_linux_sandbox_exe = linux_sandbox_executable_path(&helper_exe);
let helper_cwd = std::env::current_dir()?;
let network_policy = NetworkSandboxPolicy::Enabled;
let helper_readable_roots =
helper_readable_roots(&helper_exe, codex_linux_sandbox_exe.as_ref());
let effective_file_system_policy = sandbox_policy
.clone()
.with_additional_readable_roots(helper_cwd.as_path(), &helper_readable_roots);
let legacy_policy = helper_legacy_policy(
&effective_file_system_policy,
network_policy,
helper_cwd.as_path(),
)?;
let manager = SandboxManager::new();
let sandbox = manager.select_initial(
&effective_file_system_policy,
network_policy,
codex_sandboxing::SandboxablePreference::Auto,
WindowsSandboxLevel::Disabled,
/*has_managed_network_requirements*/ false,
);
let command = SandboxCommand {
program: helper_exe.clone().into(),
args: vec![INTERNAL_FS_OP_FLAG.to_string()],
cwd: helper_cwd.clone(),
env: HashMap::new(),
additional_permissions: None,
};
manager
.transform(SandboxTransformRequest {
command,
policy: &legacy_policy,
file_system_policy: &effective_file_system_policy,
network_policy,
sandbox,
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: helper_cwd.as_path(),
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
})
.map_err(io::Error::other)
}
fn helper_readable_roots(
helper_exe: &Path,
codex_linux_sandbox_exe: Option<&PathBuf>,
) -> Vec<AbsolutePathBuf> {
let mut roots = Vec::new();
for path in
std::iter::once(helper_exe).chain(codex_linux_sandbox_exe.into_iter().map(PathBuf::as_path))
{
if let Some(parent) = path.parent()
&& let Ok(root) = AbsolutePathBuf::from_absolute_path(parent)
&& !roots.iter().any(|existing| existing == &root)
{
roots.push(root);
}
}
roots
}
fn helper_legacy_policy(
sandbox_policy: &FileSystemSandboxPolicy,
network_policy: NetworkSandboxPolicy,
helper_cwd: &Path,
) -> io::Result<SandboxPolicy> {
match sandbox_policy.to_legacy_sandbox_policy(network_policy, helper_cwd) {
Ok(policy) => Ok(policy),
Err(_) if sandbox_policy.needs_direct_runtime_enforcement(network_policy, helper_cwd) => {
Ok(SandboxPolicy::ExternalSandbox {
network_access: if network_policy.is_enabled() {
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
},
})
}
Err(err) => Err(err),
}
}
fn helper_executable_path() -> io::Result<PathBuf> {
if let Some(path) = std::env::var_os("CODEX_EXEC_SERVER_SELF_EXE") {
return Ok(PathBuf::from(path));
}
if let Some(path) = std::env::var_os("CARGO_BIN_EXE_codex-exec-server") {
return Ok(PathBuf::from(path));
}
if let Ok(path) = which::which("codex-exec-server") {
return Ok(path);
}
let current_exe = std::env::current_exe()?;
if current_exe.file_name().and_then(|value| value.to_str()) == Some("codex-exec-server") {
return Ok(current_exe);
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"could not find codex-exec-server executable for sandboxed filesystem helper",
))
}
fn linux_sandbox_executable_path(helper_exe: &Path) -> Option<PathBuf> {
if !cfg!(target_os = "linux") {
return None;
}
std::env::var_os("CODEX_LINUX_SANDBOX_EXE")
.map(PathBuf::from)
.or_else(|| std::env::var_os("CARGO_BIN_EXE_codex-linux-sandbox").map(PathBuf::from))
.or_else(|| which::which("codex-linux-sandbox").ok())
.or_else(|| {
helper_exe.parent().and_then(|parent| {
let sibling = parent.join("codex-linux-sandbox");
sibling.exists().then_some(sibling)
})
})
}
#[cfg(test)]
mod tests {
use super::helper_legacy_policy;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
#[test]
fn helper_legacy_policy_falls_back_to_external_sandbox_for_direct_runtime_enforcement() {
let outside_workspace =
AbsolutePathBuf::try_from(std::path::PathBuf::from("/outside")).expect("absolute path");
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: outside_workspace,
},
access: FileSystemAccessMode::Write,
}]);
let resolved = helper_legacy_policy(
&policy,
NetworkSandboxPolicy::Enabled,
std::path::Path::new("/workspace"),
)
.expect("direct-enforcement policy should resolve");
assert_eq!(
resolved,
SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Enabled,
}
);
}
}
@@ -39,7 +39,7 @@ impl FileSystemHandler {
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let bytes = self
.file_system
.read_file(&params.path)
.read_file_with_sandbox_policy(&params.path, params.sandbox_policy.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsReadFileResponse {
@@ -57,7 +57,7 @@ impl FileSystemHandler {
))
})?;
self.file_system
.write_file(&params.path, bytes)
.write_file_with_sandbox_policy(&params.path, bytes, params.sandbox_policy.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
@@ -68,11 +68,12 @@ impl FileSystemHandler {
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
self.file_system
.create_directory(
.create_directory_with_sandbox_policy(
&params.path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
params.sandbox_policy.as_ref(),
)
.await
.map_err(map_fs_error)?;
@@ -85,7 +86,7 @@ impl FileSystemHandler {
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
let metadata = self
.file_system
.get_metadata(&params.path)
.get_metadata_with_sandbox_policy(&params.path, params.sandbox_policy.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
@@ -102,7 +103,7 @@ impl FileSystemHandler {
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let entries = self
.file_system
.read_directory(&params.path)
.read_directory_with_sandbox_policy(&params.path, params.sandbox_policy.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsReadDirectoryResponse {
@@ -122,12 +123,13 @@ impl FileSystemHandler {
params: FsRemoveParams,
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
self.file_system
.remove(
.remove_with_sandbox_policy(
&params.path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
},
params.sandbox_policy.as_ref(),
)
.await
.map_err(map_fs_error)?;
@@ -139,12 +141,13 @@ impl FileSystemHandler {
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
self.file_system
.copy(
.copy_with_sandbox_policy(
&params.source_path,
&params.destination_path,
CopyOptions {
recursive: params.recursive,
},
params.sandbox_policy.as_ref(),
)
.await
.map_err(map_fs_error)?;