Files
codex/codex-rs/app-server/src/request_processors/fs_processor.rs
T
starr-openai 1509ae6d8d Route local-only app-server gating through processors (#23551)
## Summary
- move local-only app-server gating out of `MessageProcessor`
- let `fs/*`, `command/exec`, and `process/spawn` resolve local
availability inside their owning processors
- keep `fs/*` mounted for the future environment-param path while
preserving current no-local error behavior

## Validation
- not run locally per Codex repo guidance
2026-05-19 14:38:03 -07:00

211 lines
6.7 KiB
Rust

use crate::error_code::internal_error;
use crate::error_code::invalid_request;
use crate::fs_watch::FsWatchManager;
use crate::outgoing_message::ConnectionId;
use base64::Engine;
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::FsUnwatchParams;
use codex_app_server_protocol::FsUnwatchResponse;
use codex_app_server_protocol::FsWatchParams;
use codex_app_server_protocol::FsWatchResponse;
use codex_app_server_protocol::FsWriteFileParams;
use codex_app_server_protocol::FsWriteFileResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions;
use std::io;
use std::sync::Arc;
#[derive(Clone)]
pub(crate) struct FsRequestProcessor {
environment_manager: Arc<EnvironmentManager>,
fs_watch_manager: FsWatchManager,
}
impl FsRequestProcessor {
pub(crate) fn new(
environment_manager: Arc<EnvironmentManager>,
fs_watch_manager: FsWatchManager,
) -> Self {
Self {
environment_manager,
fs_watch_manager,
}
}
fn file_system(&self) -> Result<Arc<dyn ExecutorFileSystem>, JSONRPCErrorError> {
self.environment_manager
.try_local_environment()
.map(|environment| environment.get_filesystem())
.ok_or_else(|| internal_error("local filesystem is not configured"))
}
pub(crate) async fn connection_closed(&self, connection_id: ConnectionId) {
self.fs_watch_manager.connection_closed(connection_id).await;
}
pub(crate) async fn read_file(
&self,
params: FsReadFileParams,
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let bytes = self
.file_system()?
.read_file(&params.path, /*sandbox*/ None)
.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/writeFile requires valid base64 dataBase64: {err}"
))
})?;
self.file_system()?
.write_file(&params.path, bytes, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
}
pub(crate) async fn create_directory(
&self,
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
self.file_system()?
.create_directory(
&params.path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
/*sandbox*/ None,
)
.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(&params.path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
is_directory: metadata.is_directory,
is_file: metadata.is_file,
is_symlink: metadata.is_symlink,
created_at_ms: metadata.created_at_ms,
modified_at_ms: metadata.modified_at_ms,
})
}
pub(crate) async fn read_directory(
&self,
params: FsReadDirectoryParams,
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let entries = self
.file_system()?
.read_directory(&params.path, /*sandbox*/ None)
.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<FsRemoveResponse, JSONRPCErrorError> {
self.file_system()?
.remove(
&params.path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
},
/*sandbox*/ None,
)
.await
.map_err(map_fs_error)?;
Ok(FsRemoveResponse {})
}
pub(crate) async fn copy(
&self,
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
self.file_system()?
.copy(
&params.source_path,
&params.destination_path,
CopyOptions {
recursive: params.recursive,
},
/*sandbox*/ None,
)
.await
.map_err(map_fs_error)?;
Ok(FsCopyResponse {})
}
pub(crate) async fn watch(
&self,
connection_id: ConnectionId,
params: FsWatchParams,
) -> Result<FsWatchResponse, JSONRPCErrorError> {
self.file_system()?;
self.fs_watch_manager.watch(connection_id, params).await
}
pub(crate) async fn unwatch(
&self,
connection_id: ConnectionId,
params: FsUnwatchParams,
) -> Result<FsUnwatchResponse, JSONRPCErrorError> {
self.file_system()?;
self.fs_watch_manager.unwatch(connection_id, params).await
}
}
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())
}
}