mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
## 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`
266 lines
8.5 KiB
Rust
266 lines
8.5 KiB
Rust
use std::io;
|
|
|
|
use base64::Engine as _;
|
|
use base64::engine::general_purpose::STANDARD;
|
|
use codex_app_server_protocol::JSONRPCErrorError;
|
|
|
|
use crate::CopyOptions;
|
|
use crate::CreateDirectoryOptions;
|
|
use crate::ExecServerRuntimePaths;
|
|
use crate::ExecutorFileSystem;
|
|
use crate::RemoveOptions;
|
|
use crate::local_file_system::LocalFileSystem;
|
|
use crate::protocol::FS_WRITE_FILE_METHOD;
|
|
use crate::protocol::FsCanonicalizeParams;
|
|
use crate::protocol::FsCanonicalizeResponse;
|
|
use crate::protocol::FsCopyParams;
|
|
use crate::protocol::FsCopyResponse;
|
|
use crate::protocol::FsCreateDirectoryParams;
|
|
use crate::protocol::FsCreateDirectoryResponse;
|
|
use crate::protocol::FsGetMetadataParams;
|
|
use crate::protocol::FsGetMetadataResponse;
|
|
use crate::protocol::FsReadDirectoryEntry;
|
|
use crate::protocol::FsReadDirectoryParams;
|
|
use crate::protocol::FsReadDirectoryResponse;
|
|
use crate::protocol::FsReadFileParams;
|
|
use crate::protocol::FsReadFileResponse;
|
|
use crate::protocol::FsRemoveParams;
|
|
use crate::protocol::FsRemoveResponse;
|
|
use crate::protocol::FsWriteFileParams;
|
|
use crate::protocol::FsWriteFileResponse;
|
|
use crate::rpc::internal_error;
|
|
use crate::rpc::invalid_request;
|
|
use crate::rpc::not_found;
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct FileSystemHandler {
|
|
file_system: LocalFileSystem,
|
|
}
|
|
|
|
impl FileSystemHandler {
|
|
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
|
Self {
|
|
file_system: LocalFileSystem::with_runtime_paths(runtime_paths),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn read_file(
|
|
&self,
|
|
params: FsReadFileParams,
|
|
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
|
|
let bytes = self
|
|
.file_system
|
|
.read_file(¶ms.path, params.sandbox.as_ref())
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsReadFileResponse {
|
|
data_base64: STANDARD.encode(bytes),
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn write_file(
|
|
&self,
|
|
params: FsWriteFileParams,
|
|
) -> Result<FsWriteFileResponse, JSONRPCErrorError> {
|
|
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
|
|
invalid_request(format!(
|
|
"{FS_WRITE_FILE_METHOD} requires valid base64 dataBase64: {err}"
|
|
))
|
|
})?;
|
|
self.file_system
|
|
.write_file(¶ms.path, bytes, params.sandbox.as_ref())
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsWriteFileResponse {})
|
|
}
|
|
|
|
pub(crate) async fn create_directory(
|
|
&self,
|
|
params: FsCreateDirectoryParams,
|
|
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
|
|
let recursive = params.recursive.unwrap_or(true);
|
|
self.file_system
|
|
.create_directory(
|
|
¶ms.path,
|
|
CreateDirectoryOptions { recursive },
|
|
params.sandbox.as_ref(),
|
|
)
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsCreateDirectoryResponse {})
|
|
}
|
|
|
|
pub(crate) async fn get_metadata(
|
|
&self,
|
|
params: FsGetMetadataParams,
|
|
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
|
|
let metadata = self
|
|
.file_system
|
|
.get_metadata(¶ms.path, params.sandbox.as_ref())
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsGetMetadataResponse {
|
|
is_directory: metadata.is_directory,
|
|
is_file: metadata.is_file,
|
|
is_symlink: metadata.is_symlink,
|
|
size: metadata.size,
|
|
created_at_ms: metadata.created_at_ms,
|
|
modified_at_ms: metadata.modified_at_ms,
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn canonicalize(
|
|
&self,
|
|
params: FsCanonicalizeParams,
|
|
) -> Result<FsCanonicalizeResponse, JSONRPCErrorError> {
|
|
let path = self
|
|
.file_system
|
|
.canonicalize(¶ms.path, params.sandbox.as_ref())
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsCanonicalizeResponse { path })
|
|
}
|
|
|
|
pub(crate) async fn read_directory(
|
|
&self,
|
|
params: FsReadDirectoryParams,
|
|
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
|
|
let entries = self
|
|
.file_system
|
|
.read_directory(¶ms.path, params.sandbox.as_ref())
|
|
.await
|
|
.map_err(map_fs_error)?
|
|
.into_iter()
|
|
.map(|entry| FsReadDirectoryEntry {
|
|
file_name: entry.file_name,
|
|
is_directory: entry.is_directory,
|
|
is_file: entry.is_file,
|
|
})
|
|
.collect();
|
|
Ok(FsReadDirectoryResponse { entries })
|
|
}
|
|
|
|
pub(crate) async fn remove(
|
|
&self,
|
|
params: FsRemoveParams,
|
|
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
|
|
let recursive = params.recursive.unwrap_or(true);
|
|
let force = params.force.unwrap_or(true);
|
|
self.file_system
|
|
.remove(
|
|
¶ms.path,
|
|
RemoveOptions { recursive, force },
|
|
params.sandbox.as_ref(),
|
|
)
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsRemoveResponse {})
|
|
}
|
|
|
|
pub(crate) async fn copy(
|
|
&self,
|
|
params: FsCopyParams,
|
|
) -> Result<FsCopyResponse, JSONRPCErrorError> {
|
|
self.file_system
|
|
.copy(
|
|
¶ms.source_path,
|
|
¶ms.destination_path,
|
|
CopyOptions {
|
|
recursive: params.recursive,
|
|
},
|
|
params.sandbox.as_ref(),
|
|
)
|
|
.await
|
|
.map_err(map_fs_error)?;
|
|
Ok(FsCopyResponse {})
|
|
}
|
|
}
|
|
|
|
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
|
|
match err.kind() {
|
|
io::ErrorKind::NotFound => not_found(err.to_string()),
|
|
io::ErrorKind::InvalidInput | io::ErrorKind::PermissionDenied => {
|
|
invalid_request(err.to_string())
|
|
}
|
|
_ => internal_error(err.to_string()),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use codex_protocol::protocol::NetworkAccess;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_utils_path_uri::PathUri;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
use crate::FileSystemSandboxContext;
|
|
use crate::protocol::FsReadFileParams;
|
|
use crate::protocol::FsWriteFileParams;
|
|
|
|
#[tokio::test]
|
|
async fn no_platform_sandbox_policies_do_not_require_configured_sandbox_helper() {
|
|
let temp_dir = tempfile::tempdir().expect("tempdir");
|
|
let runtime_paths = ExecServerRuntimePaths::new(
|
|
std::env::current_exe().expect("current exe"),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)
|
|
.expect("runtime paths");
|
|
let handler = FileSystemHandler::new(runtime_paths);
|
|
let sandbox_cwd = PathUri::from_path(temp_dir.path()).expect("tempdir URI");
|
|
let sandbox_context = |sandbox_policy| {
|
|
FileSystemSandboxContext::from_legacy_sandbox_policy(
|
|
sandbox_policy,
|
|
sandbox_cwd.clone(),
|
|
)
|
|
.expect("sandbox context")
|
|
};
|
|
|
|
for (file_name, sandbox_policy) in [
|
|
("danger.txt", SandboxPolicy::DangerFullAccess),
|
|
(
|
|
"external.txt",
|
|
SandboxPolicy::ExternalSandbox {
|
|
network_access: NetworkAccess::Restricted,
|
|
},
|
|
),
|
|
] {
|
|
let path = PathUri::from_path(temp_dir.path().join(file_name)).expect("path URI");
|
|
|
|
handler
|
|
.write_file(FsWriteFileParams {
|
|
path: path.clone(),
|
|
data_base64: STANDARD.encode("ok"),
|
|
sandbox: Some(sandbox_context(sandbox_policy.clone())),
|
|
})
|
|
.await
|
|
.expect("write file");
|
|
|
|
let canonicalized = handler
|
|
.canonicalize(FsCanonicalizeParams {
|
|
path: path.clone(),
|
|
sandbox: Some(sandbox_context(sandbox_policy.clone())),
|
|
})
|
|
.await
|
|
.expect("canonicalize file");
|
|
assert_eq!(
|
|
canonicalized.path,
|
|
PathUri::from_path(
|
|
std::fs::canonicalize(temp_dir.path().join(file_name)).expect("canonical path"),
|
|
)
|
|
.expect("canonical path URI"),
|
|
);
|
|
|
|
let response = handler
|
|
.read_file(FsReadFileParams {
|
|
path,
|
|
sandbox: Some(sandbox_context(sandbox_policy)),
|
|
})
|
|
.await
|
|
.expect("read file");
|
|
|
|
assert_eq!(response.data_base64, STANDARD.encode("ok"));
|
|
}
|
|
}
|
|
}
|