Use Environment as the exec and fs gateway

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-03-19 18:41:58 +00:00
co-authored by Codex
parent e0a7c18424
commit a4358f2c4f
12 changed files with 239 additions and 163 deletions
+70 -13
View File
@@ -3,11 +3,13 @@ use crate::ExecServerError;
use crate::RemoteExecServerConnectArgs;
use crate::fs;
use crate::fs::ExecutorFileSystem;
use std::sync::Arc;
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct Environment {
experimental_exec_server_url: Option<String>,
remote_exec_server_client: Option<ExecServerClient>,
exec_server_client: Option<ExecServerClient>,
file_system: Arc<dyn ExecutorFileSystem>,
}
impl std::fmt::Debug for Environment {
@@ -17,19 +19,24 @@ impl std::fmt::Debug for Environment {
"experimental_exec_server_url",
&self.experimental_exec_server_url,
)
.field(
"has_remote_exec_server_client",
&self.remote_exec_server_client.is_some(),
)
.field("has_exec_server_client", &self.exec_server_client.is_some())
.finish()
}
}
impl Environment {
pub fn local() -> Self {
Self {
experimental_exec_server_url: None,
exec_server_client: None,
file_system: Arc::new(fs::LocalFileSystem),
}
}
pub async fn create(
experimental_exec_server_url: Option<String>,
) -> Result<Self, ExecServerError> {
let remote_exec_server_client =
let exec_server_client =
if let Some(websocket_url) = experimental_exec_server_url.as_deref() {
Some(
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs::new(
@@ -42,35 +49,85 @@ impl Environment {
None
};
let file_system: Arc<dyn ExecutorFileSystem> = if let Some(client) = &exec_server_client {
Arc::new(fs::RemoteFileSystem::new(client.clone()))
} else {
Arc::new(fs::LocalFileSystem)
};
Ok(Self {
experimental_exec_server_url,
remote_exec_server_client,
exec_server_client,
file_system,
})
}
pub fn from_exec_server_client(client: ExecServerClient) -> Self {
Self {
experimental_exec_server_url: None,
exec_server_client: Some(client.clone()),
file_system: Arc::new(fs::RemoteFileSystem::new(client)),
}
}
pub fn experimental_exec_server_url(&self) -> Option<&str> {
self.experimental_exec_server_url.as_deref()
}
pub fn remote_exec_server_client(&self) -> Option<&ExecServerClient> {
self.remote_exec_server_client.as_ref()
pub fn exec_server_client(&self) -> Option<ExecServerClient> {
self.exec_server_client.clone()
}
pub fn get_filesystem(&self) -> impl ExecutorFileSystem + use<> {
fs::LocalFileSystem
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
Arc::clone(&self.file_system)
}
}
impl Default for Environment {
fn default() -> Self {
Self::local()
}
}
#[cfg(test)]
mod tests {
use super::Environment;
use crate::ExecServerClient;
use crate::ExecServerClientConnectOptions;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[tokio::test]
async fn create_without_remote_exec_server_url_does_not_connect() {
let environment = Environment::create(None).await.expect("create environment");
assert_eq!(environment.experimental_exec_server_url(), None);
assert!(environment.remote_exec_server_client().is_none());
assert!(environment.exec_server_client().is_none());
}
#[tokio::test]
async fn environment_uses_remote_filesystem_abstraction_when_client_is_provided() {
let client =
ExecServerClient::connect_in_process(ExecServerClientConnectOptions::default())
.await
.expect("connect in-process client");
let environment = Environment::from_exec_server_client(client);
let tempdir = TempDir::new().expect("tempdir");
let path = AbsolutePathBuf::try_from(tempdir.path().join("marker.txt")).expect("path");
environment
.get_filesystem()
.write_file(&path, b"hello".to_vec())
.await
.expect("write file through environment abstraction");
let bytes = environment
.get_filesystem()
.read_file(&path)
.await
.expect("read file through environment abstraction");
assert_eq!(bytes, b"hello");
}
}
+135
View File
@@ -1,4 +1,13 @@
use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::FsCopyParams;
use codex_app_server_protocol::FsCreateDirectoryParams;
use codex_app_server_protocol::FsGetMetadataParams;
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_utils_absolute_path::AbsolutePathBuf;
use std::path::Component;
use std::path::Path;
@@ -7,6 +16,9 @@ use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tokio::io;
use crate::ExecServerClient;
use crate::ExecServerError;
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -203,6 +215,129 @@ impl ExecutorFileSystem for LocalFileSystem {
}
}
#[derive(Clone)]
pub(crate) struct RemoteFileSystem {
client: ExecServerClient,
}
impl RemoteFileSystem {
pub(crate) fn new(client: ExecServerClient) -> Self {
Self { client }
}
}
#[async_trait]
impl ExecutorFileSystem for RemoteFileSystem {
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>> {
let response = self
.client
.fs_read_file(FsReadFileParams { path: path.clone() })
.await
.map_err(map_exec_server_error)?;
STANDARD
.decode(response.data_base64)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
self.client
.fs_write_file(FsWriteFileParams {
path: path.clone(),
data_base64: STANDARD.encode(contents),
})
.await
.map_err(map_exec_server_error)?;
Ok(())
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
) -> FileSystemResult<()> {
self.client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(options.recursive),
})
.await
.map_err(map_exec_server_error)?;
Ok(())
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
let response = self
.client
.fs_get_metadata(FsGetMetadataParams { path: path.clone() })
.await
.map_err(map_exec_server_error)?;
Ok(FileMetadata {
is_directory: response.is_directory,
is_file: response.is_file,
created_at_ms: response.created_at_ms,
modified_at_ms: response.modified_at_ms,
})
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let response = self
.client
.fs_read_directory(FsReadDirectoryParams { path: path.clone() })
.await
.map_err(map_exec_server_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: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
self.client
.fs_remove(FsRemoveParams {
path: path.clone(),
recursive: Some(options.recursive),
force: Some(options.force),
})
.await
.map_err(map_exec_server_error)?;
Ok(())
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
) -> FileSystemResult<()> {
self.client
.fs_copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: options.recursive,
})
.await
.map_err(map_exec_server_error)?;
Ok(())
}
}
fn map_exec_server_error(err: ExecServerError) -> io::Error {
match err {
ExecServerError::Server { code, message } if matches!(code, -32600 | -32602) => {
io::Error::new(io::ErrorKind::InvalidInput, message)
}
other => io::Error::other(other.to_string()),
}
}
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
std::fs::create_dir_all(target)?;
for entry in std::fs::read_dir(source)? {
+8 -8
View File
@@ -13,14 +13,6 @@ pub use client::ExecServerError;
pub use client_api::ExecServerClientConnectOptions;
pub use client_api::ExecServerEvent;
pub use client_api::RemoteExecServerConnectArgs;
pub use environment::Environment;
pub use fs::CopyOptions;
pub use fs::CreateDirectoryOptions;
pub use fs::ExecutorFileSystem;
pub use fs::FileMetadata;
pub use fs::FileSystemResult;
pub use fs::ReadDirectoryEntry;
pub use fs::RemoveOptions;
pub use codex_app_server_protocol::FsCopyParams;
pub use codex_app_server_protocol::FsCopyResponse;
pub use codex_app_server_protocol::FsCreateDirectoryParams;
@@ -36,6 +28,14 @@ pub use codex_app_server_protocol::FsRemoveParams;
pub use codex_app_server_protocol::FsRemoveResponse;
pub use codex_app_server_protocol::FsWriteFileParams;
pub use codex_app_server_protocol::FsWriteFileResponse;
pub use environment::Environment;
pub use fs::CopyOptions;
pub use fs::CreateDirectoryOptions;
pub use fs::ExecutorFileSystem;
pub use fs::FileMetadata;
pub use fs::FileSystemResult;
pub use fs::ReadDirectoryEntry;
pub use fs::RemoveOptions;
pub use local::ExecServerLaunchCommand;
pub use local::SpawnedExecServer;
pub use local::spawn_local_exec_server;
@@ -36,7 +36,7 @@ pub(crate) struct ExecServerFileSystem {
impl Default for ExecServerFileSystem {
fn default() -> Self {
Self {
file_system: Arc::new(Environment.get_filesystem()),
file_system: Environment::default().get_filesystem(),
}
}
}