Compare commits

...

3 Commits

Author SHA1 Message Date
Liang-Ting Jiang
5477a1771e Expose gated Code Mode files tools 2026-05-13 17:12:00 -07:00
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
9 changed files with 948 additions and 0 deletions

View File

@@ -0,0 +1,496 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use codex_tools::FileRef;
use codex_tools::FileScheme;
use std::collections::BTreeMap;
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,
}
const MAX_DATA_URI_EXPORT_BYTES: usize = 8 * 1024 * 1024;
const CONFIGURED_BROKER_ROUTES: &[ConfiguredBrokerRoute] = &[ConfiguredBrokerRoute {
provider: "google_drive",
operation: "upload",
required_server: CODEX_APPS_MCP_SERVER_NAME,
required_namespace: "google_drive",
required_tool: "upload_file",
}];
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,
})
}
pub(crate) fn export_data_uri(
&self,
source: &FileRef,
mime_type: &str,
) -> Result<FileBrokerDataUriResult, FileBrokerError> {
let bytes = self.read_to_bytes(source)?;
if bytes.len() > MAX_DATA_URI_EXPORT_BYTES {
return Err(FileBrokerError::DataUriTooLarge {
file_ref: source.raw().to_string(),
byte_count: bytes.len() as u64,
max_byte_count: MAX_DATA_URI_EXPORT_BYTES as u64,
});
}
let data_uri = format!(
"data:{mime_type};base64,{}",
BASE64_STANDARD.encode(bytes.as_slice())
);
Ok(FileBrokerDataUriResult {
source_ref: source.raw().to_string(),
mime_type: mime_type.to_string(),
data_uri,
byte_count: bytes.len() as u64,
})
}
pub(crate) fn active_provider_registry(mcp_tools: &[ToolInfo]) -> ActiveProviderRegistry {
ActiveProviderRegistry::from_routes(CONFIGURED_BROKER_ROUTES, mcp_tools)
}
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, Clone, Copy, Eq, PartialEq)]
struct ConfiguredBrokerRoute {
provider: &'static str,
operation: &'static str,
required_server: &'static str,
required_namespace: &'static str,
required_tool: &'static str,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct ActiveProviderRegistry {
pub(crate) providers: Vec<ActiveBrokerProvider>,
pub(crate) unavailable_routes: Vec<UnavailableBrokerRoute>,
}
impl ActiveProviderRegistry {
fn from_routes(routes: &[ConfiguredBrokerRoute], mcp_tools: &[ToolInfo]) -> Self {
let mut active_operations = BTreeMap::<String, Vec<String>>::new();
let mut unavailable_routes = Vec::new();
for route in routes {
if route_is_available(route, mcp_tools) {
active_operations
.entry(route.provider.to_string())
.or_default()
.push(route.operation.to_string());
} else {
unavailable_routes.push(UnavailableBrokerRoute {
provider: route.provider.to_string(),
operation: route.operation.to_string(),
required_server: route.required_server.to_string(),
required_namespace: route.required_namespace.to_string(),
required_tool: route.required_tool.to_string(),
});
}
}
let providers = active_operations
.into_iter()
.map(|(provider, mut operations)| {
operations.sort();
ActiveBrokerProvider {
provider,
operations,
}
})
.collect();
Self {
providers,
unavailable_routes,
}
}
pub(crate) fn summary(&self) -> String {
if self.providers.is_empty() {
return "none".to_string();
}
self.providers
.iter()
.map(|provider| format!("{}: {}", provider.provider, provider.operations.join(", ")))
.collect::<Vec<_>>()
.join("; ")
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct ActiveBrokerProvider {
pub(crate) provider: String,
pub(crate) operations: Vec<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct UnavailableBrokerRoute {
pub(crate) provider: String,
pub(crate) operation: String,
pub(crate) required_server: String,
pub(crate) required_namespace: String,
pub(crate) required_tool: String,
}
fn route_is_available(route: &ConfiguredBrokerRoute, mcp_tools: &[ToolInfo]) -> bool {
mcp_tools.iter().any(|tool| {
tool.server_name == route.required_server
&& tool.callable_namespace == route.required_namespace
&& (tool.callable_name == route.required_tool
|| tool.tool.name.as_ref() == route.required_tool)
})
}
#[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, Eq, PartialEq)]
pub(crate) struct FileBrokerDataUriResult {
pub(crate) source_ref: String,
pub(crate) mime_type: String,
pub(crate) data_uri: 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,
},
DataUriTooLarge {
file_ref: String,
byte_count: u64,
max_byte_count: u64,
},
}
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}"),
Self::DataUriTooLarge {
file_ref,
byte_count,
max_byte_count,
} => write!(
f,
"`{file_ref}` is {byte_count} bytes; max data URI export is {max_byte_count} bytes"
),
}
}
}
impl std::error::Error for FileBrokerError {}
impl FileBrokerError {
pub(crate) fn should_include_active_provider_status(&self) -> bool {
matches!(
self,
Self::UnsupportedProvider { .. } | Self::UnsupportedEnvironment { .. }
)
}
}
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 rmcp::model::Tool;
use serde_json::Map;
use std::sync::Arc;
use tempfile::TempDir;
fn file_ref(raw: &str) -> FileRef {
FileRef::parse(raw).expect("file ref should parse")
}
fn mcp_tool(server_name: &str, namespace: &str, name: &str) -> ToolInfo {
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: name.to_string(),
callable_namespace: namespace.to_string(),
namespace_description: None,
tool: Tool {
name: name.to_string().into(),
title: None,
description: None,
input_schema: Arc::new(Map::new()),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: None,
connector_name: None,
plugin_display_names: Vec::new(),
}
}
#[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 { .. })
));
}
#[test]
fn exports_env_current_ref_as_data_uri() {
let temp_dir = TempDir::new().expect("temp dir");
let broker = CodeModeFileBroker::new(temp_dir.path());
let source = file_ref("env://current/image.png");
broker
.write_bytes(&source, b"png")
.expect("write should succeed");
let result = broker
.export_data_uri(&source, "image/png")
.expect("export should succeed");
assert_eq!(
result,
FileBrokerDataUriResult {
source_ref: "env://current/image.png".to_string(),
mime_type: "image/png".to_string(),
data_uri: "data:image/png;base64,cG5n".to_string(),
byte_count: 3,
}
);
}
#[test]
fn active_provider_registry_uses_live_mcp_inventory() {
let registry = CodeModeFileBroker::active_provider_registry(&[mcp_tool(
CODEX_APPS_MCP_SERVER_NAME,
"google_drive",
"upload_file",
)]);
assert_eq!(
registry.providers,
vec![ActiveBrokerProvider {
provider: "google_drive".to_string(),
operations: vec!["upload".to_string()],
}]
);
assert!(registry.unavailable_routes.is_empty());
assert_eq!(registry.summary(), "google_drive: upload");
}
#[test]
fn active_provider_registry_reports_missing_route_dependency() {
let registry = CodeModeFileBroker::active_provider_registry(&[]);
assert!(registry.providers.is_empty());
assert_eq!(
registry.unavailable_routes,
vec![UnavailableBrokerRoute {
provider: "google_drive".to_string(),
operation: "upload".to_string(),
required_server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
required_namespace: "google_drive".to_string(),
required_tool: "upload_file".to_string(),
}]
);
assert_eq!(registry.summary(), "none");
}
}

