diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 11b3fa6e81..1103b58b92 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2011,6 +2011,7 @@ dependencies = [ "base64 0.22.1", "clap", "codex-app-server-protocol", + "codex-environment", "codex-utils-cargo-bin", "codex-utils-pty", "futures", diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 1b47760975..744b2011f3 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -15,6 +15,7 @@ workspace = true base64 = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } +codex-environment = { workspace = true } codex-utils-pty = { workspace = true } futures = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index 652c9d01d9..2f314f3db3 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -1,6 +1,20 @@ use std::sync::Arc; use std::time::Duration; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCopyResponse; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsCreateDirectoryResponse; +use codex_app_server_protocol::FsGetMetadataParams; +use codex_app_server_protocol::FsGetMetadataResponse; +use codex_app_server_protocol::FsReadDirectoryParams; +use codex_app_server_protocol::FsReadDirectoryResponse; +use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsRemoveResponse; +use codex_app_server_protocol::FsWriteFileParams; +use codex_app_server_protocol::FsWriteFileResponse; use codex_app_server_protocol::JSONRPCNotification; use serde_json::Value; use tokio::io::AsyncRead; @@ -26,6 +40,13 @@ use crate::protocol::ExecExitedNotification; use crate::protocol::ExecOutputDeltaNotification; use crate::protocol::ExecParams; use crate::protocol::ExecResponse; +use crate::protocol::FS_COPY_METHOD; +use crate::protocol::FS_CREATE_DIRECTORY_METHOD; +use crate::protocol::FS_GET_METADATA_METHOD; +use crate::protocol::FS_READ_DIRECTORY_METHOD; +use crate::protocol::FS_READ_FILE_METHOD; +use crate::protocol::FS_REMOVE_METHOD; +use crate::protocol::FS_WRITE_FILE_METHOD; use crate::protocol::INITIALIZE_METHOD; use crate::protocol::INITIALIZED_METHOD; use crate::protocol::InitializeParams; @@ -326,6 +347,129 @@ impl ExecServerClient { .map_err(Into::into) } + pub async fn fs_read_file( + &self, + params: FsReadFileParams, + ) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_read_file(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/readFile".to_string(), + )); + }; + remote + .call(FS_READ_FILE_METHOD, ¶ms) + .await + .map_err(Into::into) + } + + pub async fn fs_write_file( + &self, + params: FsWriteFileParams, + ) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_write_file(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/writeFile".to_string(), + )); + }; + remote + .call(FS_WRITE_FILE_METHOD, ¶ms) + .await + .map_err(Into::into) + } + + pub async fn fs_create_directory( + &self, + params: FsCreateDirectoryParams, + ) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_create_directory(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/createDirectory".to_string(), + )); + }; + remote + .call(FS_CREATE_DIRECTORY_METHOD, ¶ms) + .await + .map_err(Into::into) + } + + pub async fn fs_get_metadata( + &self, + params: FsGetMetadataParams, + ) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_get_metadata(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/getMetadata".to_string(), + )); + }; + remote + .call(FS_GET_METADATA_METHOD, ¶ms) + .await + .map_err(Into::into) + } + + pub async fn fs_read_directory( + &self, + params: FsReadDirectoryParams, + ) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_read_directory(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/readDirectory".to_string(), + )); + }; + remote + .call(FS_READ_DIRECTORY_METHOD, ¶ms) + .await + .map_err(Into::into) + } + + pub async fn fs_remove( + &self, + params: FsRemoveParams, + ) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_remove(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/remove".to_string(), + )); + }; + remote + .call(FS_REMOVE_METHOD, ¶ms) + .await + .map_err(Into::into) + } + + pub async fn fs_copy(&self, params: FsCopyParams) -> Result { + if let Some(backend) = self.inner.backend.as_local() { + return backend.fs_copy(params).await; + } + let Some(remote) = self.inner.backend.as_remote() else { + return Err(ExecServerError::Protocol( + "remote backend missing during fs/copy".to_string(), + )); + }; + remote + .call(FS_COPY_METHOD, ¶ms) + .await + .map_err(Into::into) + } + async fn connect( connection: JsonRpcConnection, options: ExecServerClientConnectOptions, diff --git a/codex-rs/exec-server/src/client/local_backend.rs b/codex-rs/exec-server/src/client/local_backend.rs index 16b16d3b10..e23a5361d3 100644 --- a/codex-rs/exec-server/src/client/local_backend.rs +++ b/codex-rs/exec-server/src/client/local_backend.rs @@ -10,6 +10,20 @@ use crate::protocol::TerminateResponse; use crate::protocol::WriteParams; use crate::protocol::WriteResponse; use crate::server::ExecServerHandler; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCopyResponse; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsCreateDirectoryResponse; +use codex_app_server_protocol::FsGetMetadataParams; +use codex_app_server_protocol::FsGetMetadataResponse; +use codex_app_server_protocol::FsReadDirectoryParams; +use codex_app_server_protocol::FsReadDirectoryResponse; +use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsRemoveResponse; +use codex_app_server_protocol::FsWriteFileParams; +use codex_app_server_protocol::FsWriteFileResponse; use super::ExecServerError; @@ -92,4 +106,95 @@ impl LocalBackend { message: error.message, }) } + + pub(super) async fn fs_read_file( + &self, + params: FsReadFileParams, + ) -> Result { + self.handler + .fs_read_file(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } + + pub(super) async fn fs_write_file( + &self, + params: FsWriteFileParams, + ) -> Result { + self.handler + .fs_write_file(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } + + pub(super) async fn fs_create_directory( + &self, + params: FsCreateDirectoryParams, + ) -> Result { + self.handler + .fs_create_directory(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } + + pub(super) async fn fs_get_metadata( + &self, + params: FsGetMetadataParams, + ) -> Result { + self.handler + .fs_get_metadata(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } + + pub(super) async fn fs_read_directory( + &self, + params: FsReadDirectoryParams, + ) -> Result { + self.handler + .fs_read_directory(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } + + pub(super) async fn fs_remove( + &self, + params: FsRemoveParams, + ) -> Result { + self.handler + .fs_remove(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } + + pub(super) async fn fs_copy( + &self, + params: FsCopyParams, + ) -> Result { + self.handler + .fs_copy(params) + .await + .map_err(|error| ExecServerError::Server { + code: error.code, + message: error.message, + }) + } } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 13e910351e..12bf0e17f9 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -11,6 +11,21 @@ pub use client::ExecServerError; pub use client_api::ExecServerClientConnectOptions; pub use client_api::ExecServerEvent; pub use client_api::RemoteExecServerConnectArgs; +pub use codex_app_server_protocol::FsCopyParams; +pub use codex_app_server_protocol::FsCopyResponse; +pub use codex_app_server_protocol::FsCreateDirectoryParams; +pub use codex_app_server_protocol::FsCreateDirectoryResponse; +pub use codex_app_server_protocol::FsGetMetadataParams; +pub use codex_app_server_protocol::FsGetMetadataResponse; +pub use codex_app_server_protocol::FsReadDirectoryEntry; +pub use codex_app_server_protocol::FsReadDirectoryParams; +pub use codex_app_server_protocol::FsReadDirectoryResponse; +pub use codex_app_server_protocol::FsReadFileParams; +pub use codex_app_server_protocol::FsReadFileResponse; +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 local::ExecServerLaunchCommand; pub use local::SpawnedExecServer; pub use local::spawn_local_exec_server; diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 7ed8e20ae4..ca7d89d355 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -13,6 +13,13 @@ pub const EXEC_WRITE_METHOD: &str = "process/write"; pub const EXEC_TERMINATE_METHOD: &str = "process/terminate"; pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output"; pub const EXEC_EXITED_METHOD: &str = "process/exited"; +pub const FS_READ_FILE_METHOD: &str = "fs/readFile"; +pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile"; +pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory"; +pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata"; +pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory"; +pub const FS_REMOVE_METHOD: &str = "fs/remove"; +pub const FS_COPY_METHOD: &str = "fs/copy"; pub const PROTOCOL_VERSION: &str = "exec-server.v0"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/codex-rs/exec-server/src/server.rs b/codex-rs/exec-server/src/server.rs index ba074e617f..bdd00130ee 100644 --- a/codex-rs/exec-server/src/server.rs +++ b/codex-rs/exec-server/src/server.rs @@ -1,3 +1,4 @@ +mod filesystem; mod handler; mod processor; mod registry; diff --git a/codex-rs/exec-server/src/server/filesystem.rs b/codex-rs/exec-server/src/server/filesystem.rs new file mode 100644 index 0000000000..09467e9e02 --- /dev/null +++ b/codex-rs/exec-server/src/server/filesystem.rs @@ -0,0 +1,170 @@ +use std::io; +use std::sync::Arc; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCopyResponse; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsCreateDirectoryResponse; +use codex_app_server_protocol::FsGetMetadataParams; +use codex_app_server_protocol::FsGetMetadataResponse; +use codex_app_server_protocol::FsReadDirectoryEntry; +use codex_app_server_protocol::FsReadDirectoryParams; +use codex_app_server_protocol::FsReadDirectoryResponse; +use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsRemoveResponse; +use codex_app_server_protocol::FsWriteFileParams; +use codex_app_server_protocol::FsWriteFileResponse; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_environment::CopyOptions; +use codex_environment::CreateDirectoryOptions; +use codex_environment::Environment; +use codex_environment::ExecutorFileSystem; +use codex_environment::RemoveOptions; + +use crate::rpc::internal_error; +use crate::rpc::invalid_request; + +#[derive(Clone)] +pub(crate) struct ExecServerFileSystem { + file_system: Arc, +} + +impl Default for ExecServerFileSystem { + fn default() -> Self { + Self { + file_system: Arc::new(Environment.get_filesystem()), + } + } +} + +impl ExecServerFileSystem { + pub(crate) async fn read_file( + &self, + params: FsReadFileParams, + ) -> Result { + let bytes = self + .file_system + .read_file(¶ms.path) + .await + .map_err(map_fs_error)?; + Ok(FsReadFileResponse { + data_base64: STANDARD.encode(bytes), + }) + } + + pub(crate) async fn write_file( + &self, + params: FsWriteFileParams, + ) -> Result { + 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) + .await + .map_err(map_fs_error)?; + Ok(FsWriteFileResponse {}) + } + + pub(crate) async fn create_directory( + &self, + params: FsCreateDirectoryParams, + ) -> Result { + self.file_system + .create_directory( + ¶ms.path, + CreateDirectoryOptions { + recursive: params.recursive.unwrap_or(true), + }, + ) + .await + .map_err(map_fs_error)?; + Ok(FsCreateDirectoryResponse {}) + } + + pub(crate) async fn get_metadata( + &self, + params: FsGetMetadataParams, + ) -> Result { + let metadata = self + .file_system + .get_metadata(¶ms.path) + .await + .map_err(map_fs_error)?; + Ok(FsGetMetadataResponse { + is_directory: metadata.is_directory, + is_file: metadata.is_file, + created_at_ms: metadata.created_at_ms, + modified_at_ms: metadata.modified_at_ms, + }) + } + + pub(crate) async fn read_directory( + &self, + params: FsReadDirectoryParams, + ) -> Result { + let entries = self + .file_system + .read_directory(¶ms.path) + .await + .map_err(map_fs_error)?; + Ok(FsReadDirectoryResponse { + entries: entries + .into_iter() + .map(|entry| FsReadDirectoryEntry { + file_name: entry.file_name, + is_directory: entry.is_directory, + is_file: entry.is_file, + }) + .collect(), + }) + } + + pub(crate) async fn remove( + &self, + params: FsRemoveParams, + ) -> Result { + self.file_system + .remove( + ¶ms.path, + RemoveOptions { + recursive: params.recursive.unwrap_or(true), + force: params.force.unwrap_or(true), + }, + ) + .await + .map_err(map_fs_error)?; + Ok(FsRemoveResponse {}) + } + + pub(crate) async fn copy( + &self, + params: FsCopyParams, + ) -> Result { + self.file_system + .copy( + ¶ms.source_path, + ¶ms.destination_path, + CopyOptions { + recursive: params.recursive, + }, + ) + .await + .map_err(map_fs_error)?; + Ok(FsCopyResponse {}) + } +} + +fn map_fs_error(err: io::Error) -> JSONRPCErrorError { + if err.kind() == io::ErrorKind::InvalidInput { + invalid_request(err.to_string()) + } else { + internal_error(err.to_string()) + } +} diff --git a/codex-rs/exec-server/src/server/handler.rs b/codex-rs/exec-server/src/server/handler.rs index 40a13cf1f9..a5a75a8e32 100644 --- a/codex-rs/exec-server/src/server/handler.rs +++ b/codex-rs/exec-server/src/server/handler.rs @@ -5,6 +5,20 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::time::Duration; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCopyResponse; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsCreateDirectoryResponse; +use codex_app_server_protocol::FsGetMetadataParams; +use codex_app_server_protocol::FsGetMetadataResponse; +use codex_app_server_protocol::FsReadDirectoryParams; +use codex_app_server_protocol::FsReadDirectoryResponse; +use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsRemoveResponse; +use codex_app_server_protocol::FsWriteFileParams; +use codex_app_server_protocol::FsWriteFileResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_utils_pty::ExecCommandSession; use codex_utils_pty::TerminalSize; @@ -30,6 +44,7 @@ use crate::rpc::RpcNotificationSender; use crate::rpc::internal_error; use crate::rpc::invalid_params; use crate::rpc::invalid_request; +use crate::server::filesystem::ExecServerFileSystem; const RETAINED_OUTPUT_BYTES_PER_PROCESS: usize = 1024 * 1024; #[cfg(test)] @@ -61,6 +76,7 @@ enum ProcessEntry { pub(crate) struct ExecServerHandler { notifications: RpcNotificationSender, + file_system: ExecServerFileSystem, processes: Arc>>, initialize_requested: AtomicBool, initialized: AtomicBool, @@ -70,6 +86,7 @@ impl ExecServerHandler { pub(crate) fn new(notifications: RpcNotificationSender) -> Self { Self { notifications, + file_system: ExecServerFileSystem::default(), processes: Arc::new(Mutex::new(HashMap::new())), initialize_requested: AtomicBool::new(false), initialized: AtomicBool::new(false), @@ -111,22 +128,22 @@ impl ExecServerHandler { Ok(()) } - fn require_initialized(&self) -> Result<(), JSONRPCErrorError> { + fn require_initialized_for(&self, method_family: &str) -> Result<(), JSONRPCErrorError> { if !self.initialize_requested.load(Ordering::SeqCst) { - return Err(invalid_request( - "client must call initialize before using exec methods".to_string(), - )); + return Err(invalid_request(format!( + "client must call initialize before using {method_family} methods" + ))); } if !self.initialized.load(Ordering::SeqCst) { - return Err(invalid_request( - "client must send initialized before using exec methods".to_string(), - )); + return Err(invalid_request(format!( + "client must send initialized before using {method_family} methods" + ))); } Ok(()) } pub(crate) async fn exec(&self, params: ExecParams) -> Result { - self.require_initialized()?; + self.require_initialized_for("exec")?; let process_id = params.process_id.clone(); let (program, args) = params @@ -231,7 +248,7 @@ impl ExecServerHandler { &self, params: ReadParams, ) -> Result { - self.require_initialized()?; + self.require_initialized_for("exec")?; let after_seq = params.after_seq.unwrap_or(0); let max_bytes = params.max_bytes.unwrap_or(usize::MAX); let wait = Duration::from_millis(params.wait_ms.unwrap_or(0)); @@ -300,7 +317,7 @@ impl ExecServerHandler { &self, params: WriteParams, ) -> Result { - self.require_initialized()?; + self.require_initialized_for("exec")?; let writer_tx = { let process_map = self.processes.lock().await; let process = process_map.get(¶ms.process_id).ok_or_else(|| { @@ -333,7 +350,7 @@ impl ExecServerHandler { &self, params: TerminateParams, ) -> Result { - self.require_initialized()?; + self.require_initialized_for("exec")?; let running = { let process_map = self.processes.lock().await; match process_map.get(¶ms.process_id) { @@ -347,6 +364,62 @@ impl ExecServerHandler { Ok(TerminateResponse { running }) } + + pub(crate) async fn fs_read_file( + &self, + params: FsReadFileParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.read_file(params).await + } + + pub(crate) async fn fs_write_file( + &self, + params: FsWriteFileParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.write_file(params).await + } + + pub(crate) async fn fs_create_directory( + &self, + params: FsCreateDirectoryParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.create_directory(params).await + } + + pub(crate) async fn fs_get_metadata( + &self, + params: FsGetMetadataParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.get_metadata(params).await + } + + pub(crate) async fn fs_read_directory( + &self, + params: FsReadDirectoryParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.read_directory(params).await + } + + pub(crate) async fn fs_remove( + &self, + params: FsRemoveParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.remove(params).await + } + + pub(crate) async fn fs_copy( + &self, + params: FsCopyParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.copy(params).await + } } async fn stream_output( diff --git a/codex-rs/exec-server/src/server/registry.rs b/codex-rs/exec-server/src/server/registry.rs index 50efdddd71..6ddfa0a430 100644 --- a/codex-rs/exec-server/src/server/registry.rs +++ b/codex-rs/exec-server/src/server/registry.rs @@ -5,6 +5,13 @@ use crate::protocol::EXEC_READ_METHOD; use crate::protocol::EXEC_TERMINATE_METHOD; use crate::protocol::EXEC_WRITE_METHOD; use crate::protocol::ExecParams; +use crate::protocol::FS_COPY_METHOD; +use crate::protocol::FS_CREATE_DIRECTORY_METHOD; +use crate::protocol::FS_GET_METADATA_METHOD; +use crate::protocol::FS_READ_DIRECTORY_METHOD; +use crate::protocol::FS_READ_FILE_METHOD; +use crate::protocol::FS_REMOVE_METHOD; +use crate::protocol::FS_WRITE_FILE_METHOD; use crate::protocol::INITIALIZE_METHOD; use crate::protocol::INITIALIZED_METHOD; use crate::protocol::InitializeParams; @@ -13,6 +20,13 @@ use crate::protocol::TerminateParams; use crate::protocol::WriteParams; use crate::rpc::RpcRouter; use crate::server::ExecServerHandler; +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; pub(crate) fn build_router() -> RpcRouter { let mut router = RpcRouter::new(); @@ -48,5 +62,47 @@ pub(crate) fn build_router() -> RpcRouter { handler.terminate(params).await }, ); + router.request( + FS_READ_FILE_METHOD, + |handler: Arc, params: FsReadFileParams| async move { + handler.fs_read_file(params).await + }, + ); + router.request( + FS_WRITE_FILE_METHOD, + |handler: Arc, params: FsWriteFileParams| async move { + handler.fs_write_file(params).await + }, + ); + router.request( + FS_CREATE_DIRECTORY_METHOD, + |handler: Arc, params: FsCreateDirectoryParams| async move { + handler.fs_create_directory(params).await + }, + ); + router.request( + FS_GET_METADATA_METHOD, + |handler: Arc, params: FsGetMetadataParams| async move { + handler.fs_get_metadata(params).await + }, + ); + router.request( + FS_READ_DIRECTORY_METHOD, + |handler: Arc, params: FsReadDirectoryParams| async move { + handler.fs_read_directory(params).await + }, + ); + router.request( + FS_REMOVE_METHOD, + |handler: Arc, params: FsRemoveParams| async move { + handler.fs_remove(params).await + }, + ); + router.request( + FS_COPY_METHOD, + |handler: Arc, params: FsCopyParams| async move { + handler.fs_copy(params).await + }, + ); router } diff --git a/codex-rs/exec-server/tests/stdio_smoke.rs b/codex-rs/exec-server/tests/stdio_smoke.rs index c08d7f3c9b..77374dc467 100644 --- a/codex-rs/exec-server/tests/stdio_smoke.rs +++ b/codex-rs/exec-server/tests/stdio_smoke.rs @@ -4,6 +4,8 @@ use std::process::Stdio; use std::time::Duration; use anyhow::Context; +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCRequest; @@ -15,6 +17,13 @@ use codex_exec_server::ExecServerClient; use codex_exec_server::ExecServerClientConnectOptions; use codex_exec_server::ExecServerEvent; use codex_exec_server::ExecServerLaunchCommand; +use codex_exec_server::FsCopyParams; +use codex_exec_server::FsCreateDirectoryParams; +use codex_exec_server::FsGetMetadataParams; +use codex_exec_server::FsReadDirectoryParams; +use codex_exec_server::FsReadFileParams; +use codex_exec_server::FsRemoveParams; +use codex_exec_server::FsWriteFileParams; use codex_exec_server::InitializeParams; use codex_exec_server::InitializeResponse; use codex_exec_server::RemoteExecServerConnectArgs; @@ -200,6 +209,102 @@ async fn exec_server_client_connects_over_websocket() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn exec_server_client_filesystem_round_trip_over_stdio() -> anyhow::Result<()> { + let server = spawn_local_exec_server( + ExecServerLaunchCommand { + program: cargo_bin("codex-exec-server")?, + args: Vec::new(), + }, + ExecServerClientConnectOptions { + client_name: "exec-server-test".to_string(), + initialize_timeout: Duration::from_secs(5), + }, + ) + .await?; + let client = server.client(); + + let root = std::env::temp_dir().join(format!( + "codex-exec-server-fs-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + )); + let directory = root.join("dir"); + let file_path = directory.join("hello.txt"); + let copy_path = directory.join("copy.txt"); + + client + .fs_create_directory(FsCreateDirectoryParams { + path: directory.clone().try_into()?, + recursive: Some(true), + }) + .await?; + + client + .fs_write_file(FsWriteFileParams { + path: file_path.clone().try_into()?, + data_base64: BASE64_STANDARD.encode(b"hello"), + }) + .await?; + + let metadata = client + .fs_get_metadata(FsGetMetadataParams { + path: file_path.clone().try_into()?, + }) + .await?; + assert!(metadata.is_file); + assert!(!metadata.is_directory); + + let read_file = client + .fs_read_file(FsReadFileParams { + path: file_path.clone().try_into()?, + }) + .await?; + assert_eq!(read_file.data_base64, BASE64_STANDARD.encode(b"hello")); + + let read_directory = client + .fs_read_directory(FsReadDirectoryParams { + path: directory.clone().try_into()?, + }) + .await?; + assert!( + read_directory + .entries + .iter() + .any(|entry| entry.file_name == "hello.txt" && entry.is_file) + ); + + client + .fs_copy(FsCopyParams { + source_path: file_path.clone().try_into()?, + destination_path: copy_path.clone().try_into()?, + recursive: false, + }) + .await?; + let copied = client + .fs_read_file(FsReadFileParams { + path: copy_path.clone().try_into()?, + }) + .await?; + assert_eq!(copied.data_base64, BASE64_STANDARD.encode(b"hello")); + + client + .fs_remove(FsRemoveParams { + path: root.clone().try_into()?, + recursive: Some(true), + force: Some(true), + }) + .await?; + + assert!( + !root.exists(), + "filesystem cleanup should remove the test tree" + ); + Ok(()) +} + async fn read_websocket_url(lines: &mut tokio::io::Lines>) -> anyhow::Result where R: tokio::io::AsyncRead + Unpin,