Codex Handbook
core/src/mcp_tool_call.rs 2153 lines
use std::collections::BTreeMap;use std::collections::HashMap;use std::time::Duration;use std::time::Instant;use crate::config::Config;use crate::config::edit::ConfigEdit;use crate::config::edit::ConfigEditsBuilder;use crate::connectors;use crate::guardian::GuardianApprovalRequest;use crate::guardian::GuardianMcpAnnotations;use crate::guardian::guardian_rejection_message;use crate::guardian::guardian_timeout_message;use crate::guardian::new_guardian_review_id;use crate::guardian::review_approval_request;use crate::guardian::routes_approval_to_guardian_with_reviewer;use crate::hook_runtime::run_permission_request_hooks;use crate::mcp_openai_file::rewrite_mcp_tool_arguments_for_openai_files;use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::tools::hook_names::HookToolName;use crate::tools::sandboxing::PermissionRequestPayload;use crate::turn_metadata::McpTurnMetadataContext;use codex_analytics::AppInvocation;use codex_analytics::InvocationType;use codex_analytics::build_track_events_context;use codex_app_server_protocol::ConfigLayerSource;use codex_app_server_protocol::McpElicitationObjectType;use codex_app_server_protocol::McpElicitationSchema;use codex_app_server_protocol::McpServerElicitationRequest;use codex_app_server_protocol::McpServerElicitationRequestParams;use codex_config::types::AppToolApproval;use codex_config::types::ApprovalsReviewer;use codex_connectors::AppToolPolicy;use codex_connectors::AppToolPolicyEvaluator;use codex_connectors::AppToolPolicyInput;use codex_features::Feature;use codex_hooks::PermissionRequestDecision;use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;use codex_mcp::McpPermissionPromptAutoApproveContext;use codex_mcp::SandboxState;use codex_mcp::auth_elicitation_completed_result;use codex_mcp::build_auth_elicitation_plan;use codex_mcp::declared_openai_file_input_param_names;use codex_mcp::mcp_permission_prompt_is_auto_approved;use codex_otel::sanitize_metric_tag_value;use codex_protocol::items::McpToolCallError;use codex_protocol::items::McpToolCallItem;use codex_protocol::items::McpToolCallStatus;use codex_protocol::items::TurnItem;use codex_protocol::mcp::CallToolResult;use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_TOOL_APPROVAL_KIND_KEY;use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL;use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY;use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY;use codex_protocol::mcp_approval_meta::CONNECTOR_NAME_KEY as MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY;use codex_protocol::mcp_approval_meta::PERSIST_ALWAYS as MCP_TOOL_APPROVAL_PERSIST_ALWAYS;use codex_protocol::mcp_approval_meta::PERSIST_KEY as MCP_TOOL_APPROVAL_PERSIST_KEY;use codex_protocol::mcp_approval_meta::PERSIST_SESSION as MCP_TOOL_APPROVAL_PERSIST_SESSION;use codex_protocol::mcp_approval_meta::SOURCE_CONNECTOR as MCP_TOOL_APPROVAL_SOURCE_CONNECTOR;use codex_protocol::mcp_approval_meta::SOURCE_KEY as MCP_TOOL_APPROVAL_SOURCE_KEY;use codex_protocol::mcp_approval_meta::TOOL_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY;use codex_protocol::mcp_approval_meta::TOOL_PARAMS_DISPLAY_KEY as MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY;use codex_protocol::mcp_approval_meta::TOOL_PARAMS_KEY as MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY;use codex_protocol::mcp_approval_meta::TOOL_TITLE_KEY as MCP_TOOL_APPROVAL_TOOL_TITLE_KEY;use codex_protocol::openai_models::InputModality;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::McpInvocation;use codex_protocol::protocol::ReviewDecision;use codex_protocol::request_user_input::RequestUserInputAnswer;use codex_protocol::request_user_input::RequestUserInputArgs;use codex_protocol::request_user_input::RequestUserInputQuestion;use codex_protocol::request_user_input::RequestUserInputQuestionOption;use codex_protocol::request_user_input::RequestUserInputResponse;use codex_rmcp_client::ElicitationAction;use codex_rmcp_client::ElicitationResponse;use codex_rollout::state_db;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_output_truncation::TruncationPolicy;use codex_utils_output_truncation::truncate_text;use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;use rmcp::model::ToolAnnotations;use serde::Deserialize;use serde::Serialize;use serde_json::Value as JsonValue;use std::sync::Arc;use toml_edit::value;use tracing::Instrument;use tracing::Span;use tracing::error;use tracing::field::Empty;use url::Url;const MCP_CALL_COUNT_METRIC: &str = "codex.mcp.call";const MCP_CALL_DURATION_METRIC: &str = "codex.mcp.call.duration_ms";const MCP_RESULT_TELEMETRY_META_KEY: &str = "codex/telemetry";const MCP_RESULT_TELEMETRY_SPAN_KEY: &str = "span";const MCP_RESULT_TELEMETRY_TARGET_ID_KEY: &str = "target_id";const MCP_RESULT_TELEMETRY_DID_TRIGGER_SERVER_USER_FLOW_KEY: &str = "did_trigger_server_user_flow";const MCP_RESULT_TELEMETRY_TARGET_ID_SPAN_ATTR: &str = "codex.mcp.target.id";const MCP_RESULT_TELEMETRY_SERVER_USER_FLOW_SPAN_ATTR: &str =    "codex.mcp.server_user_flow.triggered";const MCP_RESULT_TELEMETRY_TARGET_ID_MAX_CHARS: usize = 256;const MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES: usize = DEFAULT_OUTPUT_BYTES_CAP;/// Handles the specified tool call and dispatches the appropriate MCP tool-call/// item lifecycle events to the `Session`.pub(crate) async fn handle_mcp_tool_call(    sess: Arc<Session>,    turn_context: &Arc<TurnContext>,    call_id: String,    server: String,    tool_name: String,    hook_tool_name: HookToolName,    arguments: String,) -> HandledMcpToolCall {    // Parse the `arguments` as JSON. An empty string is OK, but invalid JSON    // is not.    let arguments_value = if arguments.trim().is_empty() {        None    } else {        match serde_json::from_str::<serde_json::Value>(&arguments) {            Ok(value) => Some(value),            Err(e) => {                error!("failed to parse tool call arguments: {e}");                return HandledMcpToolCall {                    result: CallToolResult::from_error_text(format!("err: {e}")),                    tool_input: JsonValue::Object(serde_json::Map::new()),                };            }        }    };    let invocation = McpInvocation {        server: server.clone(),        tool: tool_name.clone(),        arguments: arguments_value.clone(),    };    let metadata =        lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await;    let item_metadata = McpToolCallItemMetadata {        mcp_app_resource_uri: metadata            .as_ref()            .and_then(|metadata| metadata.mcp_app_resource_uri.clone()),        plugin_id: metadata            .as_ref()            .and_then(|metadata| metadata.plugin_id.clone()),    };    let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {        let annotations = metadata            .as_ref()            .and_then(|metadata| metadata.annotations.as_ref());        AppToolPolicyEvaluator::new(&turn_context.config.config_layer_stack).policy(            AppToolPolicyInput {                connector_id: metadata                    .as_ref()                    .and_then(|metadata| metadata.connector_id.as_deref()),                tool_name: &tool_name,                tool_title: metadata                    .as_ref()                    .and_then(|metadata| metadata.tool_title.as_deref()),                destructive_hint: annotations.and_then(|annotations| annotations.destructive_hint),                open_world_hint: annotations.and_then(|annotations| annotations.open_world_hint),            },        )    } else {        AppToolPolicy::default()    };    let approval_mode = if server == CODEX_APPS_MCP_SERVER_NAME {        app_tool_policy.approval    } else if let Some(approval_mode) = {        // Selected-plugin registrations are absent from config.toml and the legacy plugin manager,        // so their resolved catalog entry is the authoritative source for tool approval policy.        let manager = sess.services.mcp_connection_manager.load();        manager            .is_selected_plugin_mcp_server(&server)            .then(|| manager.tool_approval_mode(&server, &tool_name))    } {        approval_mode    } else {        custom_mcp_tool_approval_mode(sess.as_ref(), turn_context.as_ref(), &server, &tool_name)            .await    };    if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled {        let result = notify_mcp_tool_call_skip(            sess.as_ref(),            turn_context.as_ref(),            &call_id,            invocation,            item_metadata.clone(),            "MCP tool call blocked by app configuration".to_string(),            /*already_started*/ false,        )        .await;        let status = if result.is_ok() { "ok" } else { "error" };        turn_context.session_telemetry.counter(            MCP_CALL_COUNT_METRIC,            /*inc*/ 1,            &[("status", status)],        );        return HandledMcpToolCall {            result: CallToolResult::from_result(result),            tool_input: arguments_value                .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),        };    }    let connector_id = metadata        .as_ref()        .and_then(|metadata| metadata.connector_id.clone());    let connector_name = metadata        .as_ref()        .and_then(|metadata| metadata.connector_name.clone());    notify_mcp_tool_call_started(        sess.as_ref(),        turn_context.as_ref(),        &call_id,        invocation.clone(),        item_metadata.clone(),    )    .await;    if let Some(decision) = maybe_request_mcp_tool_approval(        &sess,        turn_context,        &call_id,        &invocation,        &hook_tool_name,        metadata.as_ref(),        approval_mode,    )    .await    {        let result = match decision {            McpToolApprovalDecision::Accept            | McpToolApprovalDecision::AcceptForSession            | McpToolApprovalDecision::AcceptAndRemember => {                return handle_approved_mcp_tool_call(                    sess.as_ref(),                    turn_context.as_ref(),                    &call_id,                    invocation,                    metadata.as_ref(),                    item_metadata,                )                .await;            }            McpToolApprovalDecision::Decline { message } => {                let message = message.unwrap_or_else(|| "user rejected MCP tool call".to_string());                notify_mcp_tool_call_skip(                    sess.as_ref(),                    turn_context.as_ref(),                    &call_id,                    invocation,                    item_metadata.clone(),                    message,                    /*already_started*/ true,                )                .await            }            McpToolApprovalDecision::Cancel => {                let message = "user cancelled MCP tool call".to_string();                notify_mcp_tool_call_skip(                    sess.as_ref(),                    turn_context.as_ref(),                    &call_id,                    invocation,                    item_metadata.clone(),                    message,                    /*already_started*/ true,                )                .await            }        };        let status = if result.is_ok() { "ok" } else { "error" };        emit_mcp_call_metrics(            turn_context.as_ref(),            status,            &tool_name,            connector_id.as_deref(),            connector_name.as_deref(),            /*duration*/ None,        );        return HandledMcpToolCall {            result: CallToolResult::from_result(result),            tool_input: arguments_value                .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),        };    }    handle_approved_mcp_tool_call(        sess.as_ref(),        turn_context.as_ref(),        &call_id,        invocation,        metadata.as_ref(),        item_metadata,    )    .await}pub(crate) struct HandledMcpToolCall {    pub(crate) result: CallToolResult,    pub(crate) tool_input: JsonValue,}#[derive(Clone)]struct McpToolCallItemMetadata {    mcp_app_resource_uri: Option<String>,    plugin_id: Option<String>,}async fn handle_approved_mcp_tool_call(    sess: &Session,    turn_context: &TurnContext,    call_id: &str,    invocation: McpInvocation,    metadata: Option<&McpToolApprovalMetadata>,    item_metadata: McpToolCallItemMetadata,) -> HandledMcpToolCall {    let server = invocation.server.clone();    maybe_mark_thread_memory_mode_polluted(sess, turn_context, &server).await;    let tool_name = invocation.tool.clone();    let arguments_value = invocation.arguments.clone();    let connector_id = metadata.and_then(|metadata| metadata.connector_id.as_deref());    let connector_name = metadata.and_then(|metadata| metadata.connector_name.as_deref());    let server_origin = sess        .services        .mcp_connection_manager        .load_full()        .server_origin(&server)        .map(str::to_string);    let start = Instant::now();    let rewrite = rewrite_mcp_tool_arguments_for_openai_files(        sess,        turn_context,        arguments_value.clone(),        metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()),    )    .await;    let tool_input = match &rewrite {        Ok(Some(rewritten_arguments)) => rewritten_arguments.clone(),        Ok(None) | Err(_) => arguments_value            .clone()            .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),    };    let result = async {        let rewritten_arguments = rewrite?;        let request_meta =            build_mcp_tool_call_request_meta(turn_context, &server, call_id, metadata);        let result = execute_mcp_tool_call(            sess,            turn_context,            call_id,            &invocation,            rewritten_arguments,            metadata,            request_meta,        )        .await;        record_mcp_result_span_telemetry(&Span::current(), result.as_ref().ok());        result    }    .instrument(mcp_tool_call_span(        sess,        turn_context,        McpToolCallSpanFields {            server_name: &server,            tool_name: &tool_name,            call_id,            server_origin: server_origin.as_deref(),            connector_id,            connector_name,        },    ))    .await;    if let Err(error) = &result {        tracing::warn!("MCP tool call error: {error:?}");    }    let duration = start.elapsed();    notify_mcp_tool_call_completed(        sess,        turn_context,        call_id,        invocation,        item_metadata,        duration,        truncate_mcp_tool_result_for_event(&result),    )    .await;    maybe_track_codex_app_used(sess, turn_context, &server, &tool_name).await;    let status = if result.is_ok() { "ok" } else { "error" };    emit_mcp_call_metrics(        turn_context,        status,        &tool_name,        connector_id,        connector_name,        Some(duration),    );    HandledMcpToolCall {        result: CallToolResult::from_result(result),        tool_input,    }}fn emit_mcp_call_metrics(    turn_context: &TurnContext,    status: &str,    tool_name: &str,    connector_id: Option<&str>,    connector_name: Option<&str>,    duration: Option<Duration>,) {    let tags = mcp_call_metric_tags(status, tool_name, connector_id, connector_name);    let tag_refs: Vec<(&str, &str)> = tags        .iter()        .map(|(key, value)| (*key, value.as_str()))        .collect();    turn_context        .session_telemetry        .counter(MCP_CALL_COUNT_METRIC, /*inc*/ 1, &tag_refs);    if let Some(duration) = duration {        turn_context.session_telemetry.record_duration(            MCP_CALL_DURATION_METRIC,            duration,            &tag_refs,        );    }}fn mcp_call_metric_tags(    status: &str,    tool_name: &str,    connector_id: Option<&str>,    connector_name: Option<&str>,) -> Vec<(&'static str, String)> {    let mut tags = vec![        ("status", sanitize_metric_tag_value(status)),        ("tool", sanitize_metric_tag_value(tool_name)),    ];    if let Some(connector_id) = connector_id.filter(|connector_id| !connector_id.is_empty()) {        tags.push(("connector_id", sanitize_metric_tag_value(connector_id)));    }    if let Some(connector_name) = connector_name.filter(|connector_name| !connector_name.is_empty())    {        tags.push(("connector_name", sanitize_metric_tag_value(connector_name)));    }    tags}fn mcp_tool_call_span(    session: &Session,    turn_context: &TurnContext,    fields: McpToolCallSpanFields<'_>,) -> Span {    let transport = match fields.server_origin {        Some("stdio") => "stdio",        Some("in_process") => "in_process",        Some(_) => "streamable_http",        None => "",    };    let span = tracing::info_span!(        "mcp.tools.call",        otel.kind = "client",        rpc.system = "jsonrpc",        rpc.method = "tools/call",        mcp.server.name = fields.server_name,        mcp.server.origin = fields.server_origin.unwrap_or(""),        mcp.transport = transport,        mcp.connector.id = fields.connector_id.unwrap_or(""),        mcp.connector.name = fields.connector_name.unwrap_or(""),        tool.name = fields.tool_name,        tool.call_id = fields.call_id,        conversation.id = %session.thread_id,        session.id = %session.thread_id,        turn.id = turn_context.sub_id.as_str(),        server.address = Empty,        server.port = Empty,        codex.mcp.target.id = Empty,        codex.mcp.server_user_flow.triggered = Empty,    );    record_server_fields(&span, fields.server_origin);    span}struct McpToolCallSpanFields<'a> {    server_name: &'a str,    tool_name: &'a str,    call_id: &'a str,    server_origin: Option<&'a str>,    connector_id: Option<&'a str>,    connector_name: Option<&'a str>,}fn record_server_fields(span: &Span, url: Option<&str>) {    let Some(url) = url else {        return;    };    let Ok(parsed) = Url::parse(url) else {        return;    };    if let Some(host) = parsed.host_str() {        span.record("server.address", host);    }    if let Some(port) = parsed.port_or_known_default() {        span.record("server.port", port as i64);    }}fn record_mcp_result_span_telemetry(span: &Span, result: Option<&CallToolResult>) {    let Some(span_telemetry) = result        .and_then(|result| result.meta.as_ref())        .and_then(JsonValue::as_object)        .and_then(|meta| meta.get(MCP_RESULT_TELEMETRY_META_KEY))        .and_then(JsonValue::as_object)        .and_then(|telemetry| telemetry.get(MCP_RESULT_TELEMETRY_SPAN_KEY))        .and_then(JsonValue::as_object)    else {        return;    };    if let Some(target_id) = span_telemetry        .get(MCP_RESULT_TELEMETRY_TARGET_ID_KEY)        .and_then(JsonValue::as_str)        .filter(|target_id| !target_id.is_empty())    {        span.record(            MCP_RESULT_TELEMETRY_TARGET_ID_SPAN_ATTR,            truncate_str_to_char_boundary(target_id, MCP_RESULT_TELEMETRY_TARGET_ID_MAX_CHARS),        );    }    if let Some(did_trigger_server_user_flow) = span_telemetry        .get(MCP_RESULT_TELEMETRY_DID_TRIGGER_SERVER_USER_FLOW_KEY)        .and_then(JsonValue::as_bool)    {        span.record(            MCP_RESULT_TELEMETRY_SERVER_USER_FLOW_SPAN_ATTR,            did_trigger_server_user_flow,        );    }}fn truncate_str_to_char_boundary(value: &str, max_chars: usize) -> &str {    match value.char_indices().nth(max_chars) {        Some((index, _)) => &value[..index],        None => value,    }}async fn execute_mcp_tool_call(    sess: &Session,    turn_context: &TurnContext,    call_id: &str,    invocation: &McpInvocation,    rewritten_arguments: Option<JsonValue>,    metadata: Option<&McpToolApprovalMetadata>,    request_meta: Option<JsonValue>,) -> Result<CallToolResult, String> {    let request_meta = with_mcp_tool_call_thread_id_meta(request_meta, &sess.thread_id.to_string());    let request_meta = augment_mcp_tool_request_meta_with_sandbox_state(        sess,        turn_context,        &invocation.server,        request_meta,    )    .await    .map_err(|e| format!("failed to build MCP tool request metadata: {e:#}"))?;    let mcp_call_trace = sess        .services        .rollout_thread_trace        .start_mcp_call_trace(call_id);    let request_meta = mcp_call_trace.add_request_meta(request_meta);    let result = sess        .call_tool(            &invocation.server,            &invocation.tool,            rewritten_arguments,            request_meta,        )        .await        .map_err(|e| format!("tool call error: {e:?}"))?;    let result = sanitize_mcp_tool_result_for_model(        turn_context            .model_info            .input_modalities            .contains(&InputModality::Image),        Ok(result),    )?;    Ok(maybe_request_codex_apps_auth_elicitation(        sess,        turn_context,        call_id,        &invocation.server,        metadata,        result,    )    .await)}async fn maybe_request_codex_apps_auth_elicitation(    sess: &Session,    turn_context: &TurnContext,    call_id: &str,    server: &str,    metadata: Option<&McpToolApprovalMetadata>,    result: CallToolResult,) -> CallToolResult {    if !sess        .services        .mcp_connection_manager        .load_full()        .is_host_owned_codex_apps_server(server)    {        return result;    }    if !turn_context.features.enabled(Feature::AuthElicitation) {        return result;    }    match turn_context.approval_policy.value() {        AskForApproval::Never => return result,        AskForApproval::Granular(granular_config) if !granular_config.allows_mcp_elicitations() => {            return result;        }        AskForApproval::OnFailure        | AskForApproval::OnRequest        | AskForApproval::UnlessTrusted        | AskForApproval::Granular(_) => {}    }    let connector_id = metadata.and_then(|metadata| metadata.connector_id.as_deref());    let connector_name = metadata.and_then(|metadata| metadata.connector_name.as_deref());    let install_url = connector_id.map(|connector_id| {        codex_connectors::metadata::connector_install_url(            connector_name.unwrap_or(connector_id),            connector_id,        )    });    let Some(plan) =        build_auth_elicitation_plan(call_id, &result, connector_id, connector_name, install_url)    else {        return result;    };    let request_id = rmcp::model::RequestId::String(plan.elicitation.elicitation_id.clone().into());    let params = McpServerElicitationRequestParams {        thread_id: sess.thread_id.to_string(),        turn_id: Some(turn_context.sub_id.clone()),        server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),        request: McpServerElicitationRequest::Url {            meta: Some(plan.elicitation.meta),            message: plan.elicitation.message,            url: plan.elicitation.url,            elicitation_id: plan.elicitation.elicitation_id,        },    };    let response = sess        .request_mcp_server_elicitation(turn_context, request_id, params)        .await        .response;    if !response        .as_ref()        .is_some_and(|response| response.action == ElicitationAction::Accept)    {        return result;    }    refresh_codex_apps_after_connector_auth(sess, turn_context).await;    auth_elicitation_completed_result(&plan.auth_failure, result.meta)}async fn refresh_codex_apps_after_connector_auth(sess: &Session, turn_context: &TurnContext) {    let mcp_tools_result = {        let manager = sess.services.mcp_connection_manager.load_full();        manager.hard_refresh_codex_apps_tools_cache().await    };    match mcp_tools_result {        Ok(mcp_tools) => {            let auth = sess.services.auth_manager.auth().await;            connectors::refresh_accessible_connectors_cache_from_mcp_tools(                &turn_context.config,                auth.as_ref(),                &mcp_tools,            );        }        Err(err) => {            tracing::warn!("failed to refresh Codex Apps tools after connector auth: {err:#}");        }    }}async fn augment_mcp_tool_request_meta_with_sandbox_state(    sess: &Session,    turn_context: &TurnContext,    server: &str,    mut meta: Option<serde_json::Value>,) -> anyhow::Result<Option<serde_json::Value>> {    let supports_sandbox_state_meta = sess        .services        .mcp_connection_manager        .load_full()        .server_supports_sandbox_state_meta_capability(server)        .await        .unwrap_or(false);    if !supports_sandbox_state_meta {        return Ok(meta);    }    let sandbox_state = serde_json::to_value(SandboxState {        permission_profile: Some(turn_context.permission_profile()),        sandbox_policy: turn_context.sandbox_policy(),        codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),        #[allow(deprecated)]        sandbox_cwd: turn_context.cwd.to_path_buf(),        use_legacy_landlock: turn_context.features.use_legacy_landlock(),    })?;    match meta.as_mut() {        Some(serde_json::Value::Object(map)) => {            map.insert(                codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY.to_string(),                sandbox_state,            );        }        Some(_) => {}        None => {            let mut map = serde_json::Map::new();            map.insert(                codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY.to_string(),                sandbox_state,            );            meta = Some(serde_json::Value::Object(map));        }    }    Ok(meta)}async fn maybe_mark_thread_memory_mode_polluted(    sess: &Session,    turn_context: &TurnContext,    server: &str,) {    if !turn_context.config.memories.disable_on_external_context {        return;    }    let pollutes_memory = sess        .services        .mcp_connection_manager        .load_full()        .server_pollutes_memory(server);    if !pollutes_memory {        return;    }    state_db::mark_thread_memory_mode_polluted(        sess.services.state_db.as_deref(),        sess.thread_id,        "mcp_tool_call",    )    .await;}fn sanitize_mcp_tool_result_for_model(    supports_image_input: bool,    result: Result<CallToolResult, String>,) -> Result<CallToolResult, String> {    if supports_image_input {        return result;    }    result.map(|call_tool_result| CallToolResult {        content: call_tool_result            .content            .iter()            .map(|block| {                if let Some(content_type) = block.get("type").and_then(serde_json::Value::as_str)                    && content_type == "image"                {                    return serde_json::json!({                        "type": "text",                        "text": "<image content omitted because you do not support image input>",                    });                }                block.clone()            })            .collect::<Vec<_>>(),        structured_content: call_tool_result.structured_content,        is_error: call_tool_result.is_error,        meta: call_tool_result.meta,    })}fn truncate_mcp_tool_result_for_event(    result: &Result<CallToolResult, String>,) -> Result<CallToolResult, String> {    match result {        Ok(call_tool_result) => {            // The app-server rebuilds `ThreadItem::McpToolCall` from this item,            // so avoid persisting multi-megabyte results in rollout storage.            let Ok(serialized) = serde_json::to_string(call_tool_result) else {                return Ok(call_tool_result.clone());            };            if serialized.len() <= MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES {                return Ok(call_tool_result.clone());            }            // A huge MCP result can put bytes in `content`, `structuredContent`,            // or `_meta`. Collapse the event copy to a text preview of the whole            // serialized result so the UI still has useful context without            // preserving a multi-megabyte structured payload.            //            // This budget applies to the preview text, not the final event JSON.            // The preview is itself serialized into a JSON string, so quotes and            // backslashes can be escaped again and the stored event may end up            // somewhat larger than this byte budget.            let truncated = truncate_text(                &serialized,                TruncationPolicy::Bytes(MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES),            );            Ok(CallToolResult {                content: vec![serde_json::json!({                    "type": "text",                    "text": truncated,                })],                structured_content: None,                is_error: call_tool_result.is_error,                meta: None,            })        }        Err(message) => Err(truncate_text(            message,            TruncationPolicy::Bytes(MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES),        )),    }}async fn notify_mcp_tool_call_started(    sess: &Session,    turn_context: &TurnContext,    call_id: &str,    invocation: McpInvocation,    item_metadata: McpToolCallItemMetadata,) {    let McpInvocation {        server,        tool,        arguments,    } = invocation;    let item = TurnItem::McpToolCall(McpToolCallItem {        id: call_id.to_string(),        server,        tool,        arguments: arguments.unwrap_or(JsonValue::Null),        mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,        plugin_id: item_metadata.plugin_id,        status: McpToolCallStatus::InProgress,        result: None,        error: None,        duration: None,    });    sess.emit_turn_item_started(turn_context, &item).await;}async fn notify_mcp_tool_call_completed(    sess: &Session,    turn_context: &TurnContext,    call_id: &str,    invocation: McpInvocation,    item_metadata: McpToolCallItemMetadata,    duration: Duration,    result: Result<CallToolResult, String>,) {    let (status, result, error) = match result {        Ok(result) if result.is_error.unwrap_or(false) => {            (McpToolCallStatus::Failed, Some(result), None)        }        Ok(result) => (McpToolCallStatus::Completed, Some(result), None),        Err(message) => (            McpToolCallStatus::Failed,            None,            Some(McpToolCallError { message }),        ),    };    let McpInvocation {        server,        tool,        arguments,    } = invocation;    let item = TurnItem::McpToolCall(McpToolCallItem {        id: call_id.to_string(),        server,        tool,        arguments: arguments.unwrap_or(JsonValue::Null),        mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,        plugin_id: item_metadata.plugin_id,        status,        result,        error,        duration: Some(duration),    });    sess.emit_turn_item_completed(turn_context, item).await;}struct McpAppUsageMetadata {    connector_id: Option<String>,    app_name: Option<String>,}async fn maybe_track_codex_app_used(    sess: &Session,    turn_context: &TurnContext,    server: &str,    tool_name: &str,) {    if server != CODEX_APPS_MCP_SERVER_NAME {        return;    }    let metadata = lookup_mcp_app_usage_metadata(sess, server, tool_name).await;    let (connector_id, app_name) = metadata        .map(|metadata| (metadata.connector_id, metadata.app_name))        .unwrap_or((None, None));    let invocation_type = if let Some(connector_id) = connector_id.as_deref() {        let mentioned_connector_ids = sess.get_connector_selection().await;        if mentioned_connector_ids.contains(connector_id) {            InvocationType::Explicit        } else {            InvocationType::Implicit        }    } else {        InvocationType::Implicit    };    let tracking = build_track_events_context(        turn_context.model_info.slug.clone(),        sess.thread_id.to_string(),        turn_context.sub_id.clone(),    );    sess.services.analytics_events_client.track_app_used(        tracking,        AppInvocation {            connector_id,            app_name,            invocation_type: Some(invocation_type),        },    );}#[derive(Debug, Clone, PartialEq, Eq)]enum McpToolApprovalDecision {    Accept,    AcceptForSession,    AcceptAndRemember,    Decline { message: Option<String> },    Cancel,}pub(crate) struct McpToolApprovalMetadata {    annotations: Option<ToolAnnotations>,    connector_id: Option<String>,    connector_name: Option<String>,    connector_description: Option<String>,    plugin_id: Option<String>,    tool_title: Option<String>,    tool_description: Option<String>,    mcp_app_resource_uri: Option<String>,    codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,    openai_file_input_params: Option<Vec<String>>,}const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";const MCP_TOOL_PLUGIN_ID_META_KEY: &str = "plugin_id";const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";async fn custom_mcp_tool_approval_mode(    sess: &Session,    turn_context: &TurnContext,    server: &str,    tool_name: &str,) -> AppToolApproval {    let user_configured_mode = turn_context        .config        .config_layer_stack        .effective_config()        .as_table()        .and_then(|table| table.get("mcp_servers"))        .cloned()        .and_then(|value| {            HashMap::<String, codex_config::types::McpServerConfig>::deserialize(value).ok()        })        .and_then(|servers| {            let server_config = servers.get(server)?;            Some(                server_config                    .tools                    .get(tool_name)                    .and_then(|tool| tool.approval_mode)                    .or(server_config.default_tools_approval_mode)                    .unwrap_or_default(),            )        });    if let Some(user_configured_mode) = user_configured_mode {        return user_configured_mode;    }    sess.services        .plugins_manager        .plugins_for_config(&turn_context.config.plugins_config_input())        .await        .plugins()        .iter()        .filter(|plugin| plugin.is_active())        .find_map(|plugin| {            let server_config = plugin.mcp_servers.get(server)?;            server_config                .tools                .get(tool_name)                .and_then(|tool| tool.approval_mode)                .or(server_config.default_tools_approval_mode)        })        .unwrap_or_default()}fn build_mcp_tool_call_request_meta(    turn_context: &TurnContext,    server: &str,    call_id: &str,    metadata: Option<&McpToolApprovalMetadata>,) -> Option<serde_json::Value> {    let mut request_meta = serde_json::Map::new();    if let Some(turn_metadata) = turn_context        .turn_metadata_state        .current_meta_value_for_mcp_request(McpTurnMetadataContext {            model: turn_context.model_info.slug.as_str(),            reasoning_effort: turn_context.effective_reasoning_effort(),        })    {        request_meta.insert(            crate::X_CODEX_TURN_METADATA_HEADER.to_string(),            turn_metadata,        );    }    if server == CODEX_APPS_MCP_SERVER_NAME {        let mut codex_apps_meta = metadata            .and_then(|metadata| metadata.codex_apps_meta.clone())            .unwrap_or_default();        codex_apps_meta.insert(            "call_id".to_string(),            serde_json::Value::String(call_id.to_string()),        );        request_meta.insert(            MCP_TOOL_CODEX_APPS_META_KEY.to_string(),            serde_json::Value::Object(codex_apps_meta),        );    }    if let Some(plugin_id) = metadata.and_then(|metadata| metadata.plugin_id.as_ref()) {        request_meta.insert(            MCP_TOOL_PLUGIN_ID_META_KEY.to_string(),            serde_json::Value::String(plugin_id.clone()),        );    }    (!request_meta.is_empty()).then_some(serde_json::Value::Object(request_meta))}fn with_mcp_tool_call_thread_id_meta(    meta: Option<serde_json::Value>,    thread_id: &str,) -> Option<serde_json::Value> {    match meta {        Some(serde_json::Value::Object(mut map)) => {            map.insert(                MCP_TOOL_THREAD_ID_META_KEY.to_string(),                serde_json::Value::String(thread_id.to_string()),            );            Some(serde_json::Value::Object(map))        }        None => {            let mut map = serde_json::Map::new();            map.insert(                MCP_TOOL_THREAD_ID_META_KEY.to_string(),                serde_json::Value::String(thread_id.to_string()),            );            Some(serde_json::Value::Object(map))        }        other => other,    }}#[derive(Clone, Copy)]struct McpToolApprovalPromptOptions {    allow_session_remember: bool,    allow_persistent_approval: bool,}struct McpToolApprovalElicitationRequest<'a> {    server: &'a str,    metadata: Option<&'a McpToolApprovalMetadata>,    tool_params: Option<&'a serde_json::Value>,    tool_params_display: Option<&'a [RenderedMcpToolApprovalParam]>,    question: RequestUserInputQuestion,    message_override: Option<&'a str>,    prompt_options: McpToolApprovalPromptOptions,}pub(crate) const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX: &str = "mcp_tool_call_approval";pub(crate) const MCP_TOOL_APPROVAL_ACCEPT: &str = "Allow";pub(crate) const MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION: &str = "Allow for this session";// Internal-only token used when guardian auto-reviews delegated MCP approvals on the// RequestUserInput compatibility path. That legacy MCP prompt has allow/cancel labels but no// real "Decline" answer, so this lets guardian denials round-trip distinctly from user cancel.// This is not a user-facing option.pub(crate) const MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC: &str = "__codex_mcp_decline__";const MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER: &str = "Allow and don't ask me again";const MCP_TOOL_APPROVAL_CANCEL: &str = "Cancel";pub(crate) fn is_mcp_tool_approval_question_id(question_id: &str) -> bool {    question_id        .strip_prefix(MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX)        .is_some_and(|suffix| suffix.starts_with('_'))}#[derive(Clone, Debug, PartialEq, Eq, Serialize)]struct McpToolApprovalKey {    server: String,    connector_id: Option<String>,    tool_name: String,}fn mcp_tool_approval_prompt_options(    session_approval_key: Option<&McpToolApprovalKey>,    persistent_approval_key: Option<&McpToolApprovalKey>,    tool_call_mcp_elicitation_enabled: bool,) -> McpToolApprovalPromptOptions {    McpToolApprovalPromptOptions {        allow_session_remember: session_approval_key.is_some(),        allow_persistent_approval: tool_call_mcp_elicitation_enabled            && persistent_approval_key.is_some(),    }}async fn maybe_request_mcp_tool_approval(    sess: &Arc<Session>,    turn_context: &Arc<TurnContext>,    call_id: &str,    invocation: &McpInvocation,    hook_tool_name: &HookToolName,    metadata: Option<&McpToolApprovalMetadata>,    approval_mode: AppToolApproval,) -> Option<McpToolApprovalDecision> {    let approvals_reviewer = mcp_approvals_reviewer(turn_context, &invocation.server, metadata);    if mcp_permission_prompt_is_auto_approved(        turn_context.approval_policy.value(),        &turn_context.permission_profile(),        McpPermissionPromptAutoApproveContext {            tool_approval_mode: Some(approval_mode),        },    ) {        return None;    }    let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());    let approval_required = requires_mcp_tool_approval(annotations);    if !approval_required && approval_mode != AppToolApproval::Prompt {        return None;    }    let session_approval_key = session_mcp_tool_approval_key(invocation, metadata, approval_mode);    let persistent_approval_key = if sess        .services        .mcp_connection_manager        .load()        .is_selected_plugin_mcp_server(&invocation.server)    {        None    } else {        persistent_mcp_tool_approval_key(invocation, metadata, approval_mode)    };    if let Some(key) = session_approval_key.as_ref()        && mcp_tool_approval_is_remembered(sess, key).await    {        return Some(McpToolApprovalDecision::Accept);    }    match run_permission_request_hooks(        sess,        turn_context,        call_id,        PermissionRequestPayload {            tool_name: hook_tool_name.clone(),            tool_input: invocation                .arguments                .clone()                .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),        },    )    .await    {        Some(PermissionRequestDecision::Allow) => {            return Some(McpToolApprovalDecision::Accept);        }        Some(PermissionRequestDecision::Deny { message }) => {            return Some(McpToolApprovalDecision::Decline {                message: Some(message),            });        }        None => {}    }    let tool_call_mcp_elicitation_enabled = turn_context        .config        .features        .enabled(Feature::ToolCallMcpElicitation);    if routes_approval_to_guardian_with_reviewer(turn_context, approvals_reviewer) {        let review_id = new_guardian_review_id();        let decision = review_approval_request(            sess,            turn_context,            review_id.clone(),            build_guardian_mcp_tool_review_request(call_id, invocation, metadata),            /*retry_reason*/ None,        )        .await;        let decision = mcp_tool_approval_decision_from_guardian(sess, &review_id, decision).await;        apply_mcp_tool_approval_decision(            sess,            turn_context,            &decision,            session_approval_key,            persistent_approval_key,        )        .await;        return Some(decision);    }    let prompt_options = mcp_tool_approval_prompt_options(        session_approval_key.as_ref(),        persistent_approval_key.as_ref(),        tool_call_mcp_elicitation_enabled,    );    let question_id = format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}");    let rendered_template = render_mcp_tool_approval_template(        &invocation.server,        metadata.and_then(|metadata| metadata.connector_id.as_deref()),        metadata.and_then(|metadata| metadata.connector_name.as_deref()),        metadata.and_then(|metadata| metadata.tool_title.as_deref()),        invocation.arguments.as_ref(),    );    let tool_params_display = rendered_template        .as_ref()        .map(|rendered_template| rendered_template.tool_params_display.clone())        .or_else(|| build_mcp_tool_approval_display_params(invocation.arguments.as_ref()));    let question = build_mcp_tool_approval_question(        question_id.clone(),        &invocation.server,        &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()),    );    if tool_call_mcp_elicitation_enabled {        let request_id = rmcp::model::RequestId::String(            format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}").into(),        );        let params = build_mcp_tool_approval_elicitation_request(            sess.as_ref(),            turn_context.as_ref(),            McpToolApprovalElicitationRequest {                server: &invocation.server,                metadata,                tool_params: rendered_template                    .as_ref()                    .and_then(|rendered_template| rendered_template.tool_params.as_ref())                    .or(invocation.arguments.as_ref()),                tool_params_display: tool_params_display.as_deref(),                question,                message_override: rendered_template                    .as_ref()                    .map(|rendered_template| rendered_template.elicitation_message.as_str()),                prompt_options,            },        );        let decision = parse_mcp_tool_approval_elicitation_response(            sess.request_mcp_server_elicitation(turn_context.as_ref(), request_id, params)                .await                .response,            &question_id,        );        let decision = normalize_approval_decision_for_mode(decision, approval_mode);        apply_mcp_tool_approval_decision(            sess,            turn_context,            &decision,            session_approval_key,            persistent_approval_key,        )        .await;        return Some(decision);    }    let args = RequestUserInputArgs {        questions: vec![question],        auto_resolution_ms: None,    };    let response = sess        .request_user_input(turn_context.as_ref(), call_id.to_string(), args)        .await;    let decision = normalize_approval_decision_for_mode(        parse_mcp_tool_approval_response(response, &question_id),        approval_mode,    );    apply_mcp_tool_approval_decision(        sess,        turn_context,        &decision,        session_approval_key,        persistent_approval_key,    )    .await;    Some(decision)}pub(crate) fn mcp_approvals_reviewer(    turn_context: &TurnContext,    server_name: &str,    metadata: Option<&McpToolApprovalMetadata>,) -> ApprovalsReviewer {    connectors::mcp_approvals_reviewer(        turn_context.config.as_ref(),        server_name,        metadata.and_then(|metadata| metadata.connector_id.as_deref()),    )}fn session_mcp_tool_approval_key(    invocation: &McpInvocation,    metadata: Option<&McpToolApprovalMetadata>,    approval_mode: AppToolApproval,) -> Option<McpToolApprovalKey> {    if approval_mode != AppToolApproval::Auto {        return None;    }    let connector_id = metadata.and_then(|metadata| metadata.connector_id.clone());    if invocation.server == CODEX_APPS_MCP_SERVER_NAME && connector_id.is_none() {        return None;    }    Some(McpToolApprovalKey {        server: invocation.server.clone(),        connector_id,        tool_name: invocation.tool.clone(),    })}fn persistent_mcp_tool_approval_key(    invocation: &McpInvocation,    metadata: Option<&McpToolApprovalMetadata>,    approval_mode: AppToolApproval,) -> Option<McpToolApprovalKey> {    session_mcp_tool_approval_key(invocation, metadata, approval_mode)}pub(crate) fn build_guardian_mcp_tool_review_request(    call_id: &str,    invocation: &McpInvocation,    metadata: Option<&McpToolApprovalMetadata>,) -> GuardianApprovalRequest {    GuardianApprovalRequest::McpToolCall {        id: call_id.to_string(),        server: invocation.server.clone(),        tool_name: invocation.tool.clone(),        arguments: invocation.arguments.clone(),        connector_id: metadata.and_then(|metadata| metadata.connector_id.clone()),        connector_name: metadata.and_then(|metadata| metadata.connector_name.clone()),        connector_description: metadata.and_then(|metadata| metadata.connector_description.clone()),        tool_title: metadata.and_then(|metadata| metadata.tool_title.clone()),        tool_description: metadata.and_then(|metadata| metadata.tool_description.clone()),        annotations: metadata            .and_then(|metadata| metadata.annotations.as_ref())            .map(|annotations| GuardianMcpAnnotations {                destructive_hint: annotations.destructive_hint,                open_world_hint: annotations.open_world_hint,                read_only_hint: annotations.read_only_hint,            }),    }}async fn mcp_tool_approval_decision_from_guardian(    sess: &Session,    review_id: &str,    decision: ReviewDecision,) -> McpToolApprovalDecision {    match decision {        ReviewDecision::Approved        | ReviewDecision::ApprovedExecpolicyAmendment { .. }        | ReviewDecision::NetworkPolicyAmendment { .. } => McpToolApprovalDecision::Accept,        ReviewDecision::ApprovedForSession => McpToolApprovalDecision::AcceptForSession,        ReviewDecision::Denied => McpToolApprovalDecision::Decline {            message: Some(guardian_rejection_message(sess, review_id).await),        },        ReviewDecision::TimedOut => McpToolApprovalDecision::Decline {            message: Some(guardian_timeout_message()),        },        ReviewDecision::Abort => McpToolApprovalDecision::Decline { message: None },    }}pub(crate) async fn lookup_mcp_tool_metadata(    sess: &Session,    turn_context: &TurnContext,    server: &str,    tool_name: &str,) -> Option<McpToolApprovalMetadata> {    let manager = sess.services.mcp_connection_manager.load_full();    let plugin_id = manager        .plugin_id_for_mcp_server_name(server)        .map(str::to_string);    let tools = manager.list_all_tools().await;    let tool_info = tools        .into_iter()        .find(|tool_info| tool_info.server_name == server && tool_info.tool.name == tool_name)?;    let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME {        let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools(            turn_context.config.as_ref(),        )        .await        {            Some(connectors) => Some(connectors),            None => {                connectors::list_accessible_connectors_from_mcp_tools(turn_context.config.as_ref())                    .await                    .ok()            }        };        connectors.and_then(|connectors| {            let connector_id = tool_info.connector_id.as_deref()?;            connectors                .into_iter()                .find(|connector| connector.id == connector_id)                .and_then(|connector| connector.description)        })    } else {        None    };    Some(McpToolApprovalMetadata {        annotations: tool_info.tool.annotations,        connector_id: tool_info.connector_id,        connector_name: tool_info.connector_name,        connector_description,        plugin_id,        tool_title: tool_info.tool.title,        tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),        mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),        codex_apps_meta: tool_info            .tool            .meta            .as_ref()            .and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY))            .and_then(serde_json::Value::as_object)            .cloned(),        // Disallow custom MCPs from uploading files via fileParams.        openai_file_input_params: openai_file_input_params_for_server(            server,            tool_info.tool.meta.as_deref(),        ),    })}fn openai_file_input_params_for_server(    server: &str,    meta: Option<&serde_json::Map<String, serde_json::Value>>,) -> Option<Vec<String>> {    (server == CODEX_APPS_MCP_SERVER_NAME)        .then_some(declared_openai_file_input_param_names(meta))        .filter(|params| !params.is_empty())}fn get_mcp_app_resource_uri(    meta: Option<&serde_json::Map<String, serde_json::Value>>,) -> Option<String> {    meta.and_then(|meta| {        meta.get("ui")            .and_then(serde_json::Value::as_object)            .and_then(|ui| ui.get("resourceUri"))            .and_then(serde_json::Value::as_str)            .or_else(|| {                meta.get(MCP_TOOL_UI_RESOURCE_URI_META_KEY)                    .and_then(serde_json::Value::as_str)            })            .or_else(|| {                meta.get(MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY)                    .and_then(serde_json::Value::as_str)            })            .map(str::to_string)    })}async fn lookup_mcp_app_usage_metadata(    sess: &Session,    server: &str,    tool_name: &str,) -> Option<McpAppUsageMetadata> {    let tools = sess        .services        .mcp_connection_manager        .load_full()        .list_all_tools()        .await;    tools.into_iter().find_map(|tool_info| {        if tool_info.server_name == server && tool_info.tool.name == tool_name {            Some(McpAppUsageMetadata {                connector_id: tool_info.connector_id,                app_name: tool_info.connector_name,            })        } else {            None        }    })}fn build_mcp_tool_approval_question(    question_id: String,    server: &str,    tool_name: &str,    connector_name: Option<&str>,    prompt_options: McpToolApprovalPromptOptions,    question_override: Option<&str>,) -> RequestUserInputQuestion {    let question = question_override        .map(ToString::to_string)        .unwrap_or_else(|| {            build_mcp_tool_approval_fallback_message(server, tool_name, connector_name)        });    let question = format!("{}?", question.trim_end_matches('?'));    let mut options = vec![RequestUserInputQuestionOption {        label: MCP_TOOL_APPROVAL_ACCEPT.to_string(),        description: "Run the tool and continue.".to_string(),    }];    if prompt_options.allow_session_remember {        options.push(RequestUserInputQuestionOption {            label: MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string(),            description: "Run the tool and remember this choice for this session.".to_string(),        });    }    if prompt_options.allow_persistent_approval {        options.push(RequestUserInputQuestionOption {            label: MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER.to_string(),            description: "Run the tool and remember this choice for future tool calls.".to_string(),        });    }    options.push(RequestUserInputQuestionOption {        label: MCP_TOOL_APPROVAL_CANCEL.to_string(),        description: "Cancel this tool call.".to_string(),    });    RequestUserInputQuestion {        id: question_id,        header: "Approve app tool call?".to_string(),        question,        is_other: false,        is_secret: false,        options: Some(options),    }}fn build_mcp_tool_approval_fallback_message(    server: &str,    tool_name: &str,    connector_name: Option<&str>,) -> String {    let actor = connector_name        .map(str::trim)        .filter(|name| !name.is_empty())        .map(ToString::to_string)        .unwrap_or_else(|| {            if server == CODEX_APPS_MCP_SERVER_NAME {                "this app".to_string()            } else {                format!("the {server} MCP server")            }        });    format!("Allow {actor} to run tool \"{tool_name}\"?")}fn build_mcp_tool_approval_elicitation_request(    sess: &Session,    turn_context: &TurnContext,    request: McpToolApprovalElicitationRequest<'_>,) -> McpServerElicitationRequestParams {    let message = request        .message_override        .map(ToString::to_string)        .unwrap_or_else(|| request.question.question.clone());    McpServerElicitationRequestParams {        thread_id: sess.thread_id.to_string(),        turn_id: Some(turn_context.sub_id.clone()),        server_name: request.server.to_string(),        request: McpServerElicitationRequest::Form {            meta: build_mcp_tool_approval_elicitation_meta(                request.server,                request.metadata,                request.tool_params,                request.tool_params_display,                request.prompt_options,            ),            message,            requested_schema: McpElicitationSchema {                schema_uri: None,                type_: McpElicitationObjectType::Object,                properties: BTreeMap::new(),                required: None,            },        },    }}fn build_mcp_tool_approval_elicitation_meta(    server: &str,    metadata: Option<&McpToolApprovalMetadata>,    tool_params: Option<&serde_json::Value>,    tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,    prompt_options: McpToolApprovalPromptOptions,) -> Option<serde_json::Value> {    let mut meta = serde_json::Map::new();    meta.insert(        MCP_TOOL_APPROVAL_KIND_KEY.to_string(),        serde_json::Value::String(MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL.to_string()),    );    match (        prompt_options.allow_session_remember,        prompt_options.allow_persistent_approval,    ) {        (true, true) => {            meta.insert(                MCP_TOOL_APPROVAL_PERSIST_KEY.to_string(),                serde_json::json!([                    MCP_TOOL_APPROVAL_PERSIST_SESSION,                    MCP_TOOL_APPROVAL_PERSIST_ALWAYS,                ]),            );        }        (true, false) => {            meta.insert(                MCP_TOOL_APPROVAL_PERSIST_KEY.to_string(),                serde_json::Value::String(MCP_TOOL_APPROVAL_PERSIST_SESSION.to_string()),            );        }        (false, true) => {            meta.insert(                MCP_TOOL_APPROVAL_PERSIST_KEY.to_string(),                serde_json::Value::String(MCP_TOOL_APPROVAL_PERSIST_ALWAYS.to_string()),            );        }        (false, false) => {}    }    if let Some(metadata) = metadata {        if let Some(tool_title) = metadata.tool_title.as_ref() {            meta.insert(                MCP_TOOL_APPROVAL_TOOL_TITLE_KEY.to_string(),                serde_json::Value::String(tool_title.clone()),            );        }        if let Some(tool_description) = metadata.tool_description.as_ref() {            meta.insert(                MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY.to_string(),                serde_json::Value::String(tool_description.clone()),            );        }        if server == CODEX_APPS_MCP_SERVER_NAME            && (metadata.connector_id.is_some()                || metadata.connector_name.is_some()                || metadata.connector_description.is_some())        {            meta.insert(                MCP_TOOL_APPROVAL_SOURCE_KEY.to_string(),                serde_json::Value::String(MCP_TOOL_APPROVAL_SOURCE_CONNECTOR.to_string()),            );            if let Some(connector_id) = metadata.connector_id.as_deref() {                meta.insert(                    MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY.to_string(),                    serde_json::Value::String(connector_id.to_string()),                );            }            if let Some(connector_name) = metadata.connector_name.as_ref() {                meta.insert(                    MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY.to_string(),                    serde_json::Value::String(connector_name.clone()),                );            }            if let Some(connector_description) = metadata.connector_description.as_ref() {                meta.insert(                    MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY.to_string(),                    serde_json::Value::String(connector_description.clone()),                );            }        }    }    if let Some(tool_params) = tool_params {        meta.insert(            MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY.to_string(),            tool_params.clone(),        );    }    if let Some(tool_params_display) = tool_params_display        && let Ok(tool_params_display) = serde_json::to_value(tool_params_display)    {        meta.insert(            MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY.to_string(),            tool_params_display,        );    }    (!meta.is_empty()).then_some(serde_json::Value::Object(meta))}fn build_mcp_tool_approval_display_params(    tool_params: Option<&serde_json::Value>,) -> Option<Vec<crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam>> {    let tool_params = tool_params?.as_object()?;    let mut display_params = tool_params        .iter()        .map(            |(name, value)| crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam {                name: name.clone(),                value: value.clone(),                display_name: name.clone(),            },        )        .collect::<Vec<_>>();    display_params.sort_by(|left, right| left.name.cmp(&right.name));    Some(display_params)}fn parse_mcp_tool_approval_elicitation_response(    response: Option<ElicitationResponse>,    question_id: &str,) -> McpToolApprovalDecision {    let Some(response) = response else {        return McpToolApprovalDecision::Cancel;    };    match response.action {        ElicitationAction::Accept => {            match response                .meta                .as_ref()                .and_then(serde_json::Value::as_object)                .and_then(|meta| meta.get(MCP_TOOL_APPROVAL_PERSIST_KEY))                .and_then(serde_json::Value::as_str)            {                Some(MCP_TOOL_APPROVAL_PERSIST_SESSION) => {                    return McpToolApprovalDecision::AcceptForSession;                }                Some(MCP_TOOL_APPROVAL_PERSIST_ALWAYS) => {                    return McpToolApprovalDecision::AcceptAndRemember;                }                _ => {}            }            match parse_mcp_tool_approval_response(                request_user_input_response_from_elicitation_content(response.content),                question_id,            ) {                McpToolApprovalDecision::Cancel => McpToolApprovalDecision::Accept,                decision => decision,            }        }        ElicitationAction::Decline => McpToolApprovalDecision::Decline { message: None },        ElicitationAction::Cancel => McpToolApprovalDecision::Cancel,    }}fn request_user_input_response_from_elicitation_content(    content: Option<serde_json::Value>,) -> Option<RequestUserInputResponse> {    let Some(content) = content else {        return Some(RequestUserInputResponse {            answers: std::collections::HashMap::new(),        });    };    let content = content.as_object()?;    let answers = content        .iter()        .filter_map(|(question_id, value)| {            let answers = match value {                serde_json::Value::String(answer) => vec![answer.clone()],                serde_json::Value::Array(values) => values                    .iter()                    .filter_map(|value| value.as_str().map(ToString::to_string))                    .collect(),                _ => return None,            };            Some((question_id.clone(), RequestUserInputAnswer { answers }))        })        .collect();    Some(RequestUserInputResponse { answers })}fn parse_mcp_tool_approval_response(    response: Option<RequestUserInputResponse>,    question_id: &str,) -> McpToolApprovalDecision {    let Some(response) = response else {        return McpToolApprovalDecision::Cancel;    };    let answers = response        .answers        .get(question_id)        .map(|answer| answer.answers.as_slice());    let Some(answers) = answers else {        return McpToolApprovalDecision::Cancel;    };    if answers        .iter()        .any(|answer| answer == MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC)    {        McpToolApprovalDecision::Decline { message: None }    } else if answers        .iter()        .any(|answer| answer == MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION)    {        McpToolApprovalDecision::AcceptForSession    } else if answers        .iter()        .any(|answer| answer == MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER)    {        McpToolApprovalDecision::AcceptAndRemember    } else if answers        .iter()        .any(|answer| answer == MCP_TOOL_APPROVAL_ACCEPT)    {        McpToolApprovalDecision::Accept    } else {        McpToolApprovalDecision::Cancel    }}fn normalize_approval_decision_for_mode(    decision: McpToolApprovalDecision,    approval_mode: AppToolApproval,) -> McpToolApprovalDecision {    if approval_mode == AppToolApproval::Prompt        && matches!(            decision,            McpToolApprovalDecision::AcceptForSession | McpToolApprovalDecision::AcceptAndRemember        )    {        McpToolApprovalDecision::Accept    } else {        decision    }}async fn mcp_tool_approval_is_remembered(sess: &Session, key: &McpToolApprovalKey) -> bool {    let store = sess.services.tool_approvals.lock().await;    matches!(store.get(key), Some(ReviewDecision::ApprovedForSession))}async fn remember_mcp_tool_approval(sess: &Session, key: McpToolApprovalKey) {    let mut store = sess.services.tool_approvals.lock().await;    store.put(key, ReviewDecision::ApprovedForSession);}async fn apply_mcp_tool_approval_decision(    sess: &Session,    turn_context: &TurnContext,    decision: &McpToolApprovalDecision,    session_approval_key: Option<McpToolApprovalKey>,    persistent_approval_key: Option<McpToolApprovalKey>,) {    match decision {        McpToolApprovalDecision::AcceptForSession => {            if let Some(key) = session_approval_key {                remember_mcp_tool_approval(sess, key).await;            }        }        McpToolApprovalDecision::AcceptAndRemember => {            if let Some(key) = persistent_approval_key {                maybe_persist_mcp_tool_approval(sess, turn_context, key).await;            } else if let Some(key) = session_approval_key {                remember_mcp_tool_approval(sess, key).await;            }        }        McpToolApprovalDecision::Accept        | McpToolApprovalDecision::Decline { .. }        | McpToolApprovalDecision::Cancel => {}    }}async fn maybe_persist_mcp_tool_approval(    sess: &Session,    turn_context: &TurnContext,    key: McpToolApprovalKey,) {    let tool_name = key.tool_name.clone();    let persist_result = if key.server == CODEX_APPS_MCP_SERVER_NAME {        let Some(connector_id) = key.connector_id.clone() else {            remember_mcp_tool_approval(sess, key).await;            return;        };        persist_codex_app_tool_approval(&turn_context.config, &connector_id, &tool_name).await    } else {        persist_non_app_mcp_tool_approval(sess, &turn_context.config, &key.server, &tool_name).await    };    if let Err(err) = persist_result {        error!(            error = %err,            server = key.server,            tool_name,            "failed to persist MCP tool approval"        );        remember_mcp_tool_approval(sess, key).await;        return;    }    sess.reload_user_config_layer().await;    remember_mcp_tool_approval(sess, key).await;}async fn persist_codex_app_tool_approval(    config: &Config,    connector_id: &str,    tool_name: &str,) -> anyhow::Result<()> {    ConfigEditsBuilder::for_config(config)        .with_edits([ConfigEdit::SetPath {            segments: vec![                "apps".to_string(),                connector_id.to_string(),                "tools".to_string(),                tool_name.to_string(),                "approval_mode".to_string(),            ],            value: value("approve"),        }])        .apply()        .await}#[cfg(test)]async fn persist_custom_mcp_tool_approval(    config: &Config,    server: &str,    tool_name: &str,) -> anyhow::Result<()> {    let Some(config_edits_builder) = custom_mcp_tool_approval_config_builder(config, server)?    else {        anyhow::bail!("MCP server `{server}` is not configured in config.toml");    };    persist_custom_mcp_tool_approval_with(config_edits_builder, server, tool_name).await}async fn persist_non_app_mcp_tool_approval(    sess: &Session,    config: &Config,    server: &str,    tool_name: &str,) -> anyhow::Result<()> {    if let Some(config_edits_builder) = custom_mcp_tool_approval_config_builder(config, server)? {        return persist_custom_mcp_tool_approval_with(config_edits_builder, server, tool_name)            .await;    }    let plugin_config_name = sess        .services        .plugins_manager        .plugins_for_config(&config.plugins_config_input())        .await        .plugins()        .iter()        .filter(|plugin| plugin.is_active())        .find(|plugin| plugin.mcp_servers.contains_key(server))        .map(|plugin| plugin.config_name.clone());    if let Some(plugin_config_name) = plugin_config_name {        return ConfigEditsBuilder::for_config(config)            .with_edits([ConfigEdit::SetPath {                segments: vec![                    "plugins".to_string(),                    plugin_config_name,                    "mcp_servers".to_string(),                    server.to_string(),                    "tools".to_string(),                    tool_name.to_string(),                    "approval_mode".to_string(),                ],                value: value("approve"),            }])            .apply()            .await;    }    anyhow::bail!("MCP server `{server}` is not configured in config.toml or an enabled plugin")}fn custom_mcp_tool_approval_config_builder(    config: &Config,    server: &str,) -> anyhow::Result<Option<ConfigEditsBuilder>> {    if let Some(project_config_folder) = project_mcp_tool_approval_config_folder(config, server) {        return Ok(Some(ConfigEditsBuilder::new(&project_config_folder)));    }    Ok(user_mcp_server_is_configured(config, server)?        .then(|| ConfigEditsBuilder::for_config(config)))}async fn persist_custom_mcp_tool_approval_with(    config_edits_builder: ConfigEditsBuilder,    server: &str,    tool_name: &str,) -> anyhow::Result<()> {    config_edits_builder        .with_edits([ConfigEdit::SetPath {            segments: vec![                "mcp_servers".to_string(),                server.to_string(),                "tools".to_string(),                tool_name.to_string(),                "approval_mode".to_string(),            ],            value: value("approve"),        }])        .apply()        .await}fn user_mcp_server_is_configured(config: &Config, server: &str) -> anyhow::Result<bool> {    let Some(mcp_servers_toml) = config        .config_layer_stack        .effective_user_config()        .as_ref()        .and_then(|user_config| user_config.get("mcp_servers"))        .cloned()    else {        return Ok(false);    };    let servers =        HashMap::<String, codex_config::types::McpServerConfig>::deserialize(mcp_servers_toml)?;    Ok(servers.contains_key(server))}fn project_mcp_tool_approval_config_folder(    config: &Config,    server: &str,) -> Option<AbsolutePathBuf> {    config        .config_layer_stack        .layers_high_to_low()        .into_iter()        .find_map(|layer| {            if !matches!(layer.name, ConfigLayerSource::Project { .. }) {                return None;            }            let servers = layer                .config                .as_table()                .and_then(|table| table.get("mcp_servers"))                .cloned()                .and_then(|value| {                    HashMap::<String, codex_config::types::McpServerConfig>::deserialize(value).ok()                })?;            if servers.contains_key(server) {                layer.config_folder()            } else {                None            }        })}fn requires_mcp_tool_approval(annotations: Option<&ToolAnnotations>) -> bool {    let destructive_hint = annotations.and_then(|annotations| annotations.destructive_hint);    if destructive_hint == Some(true) {        return true;    }    let read_only_hint = annotations        .and_then(|annotations| annotations.read_only_hint)        .unwrap_or(false);    if read_only_hint {        return false;    }    destructive_hint.unwrap_or(true)        || annotations            .and_then(|annotations| annotations.open_world_hint)            .unwrap_or(true)}async fn notify_mcp_tool_call_skip(    sess: &Session,    turn_context: &TurnContext,    call_id: &str,    invocation: McpInvocation,    item_metadata: McpToolCallItemMetadata,    message: String,    already_started: bool,) -> Result<CallToolResult, String> {    if !already_started {        notify_mcp_tool_call_started(            sess,            turn_context,            call_id,            invocation.clone(),            item_metadata.clone(),        )        .await;    }    notify_mcp_tool_call_completed(        sess,        turn_context,        call_id,        invocation,        item_metadata,        Duration::ZERO,        truncate_mcp_tool_result_for_event(&Err(message.clone())),    )    .await;    Err(message)}#[cfg(test)]#[path = "mcp_tool_call_tests.rs"]mod tests;