View File

@@ -0,0 +1,170 @@
use crate::function_tool::FunctionCallError;
use crate::session::session::Session;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::file_broker::CodeModeFileBroker;
use crate::tools::file_broker::FileBrokerError;
use crate::tools::handlers::files_spec::FILES_COPY_TOOL_NAME;
use crate::tools::handlers::files_spec::FILES_EXPORT_FOR_TOOL_NAME;
use crate::tools::handlers::files_spec::FILES_MATERIALIZE_TOOL_NAME;
use crate::tools::handlers::files_spec::FILES_NAMESPACE;
use crate::tools::handlers::files_spec::create_files_namespace_tool;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::FileRef;
use codex_tools::JsonToolOutput;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;
pub(crate) struct FilesMaterializeHandler;
pub(crate) struct FilesCopyHandler;
pub(crate) struct FilesExportForToolHandler;
#[derive(Debug, Deserialize)]
struct SourceTargetArgs {
source_uri: String,
target_uri: String,
}
#[derive(Debug, Deserialize)]
struct ExportForToolArgs {
file_uri: String,
mime_type: String,
}
impl ToolExecutor<ToolInvocation> for FilesMaterializeHandler {
type Output = JsonToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::namespaced(FILES_NAMESPACE, FILES_MATERIALIZE_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_files_namespace_tool())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let session = invocation.session.clone();
let args = parse_source_target_args(invocation.payload, FILES_MATERIALIZE_TOOL_NAME)?;
let broker = CodeModeFileBroker::new(invocation.turn.cwd.as_path());
let result = match broker.copy(
&parse_file_ref(&args.source_uri)?,
&parse_file_ref(&args.target_uri)?,
) {
Ok(result) => result,
Err(error) => return Err(file_broker_error(error, &session).await),
};
Ok(JsonToolOutput::new(json!({
"source_uri": result.source_ref,
"file_uri": result.target_ref,
"byte_count": result.byte_count,
})))
}
}
impl ToolHandler for FilesMaterializeHandler {}
impl ToolExecutor<ToolInvocation> for FilesCopyHandler {
type Output = JsonToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::namespaced(FILES_NAMESPACE, FILES_COPY_TOOL_NAME)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let session = invocation.session.clone();
let args = parse_source_target_args(invocation.payload, FILES_COPY_TOOL_NAME)?;
let broker = CodeModeFileBroker::new(invocation.turn.cwd.as_path());
let result = match broker.copy(
&parse_file_ref(&args.source_uri)?,
&parse_file_ref(&args.target_uri)?,
) {
Ok(result) => result,
Err(error) => return Err(file_broker_error(error, &session).await),
};
Ok(JsonToolOutput::new(json!({
"source_uri": result.source_ref,
"target_uri": result.target_ref,
"byte_count": result.byte_count,
})))
}
}
impl ToolHandler for FilesCopyHandler {}
impl ToolExecutor<ToolInvocation> for FilesExportForToolHandler {
type Output = JsonToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::namespaced(FILES_NAMESPACE, FILES_EXPORT_FOR_TOOL_NAME)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let session = invocation.session.clone();
let args = parse_export_for_tool_args(invocation.payload)?;
let broker = CodeModeFileBroker::new(invocation.turn.cwd.as_path());
let result = match broker.export_data_uri(&parse_file_ref(&args.file_uri)?, &args.mime_type)
{
Ok(result) => result,
Err(error) => return Err(file_broker_error(error, &session).await),
};
Ok(JsonToolOutput::new(json!({
"file_uri": result.source_ref,
"mime_type": result.mime_type,
"data_uri": result.data_uri,
"byte_count": result.byte_count,
})))
}
}
impl ToolHandler for FilesExportForToolHandler {}
fn parse_source_target_args(
payload: ToolPayload,
tool_name: &str,
) -> Result<SourceTargetArgs, FunctionCallError> {
parse_payload_arguments(payload, tool_name).and_then(|arguments| parse_arguments(&arguments))
}
fn parse_export_for_tool_args(
payload: ToolPayload,
) -> Result<ExportForToolArgs, FunctionCallError> {
parse_payload_arguments(payload, FILES_EXPORT_FOR_TOOL_NAME)
.and_then(|arguments| parse_arguments(&arguments))
}
fn parse_payload_arguments(
payload: ToolPayload,
tool_name: &str,
) -> Result<String, FunctionCallError> {
match payload {
ToolPayload::Function { arguments } => Ok(arguments),
_ => Err(FunctionCallError::RespondToModel(format!(
"{FILES_NAMESPACE}.{tool_name} received unsupported payload"
))),
}
}
fn parse_file_ref(raw: &str) -> Result<FileRef, FunctionCallError> {
FileRef::parse(raw)
.map_err(|err| FunctionCallError::RespondToModel(format!("invalid file ref: {err}")))
}
async fn file_broker_error(error: FileBrokerError, session: &Arc<Session>) -> FunctionCallError {
if !error.should_include_active_provider_status() {
return FunctionCallError::RespondToModel(error.to_string());
}
let manager = session.services.mcp_connection_manager.read().await;
let mcp_tools = manager.list_all_tools().await;
let registry = CodeModeFileBroker::active_provider_registry(&mcp_tools);
FunctionCallError::RespondToModel(format!(
"{}. Active broker providers: {}",
error,
registry.summary()
))
}

