Compare commits

...

2 Commits

Author SHA1 Message Date
Liang-Ting Jiang
019402a8d0 Add Code Mode file broker 2026-05-13 10:35:47 -07:00
Liang-Ting Jiang
6fd7c23dca Define file ref contract for Code Mode 2026-05-13 10:31:54 -07:00
4 changed files with 382 additions and 0 deletions

View File

@@ -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<PathBuf>) -> Self {
Self {
current_root: current_root.into(),
}
}
pub(crate) fn read_to_bytes(&self, source: &FileRef) -> Result<Vec<u8>, 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<FileBrokerWriteResult, FileBrokerError> {
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<FileBrokerCopyResult, FileBrokerError> {
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<PathBuf, FileBrokerError> {
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<PathBuf> {
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 { .. })
));
}
}

View File

@@ -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;

View File

@@ -0,0 +1,134 @@
use std::fmt;
/// Fully qualified reference to a file-like asset known to Code Mode.
///
/// The scheme tells the file broker which provider owns the asset. The broker
/// should keep provider-specific credentials and bytes out of model-visible
/// arguments while still letting Code Mode pass stable refs between tools.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileRef {
raw: String,
scheme: FileScheme,
body: String,
}
impl FileRef {
pub fn parse(raw: impl Into<String>) -> Result<Self, FileRefParseError> {
let raw = raw.into();
let Some((scheme, body)) = raw.split_once("://") else {
return Err(FileRefParseError::MissingScheme);
};
if body.is_empty() {
return Err(FileRefParseError::MissingBody);
}
let scheme = FileScheme::parse(scheme)?;
let body = body.to_string();
Ok(Self { raw, scheme, body })
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn scheme(&self) -> FileScheme {
self.scheme
}
pub fn body(&self) -> &str {
&self.body
}
}
/// Provider family that owns a file ref.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FileScheme {
Env,
Library,
Connector,
Other,
}
impl FileScheme {
fn parse(scheme: &str) -> Result<Self, FileRefParseError> {
if scheme.is_empty() {
return Err(FileRefParseError::MissingScheme);
}
if !scheme
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-')
{
return Err(FileRefParseError::InvalidScheme(scheme.to_string()));
}
Ok(match scheme {
"env" => Self::Env,
"oai_library" => Self::Library,
"connector" => Self::Connector,
_ => Self::Other,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FileRefParseError {
MissingScheme,
InvalidScheme(String),
MissingBody,
}
impl fmt::Display for FileRefParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingScheme => write!(f, "file ref must start with a provider scheme"),
Self::InvalidScheme(scheme) => write!(f, "invalid file ref scheme `{scheme}`"),
Self::MissingBody => write!(f, "file ref must include a provider-owned path or id"),
}
}
}
impl std::error::Error for FileRefParseError {}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn parses_env_file_ref() {
assert_eq!(
FileRef::parse("env://current/work/report.pdf"),
Ok(FileRef {
raw: "env://current/work/report.pdf".to_string(),
scheme: FileScheme::Env,
body: "current/work/report.pdf".to_string(),
})
);
}
#[test]
fn classifies_known_provider_schemes() {
assert_eq!(
FileRef::parse("oai_library://file_123")
.expect("library ref should parse")
.scheme(),
FileScheme::Library
);
assert_eq!(
FileRef::parse("connector://google_drive/file_123")
.expect("connector ref should parse")
.scheme(),
FileScheme::Connector
);
}
#[test]
fn rejects_ambiguous_refs() {
assert_eq!(
FileRef::parse("report.pdf"),
Err(FileRefParseError::MissingScheme)
);
assert_eq!(
FileRef::parse("env://"),
Err(FileRefParseError::MissingBody)
);
}
}

View File

@@ -3,6 +3,7 @@
mod code_mode;
mod dynamic_tool;
mod file_ref;
mod function_call_error;
mod image_detail;
mod json_schema;
@@ -25,6 +26,9 @@ pub use code_mode::collect_code_mode_tool_definitions;
pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use codex_protocol::ToolName;
pub use dynamic_tool::parse_dynamic_tool;
pub use file_ref::FileRef;
pub use file_ref::FileRefParseError;
pub use file_ref::FileScheme;
pub use function_call_error::FunctionCallError;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;