From 019402a8d07daba2eae1deaee6c5721be4c4761f Mon Sep 17 00:00:00 2001 From: Liang-Ting Jiang Date: Wed, 13 May 2026 10:35:47 -0700 Subject: [PATCH] Add Code Mode file broker --- codex-rs/core/src/tools/file_broker.rs | 243 +++++++++++++++++++++++++ codex-rs/core/src/tools/mod.rs | 1 + 2 files changed, 244 insertions(+) create mode 100644 codex-rs/core/src/tools/file_broker.rs diff --git a/codex-rs/core/src/tools/file_broker.rs b/codex-rs/core/src/tools/file_broker.rs new file mode 100644 index 0000000000..68d581049b --- /dev/null +++ b/codex-rs/core/src/tools/file_broker.rs @@ -0,0 +1,243 @@ +#![allow(dead_code)] + +use codex_tools::FileRef; +use codex_tools::FileScheme; +use std::fmt; +use std::fs; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +/// Minimal broker for moving bytes across Code Mode file refs. +/// +/// This POC intentionally implements only the workspace environment provider. +/// Connector, Library, and remote-environment adapters can plug in behind this +/// boundary without changing model-facing tool contracts. +#[derive(Debug)] +pub(crate) struct CodeModeFileBroker { + current_root: PathBuf, +} + +impl CodeModeFileBroker { + pub(crate) fn new(current_root: impl Into) -> Self { + Self { + current_root: current_root.into(), + } + } + + pub(crate) fn read_to_bytes(&self, source: &FileRef) -> Result, FileBrokerError> { + let source_path = self.resolve_env_path(source)?; + fs::read(&source_path).map_err(|source| FileBrokerError::Io { + action: "read", + source, + }) + } + + pub(crate) fn write_bytes( + &self, + target: &FileRef, + bytes: &[u8], + ) -> Result { + let target_path = self.resolve_env_path(target)?; + if let Some(parent) = target_path.parent() { + fs::create_dir_all(parent).map_err(|source| FileBrokerError::Io { + action: "create target directory", + source, + })?; + } + fs::write(&target_path, bytes).map_err(|source| FileBrokerError::Io { + action: "write", + source, + })?; + Ok(FileBrokerWriteResult { + file_ref: target.raw().to_string(), + byte_count: bytes.len() as u64, + }) + } + + pub(crate) fn copy( + &self, + source: &FileRef, + target: &FileRef, + ) -> Result { + let bytes = self.read_to_bytes(source)?; + let write_result = self.write_bytes(target, &bytes)?; + Ok(FileBrokerCopyResult { + source_ref: source.raw().to_string(), + target_ref: write_result.file_ref, + byte_count: write_result.byte_count, + }) + } + + fn resolve_env_path(&self, file_ref: &FileRef) -> Result { + if file_ref.scheme() != FileScheme::Env { + return Err(FileBrokerError::UnsupportedProvider { + file_ref: file_ref.raw().to_string(), + }); + } + + let Some(path) = file_ref.body().strip_prefix("current/") else { + return Err(FileBrokerError::UnsupportedEnvironment { + file_ref: file_ref.raw().to_string(), + }); + }; + if path.is_empty() { + return Err(FileBrokerError::InvalidEnvPath { + file_ref: file_ref.raw().to_string(), + }); + } + + let relative_path = + clean_relative_path(path).ok_or_else(|| FileBrokerError::InvalidEnvPath { + file_ref: file_ref.raw().to_string(), + })?; + Ok(self.current_root.join(relative_path)) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct FileBrokerWriteResult { + pub(crate) file_ref: String, + pub(crate) byte_count: u64, +} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct FileBrokerCopyResult { + pub(crate) source_ref: String, + pub(crate) target_ref: String, + pub(crate) byte_count: u64, +} + +#[derive(Debug)] +pub(crate) enum FileBrokerError { + UnsupportedProvider { + file_ref: String, + }, + UnsupportedEnvironment { + file_ref: String, + }, + InvalidEnvPath { + file_ref: String, + }, + Io { + action: &'static str, + source: std::io::Error, + }, +} + +impl fmt::Display for FileBrokerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedProvider { file_ref } => { + write!(f, "file provider for `{file_ref}` is not available") + } + Self::UnsupportedEnvironment { file_ref } => { + write!(f, "`{file_ref}` must use env://current/... in this runtime") + } + Self::InvalidEnvPath { file_ref } => { + write!(f, "`{file_ref}` must resolve to a relative workspace path") + } + Self::Io { action, source } => write!(f, "failed to {action} file: {source}"), + } + } +} + +impl std::error::Error for FileBrokerError {} + +fn clean_relative_path(path: &str) -> Option { + let mut clean = PathBuf::new(); + for component in Path::new(path).components() { + match component { + Component::Normal(part) => clean.push(part), + Component::CurDir => {} + Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None, + } + } + (!clean.as_os_str().is_empty()).then_some(clean) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use tempfile::TempDir; + + fn file_ref(raw: &str) -> FileRef { + FileRef::parse(raw).expect("file ref should parse") + } + + #[test] + fn writes_and_reads_env_current_refs() { + let temp_dir = TempDir::new().expect("temp dir"); + let broker = CodeModeFileBroker::new(temp_dir.path()); + + let target = file_ref("env://current/out/report.txt"); + let write_result = broker + .write_bytes(&target, b"hello") + .expect("write should succeed"); + + assert_eq!( + write_result, + FileBrokerWriteResult { + file_ref: "env://current/out/report.txt".to_string(), + byte_count: 5, + } + ); + assert_eq!( + broker.read_to_bytes(&target).expect("read should succeed"), + b"hello" + ); + } + + #[test] + fn copies_between_env_current_refs() { + let temp_dir = TempDir::new().expect("temp dir"); + let broker = CodeModeFileBroker::new(temp_dir.path()); + let source = file_ref("env://current/source.bin"); + let target = file_ref("env://current/nested/target.bin"); + broker + .write_bytes(&source, b"payload") + .expect("write should succeed"); + + let copy_result = broker.copy(&source, &target).expect("copy should succeed"); + + assert_eq!( + copy_result, + FileBrokerCopyResult { + source_ref: "env://current/source.bin".to_string(), + target_ref: "env://current/nested/target.bin".to_string(), + byte_count: 7, + } + ); + assert_eq!( + broker + .read_to_bytes(&target) + .expect("copied target should exist"), + b"payload" + ); + } + + #[test] + fn rejects_env_path_traversal() { + let temp_dir = TempDir::new().expect("temp dir"); + let broker = CodeModeFileBroker::new(temp_dir.path()); + let source = file_ref("env://current/../secret.txt"); + + assert!(matches!( + broker.read_to_bytes(&source), + Err(FileBrokerError::InvalidEnvPath { .. }) + )); + } + + #[test] + fn rejects_provider_refs_without_adapter() { + let temp_dir = TempDir::new().expect("temp dir"); + let broker = CodeModeFileBroker::new(temp_dir.path()); + let source = file_ref("oai_library://file_123"); + + assert!(matches!( + broker.read_to_bytes(&source), + Err(FileBrokerError::UnsupportedProvider { .. }) + )); + } +} diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 3073d9f9da..3a524bfd0a 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod code_mode; pub(crate) mod context; pub(crate) mod events; +pub(crate) mod file_broker; pub(crate) mod handlers; pub(crate) mod hook_names; pub(crate) mod hosted_spec;