Codex Handbook
core/src/mcp_tool_approval_templates.rs 371 lines
use std::collections::HashSet;use std::sync::LazyLock;use serde::Deserialize;use serde::Serialize;use serde_json::Map;use serde_json::Value;use tracing::warn;const CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION: u8 = 4;const CONNECTOR_NAME_TEMPLATE_VAR: &str = "{connector_name}";static CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES: LazyLock<    Option<Vec<ConsequentialToolMessageTemplate>>,> = LazyLock::new(load_consequential_tool_message_templates);#[derive(Clone, Debug, PartialEq)]pub(crate) struct RenderedMcpToolApprovalTemplate {    pub(crate) question: String,    pub(crate) elicitation_message: String,    pub(crate) tool_params: Option<Value>,    pub(crate) tool_params_display: Vec<RenderedMcpToolApprovalParam>,}#[derive(Clone, Debug, PartialEq, Serialize)]pub(crate) struct RenderedMcpToolApprovalParam {    pub(crate) name: String,    pub(crate) value: Value,    pub(crate) display_name: String,}#[derive(Debug, Deserialize)]struct ConsequentialToolMessageTemplatesFile {    schema_version: u8,    templates: Vec<ConsequentialToolMessageTemplate>,}#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]struct ConsequentialToolMessageTemplate {    connector_id: String,    server_name: String,    tool_title: String,    template: String,    template_params: Vec<ConsequentialToolTemplateParam>,}#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]struct ConsequentialToolTemplateParam {    name: String,    label: String,}pub(crate) fn render_mcp_tool_approval_template(    server_name: &str,    connector_id: Option<&str>,    connector_name: Option<&str>,    tool_title: Option<&str>,    tool_params: Option<&Value>,) -> Option<RenderedMcpToolApprovalTemplate> {    let templates = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.as_ref()?;    render_mcp_tool_approval_template_from_templates(        templates,        server_name,        connector_id,        connector_name,        tool_title,        tool_params,    )}fn load_consequential_tool_message_templates() -> Option<Vec<ConsequentialToolMessageTemplate>> {    let templates = match serde_json::from_str::<ConsequentialToolMessageTemplatesFile>(        include_str!("consequential_tool_message_templates.json"),    ) {        Ok(templates) => templates,        Err(err) => {            warn!(error = %err, "failed to parse consequential tool approval templates");            return None;        }    };    if templates.schema_version != CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION {        warn!(            found_schema_version = templates.schema_version,            expected_schema_version = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION,            "unexpected consequential tool approval templates schema version"        );        return None;    }    Some(templates.templates)}fn render_mcp_tool_approval_template_from_templates(    templates: &[ConsequentialToolMessageTemplate],    server_name: &str,    connector_id: Option<&str>,    connector_name: Option<&str>,    tool_title: Option<&str>,    tool_params: Option<&Value>,) -> Option<RenderedMcpToolApprovalTemplate> {    let connector_id = connector_id?;    let tool_title = tool_title.map(str::trim).filter(|name| !name.is_empty())?;    let template = templates.iter().find(|template| {        template.server_name == server_name            && template.connector_id == connector_id            && template.tool_title == tool_title    })?;    let elicitation_message = render_question_template(&template.template, connector_name)?;    let (tool_params, tool_params_display) = match tool_params {        Some(Value::Object(tool_params)) => {            render_tool_params(tool_params, &template.template_params)?        }        Some(_) => return None,        None => (None, Vec::new()),    };    Some(RenderedMcpToolApprovalTemplate {        question: elicitation_message.clone(),        elicitation_message,        tool_params,        tool_params_display,    })}fn render_question_template(template: &str, connector_name: Option<&str>) -> Option<String> {    let template = template.trim();    if template.is_empty() {        return None;    }    if template.contains(CONNECTOR_NAME_TEMPLATE_VAR) {        let connector_name = connector_name            .map(str::trim)            .filter(|name| !name.is_empty())?;        return Some(template.replace(CONNECTOR_NAME_TEMPLATE_VAR, connector_name));    }    Some(template.to_string())}fn render_tool_params(    tool_params: &Map<String, Value>,    template_params: &[ConsequentialToolTemplateParam],) -> Option<(Option<Value>, Vec<RenderedMcpToolApprovalParam>)> {    let mut display_params = Vec::new();    let mut display_names = HashSet::new();    let mut handled_names = HashSet::new();    for template_param in template_params {        let label = template_param.label.trim();        if label.is_empty() {            return None;        }        let Some(value) = tool_params.get(&template_param.name) else {            continue;        };        if !display_names.insert(label.to_string()) {            return None;        }        display_params.push(RenderedMcpToolApprovalParam {            name: template_param.name.clone(),            value: value.clone(),            display_name: label.to_string(),        });        handled_names.insert(template_param.name.as_str());    }    let mut remaining_params = tool_params        .iter()        .filter(|(name, _)| !handled_names.contains(name.as_str()))        .collect::<Vec<_>>();    remaining_params.sort_by_key(|(name, _)| *name);    for (name, value) in remaining_params {        if handled_names.contains(name.as_str()) {            continue;        }        if !display_names.insert(name.clone()) {            return None;        }        display_params.push(RenderedMcpToolApprovalParam {            name: name.clone(),            value: value.clone(),            display_name: name.clone(),        });    }    Some((Some(Value::Object(tool_params.clone())), display_params))}#[cfg(test)]mod tests {    use pretty_assertions::assert_eq;    use serde_json::json;    use super::*;    #[test]    fn renders_exact_match_with_readable_param_labels() {        let templates = vec![ConsequentialToolMessageTemplate {            connector_id: "calendar".to_string(),            server_name: "codex_apps".to_string(),            tool_title: "create_event".to_string(),            template: "Allow {connector_name} to create an event?".to_string(),            template_params: vec![                ConsequentialToolTemplateParam {                    name: "calendar_id".to_string(),                    label: "Calendar".to_string(),                },                ConsequentialToolTemplateParam {                    name: "title".to_string(),                    label: "Title".to_string(),                },            ],        }];        let rendered = render_mcp_tool_approval_template_from_templates(            &templates,            "codex_apps",            Some("calendar"),            Some("Calendar"),            Some("create_event"),            Some(&json!({                "title": "Roadmap review",                "calendar_id": "primary",                "timezone": "UTC",            })),        );        assert_eq!(            rendered,            Some(RenderedMcpToolApprovalTemplate {                question: "Allow Calendar to create an event?".to_string(),                elicitation_message: "Allow Calendar to create an event?".to_string(),                tool_params: Some(json!({                    "title": "Roadmap review",                    "calendar_id": "primary",                    "timezone": "UTC",                })),                tool_params_display: vec![                    RenderedMcpToolApprovalParam {                        name: "calendar_id".to_string(),                        value: json!("primary"),                        display_name: "Calendar".to_string(),                    },                    RenderedMcpToolApprovalParam {                        name: "title".to_string(),                        value: json!("Roadmap review"),                        display_name: "Title".to_string(),                    },                    RenderedMcpToolApprovalParam {                        name: "timezone".to_string(),                        value: json!("UTC"),                        display_name: "timezone".to_string(),                    },                ],            })        );    }    #[test]    fn returns_none_when_no_exact_match_exists() {        let templates = vec![ConsequentialToolMessageTemplate {            connector_id: "calendar".to_string(),            server_name: "codex_apps".to_string(),            tool_title: "create_event".to_string(),            template: "Allow {connector_name} to create an event?".to_string(),            template_params: Vec::new(),        }];        assert_eq!(            render_mcp_tool_approval_template_from_templates(                &templates,                "codex_apps",                Some("calendar"),                Some("Calendar"),                Some("delete_event"),                Some(&json!({})),            ),            None        );    }    #[test]    fn returns_none_when_relabeling_would_collide() {        let templates = vec![ConsequentialToolMessageTemplate {            connector_id: "calendar".to_string(),            server_name: "codex_apps".to_string(),            tool_title: "create_event".to_string(),            template: "Allow {connector_name} to create an event?".to_string(),            template_params: vec![ConsequentialToolTemplateParam {                name: "calendar_id".to_string(),                label: "timezone".to_string(),            }],        }];        assert_eq!(            render_mcp_tool_approval_template_from_templates(                &templates,                "codex_apps",                Some("calendar"),                Some("Calendar"),                Some("create_event"),                Some(&json!({                    "calendar_id": "primary",                    "timezone": "UTC",                })),            ),            None        );    }    #[test]    fn bundled_templates_load() {        assert_eq!(CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.is_some(), true);    }    #[test]    fn renders_literal_template_without_connector_substitution() {        let templates = vec![ConsequentialToolMessageTemplate {            connector_id: "github".to_string(),            server_name: "codex_apps".to_string(),            tool_title: "add_comment".to_string(),            template: "Allow GitHub to add a comment to a pull request?".to_string(),            template_params: Vec::new(),        }];        let rendered = render_mcp_tool_approval_template_from_templates(            &templates,            "codex_apps",            Some("github"),            /*connector_name*/ None,            Some("add_comment"),            Some(&json!({})),        );        assert_eq!(            rendered,            Some(RenderedMcpToolApprovalTemplate {                question: "Allow GitHub to add a comment to a pull request?".to_string(),                elicitation_message: "Allow GitHub to add a comment to a pull request?".to_string(),                tool_params: Some(json!({})),                tool_params_display: Vec::new(),            })        );    }    #[test]    fn returns_none_when_connector_placeholder_has_no_value() {        let templates = vec![ConsequentialToolMessageTemplate {            connector_id: "calendar".to_string(),            server_name: "codex_apps".to_string(),            tool_title: "create_event".to_string(),            template: "Allow {connector_name} to create an event?".to_string(),            template_params: Vec::new(),        }];        assert_eq!(            render_mcp_tool_approval_template_from_templates(                &templates,                "codex_apps",                Some("calendar"),                /*connector_name*/ None,                Some("create_event"),                Some(&json!({})),            ),            None        );    }}