mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[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:
1 parent
6d3cde7fcd
commit
8347b8de21
21 files changed
+613
-233
No files matched your search
@@ -1,7 +1,6 @@
|
||||
use super::*;
|
||||
use crate::codex_apps_cache::CodexAppsToolsCache;
|
||||
use crate::codex_apps_cache::CodexAppsToolsCacheContext;
|
||||
use crate::declared_openai_file_input_param_names;
|
||||
use crate::elicitation::ElicitationLifecycle;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::ElicitationRequestRouter;
|
||||
@@ -19,7 +18,6 @@ use crate::tools::ToolFilter;
|
||||
use crate::tools::ToolInfo;
|
||||
use crate::tools::filter_tools;
|
||||
use crate::tools::normalize_tools_for_model_with_prefix;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use codex_config::AppToolApproval;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
@@ -40,9 +38,9 @@ use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::NumberOrString;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
@@ -63,6 +61,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
format!("Test tool: {tool_name}"),
|
||||
Arc::new(JsonObject::default()),
|
||||
),
|
||||
openai_file_input_optional_fields: Default::default(),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
@@ -205,85 +204,6 @@ fn is_code_mode_compatible_tool_name(name: &ToolName) -> bool {
|
||||
.flat_map(str::chars)
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
#[test]
|
||||
fn declared_openai_file_fields_treat_names_literally() {
|
||||
let meta = serde_json::json!({
|
||||
"openai/fileParams": ["file", "input_file", "attachments"]
|
||||
});
|
||||
let meta = meta.as_object().expect("meta object");
|
||||
|
||||
assert_eq!(
|
||||
declared_openai_file_input_param_names(Some(meta)),
|
||||
vec![
|
||||
"file".to_string(),
|
||||
"input_file".to_string(),
|
||||
"attachments".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_with_model_visible_input_schema_masks_file_params() {
|
||||
let mut tool = create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "upload").tool;
|
||||
tool.input_schema = Arc::new(
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "object",
|
||||
"description": "Original file payload."
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"}
|
||||
}
|
||||
}
|
||||
})
|
||||
.as_object()
|
||||
.expect("object")
|
||||
.clone(),
|
||||
);
|
||||
tool.meta = Some(Meta(
|
||||
serde_json::json!({
|
||||
"openai/fileParams": ["file", "files"]
|
||||
})
|
||||
.as_object()
|
||||
.expect("object")
|
||||
.clone(),
|
||||
));
|
||||
|
||||
let tool = tool_with_model_visible_input_schema(&tool);
|
||||
|
||||
assert_eq!(
|
||||
*tool.input_schema,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Original file payload. This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here."
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here."
|
||||
}
|
||||
}
|
||||
})
|
||||
.as_object()
|
||||
.expect("object")
|
||||
.clone()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_with_model_visible_input_schema_leaves_tools_without_file_params_unchanged() {
|
||||
let original_tool = create_test_tool("custom", "upload").tool;
|
||||
|
||||
let tool = tool_with_model_visible_input_schema(&original_tool);
|
||||
|
||||
assert_eq!(tool, original_tool);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elicitation_granular_policy_defaults_to_prompting() {
|
||||
|
||||
Reference in new issue
Block a user