ExecServer文件系统RPC
exec-server 的 filesystem RPC 不是把 std::fs 直接暴露给远端。FileSystemHandler 先校验 handle、base64 和目录规模,再把路径与可选 FileSystemSandboxContext 交给 LocalFileSystem;后者依据 sandbox context 选择 unsandboxed 或 platform-sandbox backend。流式读取由独立 FileReadHandleManager 管理,句柄数量、块大小和关闭时机都有明确上限;sandbox backend 同样可以提供受控的文件打开能力。
本文承接ExecServer进程RPC和ExecServer消息模型,面向理解 PathUri、异步文件 I/O 和 sandbox context 的读者。范围是 read/open/readBlock/close、write、目录和错误映射,不展开 sandbox helper 内部实现。读完后,你应能判断一次文件请求由谁拥有 handle、何时进入 sandbox,以及错误为何映射为不同 JSON-RPC code。
1. Handler分层
源码位置:codex-rs/exec-server/src/server/file_system_handler.rs :: FileSystemHandler
#[derive(Clone)]
pub(crate) struct FileSystemHandler {
file_system: LocalFileSystem,
file_reads: FileReadHandleManager,
}
pub(crate) async fn shutdown(&self) {
self.file_reads.close_all().await;
}handler 只拥有协议层资源:一次性 read/write 直接下沉到 LocalFileSystem;open 则把文件对象交给 handle manager,连接关闭时统一 close_all。
2. 路径选择
LocalFileSystem::file_system_for 只在 sandbox context 明确要求 platform sandbox 时选择 sandboxed backend;否则使用 unsandboxed backend,但仍把 context 传给底层策略检查。open_file_for_read 走同一选择逻辑,因此不能把“流式”误解为绕过 sandbox。
源码位置:codex-rs/exec-server/src/local_file_system.rs :: file_system_for
fn file_system_for<'a>(
&'a self,
sandbox: Option<&'a FileSystemSandboxContext>,
) -> io::Result<(&'a dyn ExecutorFileSystem, Option<&'a FileSystemSandboxContext>)> {
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
Ok((self.sandboxed()?, sandbox))
} else {
Ok((&self.unsandboxed, sandbox))
}
}源码位置:codex-rs/exec-server/src/local_file_system.rs :: LocalFileSystem::open_file_for_read
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
return self.sandboxed()?.open_file_for_read(path, sandbox).await;
}
self.unsandboxed.open_file_for_read(path, sandbox).await3. 读取句柄
open 校验 handle ID 不超过 32 字节,FileReadHandleManager 最多保存每条连接 128 个打开文件;readBlock 校验块长度不超过 FILE_READ_CHUNK_SIZE(1 MiB),使用 spawn_blocking 和平台对应的 read_at/seek_read,close 是幂等的释放入口。读取出错时 manager 会主动移除该句柄。
源码位置:
codex-rs/exec-server/src/server/file_system_handler.rs::FileSystemHandler::open、read_block、closecodex-rs/exec-server/src/file_read.rs::FileReadHandleManager::open、read_block、close_all
validate_file_read_handle_id(¶ms.handle_id)?;
let file = self
.file_system
.open_file_for_read(¶ms.path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
let handle_id = self.file_reads.open(params.handle_id, file)
.await
.map_err(map_fs_error)?;
Ok(FsOpenResponse { handle_id })源码位置:codex-rs/exec-server/src/file_read.rs :: FileReadHandleManager::read_block
validate_read_block_len(len)?;
let file = {
let handles = self.handles.lock().await;
handles
.get(handle_id)
.cloned()
.ok_or_else(|| unknown_handle_error(handle_id))?
};
let result = tokio::task::spawn_blocking(move || read_block_at(&file, offset, len))
.await
.map_err(|error| io::Error::other(format!("file read task stopped unexpectedly: {error}")))?;
if result.is_err() {
self.close(handle_id).await;
}4. 一次性读写
read_file 读取完整 bytes 后 base64 编码,并在 LocalFileSystem 中限制单次完整读取不超过 512 MiB;write_file 先解码 dataBase64,非法 base64 在触碰文件系统前返回 invalid request。目录读取限制 50,000 entries;递归 walk 还受深度 64、目录 10,000、条目 50,000 和响应 4 MiB 上限约束,避免单个 JSON response 失控。
源码位置:
codex-rs/exec-server/src/server/file_system_handler.rs::FileSystemHandler::read_file、write_file、read_directory、walkcodex-rs/exec-server/src/local_file_system.rs::DirectFileSystem::read_file、LocalFileSystem::walk
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
invalid_request(format!("fs/writeFile requires valid base64 dataBase64: {err}"))
})?;
self.file_system
.write_file(¶ms.path, bytes, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;源码位置:codex-rs/exec-server/src/local_file_system.rs :: DirectFileSystem::read_file
let file = if options.follow_symlinks {
self.open_file_for_read(path, /*sandbox*/ None).await?
} else {
no_follow::open_file(path.to_abs_path()?.as_path()).await?
};
let metadata = file.metadata().await?;
if metadata.len() > MAX_READ_FILE_BYTES {
return Err(file_too_large_error());
}
let mut bytes = Vec::with_capacity(metadata.len() as usize);
file.take(MAX_READ_FILE_BYTES + 1).read_to_end(&mut bytes).await?;
if bytes.len() as u64 > MAX_READ_FILE_BYTES {
return Err(file_too_large_error());
}
Ok(bytes)5. 错误映射
filesystem io::ErrorKind 被映射为协议错误:NotFound → not_found,InvalidInput/PermissionDenied → invalid_request,其余 → internal_error。这样客户端可以区分路径不存在、请求不合法和服务端故障。
源码位置:codex-rs/exec-server/src/server/file_system_handler.rs :: map_fs_error
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()),
}
}6. 验证
6.1 文件回路
exec-server filesystem tests 覆盖 read/write、metadata、directory、copy/remove 和 sandbox path;输入是 PathUri 与可选 sandbox context,断言返回 bytes、存在性或错误 code。
源码位置:codex-rs/exec-server/tests/file_system_unix.rs、file_stream.rs
cd codex-rs
cargo test -p codex-exec-server --test file_system_unix -- --test-threads=1
cargo test -p codex-exec-server --test file_stream -- --test-threads=16.2 handler边界
handler 单测验证 platform sandbox streaming read 被拒绝、base64 非法输入被拒绝和目录数量上限;这些断言证明协议边界,不证明底层 sandbox helper 的系统调用细节。
源码位置:codex-rs/exec-server/src/server/file_system_handler.rs :: tests
cd codex-rs
cargo test -p codex-exec-server --lib server::file_system_handler::tests -- --test-threads=17. 源码排查
rg -n "struct FileSystemHandler|validate_file_read_handle_id|map_fs_error" codex-rs/exec-server/src/server/file_system_handler.rs
rg -n "file_system_for|open_file_for_read|read_file\(|write_file\(" codex-rs/exec-server/src/local_file_system.rs
rg -n "file_stream|file_system_unix|FsRead|FsWrite" codex-rs/exec-server/tests文件 RPC 主线是:handler 校验协议输入,LocalFileSystem 按 sandbox context 选择 backend,streaming read 由 handle manager 负责生命周期,一次性读写直接返回 base64/metadata,最后统一把 io error 映射为协议错误。下一篇将分析 exec-server 的网络 RPC。
