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
@@ -5,6 +5,11 @@
|
|||||||
|
|
||||||
use codex_utils_plugins::mcp_connector::sanitize_name;
|
use codex_utils_plugins::mcp_connector::sanitize_name;
|
||||||
|
|
||||||
|
mod file_params;
|
||||||
|
|
||||||
|
pub use file_params::declared_openai_file_input_param_names;
|
||||||
|
pub(crate) use file_params::prepare_openai_file_params_for_model;
|
||||||
|
|
||||||
pub(crate) fn normalize_codex_apps_tool_title(connector_name: Option<&str>, value: &str) -> String {
|
pub(crate) fn normalize_codex_apps_tool_title(connector_name: Option<&str>, value: &str) -> String {
|
||||||
let Some(connector_name) = connector_name
|
let Some(connector_name) = connector_name
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
//! Apps SDK `openai/fileParams` metadata and schema shaping.
|
||||||
|
//!
|
||||||
|
//! For each declared file argument, this module derives the provided-file fields
|
||||||
|
//! accepted by its input schema and records them on `ToolInfo` for execution-time
|
||||||
|
//! argument rewriting. It also presents file arguments to the model as local paths.
|
||||||
|
//!
|
||||||
|
//! See <https://developers.openai.com/apps-sdk/reference#tool-descriptor-parameters>.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rmcp::model::Tool;
|
||||||
|
use serde_json::Map;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
use crate::tools::ToolInfo;
|
||||||
|
|
||||||
|
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct OpenAiFileSchemaInfo {
|
||||||
|
accepts_mime_type: bool,
|
||||||
|
accepts_file_name: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn declared_openai_file_input_param_names(
|
||||||
|
meta: Option<&Map<String, JsonValue>>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let Some(meta) = meta else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
meta.get(META_OPENAI_FILE_PARAMS)
|
||||||
|
.and_then(JsonValue::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(JsonValue::as_str)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives execution-time file capabilities from the raw schema, then masks
|
||||||
|
/// declared file arguments as local paths for the model.
|
||||||
|
pub(crate) fn prepare_openai_file_params_for_model(tool_info: &mut ToolInfo) {
|
||||||
|
let file_params = declared_openai_file_input_param_names(tool_info.tool.meta.as_deref());
|
||||||
|
tool_info.openai_file_input_optional_fields =
|
||||||
|
supported_openai_file_input_optional_fields(&tool_info.tool, &file_params);
|
||||||
|
|
||||||
|
if file_params.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tool = tool_info.tool.clone();
|
||||||
|
let mut input_schema = JsonValue::Object(tool.input_schema.as_ref().clone());
|
||||||
|
rewrite_input_schema_for_local_file_paths(&mut input_schema, &file_params);
|
||||||
|
if let JsonValue::Object(input_schema) = input_schema {
|
||||||
|
tool.input_schema = Arc::new(input_schema);
|
||||||
|
}
|
||||||
|
tool_info.tool = tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_openai_file_input_optional_fields(
|
||||||
|
tool: &Tool,
|
||||||
|
file_params: &[String],
|
||||||
|
) -> HashMap<String, Vec<String>> {
|
||||||
|
let properties = tool
|
||||||
|
.input_schema
|
||||||
|
.get("properties")
|
||||||
|
.and_then(JsonValue::as_object);
|
||||||
|
|
||||||
|
file_params
|
||||||
|
.iter()
|
||||||
|
.map(|field_name| {
|
||||||
|
let optional_fields = properties
|
||||||
|
.and_then(|properties| properties.get(field_name))
|
||||||
|
.map(|schema| {
|
||||||
|
let schema_info = openai_file_schema_info(schema, tool.input_schema.as_ref());
|
||||||
|
let mut optional_fields = Vec::new();
|
||||||
|
if schema_info.accepts_mime_type {
|
||||||
|
optional_fields.push("mime_type".to_string());
|
||||||
|
}
|
||||||
|
if schema_info.accepts_file_name {
|
||||||
|
optional_fields.push("file_name".to_string());
|
||||||
|
}
|
||||||
|
optional_fields
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
(field_name.clone(), optional_fields)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openai_file_schema_info(
|
||||||
|
schema: &JsonValue,
|
||||||
|
root_schema: &Map<String, JsonValue>,
|
||||||
|
) -> OpenAiFileSchemaInfo {
|
||||||
|
let mut info = OpenAiFileSchemaInfo::default();
|
||||||
|
let mut pending = vec![schema];
|
||||||
|
let mut visited_refs = HashSet::new();
|
||||||
|
|
||||||
|
while let Some(schema) = pending.pop() {
|
||||||
|
let Some(schema) = schema.as_object() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(schema_ref) = schema.get("$ref").and_then(JsonValue::as_str)
|
||||||
|
&& visited_refs.insert(schema_ref)
|
||||||
|
&& let Some(referenced_schema) = resolve_local_schema_ref(root_schema, schema_ref)
|
||||||
|
{
|
||||||
|
pending.push(referenced_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
for keyword in ["anyOf", "oneOf", "allOf"] {
|
||||||
|
if let Some(variants) = schema.get(keyword).and_then(JsonValue::as_array) {
|
||||||
|
pending.extend(variants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.get("type").and_then(JsonValue::as_str) == Some("array")
|
||||||
|
|| schema.contains_key("items")
|
||||||
|
{
|
||||||
|
if let Some(items) = schema.get("items") {
|
||||||
|
pending.push(items);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let properties = schema.get("properties").and_then(JsonValue::as_object);
|
||||||
|
let is_object_schema = schema.get("type").and_then(JsonValue::as_str) == Some("object")
|
||||||
|
|| properties.is_some()
|
||||||
|
|| schema.contains_key("additionalProperties");
|
||||||
|
if !is_object_schema {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let accepts_additional_properties = !matches!(
|
||||||
|
schema.get("additionalProperties"),
|
||||||
|
Some(JsonValue::Bool(false) | JsonValue::Object(_))
|
||||||
|
);
|
||||||
|
info.accepts_mime_type |= accepts_additional_properties
|
||||||
|
|| properties.is_some_and(|properties| properties.contains_key("mime_type"));
|
||||||
|
info.accepts_file_name |= accepts_additional_properties
|
||||||
|
|| properties.is_some_and(|properties| properties.contains_key("file_name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_local_schema_ref<'a>(
|
||||||
|
root_schema: &'a Map<String, JsonValue>,
|
||||||
|
schema_ref: &str,
|
||||||
|
) -> Option<&'a JsonValue> {
|
||||||
|
let pointer = schema_ref.strip_prefix("#/")?;
|
||||||
|
let mut segments = pointer.split('/');
|
||||||
|
let first_segment = segments.next()?.replace("~1", "/").replace("~0", "~");
|
||||||
|
let mut referenced_schema = root_schema.get(&first_segment)?;
|
||||||
|
|
||||||
|
for segment in segments {
|
||||||
|
let segment = segment.replace("~1", "/").replace("~0", "~");
|
||||||
|
referenced_schema = match referenced_schema {
|
||||||
|
JsonValue::Object(object) => object.get(&segment)?,
|
||||||
|
JsonValue::Array(array) => array.get(segment.parse::<usize>().ok()?)?,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(referenced_schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) {
|
||||||
|
let Some(properties) = input_schema
|
||||||
|
.as_object_mut()
|
||||||
|
.and_then(|schema| schema.get_mut("properties"))
|
||||||
|
.and_then(JsonValue::as_object_mut)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for field_name in file_params {
|
||||||
|
let Some(property_schema) = properties.get_mut(field_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
rewrite_input_property_schema_as_local_file_path(property_schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_input_property_schema_as_local_file_path(schema: &mut JsonValue) {
|
||||||
|
let Some(object) = schema.as_object_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut description = object
|
||||||
|
.get("description")
|
||||||
|
.and_then(JsonValue::as_str)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let guidance = "This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here.";
|
||||||
|
if description.is_empty() {
|
||||||
|
description = guidance.to_string();
|
||||||
|
} else if !description.contains(guidance) {
|
||||||
|
description = format!("{description} {guidance}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_array = object.get("type").and_then(JsonValue::as_str) == Some("array")
|
||||||
|
|| object.get("items").is_some();
|
||||||
|
object.clear();
|
||||||
|
object.insert("description".to_string(), JsonValue::String(description));
|
||||||
|
if is_array {
|
||||||
|
object.insert("type".to_string(), JsonValue::String("array".to_string()));
|
||||||
|
object.insert("items".to_string(), serde_json::json!({ "type": "string" }));
|
||||||
|
} else {
|
||||||
|
object.insert("type".to_string(), JsonValue::String("string".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "file_params_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use rmcp::model::JsonObject;
|
||||||
|
use rmcp::model::Meta;
|
||||||
|
use rmcp::model::Tool;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::tools::ToolInfo;
|
||||||
|
|
||||||
|
fn tool_info(tool: Tool) -> ToolInfo {
|
||||||
|
ToolInfo {
|
||||||
|
server_name: "codex_apps".to_string(),
|
||||||
|
supports_parallel_tool_calls: false,
|
||||||
|
server_origin: None,
|
||||||
|
callable_name: tool.name.to_string(),
|
||||||
|
callable_namespace: "codex_apps".to_string(),
|
||||||
|
namespace_description: None,
|
||||||
|
tool,
|
||||||
|
openai_file_input_optional_fields: HashMap::new(),
|
||||||
|
connector_id: None,
|
||||||
|
connector_name: None,
|
||||||
|
plugin_display_names: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_tool(name: &str) -> Tool {
|
||||||
|
Tool::new(
|
||||||
|
name.to_string(),
|
||||||
|
format!("Test tool: {name}"),
|
||||||
|
Arc::new(JsonObject::default()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 prepare_openai_file_params_for_model_masks_file_params() {
|
||||||
|
let mut tool = test_tool("upload");
|
||||||
|
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 mut tool_info = tool_info(tool);
|
||||||
|
|
||||||
|
prepare_openai_file_params_for_model(&mut tool_info);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*tool_info.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 prepare_openai_file_params_for_model_derives_supported_optional_fields() {
|
||||||
|
let mut tool = Tool::new(
|
||||||
|
"upload".to_string(),
|
||||||
|
"Upload files".to_string(),
|
||||||
|
Arc::new(
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"$defs": {
|
||||||
|
"Rich/File": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"},
|
||||||
|
"file_name": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"photoshop_image": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"drive_import": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"},
|
||||||
|
"mime_type": {"type": "string"},
|
||||||
|
"file_name": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mime_type": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{"type": "null"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"type": "null"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"referenced_file": {
|
||||||
|
"$ref": "#/$defs/Rich~1File"
|
||||||
|
},
|
||||||
|
"custom_file": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"},
|
||||||
|
"mime_type": {"type": "string"},
|
||||||
|
"uri": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"open_file": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"explicitly_open_file": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"items_only_files": {
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"download_url": {"type": "string"},
|
||||||
|
"file_id": {"type": "string"},
|
||||||
|
"file_name": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.as_object()
|
||||||
|
.expect("object")
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
tool.meta = Some(Meta(
|
||||||
|
serde_json::json!({
|
||||||
|
"openai/fileParams": [
|
||||||
|
"photoshop_image",
|
||||||
|
"drive_import",
|
||||||
|
"attachments",
|
||||||
|
"referenced_file",
|
||||||
|
"custom_file",
|
||||||
|
"open_file",
|
||||||
|
"explicitly_open_file",
|
||||||
|
"items_only_files",
|
||||||
|
"missing_file"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.as_object()
|
||||||
|
.expect("object")
|
||||||
|
.clone(),
|
||||||
|
));
|
||||||
|
let mut tool_info = tool_info(tool);
|
||||||
|
|
||||||
|
prepare_openai_file_params_for_model(&mut tool_info);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tool_info.openai_file_input_optional_fields,
|
||||||
|
HashMap::from([
|
||||||
|
("photoshop_image".to_string(), Vec::new()),
|
||||||
|
(
|
||||||
|
"drive_import".to_string(),
|
||||||
|
vec!["mime_type".to_string(), "file_name".to_string()]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"attachments".to_string(),
|
||||||
|
vec!["mime_type".to_string(), "file_name".to_string()]
|
||||||
|
),
|
||||||
|
("referenced_file".to_string(), vec!["file_name".to_string()]),
|
||||||
|
("custom_file".to_string(), vec!["mime_type".to_string()]),
|
||||||
|
(
|
||||||
|
"open_file".to_string(),
|
||||||
|
vec!["mime_type".to_string(), "file_name".to_string()]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"explicitly_open_file".to_string(),
|
||||||
|
vec!["mime_type".to_string(), "file_name".to_string()]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"items_only_files".to_string(),
|
||||||
|
vec!["file_name".to_string()]
|
||||||
|
),
|
||||||
|
("missing_file".to_string(), Vec::new()),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prepare_openai_file_params_for_model_leaves_tools_without_file_params_unchanged() {
|
||||||
|
let original_tool = test_tool("upload");
|
||||||
|
let mut tool_info = tool_info(original_tool.clone());
|
||||||
|
|
||||||
|
prepare_openai_file_params_for_model(&mut tool_info);
|
||||||
|
|
||||||
|
assert_eq!(tool_info.tool, original_tool);
|
||||||
|
assert!(tool_info.openai_file_input_optional_fields.is_empty());
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
|||||||
format!("Test tool: {tool_name}"),
|
format!("Test tool: {tool_name}"),
|
||||||
Arc::new(JsonObject::default()),
|
Arc::new(JsonObject::default()),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use std::time::Duration;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::McpAuthStatusEntry;
|
use crate::McpAuthStatusEntry;
|
||||||
|
use crate::codex_apps::prepare_openai_file_params_for_model;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsCache;
|
use crate::codex_apps_cache::CodexAppsToolsCache;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsCacheKey;
|
use crate::codex_apps_cache::CodexAppsToolsCacheKey;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsFetchSource;
|
use crate::codex_apps_cache::CodexAppsToolsFetchSource;
|
||||||
@@ -36,7 +37,6 @@ use crate::server::McpServerMetadata;
|
|||||||
use crate::tools::ToolInfo;
|
use crate::tools::ToolInfo;
|
||||||
use crate::tools::filter_tools;
|
use crate::tools::filter_tools;
|
||||||
use crate::tools::normalize_tools_for_model_with_prefix;
|
use crate::tools::normalize_tools_for_model_with_prefix;
|
||||||
use crate::tools::tool_with_model_visible_input_schema;
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@@ -599,7 +599,7 @@ impl McpConnectionManager {
|
|||||||
let tools = filter_tools(tools, &managed_client.tool_filter)
|
let tools = filter_tools(tools, &managed_client.tool_filter)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut tool| {
|
.map(|mut tool| {
|
||||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
prepare_openai_file_params_for_model(&mut tool);
|
||||||
self.with_server_metadata(tool)
|
self.with_server_metadata(tool)
|
||||||
});
|
});
|
||||||
let tools = normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names);
|
let tools = normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsCache;
|
use crate::codex_apps_cache::CodexAppsToolsCache;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsCacheContext;
|
use crate::codex_apps_cache::CodexAppsToolsCacheContext;
|
||||||
use crate::declared_openai_file_input_param_names;
|
|
||||||
use crate::elicitation::ElicitationLifecycle;
|
use crate::elicitation::ElicitationLifecycle;
|
||||||
use crate::elicitation::ElicitationRequestManager;
|
use crate::elicitation::ElicitationRequestManager;
|
||||||
use crate::elicitation::ElicitationRequestRouter;
|
use crate::elicitation::ElicitationRequestRouter;
|
||||||
@@ -19,7 +18,6 @@ use crate::tools::ToolFilter;
|
|||||||
use crate::tools::ToolInfo;
|
use crate::tools::ToolInfo;
|
||||||
use crate::tools::filter_tools;
|
use crate::tools::filter_tools;
|
||||||
use crate::tools::normalize_tools_for_model_with_prefix;
|
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::AppToolApproval;
|
||||||
use codex_config::Constrained;
|
use codex_config::Constrained;
|
||||||
use codex_config::McpServerConfig;
|
use codex_config::McpServerConfig;
|
||||||
@@ -40,9 +38,9 @@ use rmcp::model::CreateElicitationRequestParams;
|
|||||||
use rmcp::model::ElicitationAction;
|
use rmcp::model::ElicitationAction;
|
||||||
use rmcp::model::ElicitationCapability;
|
use rmcp::model::ElicitationCapability;
|
||||||
use rmcp::model::JsonObject;
|
use rmcp::model::JsonObject;
|
||||||
use rmcp::model::Meta;
|
|
||||||
use rmcp::model::NumberOrString;
|
use rmcp::model::NumberOrString;
|
||||||
use rmcp::model::Tool;
|
use rmcp::model::Tool;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -63,6 +61,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
|||||||
format!("Test tool: {tool_name}"),
|
format!("Test tool: {tool_name}"),
|
||||||
Arc::new(JsonObject::default()),
|
Arc::new(JsonObject::default()),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
@@ -205,85 +204,6 @@ fn is_code_mode_compatible_tool_name(name: &ToolName) -> bool {
|
|||||||
.flat_map(str::chars)
|
.flat_map(str::chars)
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
.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]
|
#[test]
|
||||||
fn elicitation_granular_policy_defaults_to_prompting() {
|
fn elicitation_granular_policy_defaults_to_prompting() {
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ pub use mcp::oauth_login_support_with_http_client;
|
|||||||
pub use mcp::resolve_oauth_scopes;
|
pub use mcp::resolve_oauth_scopes;
|
||||||
pub use mcp::should_retry_without_scopes;
|
pub use mcp::should_retry_without_scopes;
|
||||||
|
|
||||||
|
pub use codex_apps::declared_openai_file_input_param_names;
|
||||||
pub use mcp::McpPermissionPromptAutoApproveContext;
|
pub use mcp::McpPermissionPromptAutoApproveContext;
|
||||||
pub use mcp::mcp_permission_prompt_is_auto_approved;
|
pub use mcp::mcp_permission_prompt_is_auto_approved;
|
||||||
pub use mcp::qualified_mcp_tool_name_prefix;
|
pub use mcp::qualified_mcp_tool_name_prefix;
|
||||||
pub use tools::declared_openai_file_input_param_names;
|
|
||||||
|
|
||||||
pub(crate) mod auth_elicitation;
|
pub(crate) mod auth_elicitation;
|
||||||
mod catalog;
|
mod catalog;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use std::time::Instant;
|
|||||||
use crate::codex_apps::normalize_codex_apps_callable_name;
|
use crate::codex_apps::normalize_codex_apps_callable_name;
|
||||||
use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
||||||
use crate::codex_apps::normalize_codex_apps_tool_title;
|
use crate::codex_apps::normalize_codex_apps_tool_title;
|
||||||
|
use crate::codex_apps::prepare_openai_file_params_for_model;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsCacheContext;
|
use crate::codex_apps_cache::CodexAppsToolsCacheContext;
|
||||||
use crate::codex_apps_cache::CodexAppsToolsFetchSource;
|
use crate::codex_apps_cache::CodexAppsToolsFetchSource;
|
||||||
use crate::codex_apps_cache::load_startup_cached_codex_apps_server_info;
|
use crate::codex_apps_cache::load_startup_cached_codex_apps_server_info;
|
||||||
@@ -34,7 +35,6 @@ use crate::server::McpServerLaunch;
|
|||||||
use crate::tools::ToolFilter;
|
use crate::tools::ToolFilter;
|
||||||
use crate::tools::ToolInfo;
|
use crate::tools::ToolInfo;
|
||||||
use crate::tools::filter_tools;
|
use crate::tools::filter_tools;
|
||||||
use crate::tools::tool_with_model_visible_input_schema;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
@@ -620,7 +620,7 @@ fn prepare_codex_apps_tools_for_model(
|
|||||||
tool_plugin_provenance: &ToolPluginProvenance,
|
tool_plugin_provenance: &ToolPluginProvenance,
|
||||||
) -> Vec<ToolInfo> {
|
) -> Vec<ToolInfo> {
|
||||||
for tool in &mut tools {
|
for tool in &mut tools {
|
||||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
prepare_openai_file_params_for_model(tool);
|
||||||
let plugin_names = match tool.connector_id.as_deref() {
|
let plugin_names = match tool.connector_id.as_deref() {
|
||||||
Some(connector_id) => {
|
Some(connector_id) => {
|
||||||
tool_plugin_provenance.plugin_display_names_for_connector_id(connector_id)
|
tool_plugin_provenance.plugin_display_names_for_connector_id(connector_id)
|
||||||
@@ -733,6 +733,7 @@ fn codex_apps_tool_info_from_listed_tool(
|
|||||||
callable_namespace,
|
callable_namespace,
|
||||||
namespace_description,
|
namespace_description,
|
||||||
tool: tool_def,
|
tool: tool_def,
|
||||||
|
openai_file_input_optional_fields: HashMap::new(),
|
||||||
connector_id,
|
connector_id,
|
||||||
connector_name,
|
connector_name,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
@@ -756,6 +757,7 @@ fn regular_mcp_tool_info_from_listed_tool(
|
|||||||
callable_namespace: server_name.to_string(),
|
callable_namespace: server_name.to_string(),
|
||||||
namespace_description: server_instructions.map(str::to_string),
|
namespace_description: server_instructions.map(str::to_string),
|
||||||
tool: tool_def,
|
tool: tool_def,
|
||||||
|
openai_file_input_optional_fields: HashMap::new(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
@@ -1138,6 +1140,7 @@ mod tests {
|
|||||||
callable_namespace: "codex_apps__gmail".to_string(),
|
callable_namespace: "codex_apps__gmail".to_string(),
|
||||||
namespace_description: Some("Mail connector".to_string()),
|
namespace_description: Some("Mail connector".to_string()),
|
||||||
tool: expected_tool,
|
tool: expected_tool,
|
||||||
|
openai_file_input_optional_fields: HashMap::new(),
|
||||||
connector_id: Some("connector_gmail".to_string()),
|
connector_id: Some("connector_gmail".to_string()),
|
||||||
connector_name: Some("Gmail".to_string()),
|
connector_name: Some("Gmail".to_string()),
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
//! MCP tool metadata, filtering, schema shaping, and name normalization.
|
//! MCP tool metadata, filtering, and name normalization.
|
||||||
//!
|
//!
|
||||||
//! Raw MCP tool identities must be preserved for protocol calls, while
|
//! Raw MCP tool identities must be preserved for protocol calls, while
|
||||||
//! model-visible tool names must be sanitized, deduplicated, and kept within API
|
//! model-visible tool names must be sanitized, deduplicated, and kept within API
|
||||||
//! limits. This module owns that translation as well as the shared [`ToolInfo`]
|
//! limits. This module owns that translation as well as the shared [`ToolInfo`]
|
||||||
//! type and helpers that adjust tool schemas before exposing them to the model.
|
//! type.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use codex_config::McpServerConfig;
|
use codex_config::McpServerConfig;
|
||||||
use codex_protocol::ToolName;
|
use codex_protocol::ToolName;
|
||||||
use rmcp::model::Tool;
|
use rmcp::model::Tool;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Map;
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use sha1::Digest;
|
use sha1::Digest;
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
@@ -49,6 +46,11 @@ pub struct ToolInfo {
|
|||||||
pub namespace_description: Option<String>,
|
pub namespace_description: Option<String>,
|
||||||
/// Raw MCP tool definition; `tool.name` is sent back to the MCP server.
|
/// Raw MCP tool definition; `tool.name` is sent back to the MCP server.
|
||||||
pub tool: Tool,
|
pub tool: Tool,
|
||||||
|
/// Optional provided-file fields accepted by each declared `openai/fileParams`
|
||||||
|
/// argument. This is derived from the raw MCP schema before file arguments are
|
||||||
|
/// masked as local paths for the model.
|
||||||
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
|
pub openai_file_input_optional_fields: HashMap<String, Vec<String>>,
|
||||||
pub connector_id: Option<String>,
|
pub connector_id: Option<String>,
|
||||||
pub connector_name: Option<String>,
|
pub connector_name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -61,23 +63,6 @@ impl ToolInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn declared_openai_file_input_param_names(
|
|
||||||
meta: Option<&Map<String, JsonValue>>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let Some(meta) = meta else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
meta.get(META_OPENAI_FILE_PARAMS)
|
|
||||||
.and_then(JsonValue::as_array)
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.filter_map(JsonValue::as_str)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A tool is allowed to be used if both are true:
|
/// A tool is allowed to be used if both are true:
|
||||||
/// 1. enabled is None (no allowlist is set) or the tool is explicitly enabled.
|
/// 1. enabled is None (no allowlist is set) or the tool is explicitly enabled.
|
||||||
/// 2. The tool is not explicitly disabled.
|
/// 2. The tool is not explicitly disabled.
|
||||||
@@ -113,24 +98,6 @@ impl ToolFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the model-visible view of a tool while preserving the raw metadata used by execution.
|
|
||||||
/// Declared file parameters are presented as local file paths; execution later uploads those files
|
|
||||||
/// and replaces the paths with the uploaded-file objects expected by the app.
|
|
||||||
pub(crate) fn tool_with_model_visible_input_schema(tool: &Tool) -> Tool {
|
|
||||||
let file_params = declared_openai_file_input_param_names(tool.meta.as_deref());
|
|
||||||
if file_params.is_empty() {
|
|
||||||
return tool.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut tool = tool.clone();
|
|
||||||
let mut input_schema = JsonValue::Object(tool.input_schema.as_ref().clone());
|
|
||||||
rewrite_input_schema_for_local_file_paths(&mut input_schema, &file_params);
|
|
||||||
if let JsonValue::Object(input_schema) = input_schema {
|
|
||||||
tool.input_schema = Arc::new(input_schema);
|
|
||||||
}
|
|
||||||
tool
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<ToolInfo> {
|
pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<ToolInfo> {
|
||||||
tools
|
tools
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -260,54 +227,6 @@ struct CallableToolCandidate {
|
|||||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||||
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||||
const CALLABLE_NAME_HASH_LEN: usize = 12;
|
const CALLABLE_NAME_HASH_LEN: usize = 12;
|
||||||
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
|
|
||||||
|
|
||||||
fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) {
|
|
||||||
let Some(properties) = input_schema
|
|
||||||
.as_object_mut()
|
|
||||||
.and_then(|schema| schema.get_mut("properties"))
|
|
||||||
.and_then(JsonValue::as_object_mut)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
for field_name in file_params {
|
|
||||||
let Some(property_schema) = properties.get_mut(field_name) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
rewrite_input_property_schema_as_local_file_path(property_schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rewrite_input_property_schema_as_local_file_path(schema: &mut JsonValue) {
|
|
||||||
let Some(object) = schema.as_object_mut() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut description = object
|
|
||||||
.get("description")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.map(str::to_string)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let guidance = "This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here.";
|
|
||||||
if description.is_empty() {
|
|
||||||
description = guidance.to_string();
|
|
||||||
} else if !description.contains(guidance) {
|
|
||||||
description = format!("{description} {guidance}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_array = object.get("type").and_then(JsonValue::as_str) == Some("array")
|
|
||||||
|| object.get("items").is_some();
|
|
||||||
object.clear();
|
|
||||||
object.insert("description".to_string(), JsonValue::String(description));
|
|
||||||
if is_array {
|
|
||||||
object.insert("type".to_string(), JsonValue::String("array".to_string()));
|
|
||||||
object.insert("items".to_string(), serde_json::json!({ "type": "string" }));
|
|
||||||
} else {
|
|
||||||
object.insert("type".to_string(), JsonValue::String("string".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
|
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
|
||||||
if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
|
if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
|
||||||
namespace.to_string()
|
namespace.to_string()
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ fn codex_app_tool(
|
|||||||
callable_namespace: tool_namespace,
|
callable_namespace: tool_namespace,
|
||||||
namespace_description: None,
|
namespace_description: None,
|
||||||
tool: test_tool_definition(tool_name),
|
tool: test_tool_definition(tool_name),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: Some(connector_id.to_string()),
|
connector_id: Some(connector_id.to_string()),
|
||||||
connector_name: connector_name.map(ToOwned::to_owned),
|
connector_name: connector_name.map(ToOwned::to_owned),
|
||||||
plugin_display_names: plugin_names(plugin_display_names),
|
plugin_display_names: plugin_names(plugin_display_names),
|
||||||
@@ -115,6 +116,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
|||||||
callable_namespace: "sample".to_string(),
|
callable_namespace: "sample".to_string(),
|
||||||
namespace_description: None,
|
namespace_description: None,
|
||||||
tool: test_tool_definition("echo"),
|
tool: test_tool_definition("echo"),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: plugin_names(&["ignored"]),
|
plugin_display_names: plugin_names(&["ignored"]),
|
||||||
@@ -297,6 +299,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
|
|||||||
"Create a calendar event",
|
"Create a calendar event",
|
||||||
Arc::new(JsonObject::default()),
|
Arc::new(JsonObject::default()),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: Some("calendar".to_string()),
|
connector_id: Some("calendar".to_string()),
|
||||||
connector_name: Some("Calendar".to_string()),
|
connector_name: Some("Calendar".to_string()),
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ use codex_api::upload_openai_file;
|
|||||||
use codex_login::CodexAuth;
|
use codex_login::CodexAuth;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
|
pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
|
||||||
sess: &Session,
|
sess: &Session,
|
||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
arguments_value: Option<JsonValue>,
|
arguments_value: Option<JsonValue>,
|
||||||
openai_file_input_params: Option<&[String]>,
|
openai_file_input_optional_fields: Option<&HashMap<String, Vec<String>>>,
|
||||||
) -> Result<Option<JsonValue>, String> {
|
) -> Result<Option<JsonValue>, String> {
|
||||||
let Some(openai_file_input_params) = openai_file_input_params else {
|
let Some(openai_file_input_optional_fields) = openai_file_input_optional_fields else {
|
||||||
return Ok(arguments_value);
|
return Ok(arguments_value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,13 +39,18 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
|
|||||||
let auth = sess.services.auth_manager.auth().await;
|
let auth = sess.services.auth_manager.auth().await;
|
||||||
let mut rewritten_arguments = arguments.clone();
|
let mut rewritten_arguments = arguments.clone();
|
||||||
|
|
||||||
for field_name in openai_file_input_params {
|
for (field_name, optional_fields) in openai_file_input_optional_fields {
|
||||||
let Some(value) = arguments.get(field_name) else {
|
let Some(value) = arguments.get(field_name) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(uploaded_value) =
|
let Some(uploaded_value) = rewrite_argument_value_for_openai_files(
|
||||||
rewrite_argument_value_for_openai_files(turn_context, auth.as_ref(), field_name, value)
|
turn_context,
|
||||||
.await?
|
auth.as_ref(),
|
||||||
|
field_name,
|
||||||
|
optional_fields,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -62,6 +68,7 @@ async fn rewrite_argument_value_for_openai_files(
|
|||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
field_name: &str,
|
field_name: &str,
|
||||||
|
optional_fields: &[String],
|
||||||
value: &JsonValue,
|
value: &JsonValue,
|
||||||
) -> Result<Option<JsonValue>, String> {
|
) -> Result<Option<JsonValue>, String> {
|
||||||
match value {
|
match value {
|
||||||
@@ -71,6 +78,7 @@ async fn rewrite_argument_value_for_openai_files(
|
|||||||
auth,
|
auth,
|
||||||
field_name,
|
field_name,
|
||||||
/*index*/ None,
|
/*index*/ None,
|
||||||
|
optional_fields,
|
||||||
file_path,
|
file_path,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -87,6 +95,7 @@ async fn rewrite_argument_value_for_openai_files(
|
|||||||
auth,
|
auth,
|
||||||
field_name,
|
field_name,
|
||||||
Some(index),
|
Some(index),
|
||||||
|
optional_fields,
|
||||||
file_path,
|
file_path,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -103,6 +112,7 @@ async fn build_uploaded_argument_value(
|
|||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
field_name: &str,
|
field_name: &str,
|
||||||
index: Option<usize>,
|
index: Option<usize>,
|
||||||
|
optional_fields: &[String],
|
||||||
file_path: &str,
|
file_path: &str,
|
||||||
) -> Result<JsonValue, String> {
|
) -> Result<JsonValue, String> {
|
||||||
let contextualize_error = |error: String| match index {
|
let contextualize_error = |error: String| match index {
|
||||||
@@ -170,12 +180,29 @@ async fn build_uploaded_argument_value(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| contextualize_error(error.to_string()))?;
|
.map_err(|error| contextualize_error(error.to_string()))?;
|
||||||
Ok(serde_json::json!({
|
let mut payload = serde_json::Map::new();
|
||||||
"download_url": uploaded.download_url,
|
payload.insert(
|
||||||
"file_id": uploaded.file_id,
|
"download_url".to_string(),
|
||||||
"mime_type": uploaded.mime_type,
|
JsonValue::String(uploaded.download_url),
|
||||||
"file_name": uploaded.file_name,
|
);
|
||||||
}))
|
payload.insert("file_id".to_string(), JsonValue::String(uploaded.file_id));
|
||||||
|
if optional_fields
|
||||||
|
.iter()
|
||||||
|
.any(|optional_field| optional_field == "mime_type")
|
||||||
|
&& let Some(mime_type) = uploaded.mime_type
|
||||||
|
{
|
||||||
|
payload.insert("mime_type".to_string(), JsonValue::String(mime_type));
|
||||||
|
}
|
||||||
|
if optional_fields
|
||||||
|
.iter()
|
||||||
|
.any(|optional_field| optional_field == "file_name")
|
||||||
|
{
|
||||||
|
payload.insert(
|
||||||
|
"file_name".to_string(),
|
||||||
|
JsonValue::String(uploaded.file_name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(JsonValue::Object(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -217,7 +244,7 @@ mod tests {
|
|||||||
&session,
|
&session,
|
||||||
&Arc::new(turn_context),
|
&Arc::new(turn_context),
|
||||||
arguments.clone(),
|
arguments.clone(),
|
||||||
/*openai_file_input_params*/ None,
|
/*openai_file_input_optional_fields*/ None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("rewrite should succeed");
|
.expect("rewrite should succeed");
|
||||||
@@ -226,7 +253,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn build_uploaded_argument_value_uploads_environment_file() {
|
async fn build_uploaded_argument_value_includes_schema_declared_optional_fields() {
|
||||||
use wiremock::Mock;
|
use wiremock::Mock;
|
||||||
use wiremock::MockServer;
|
use wiremock::MockServer;
|
||||||
use wiremock::ResponseTemplate;
|
use wiremock::ResponseTemplate;
|
||||||
@@ -288,6 +315,7 @@ mod tests {
|
|||||||
Some(&auth),
|
Some(&auth),
|
||||||
"file",
|
"file",
|
||||||
/*index*/ None,
|
/*index*/ None,
|
||||||
|
&["mime_type".to_string(), "file_name".to_string()],
|
||||||
"file_report.csv",
|
"file_report.csv",
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -320,6 +348,7 @@ mod tests {
|
|||||||
Some(&auth),
|
Some(&auth),
|
||||||
"file",
|
"file",
|
||||||
/*index*/ None,
|
/*index*/ None,
|
||||||
|
&[],
|
||||||
"oversized.bin",
|
"oversized.bin",
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -330,7 +359,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rewrite_argument_value_for_openai_files_rewrites_scalar_path() {
|
async fn rewrite_argument_value_for_openai_files_omits_undeclared_optional_fields() {
|
||||||
use wiremock::Mock;
|
use wiremock::Mock;
|
||||||
use wiremock::MockServer;
|
use wiremock::MockServer;
|
||||||
use wiremock::ResponseTemplate;
|
use wiremock::ResponseTemplate;
|
||||||
@@ -390,6 +419,7 @@ mod tests {
|
|||||||
&turn_context,
|
&turn_context,
|
||||||
Some(&auth),
|
Some(&auth),
|
||||||
"file",
|
"file",
|
||||||
|
&[],
|
||||||
&serde_json::json!("file_report.csv"),
|
&serde_json::json!("file_report.csv"),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -400,8 +430,6 @@ mod tests {
|
|||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"download_url": format!("{}/download/file_123", server.uri()),
|
"download_url": format!("{}/download/file_123", server.uri()),
|
||||||
"file_id": "file_123",
|
"file_id": "file_123",
|
||||||
"mime_type": "text/csv",
|
|
||||||
"file_name": "file_report.csv",
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -502,6 +530,7 @@ mod tests {
|
|||||||
&turn_context,
|
&turn_context,
|
||||||
Some(&auth),
|
Some(&auth),
|
||||||
"files",
|
"files",
|
||||||
|
&[],
|
||||||
&serde_json::json!(["one.csv", "two.csv"]),
|
&serde_json::json!(["one.csv", "two.csv"]),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -513,14 +542,10 @@ mod tests {
|
|||||||
{
|
{
|
||||||
"download_url": format!("{}/download/file_1", server.uri()),
|
"download_url": format!("{}/download/file_1", server.uri()),
|
||||||
"file_id": "file_1",
|
"file_id": "file_1",
|
||||||
"mime_type": "text/csv",
|
|
||||||
"file_name": "one.csv",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"download_url": format!("{}/download/file_2", server.uri()),
|
"download_url": format!("{}/download/file_2", server.uri()),
|
||||||
"file_id": "file_2",
|
"file_id": "file_2",
|
||||||
"mime_type": "text/csv",
|
|
||||||
"file_name": "two.csv",
|
|
||||||
}
|
}
|
||||||
]))
|
]))
|
||||||
);
|
);
|
||||||
@@ -538,7 +563,7 @@ mod tests {
|
|||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"file": "/definitely/missing/file.csv",
|
"file": "/definitely/missing/file.csv",
|
||||||
})),
|
})),
|
||||||
Some(&["file".to_string()]),
|
Some(&HashMap::from([("file".to_string(), Vec::new())])),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect_err("missing file should fail");
|
.expect_err("missing file should fail");
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ use codex_mcp::McpPermissionPromptAutoApproveContext;
|
|||||||
use codex_mcp::SandboxState;
|
use codex_mcp::SandboxState;
|
||||||
use codex_mcp::auth_elicitation_completed_result;
|
use codex_mcp::auth_elicitation_completed_result;
|
||||||
use codex_mcp::build_auth_elicitation_plan;
|
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_mcp::mcp_permission_prompt_is_auto_approved;
|
||||||
use codex_protocol::approvals::ElicitationRequest;
|
use codex_protocol::approvals::ElicitationRequest;
|
||||||
use codex_protocol::items::McpToolCallError;
|
use codex_protocol::items::McpToolCallError;
|
||||||
@@ -382,7 +381,7 @@ async fn handle_approved_mcp_tool_call(
|
|||||||
sess,
|
sess,
|
||||||
turn_context,
|
turn_context,
|
||||||
arguments_value.clone(),
|
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;
|
.await;
|
||||||
let tool_input = match &rewrite {
|
let tool_input = match &rewrite {
|
||||||
@@ -1019,7 +1018,7 @@ pub(crate) struct McpToolApprovalMetadata {
|
|||||||
mcp_app_resource_uri: Option<String>,
|
mcp_app_resource_uri: Option<String>,
|
||||||
template_id: Option<String>,
|
template_id: Option<String>,
|
||||||
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
|
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";
|
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),
|
.map(str::to_string),
|
||||||
codex_apps_meta,
|
codex_apps_meta,
|
||||||
// Disallow custom MCPs from uploading files via fileParams.
|
// 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,
|
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,
|
server: &str,
|
||||||
meta: Option<&serde_json::Map<String, serde_json::Value>>,
|
openai_file_input_optional_fields: &HashMap<String, Vec<String>>,
|
||||||
) -> Option<Vec<String>> {
|
) -> Option<HashMap<String, Vec<String>>> {
|
||||||
(server == CODEX_APPS_MCP_SERVER_NAME)
|
(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())
|
.filter(|params| !params.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ fn approval_metadata(
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,17 +349,14 @@ fn mcp_app_resource_uri_reads_known_tool_meta_keys() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_file_params_are_only_honored_for_codex_apps() {
|
fn openai_file_params_are_only_honored_for_codex_apps() {
|
||||||
let meta = serde_json::json!({
|
let params = HashMap::from([("file".to_string(), Vec::new())]);
|
||||||
"openai/fileParams": ["file"],
|
|
||||||
});
|
|
||||||
let meta = meta.as_object();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
openai_file_input_params_for_server(CODEX_APPS_MCP_SERVER_NAME, meta),
|
openai_file_input_optional_fields_for_server(CODEX_APPS_MCP_SERVER_NAME, ¶ms),
|
||||||
Some(vec!["file".to_string()])
|
Some(params.clone())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
openai_file_input_params_for_server("minimaltest", meta),
|
openai_file_input_optional_fields_for_server("minimaltest", ¶ms),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1341,7 +1338,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
|
|||||||
.cloned()
|
.cloned()
|
||||||
.expect("_codex_apps metadata should be an object"),
|
.expect("_codex_apps metadata should be an object"),
|
||||||
),
|
),
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1821,7 +1818,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = build_guardian_mcp_tool_review_request("call-1", &invocation, Some(&metadata));
|
let request = build_guardian_mcp_tool_review_request("call-1", &invocation, Some(&metadata));
|
||||||
@@ -2524,7 +2521,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let decision = maybe_request_mcp_tool_approval(
|
let decision = maybe_request_mcp_tool_approval(
|
||||||
@@ -2601,7 +2598,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let decision = maybe_request_mcp_tool_approval(
|
let decision = maybe_request_mcp_tool_approval(
|
||||||
@@ -2661,7 +2658,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let decision = maybe_request_mcp_tool_approval(
|
let decision = maybe_request_mcp_tool_approval(
|
||||||
@@ -2800,7 +2797,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
let remembered_key =
|
let remembered_key =
|
||||||
session_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto)
|
session_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto)
|
||||||
@@ -2890,7 +2887,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let decision = maybe_request_mcp_tool_approval(
|
let decision = maybe_request_mcp_tool_approval(
|
||||||
@@ -2947,7 +2944,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut approval_task = {
|
let mut approval_task = {
|
||||||
@@ -3005,7 +3002,7 @@ async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
for approval_mode in [
|
for approval_mode in [
|
||||||
@@ -3061,7 +3058,7 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() {
|
|||||||
mcp_app_resource_uri: None,
|
mcp_app_resource_uri: None,
|
||||||
template_id: None,
|
template_id: None,
|
||||||
codex_apps_meta: None,
|
codex_apps_meta: None,
|
||||||
openai_file_input_params: None,
|
openai_file_input_optional_fields: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
for approval_policy in [
|
for approval_policy in [
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ fn make_mcp_tool(
|
|||||||
format!("Test tool: {tool_name}"),
|
format!("Test tool: {tool_name}"),
|
||||||
Arc::new(JsonObject::default()),
|
Arc::new(JsonObject::default()),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: connector_id.map(str::to_string),
|
connector_id: connector_id.map(str::to_string),
|
||||||
connector_name: connector_name.map(str::to_string),
|
connector_name: connector_name.map(str::to_string),
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ mod tests {
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
}))),
|
}))),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ fn tool_info() -> ToolInfo {
|
|||||||
}))),
|
}))),
|
||||||
)
|
)
|
||||||
.with_title("Create event"),
|
.with_title("Create event"),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: Some("Calendar".to_string()),
|
connector_name: Some("Calendar".to_string()),
|
||||||
plugin_display_names: vec![" Calendar plugin ".to_string(), " ".to_string()],
|
plugin_display_names: vec![" Calendar plugin ".to_string(), " ".to_string()],
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ mod tests {
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
}))),
|
}))),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ fn mcp_tool_info(
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
}))),
|
}))),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ fn mcp_tool(server: &str, namespace: &str, name: &str) -> ToolInfo {
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
}))),
|
}))),
|
||||||
),
|
),
|
||||||
|
openai_file_input_optional_fields: Default::default(),
|
||||||
connector_id: None,
|
connector_id: None,
|
||||||
connector_name: None,
|
connector_name: None,
|
||||||
plugin_display_names: Vec::new(),
|
plugin_display_names: Vec::new(),
|
||||||
|
|||||||
@@ -598,9 +598,7 @@ impl Respond for CodexAppsJsonRpcResponder {
|
|||||||
"description": "Document file payload.",
|
"description": "Document file payload.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"download_url": { "type": "string" },
|
"download_url": { "type": "string" },
|
||||||
"file_id": { "type": "string" },
|
"file_id": { "type": "string" }
|
||||||
"mime_type": { "type": "string" },
|
|
||||||
"file_name": { "type": "string" }
|
|
||||||
},
|
},
|
||||||
"required": ["download_url", "file_id"],
|
"required": ["download_url", "file_id"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -91,12 +91,10 @@ fn read_post_tool_use_hook_inputs(home: &Path) -> Result<Vec<Value>> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uploaded_file(server: &MockServer) -> Value {
|
fn schema_filtered_uploaded_file(server: &MockServer) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"download_url": format!("{}/download/file_123", server.uri()),
|
"download_url": format!("{}/download/file_123", server.uri()),
|
||||||
"file_id": "file_123",
|
"file_id": "file_123",
|
||||||
"mime_type": "text/plain",
|
|
||||||
"file_name": "report.txt",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +180,7 @@ async fn run_extract_turn(test: &TestCodex, server: &MockServer) -> Result<Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn codex_apps_file_params_upload_environment_files_before_mcp_tool_call() -> Result<()> {
|
async fn codex_apps_file_params_omit_fields_absent_from_tool_schema() -> Result<()> {
|
||||||
// TODO(anp): Remove after file-upload fixtures support target-native Windows paths.
|
// 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(()), "uses a host-native file-upload path");
|
||||||
|
|
||||||
@@ -228,7 +226,7 @@ async fn codex_apps_file_params_upload_environment_files_before_mcp_tool_call()
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apps_tool_call.pointer("/params/arguments/file"),
|
apps_tool_call.pointer("/params/arguments/file"),
|
||||||
Some(&uploaded_file(&server))
|
Some(&schema_filtered_uploaded_file(&server))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apps_tool_call.pointer("/params/_meta/_codex_apps"),
|
apps_tool_call.pointer("/params/_meta/_codex_apps"),
|
||||||
@@ -265,7 +263,10 @@ async fn codex_apps_file_params_pass_uploaded_file_to_post_tool_use_hook() -> Re
|
|||||||
|
|
||||||
let hook_inputs = read_post_tool_use_hook_inputs(test.codex_home_path())?;
|
let hook_inputs = read_post_tool_use_hook_inputs(test.codex_home_path())?;
|
||||||
assert_eq!(hook_inputs.len(), 1);
|
assert_eq!(hook_inputs.len(), 1);
|
||||||
assert_eq!(hook_inputs[0]["tool_input"]["file"], uploaded_file(&server));
|
assert_eq!(
|
||||||
|
hook_inputs[0]["tool_input"]["file"],
|
||||||
|
schema_filtered_uploaded_file(&server)
|
||||||
|
);
|
||||||
|
|
||||||
server.verify().await;
|
server.verify().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in new issue
Block a user