Compare commits

...

2 Commits

Author SHA1 Message Date
Matthew Zeng
b98f286c55 Merge branch 'main' of github.com:openai/codex into dev/mzeng/readable_elicitation_model_driven 2026-03-14 22:32:15 -07:00
Matthew Zeng
ce1ce00edf update 2026-03-14 14:19:21 -07:00
5 changed files with 539 additions and 14 deletions

View File

@@ -1,10 +1,31 @@
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_tool_call::MCP_TOOL_ARGS_CODEX_KEY;
use crate::mcp_tool_call::MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY;
use crate::mcp_tool_call::MCP_TOOL_ARGS_META_KEY;
use codex_protocol::protocol::APPS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
pub(crate) fn render_apps_section() -> String {
let body = format!(
"## Apps (Connectors)\nApps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{{connector_id}})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps, the available apps will be listed by the `tool_search` tool.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nAn installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool.\nDo not additionally call list_mcp_resources or list_mcp_resource_templates for apps."
"## Apps (Connectors)\nApps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{{connector_id}})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps, the available apps will be listed by the `tool_search` tool.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nAn installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool.\nFor consequential `{CODEX_APPS_MCP_SERVER_NAME}` tool calls that require approval, you need to include user-facing approval copy in the tool arguments under:\n```json\n{{\n \"{MCP_TOOL_ARGS_META_KEY}\": {{\n \"{MCP_TOOL_ARGS_CODEX_KEY}\": {{\n \"{MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY}\": \"Allow Calendar to create this event?\"\n }}\n }}\n}}\n```\nUse `{MCP_TOOL_ARGS_META_KEY}.{MCP_TOOL_ARGS_CODEX_KEY}.{MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY}` only for consequential app tools that may require approval. Make it a short user-facing approval sentence, not execution data, and do not rely on it being forwarded to the app.\nDo not additionally call list_mcp_resources or list_mcp_resource_templates for apps."
);
format!("{APPS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{APPS_INSTRUCTIONS_CLOSE_TAG}")
}
#[cfg(test)]
mod tests {
use super::render_apps_section;
#[test]
fn render_apps_section_mentions_elicitation_description_contract() {
let rendered = render_apps_section();
assert!(rendered.contains("`_meta._codex.elicitation_description`"));
assert!(
rendered
.contains("\"elicitation_description\": \"Allow Calendar to create this event?\"")
);
assert!(rendered.contains("not execution data"));
assert!(rendered.contains("do not rely on it being forwarded to the app"));
}
}

View File