View File

@@ -0,0 +1,97 @@
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolSpec;
use std::collections::BTreeMap;
pub(crate) const FILES_NAMESPACE: &str = "files";
pub(crate) const FILES_MATERIALIZE_TOOL_NAME: &str = "materialize";
pub(crate) const FILES_COPY_TOOL_NAME: &str = "copy";
pub(crate) const FILES_EXPORT_FOR_TOOL_NAME: &str = "export_for_tool";
pub(crate) fn create_files_namespace_tool() -> ToolSpec {
ToolSpec::Namespace(ResponsesApiNamespace {
name: FILES_NAMESPACE.to_string(),
description: "Move Code Mode file refs between the workspace and provider/tool boundaries."
.to_string(),
tools: vec![
ResponsesApiNamespaceTool::Function(materialize_tool()),
ResponsesApiNamespaceTool::Function(copy_tool()),
ResponsesApiNamespaceTool::Function(export_for_tool_tool()),
],
})
}
fn materialize_tool() -> ResponsesApiTool {
ResponsesApiTool {
name: FILES_MATERIALIZE_TOOL_NAME.to_string(),
description: "Materialize a source file ref into an environment file ref. The initial POC supports env://current/... refs; provider adapters can be added behind the same contract.".to_string(),
strict: false,
defer_loading: None,
parameters: source_target_schema(),
output_schema: None,
}
}
fn copy_tool() -> ResponsesApiTool {
ResponsesApiTool {
name: FILES_COPY_TOOL_NAME.to_string(),
description:
"Copy bytes from one file ref to another without exposing provider credentials to the model."
.to_string(),
strict: false,
defer_loading: None,
parameters: source_target_schema(),
output_schema: None,
}
}
fn export_for_tool_tool() -> ResponsesApiTool {
ResponsesApiTool {
name: FILES_EXPORT_FOR_TOOL_NAME.to_string(),
description: "Export a file ref as a base64 data URI for tools that declare fileParam-style inputs. Use this from Code Mode generated code immediately before invoking the destination tool; do not log the returned data URI.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(
BTreeMap::from([
(
"file_uri".to_string(),
JsonSchema::string(Some("Source file ref, for example env://current/out.png.".to_string())),
),
(
"mime_type".to_string(),
JsonSchema::string(Some("MIME type to use in the returned data URI, for example image/png.".to_string())),
),
]),
Some(vec!["file_uri".to_string(), "mime_type".to_string()]),
Some(false.into()),
),
output_schema: None,
}
}
fn source_target_schema() -> JsonSchema {
JsonSchema::object(
BTreeMap::from([
(
"source_uri".to_string(),
JsonSchema::string(Some(
"Source file ref, for example env://current/report.pdf.".to_string(),
)),
),
(
"target_uri".to_string(),
JsonSchema::string(Some(
"Target file ref, for example env://current/out/report.pdf.".to_string(),
)),
),
]),
Some(vec!["source_uri".to_string(), "target_uri".to_string()]),
Some(false.into()),
)
}
#[cfg(test)]
#[path = "files_spec_tests.rs"]
mod tests;

