Files
codex/codex-rs/exec-server/src/sandboxed_file_system.rs
T
pakrym-oai 76d8f20241 [codex] Add size to internal filesystem metadata (#27927)
## Why

`ExecutorFileSystem::get_metadata` reports file kind and timestamps but
not size. Internal callers that need to enforce a size limit therefore
have to read the complete file first, which is especially wasteful for
remote filesystems.

This adds the missing internal metadata so consumers can reject
oversized files before transferring or buffering them. The field is
named `size`, matching VS Code's `FileStat.size` filesystem convention.

## What changed

- add `size: u64` to internal `FileMetadata`
- populate it from the underlying filesystem metadata
- carry it through sandbox-helper and remote exec-server responses
- cover files, directories, symlink targets, and sandboxed reads across
local and remote filesystem implementations

The new field is intentionally not exposed through the app-server API.

## Testing

- `just test -p codex-exec-server get_metadata`
- `just test -p codex-exec-server
file_system_sandboxed_metadata_and_read_allow_readable_root`
- `just test -p codex-core-plugins`
- `just test -p codex-skills-extension`
2026-06-12 12:12:08 -07:00

365 lines
11 KiB
Rust

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::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 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;