diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index 5044568cd6..56f83f9f08 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -16,7 +16,6 @@ use crate::session::turn_context::TurnContext; use codex_api::OPENAI_FILE_UPLOAD_LIMIT_BYTES; use codex_api::upload_openai_file; use codex_login::CodexAuth; -use codex_utils_path_uri::PathUri; use serde_json::Value as JsonValue; pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( @@ -122,14 +121,11 @@ async fn build_uploaded_argument_value( "no primary turn environment is available".to_string(), )); }; - // TODO(anp): Resolve app tool file arguments using the selected environment's native path - // convention so uploads can read relative paths from foreign environments. - let native_environment_cwd = turn_environment + let path_uri = turn_environment .cwd() - .to_abs_path() + .join(file_path) .map_err(|error| contextualize_error(error.to_string()))?; - let resolved_path = native_environment_cwd.join(file_path); - let path_uri = PathUri::from_abs_path(&resolved_path); + let display_path = path_uri.inferred_native_path_string(); let fs = turn_environment.environment.get_filesystem(); let metadata = fs .get_metadata(&path_uri, /*sandbox*/ None) @@ -137,27 +133,20 @@ async fn build_uploaded_argument_value( .map_err(|error| contextualize_error(error.to_string()))?; if !metadata.is_file { return Err(contextualize_error(format!( - "path `{}` is not a file", - resolved_path.display() + "path `{display_path}` is not a file" ))); } if metadata.size > OPENAI_FILE_UPLOAD_LIMIT_BYTES { return Err(contextualize_error(format!( - "file `{}` is too large: {} bytes exceeds the limit of {} bytes", - resolved_path.display(), - metadata.size, - OPENAI_FILE_UPLOAD_LIMIT_BYTES, + "file `{display_path}` is too large: {} bytes exceeds the limit of {} bytes", + metadata.size, OPENAI_FILE_UPLOAD_LIMIT_BYTES, ))); } let contents = fs .read_file_stream(&path_uri, /*sandbox*/ None) .await .map_err(|error| contextualize_error(error.to_string()))?; - let file_name = resolved_path - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or("file") - .to_string(); + let file_name = path_uri.basename().unwrap_or_else(|| "file".to_string()); let upload_auth = codex_model_provider::auth_provider_from_auth(auth); let uploaded = upload_openai_file( turn_context.config.chatgpt_base_url.trim_end_matches('/'), diff --git a/codex-rs/core/tests/suite/openai_file_mcp.rs b/codex-rs/core/tests/suite/openai_file_mcp.rs index c6d7a1d437..3c9445a36f 100644 --- a/codex-rs/core/tests/suite/openai_file_mcp.rs +++ b/codex-rs/core/tests/suite/openai_file_mcp.rs @@ -185,9 +185,10 @@ async fn run_extract_turn(test: &TestCodex, server: &MockServer) -> Result Result<()> { - // TODO(anp): Remove after file-upload fixtures support target-native Windows paths. - skip_if_wine_exec!(Ok(()), "uses a host-native file-upload path"); - + skip_if_wine_exec!( + Ok(()), + "13 MiB fixture exceeds the Wine exec WebSocket frame limit after base64 encoding" + ); let server = start_mock_server().await; let apps_server = AppsTestServer::mount(&server).await?; mount_file_upload_mocks(&server, STREAMED_FILE_SIZE as u64).await; @@ -246,6 +247,33 @@ async fn codex_apps_file_params_upload_environment_files_before_mcp_tool_call() Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn codex_apps_file_params_upload_relative_path_from_selected_environment() -> Result<()> { + let server = start_mock_server().await; + let apps_server = AppsTestServer::mount(&server).await?; + mount_file_upload_mocks(&server, /*file_size_bytes*/ 11).await; + + let mut builder = apps_enabled_builder(apps_server.chatgpt_base_url.clone()) + .with_workspace_setup(|cwd, fs| async move { + let report_path = PathUri::from_abs_path(&cwd.join("report.txt")); + fs.write_file(&report_path, b"hello world".to_vec(), /*sandbox*/ None) + .await?; + Ok(()) + }); + let test = builder.build_with_auto_env(&server).await?; + let _responses = run_extract_turn(&test, &server).await?; + + let apps_tool_call = + recorded_apps_tool_call_by_name(&server, CALENDAR_EXTRACT_TEXT_TOOL_NAME).await; + assert_eq!( + apps_tool_call.pointer("/params/arguments/file"), + Some(&uploaded_file(&server, /*file_size_bytes*/ 11)) + ); + + server.verify().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn codex_apps_file_params_pass_uploaded_file_to_post_tool_use_hook() -> Result<()> { let server = start_mock_server().await;