View File

@@ -0,0 +1,28 @@
use super::*;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolSpec;
use pretty_assertions::assert_eq;
#[test]
fn files_namespace_contains_poc_actions() {
let ToolSpec::Namespace(namespace) = create_files_namespace_tool() else {
panic!("files tool should be a namespace");
};
assert_eq!(namespace.name, FILES_NAMESPACE);
let tool_names = namespace
.tools
.iter()
.map(|tool| match tool {
ResponsesApiNamespaceTool::Function(tool) => tool.name.as_str(),
})
.collect::<Vec<_>>();
assert_eq!(
tool_names,
vec![
FILES_MATERIALIZE_TOOL_NAME,
FILES_COPY_TOOL_NAME,
FILES_EXPORT_FOR_TOOL_NAME,
]
);
}

View File

@@ -4,6 +4,8 @@ pub(crate) mod apply_patch;
pub(crate) mod apply_patch_spec;
mod dynamic;
pub(crate) mod extension_tools;
mod files;
pub(crate) mod files_spec;
mod goal;
pub(crate) mod goal_spec;
mod mcp;
@@ -51,6 +53,9 @@ pub use apply_patch::ApplyPatchHandler;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::AskForApproval;
pub use dynamic::DynamicToolHandler;
pub(crate) use files::FilesCopyHandler;
pub(crate) use files::FilesExportForToolHandler;
pub(crate) use files::FilesMaterializeHandler;
pub use goal::CreateGoalHandler;
pub use goal::GetGoalHandler;
pub use goal::UpdateGoalHandler;

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

@@ -6,6 +6,9 @@ use crate::tools::handlers::CreateGoalHandler;
use crate::tools::handlers::DynamicToolHandler;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::FilesCopyHandler;
use crate::tools::handlers::FilesExportForToolHandler;
use crate::tools::handlers::FilesMaterializeHandler;
use crate::tools::handlers::GetGoalHandler;
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
use crate::tools::handlers::ListMcpResourcesHandler;
@@ -333,6 +336,16 @@ fn collect_handler_tools(
handlers.push(Arc::new(TestSyncHandler));
}
if config
.experimental_supported_tools
.iter()
.any(|tool| tool == "code_mode_files")
{
handlers.push(Arc::new(FilesMaterializeHandler));
handlers.push(Arc::new(FilesCopyHandler));
handlers.push(Arc::new(FilesExportForToolHandler));
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);

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;