@@ -27,6 +27,7 @@ use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalTemplate;
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
use crate::protocol::EventMsg;
use crate::protocol::McpInvocation;
@@ -51,6 +52,10 @@ use std::path::Path;
use std::sync::Arc;
use toml_edit::value;
pub(crate) const MCP_TOOL_ARGS_META_KEY: &str = "_meta";
pub(crate) const MCP_TOOL_ARGS_CODEX_KEY: &str = "_codex";
pub(crate) const MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY: &str = "elicitation_description";
/// Handles the specified tool call dispatches the appropriate
/// `McpToolCallBegin` and `McpToolCallEnd` events to the `Session`.
pub(crate) async fn handle_mcp_tool_call(
@@ -75,6 +80,12 @@ pub(crate) async fn handle_mcp_tool_call(
}
};
let (arguments_value, model_elicitation_description) = if server == CODEX_APPS_MCP_SERVER_NAME {
extract_codex_app_elicitation_description(arguments_value)
} else {
(arguments_value, None)
};
let invocation = McpInvocation {
server: server.clone(),
tool: tool_name.clone(),
@@ -130,6 +141,7 @@ pub(crate) async fn handle_mcp_tool_call(
turn_context,
&call_id,
&invocation,
model_elicitation_description.as_deref(),
metadata.as_ref(),
app_tool_policy.approval,
)
@@ -406,6 +418,12 @@ struct McpToolApprovalPromptOptions {
allow_persistent_approval: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct McpToolApprovalPromptCopy<'a> {
question_override: Option<&'a str>,
message_override: Option<&'a str>,
}
struct McpToolApprovalElicitationRequest<'a> {
server: &'a str,
metadata: Option<&'a McpToolApprovalMetadata>,
@@ -471,6 +489,7 @@ async fn maybe_request_mcp_tool_approval(
turn_context: &Arc<TurnContext>,
call_id: &str,
invocation: &McpInvocation,
model_elicitation_description: Option<&str>,
metadata: Option<&McpToolApprovalMetadata>,
approval_mode: AppToolApproval,
) -> Option<McpToolApprovalDecision> {
@@ -553,6 +572,11 @@ async fn maybe_request_mcp_tool_approval(
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
invocation.arguments.as_ref(),
);
let prompt_copy = build_mcp_tool_approval_prompt_copy(
model_elicitation_description,
rendered_template.as_ref(),
monitor_reason.as_deref(),
);
let tool_params_display = rendered_template
.as_ref()
.map(|rendered_template| rendered_template.tool_params_display.clone())
@@ -563,9 +587,7 @@ async fn maybe_request_mcp_tool_approval(
&invocation.tool,
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
prompt_options,
rendered_template
.as_ref()
.map(|rendered_template| rendered_template.question.as_str()),
prompt_copy.question_override,
);
question.question =
mcp_tool_approval_question_text(question.question, monitor_reason.as_deref());
@@ -585,11 +607,7 @@ async fn maybe_request_mcp_tool_approval(
.or(invocation.arguments.as_ref()),
tool_params_display: tool_params_display.as_deref(),
question,
message_override: rendered_template.as_ref().and_then(|rendered_template| {
monitor_reason
.is_none()
.then_some(rendered_template.elicitation_message.as_str())
}),
message_override: prompt_copy.message_override,
prompt_options,
},
);
@@ -631,6 +649,66 @@ async fn maybe_request_mcp_tool_approval(
Some(decision)
}
fn extract_codex_app_elicitation_description(
arguments: Option<serde_json::Value>,
) -> (Option<serde_json::Value>, Option<String>) {
let Some(serde_json::Value::Object(mut arguments)) = arguments else {
return (arguments, None);
};
let mut remove_meta = false;
let mut elicitation_description = None;
if let Some(meta_value) = arguments.get_mut(MCP_TOOL_ARGS_META_KEY)
&& let Some(meta) = meta_value.as_object_mut()
{
elicitation_description = meta
.remove(MCP_TOOL_ARGS_CODEX_KEY)
.as_ref()
.and_then(serde_json::Value::as_object)
.and_then(|codex_meta| codex_meta.get(MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|description| !description.is_empty())
.map(ToString::to_string);
remove_meta = meta.is_empty();
}
if remove_meta {
arguments.remove(MCP_TOOL_ARGS_META_KEY);
}
(
(!arguments.is_empty()).then_some(serde_json::Value::Object(arguments)),
elicitation_description,
)
}
fn build_mcp_tool_approval_prompt_copy<'a>(
model_elicitation_description: Option<&'a str>,
rendered_template: Option<&'a RenderedMcpToolApprovalTemplate>,
monitor_reason: Option<&str>,
) -> McpToolApprovalPromptCopy<'a> {
let model_elicitation_description = model_elicitation_description
.map(str::trim)
.filter(|description| !description.is_empty());
let question_override = model_elicitation_description
.or_else(|| rendered_template.map(|rendered_template| rendered_template.question.as_str()));
let message_override = if monitor_reason.is_some() {
None
} else {
model_elicitation_description.or_else(|| {
rendered_template
.map(|rendered_template| rendered_template.elicitation_message.as_str())
})
};
McpToolApprovalPromptCopy {
question_override,
message_override,
}
}
async fn maybe_monitor_auto_approved_mcp_tool_call(
sess: &Session,
turn_context: &TurnContext,
@@ -1246,7 +1324,7 @@ async fn persist_codex_app_tool_approval(
.await
}
fn requires_mcp_tool_approval(annotations: &ToolAnnotations) -> bool {
pub(crate) fn requires_mcp_tool_approval(annotations: &ToolAnnotations) -> bool {
if annotations.destructive_hint == Some(true) {
return true;
}

View File

@@ -6,6 +6,7 @@ use crate::config::types::AppConfig;
use crate::config::types::AppToolConfig;
use crate::config::types::AppToolsConfig;
use crate::config::types::AppsConfigToml;
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalTemplate;
use codex_config::CONFIG_TOML_FILE;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -108,6 +109,141 @@ fn approval_question_text_prepends_safety_reason() {
);
}
#[test]
fn extract_codex_app_elicitation_description_strips_control_meta_and_prunes_empty_meta() {
let (arguments, elicitation_description) =
extract_codex_app_elicitation_description(Some(serde_json::json!({
"title": "Roadmap review",
"_meta": {
"_codex": {
"elicitation_description": " Allow Calendar to create this event? "
}
}
})));
assert_eq!(
arguments,
Some(serde_json::json!({
"title": "Roadmap review",
}))
);
assert_eq!(
elicitation_description,
Some("Allow Calendar to create this event?".to_string())
);
}
#[test]
fn extract_codex_app_elicitation_description_preserves_other_meta_fields() {
let (arguments, elicitation_description) =
extract_codex_app_elicitation_description(Some(serde_json::json!({
"title": "Roadmap review",
"_meta": {
"request_id": "req-1",
"_codex": {
"elicitation_description": "Allow Calendar to create this event?"
}
}
})));
assert_eq!(
arguments,
Some(serde_json::json!({
"title": "Roadmap review",
"_meta": {
"request_id": "req-1",
}
}))
);
assert_eq!(
elicitation_description,
Some("Allow Calendar to create this event?".to_string())
);
}
#[test]
fn extract_codex_app_elicitation_description_ignores_blank_strings() {
let (arguments, elicitation_description) =
extract_codex_app_elicitation_description(Some(serde_json::json!({
"_meta": {
"_codex": {
"elicitation_description": " "
}
}
})));
assert_eq!(arguments, None);
assert_eq!(elicitation_description, None);
}
#[test]
fn stripped_arguments_omit_codex_meta_from_approval_display_params() {
let (arguments, _) = extract_codex_app_elicitation_description(Some(serde_json::json!({
"title": "Roadmap review",
"_meta": {
"_codex": {
"elicitation_description": "Allow Calendar to create this event?"
}
}
})));
assert_eq!(
build_mcp_tool_approval_display_params(arguments.as_ref()),
Some(vec![RenderedMcpToolApprovalParam {
name: "title".to_string(),
value: serde_json::json!("Roadmap review"),
}])
);
}
#[test]
fn approval_prompt_copy_prefers_model_text_over_template() {
let rendered_template = RenderedMcpToolApprovalTemplate {
question: "Allow Calendar to create an event?".to_string(),
elicitation_message: "Allow Calendar to create an event?".to_string(),
tool_params: None,
tool_params_display: Vec::new(),
};
assert_eq!(
build_mcp_tool_approval_prompt_copy(
Some("Allow Calendar to create this event?"),
Some(&rendered_template),
None,
),
McpToolApprovalPromptCopy {
question_override: Some("Allow Calendar to create this event?"),
message_override: Some("Allow Calendar to create this event?"),
}
);
}
#[test]
fn approval_prompt_copy_defers_to_safety_reason_over_model_text() {
let prompt_copy = build_mcp_tool_approval_prompt_copy(
Some("Allow Calendar to create this event?"),
None,
Some("This tool may contact an external system."),
);
let question = build_mcp_tool_approval_question(
"q".to_string(),
CODEX_APPS_MCP_SERVER_NAME,
"create_event",
Some("Calendar"),
prompt_options(true, true),
prompt_copy.question_override,
);
assert_eq!(
mcp_tool_approval_question_text(
question.question,
Some("This tool may contact an external system."),
),
"Tool call needs your approval. Reason: This tool may contact an external system."
);
assert_eq!(prompt_copy.message_override, None);
}
#[tokio::test]
async fn approval_elicitation_request_uses_message_override_and_readable_tool_params() {
let (session, turn_context) = make_session_and_context().await;
@@ -899,6 +1035,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
&turn_context,
"call-1",
&invocation,
None,
Some(&metadata),
AppToolApproval::Approve,
)
@@ -963,6 +1100,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() {
&turn_context,
"call-2",
&invocation,
None,
Some(&metadata),
AppToolApproval::Approve,
)
@@ -1066,6 +1204,7 @@ async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_
&turn_context,
"call-3",
&invocation,
None,
Some(&metadata),
AppToolApproval::Approve,
)

View File

@@ -6,7 +6,12 @@ use crate::config::AgentRoleConfig;
use crate::features::Feature;
use crate::features::Features;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp::split_qualified_tool_name;
use crate::mcp_connection_manager::ToolInfo;
use crate::mcp_tool_call::MCP_TOOL_ARGS_CODEX_KEY;
use crate::mcp_tool_call::MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY;
use crate::mcp_tool_call::MCP_TOOL_ARGS_META_KEY;
use crate::mcp_tool_call::requires_mcp_tool_approval;
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use crate::original_image_detail::can_request_original_image_detail;
use crate::shell::Shell;
@@ -50,6 +55,7 @@ use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use rmcp::model::ToolAnnotations;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
@@ -63,6 +69,9 @@ const TOOL_SEARCH_DESCRIPTION_TEMPLATE: &str =
const TOOL_SUGGEST_DESCRIPTION_TEMPLATE: &str =
include_str!("../../templates/search_tool/tool_suggest_description.md");
const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"];
const CODEX_APP_ELICITATION_DESCRIPTION_FIELD_DESCRIPTION: &str =
"Short user-facing approval sentence. This field is not forwarded to the app.";
const CODEX_APP_ELICITATION_DESCRIPTION_NOTE: &str = "For consequential codex_apps tools that may require approval, you may optionally include `_meta._codex.elicitation_description` as a short user-facing approval sentence. This field is used for approval UI and is not forwarded to the app.";
fn unified_exec_output_schema() -> JsonValue {
json!({
@@ -2268,7 +2277,10 @@ pub(crate) fn mcp_tool_to_openai_tool(
fully_qualified_name: String,
tool: rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
let (description, input_schema, output_schema) = mcp_tool_to_openai_tool_parts(tool)?;
let add_elicitation_metadata =
should_add_codex_app_elicitation_metadata(&fully_qualified_name, tool.annotations.as_ref());
let (description, input_schema, output_schema) =
mcp_tool_to_openai_tool_parts(tool, add_elicitation_metadata)?;
Ok(ResponsesApiTool {
name: fully_qualified_name,
@@ -2284,7 +2296,10 @@ pub(crate) fn mcp_tool_to_deferred_openai_tool(
name: String,
tool: rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
let (description, input_schema, _) = mcp_tool_to_openai_tool_parts(tool)?;
let add_elicitation_metadata =
should_add_codex_app_elicitation_metadata(&name, tool.annotations.as_ref());
let (description, input_schema, _) =
mcp_tool_to_openai_tool_parts(tool, add_elicitation_metadata)?;
Ok(ResponsesApiTool {
name,
@@ -2320,6 +2335,7 @@ pub fn parse_tool_input_schema(input_schema: &JsonValue) -> Result<JsonSchema, s
fn mcp_tool_to_openai_tool_parts(
tool: rmcp::model::Tool,
add_elicitation_metadata: bool,
) -> Result<(String, JsonSchema, Option<JsonValue>), serde_json::Error> {
let rmcp::model::Tool {
description,
@@ -2348,18 +2364,94 @@ fn mcp_tool_to_openai_tool_parts(
// `integer`. Our internal JsonSchema is a small subset and requires
// `type`, so we coerce/sanitize here for compatibility.
sanitize_json_schema(&mut serialized_input_schema);
let input_schema = serde_json::from_value::<JsonSchema>(serialized_input_schema)?;
let mut input_schema = serde_json::from_value::<JsonSchema>(serialized_input_schema)?;
let structured_content_schema = output_schema
.map(|output_schema| serde_json::Value::Object(output_schema.as_ref().clone()))
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));
let output_schema = Some(mcp_call_tool_result_output_schema(
structured_content_schema,
));
let description = description.map(Into::into).unwrap_or_default();
let mut description = description
.map(std::borrow::Cow::into_owned)
.unwrap_or_default();
if add_elicitation_metadata
&& add_codex_app_elicitation_metadata_to_input_schema(&mut input_schema)
{
description = append_codex_app_elicitation_description_note(&description);
}
Ok((description, input_schema, output_schema))
}
fn should_add_codex_app_elicitation_metadata(
fully_qualified_name: &str,
annotations: Option<&ToolAnnotations>,
) -> bool {
split_qualified_tool_name(fully_qualified_name)
.is_some_and(|(server_name, _)| server_name == CODEX_APPS_MCP_SERVER_NAME)
&& annotations.is_some_and(requires_mcp_tool_approval)
}
fn append_codex_app_elicitation_description_note(description: &str) -> String {
if description.is_empty() {
return CODEX_APP_ELICITATION_DESCRIPTION_NOTE.to_string();
}
format!("{description}\n\n{CODEX_APP_ELICITATION_DESCRIPTION_NOTE}")
}
fn add_codex_app_elicitation_metadata_to_input_schema(input_schema: &mut JsonSchema) -> bool {
let JsonSchema::Object { properties, .. } = input_schema else {
return false;
};
let meta_schema = properties
.entry(MCP_TOOL_ARGS_META_KEY.to_string())
.or_insert_with(new_codex_app_meta_schema);
add_codex_app_elicitation_description_to_meta_schema(meta_schema)
}
fn add_codex_app_elicitation_description_to_meta_schema(meta_schema: &mut JsonSchema) -> bool {
let JsonSchema::Object { properties, .. } = meta_schema else {
return false;
};
let codex_schema = properties
.entry(MCP_TOOL_ARGS_CODEX_KEY.to_string())
.or_insert_with(new_codex_app_control_meta_schema);
add_codex_app_elicitation_description_to_codex_schema(codex_schema)
}
fn add_codex_app_elicitation_description_to_codex_schema(codex_schema: &mut JsonSchema) -> bool {
let JsonSchema::Object { properties, .. } = codex_schema else {
return false;
};
properties.insert(
MCP_TOOL_ARGS_ELICITATION_DESCRIPTION_KEY.to_string(),
JsonSchema::String {
description: Some(CODEX_APP_ELICITATION_DESCRIPTION_FIELD_DESCRIPTION.to_string()),
},
);
true
}
fn new_codex_app_meta_schema() -> JsonSchema {
JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: Some(false.into()),
}
}
fn new_codex_app_control_meta_schema() -> JsonSchema {
JsonSchema::Object {
properties: BTreeMap::new(),
required: None,
additional_properties: Some(false.into()),
}
}
fn mcp_call_tool_result_output_schema(structured_content_schema: JsonValue) -> JsonValue {
json!({
"type": "object",

View File

@@ -13,6 +13,7 @@ use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use rmcp::model::ToolAnnotations;
use std::path::PathBuf;
use super::*;
@@ -31,6 +32,32 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
}
}
fn mcp_tool_with_annotations(
name: &str,
description: &str,
input_schema: serde_json::Value,
annotations: ToolAnnotations,
) -> rmcp::model::Tool {
rmcp::model::Tool {
annotations: Some(annotations),
..mcp_tool(name, description, input_schema)
}
}
fn annotations(
read_only: Option<bool>,
destructive: Option<bool>,
open_world: Option<bool>,
) -> ToolAnnotations {
ToolAnnotations {
destructive_hint: destructive,
idempotent_hint: None,
open_world_hint: open_world,
read_only_hint: read_only,
title: None,
}
}
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
let slug = name.replace(' ', "-").to_lowercase();
DiscoverableTool::Connector(Box::new(AppInfo {
@@ -254,6 +281,121 @@ fn deferred_responses_api_tool_serializes_with_defer_loading() {
);
}
#[test]
fn consequential_codex_apps_tools_include_elicitation_description_meta() {
let openai_tool = mcp_tool_to_openai_tool(
"mcp__codex_apps__calendar_create_event".to_string(),
mcp_tool_with_annotations(
"calendar_create_event",
"Create an event",
serde_json::json!({
"type": "object",
"properties": {
"title": {"type": "string"}
},
"required": ["title"],
"additionalProperties": false
}),
annotations(Some(false), None, Some(true)),
),
)
.expect("convert tool");
assert_eq!(
serde_json::to_value(openai_tool.parameters).expect("serialize parameters"),
serde_json::json!({
"type": "object",
"properties": {
"_meta": {
"type": "object",
"properties": {
"_codex": {
"type": "object",
"properties": {
"elicitation_description": {
"type": "string",
"description": "Short user-facing approval sentence. This field is not forwarded to the app."
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"title": {"type": "string"}
},
"required": ["title"],
"additionalProperties": false
})
);
assert!(
openai_tool
.description
.contains("`_meta._codex.elicitation_description`")
);
assert!(openai_tool.description.contains("not forwarded to the app"));
}
#[test]
fn non_consequential_codex_apps_tools_do_not_include_elicitation_description_meta() {
let openai_tool = mcp_tool_to_openai_tool(
"mcp__codex_apps__calendar_list_events".to_string(),
mcp_tool_with_annotations(
"calendar_list_events",
"List events",
serde_json::json!({
"type": "object",
"properties": {
"calendar_id": {"type": "string"}
},
"additionalProperties": false
}),
annotations(Some(true), None, None),
),
)
.expect("convert tool");
let parameters = serde_json::to_value(openai_tool.parameters).expect("serialize parameters");
assert_eq!(
parameters
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|properties| properties.get("_meta")),
None
);
assert_eq!(openai_tool.description, "List events");
}
#[test]
fn non_codex_apps_tools_do_not_include_elicitation_description_meta() {
let openai_tool = mcp_tool_to_openai_tool(
"mcp__custom_server__dangerous_tool".to_string(),
mcp_tool_with_annotations(
"dangerous_tool",
"Dangerous action",
serde_json::json!({
"type": "object",
"properties": {
"id": {"type": "string"}
},
"additionalProperties": false
}),
annotations(Some(false), Some(true), Some(true)),
),
)
.expect("convert tool");
let parameters = serde_json::to_value(openai_tool.parameters).expect("serialize parameters");
assert_eq!(
parameters
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|properties| properties.get("_meta")),
None
);
assert_eq!(openai_tool.description, "Dangerous action");
}
fn tool_name(tool: &ToolSpec) -> &str {
match tool {
ToolSpec::Function(ResponsesApiTool { name, .. }) => name,
@@ -2682,6 +2824,59 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
);
}
#[test]
fn code_mode_augments_consequential_codex_apps_tool_descriptions_with_elicitation_meta() {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::UnifiedExec);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([(
"mcp__codex_apps__calendar_create_event".to_string(),
mcp_tool_with_annotations(
"calendar_create_event",
"Create an event",
serde_json::json!({
"type": "object",
"properties": {
"title": {"type": "string"}
},
"required": ["title"],
"additionalProperties": false
}),
annotations(Some(false), None, Some(true)),
),
)])),
None,
&[],
)
.build();
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "mcp__codex_apps__calendar_create_event").spec
else {
panic!("expected function tool");
};
assert!(description.contains("`_meta._codex.elicitation_description`"));
assert!(description.contains(
"mcp__codex_apps__calendar_create_event(args: { _meta?: { _codex?: { elicitation_description?: string; }; }; title: string; })"
));
}
#[test]
fn code_mode_only_restricts_model_tools_to_exec_tools() {
let mut features = Features::with_defaults();