[codex-apps] Filter optional file fields by tool schema (#31686)

## Summary

Codex Apps file parameters are exposed to the model as local paths,
uploaded at execution time, and rewritten into provided-file payloads
before the MCP tool call.

The rewrite currently includes the documented optional fields
`mime_type` and `file_name` for every file parameter. Apps with strict
schemas can reject those fields when they are not declared.

## Changes

- Derive the supported optional file fields from each
`openai/fileParams` parameter's raw input schema before replacing it
with the model-visible local-path schema.
- Always include `download_url` and `file_id`.
- Include `mime_type` and `file_name` only when that specific file
parameter's schema accepts them, including schemas that allow additional
properties.
- Handle scalar and array file parameters, including items-only arrays,
composed schemas, and local JSON Schema references.
- Preserve the existing restriction that only the host-owned Codex Apps
MCP server can use this upload path.

This supports tools with different file contracts in the same app: one
parameter can accept the optional fields while another remains strict.

## Validation

- `just test -p codex-mcp`
- `just test -p codex-core mcp_openai_file`
- `just test -p codex-core codex_apps_file_params_`
- `just fix -p codex-mcp`
- `just fix -p codex-core`
- `just fmt`
- `git diff --check`
- Manually verified in the Codex Electron app that:
  - a strict file schema receives only `download_url` and `file_id`
  - a rich file schema also receives `mime_type` and `file_name`

Related: #31330
This commit is contained in:
thomas authored and GitHub committed 2026-07-09 14:32:23 -07:00
1 parent 6d3cde7fcd
commit 8347b8de21
21 files changed
+613 -233

No files matched your search

+8 -9
View File
@@ -41,7 +41,6 @@ use codex_mcp::McpPermissionPromptAutoApproveContext;
use codex_mcp::SandboxState;
use codex_mcp::auth_elicitation_completed_result;
use codex_mcp::build_auth_elicitation_plan;
use codex_mcp::declared_openai_file_input_param_names;
use codex_mcp::mcp_permission_prompt_is_auto_approved;
use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::items::McpToolCallError;
@@ -382,7 +381,7 @@ async fn handle_approved_mcp_tool_call(
sess,
turn_context,
arguments_value.clone(),
metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()),
metadata.and_then(|metadata| metadata.openai_file_input_optional_fields.as_ref()),
)
.await;
let tool_input = match &rewrite {
@@ -1019,7 +1018,7 @@ pub(crate) struct McpToolApprovalMetadata {
mcp_app_resource_uri: Option<String>,
template_id: Option<String>,
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
openai_file_input_params: Option<Vec<String>>,
openai_file_input_optional_fields: Option<HashMap<String, Vec<String>>>,
}
const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
@@ -1558,19 +1557,19 @@ pub(crate) async fn lookup_mcp_tool_metadata(
.map(str::to_string),
codex_apps_meta,
// Disallow custom MCPs from uploading files via fileParams.
openai_file_input_params: openai_file_input_params_for_server(
openai_file_input_optional_fields: openai_file_input_optional_fields_for_server(
server,
tool_info.tool.meta.as_deref(),
&tool_info.openai_file_input_optional_fields,
),
})
}
fn openai_file_input_params_for_server(
fn openai_file_input_optional_fields_for_server(
server: &str,
meta: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<Vec<String>> {
openai_file_input_optional_fields: &HashMap<String, Vec<String>>,
) -> Option<HashMap<String, Vec<String>>> {
(server == CODEX_APPS_MCP_SERVER_NAME)
.then_some(declared_openai_file_input_param_names(meta))
.then(|| openai_file_input_optional_fields.clone())
.filter(|params| !params.is_empty())
}