Codex Handbook
exec-server/src/sandboxed_file_system.rs 378 lines
use base64::Engine as _;use base64::engine::general_purpose::STANDARD;use codex_app_server_protocol::JSONRPCErrorError;use codex_utils_path_uri::PathUri;use tokio::io;use crate::CopyOptions;use crate::CreateDirectoryOptions;use crate::ExecServerRuntimePaths;use crate::ExecutorFileSystem;use crate::ExecutorFileSystemFuture;use crate::FileMetadata;use crate::FileSystemReadStream;use crate::FileSystemResult;use crate::FileSystemSandboxContext;use crate::ReadDirectoryEntry;use crate::RemoveOptions;use crate::fs_helper::FsHelperPayload;use crate::fs_helper::FsHelperRequest;use crate::fs_sandbox::FileSystemSandboxRunner;use crate::protocol::FsCanonicalizeParams;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;#[derive(Clone)]pub struct SandboxedFileSystem {    sandbox_runner: FileSystemSandboxRunner,}impl SandboxedFileSystem {    pub fn new(runtime_paths: ExecServerRuntimePaths) -> Self {        Self {            sandbox_runner: FileSystemSandboxRunner::new(runtime_paths),        }    }    async fn run_sandboxed(        &self,        sandbox: &FileSystemSandboxContext,        request: FsHelperRequest,    ) -> FileSystemResult<FsHelperPayload> {        self.sandbox_runner            .run(sandbox, request)            .await            .map_err(map_sandbox_error)    }}impl SandboxedFileSystem {    async fn canonicalize(        &self,        path: &PathUri,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<PathUri> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        let response = self            .run_sandboxed(                sandbox,                FsHelperRequest::Canonicalize(FsCanonicalizeParams {                    path: path.clone(),                    sandbox: None,                }),            )            .await?            .expect_canonicalize()            .map_err(map_sandbox_error)?;        Ok(response.path)    }    async fn read_file(        &self,        path: &PathUri,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<Vec<u8>> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        let response = self            .run_sandboxed(                sandbox,                FsHelperRequest::ReadFile(FsReadFileParams {                    path: path.clone(),                    sandbox: None,                }),            )            .await?            .expect_read_file()            .map_err(map_sandbox_error)?;        STANDARD.decode(response.data_base64).map_err(|err| {            io::Error::new(                io::ErrorKind::InvalidData,                format!("fs/readFile returned invalid base64 dataBase64: {err}"),            )        })    }    async fn write_file(        &self,        path: &PathUri,        contents: Vec<u8>,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<()> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        self.run_sandboxed(            sandbox,            FsHelperRequest::WriteFile(FsWriteFileParams {                path: path.clone(),                data_base64: STANDARD.encode(contents),                sandbox: None,            }),        )        .await?        .expect_write_file()        .map_err(map_sandbox_error)?;        Ok(())    }    async fn create_directory(        &self,        path: &PathUri,        options: CreateDirectoryOptions,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<()> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        self.run_sandboxed(            sandbox,            FsHelperRequest::CreateDirectory(FsCreateDirectoryParams {                path: path.clone(),                recursive: Some(options.recursive),                sandbox: None,            }),        )        .await?        .expect_create_directory()        .map_err(map_sandbox_error)?;        Ok(())    }    async fn get_metadata(        &self,        path: &PathUri,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<FileMetadata> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        let response = self            .run_sandboxed(                sandbox,                FsHelperRequest::GetMetadata(FsGetMetadataParams {                    path: path.clone(),                    sandbox: None,                }),            )            .await?            .expect_get_metadata()            .map_err(map_sandbox_error)?;        Ok(FileMetadata {            is_directory: response.is_directory,            is_file: response.is_file,            is_symlink: response.is_symlink,            size: response.size,            created_at_ms: response.created_at_ms,            modified_at_ms: response.modified_at_ms,        })    }    async fn read_directory(        &self,        path: &PathUri,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<Vec<ReadDirectoryEntry>> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        let response = self            .run_sandboxed(                sandbox,                FsHelperRequest::ReadDirectory(FsReadDirectoryParams {                    path: path.clone(),                    sandbox: None,                }),            )            .await?            .expect_read_directory()            .map_err(map_sandbox_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 remove(        &self,        path: &PathUri,        remove_options: RemoveOptions,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<()> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(path)?;        self.run_sandboxed(            sandbox,            FsHelperRequest::Remove(FsRemoveParams {                path: path.clone(),                recursive: Some(remove_options.recursive),                force: Some(remove_options.force),                sandbox: None,            }),        )        .await?        .expect_remove()        .map_err(map_sandbox_error)?;        Ok(())    }    async fn copy(        &self,        source_path: &PathUri,        destination_path: &PathUri,        options: CopyOptions,        sandbox: Option<&FileSystemSandboxContext>,    ) -> FileSystemResult<()> {        let sandbox = require_platform_sandbox(sandbox)?;        validate_native_path(source_path)?;        validate_native_path(destination_path)?;        self.run_sandboxed(            sandbox,            FsHelperRequest::Copy(FsCopyParams {                source_path: source_path.clone(),                destination_path: destination_path.clone(),                recursive: options.recursive,                sandbox: None,            }),        )        .await?        .expect_copy()        .map_err(map_sandbox_error)?;        Ok(())    }}impl ExecutorFileSystem for SandboxedFileSystem {    fn canonicalize<'a>(        &'a self,        path: &'a PathUri,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, PathUri> {        Box::pin(SandboxedFileSystem::canonicalize(self, path, sandbox))    }    fn read_file<'a>(        &'a self,        path: &'a PathUri,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, Vec<u8>> {        Box::pin(SandboxedFileSystem::read_file(self, path, sandbox))    }    fn read_file_stream<'a>(        &'a self,        _path: &'a PathUri,        _sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, FileSystemReadStream> {        Box::pin(async {            Err(io::Error::new(                io::ErrorKind::Unsupported,                "streaming file reads do not support platform sandboxing",            ))        })    }    fn write_file<'a>(        &'a self,        path: &'a PathUri,        contents: Vec<u8>,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(SandboxedFileSystem::write_file(            self, path, contents, sandbox,        ))    }    fn create_directory<'a>(        &'a self,        path: &'a PathUri,        options: CreateDirectoryOptions,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(SandboxedFileSystem::create_directory(            self, path, options, sandbox,        ))    }    fn get_metadata<'a>(        &'a self,        path: &'a PathUri,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, FileMetadata> {        Box::pin(SandboxedFileSystem::get_metadata(self, path, sandbox))    }    fn read_directory<'a>(        &'a self,        path: &'a PathUri,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {        Box::pin(SandboxedFileSystem::read_directory(self, path, sandbox))    }    fn remove<'a>(        &'a self,        path: &'a PathUri,        remove_options: RemoveOptions,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(SandboxedFileSystem::remove(            self,            path,            remove_options,            sandbox,        ))    }    fn copy<'a>(        &'a self,        source_path: &'a PathUri,        destination_path: &'a PathUri,        options: CopyOptions,        sandbox: Option<&'a FileSystemSandboxContext>,    ) -> ExecutorFileSystemFuture<'a, ()> {        Box::pin(SandboxedFileSystem::copy(            self,            source_path,            destination_path,            options,            sandbox,        ))    }}fn validate_native_path(path: &PathUri) -> FileSystemResult<()> {    path.to_abs_path().map(drop)}fn require_platform_sandbox(    sandbox: Option<&FileSystemSandboxContext>,) -> FileSystemResult<&FileSystemSandboxContext> {    sandbox        .filter(|sandbox| sandbox.should_run_in_sandbox())        .ok_or_else(|| {            io::Error::new(                io::ErrorKind::InvalidInput,                "sandboxed filesystem operations require ReadOnly or WorkspaceWrite sandbox policy",            )        })}fn map_sandbox_error(error: JSONRPCErrorError) -> io::Error {    match error.code {        -32004 => io::Error::new(io::ErrorKind::NotFound, error.message),        -32600 => io::Error::new(io::ErrorKind::InvalidInput, error.message),        _ => io::Error::other(error.message),    }}#[cfg(all(test, any(unix, windows)))]#[path = "sandboxed_file_system_path_uri_tests.rs"]mod path_uri_tests;