move MCP approval prompts back to MCP

This commit is contained in:
Abhinav Vedmala
2026-05-02 00:31:09 -07:00
parent ad605dc791
commit f06870f376
6 changed files with 500 additions and 569 deletions
+3 -2
View File
@@ -32,12 +32,13 @@ use tokio_util::sync::CancellationToken;
use crate::config::Config;
use crate::environment_selection::ResolvedTurnEnvironments;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::is_mcp_tool_approval_question_id;
use crate::guardian::new_guardian_review_id;
use crate::guardian::routes_approval_to_guardian;
use crate::guardian::spawn_approval_request_review;
use crate::mcp_tool_call::build_mcp_tool_approval_request;
use crate::mcp_tool_call::is_mcp_tool_approval_question_id;
use crate::mcp_tool_call::lookup_mcp_tool_metadata;
use crate::mcp_tool_call::mcp_tool_approval_compat_response;
use crate::session::Codex;
use crate::session::CodexSpawnArgs;
use crate::session::CodexSpawnOk;
@@ -707,7 +708,7 @@ async fn maybe_auto_review_mcp_request_user_input(
Some(&review_cancel),
)
.await;
approval_request.mcp_tool_approval_compat_response(question, decision)
mcp_tool_approval_compat_response(&approval_request, question, decision)
}
async fn handle_request_permissions(
+2 -2
View File
@@ -1,6 +1,6 @@
use super::*;
use crate::guardian::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
use crate::guardian::MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX;
use async_channel::bounded;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::NetworkPermissions;
+15 -438
View File
@@ -16,18 +16,12 @@ use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsEvent;
use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Serialize;
use serde_json::Value;
use super::GUARDIAN_MAX_ACTION_STRING_TOKENS;
use super::prompt::guardian_truncate_text;
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
use crate::tools::hook_names::HookToolName;
use crate::tools::sandboxing::PermissionRequestPayload;
@@ -105,41 +99,6 @@ pub(crate) enum GuardianApprovalRequest {
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct McpToolApprovalPromptOptions {
pub(crate) allow_session_remember: bool,
pub(crate) allow_persistent_approval: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct McpToolApprovalPrompt {
pub(crate) question: RequestUserInputQuestion,
pub(crate) message_override: Option<String>,
pub(crate) tool_params: Option<Value>,
pub(crate) tool_params_display: Option<Vec<RenderedMcpToolApprovalParam>>,
}
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT: &str = "Allow";
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION: &str = "Allow for this session";
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER: &str = "Allow and don't ask me again";
pub(crate) const MCP_TOOL_APPROVAL_CANCEL: &str = "Cancel";
pub(crate) const MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC: &str = "__codex_mcp_decline__";
pub(crate) const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX: &str = "mcp_tool_call_approval";
pub(crate) const MCP_TOOL_APPROVAL_KIND_KEY: &str = "codex_approval_kind";
pub(crate) const MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL: &str = "mcp_tool_call";
pub(crate) const MCP_TOOL_APPROVAL_PERSIST_KEY: &str = "persist";
pub(crate) const MCP_TOOL_APPROVAL_PERSIST_SESSION: &str = "session";
pub(crate) const MCP_TOOL_APPROVAL_PERSIST_ALWAYS: &str = "always";
pub(crate) const MCP_TOOL_APPROVAL_SOURCE_KEY: &str = "source";
pub(crate) const MCP_TOOL_APPROVAL_SOURCE_CONNECTOR: &str = "connector";
pub(crate) const MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: &str = "connector_id";
pub(crate) const MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: &str = "connector_name";
pub(crate) const MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: &str = "connector_description";
pub(crate) const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: &str = "tool_title";
pub(crate) const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: &str = "tool_description";
pub(crate) const MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
pub(crate) const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
impl GuardianApprovalRequest {
pub(crate) fn permission_request_payload(&self) -> Option<PermissionRequestPayload> {
match self {
@@ -350,311 +309,6 @@ impl GuardianApprovalRequest {
Self::Execve { .. } => None,
}
}
pub(crate) fn mcp_tool_approval_prompt(
&self,
question_id: String,
prompt_options: McpToolApprovalPromptOptions,
monitor_reason: Option<&str>,
) -> Option<McpToolApprovalPrompt> {
let Self::McpToolCall {
server,
tool_name,
arguments,
connector_id,
connector_name,
tool_title,
..
} = self
else {
return None;
};
let rendered_template = render_mcp_tool_approval_template(
server,
connector_id.as_deref(),
connector_name.as_deref(),
tool_title.as_deref(),
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(arguments.as_ref()));
let question_override = rendered_template
.as_ref()
.map(|rendered_template| rendered_template.question.as_str());
let mut question = build_mcp_tool_approval_question(
question_id,
server,
tool_name,
connector_name.as_deref(),
prompt_options,
question_override,
);
question.question = mcp_tool_approval_question_text(question.question, monitor_reason);
Some(McpToolApprovalPrompt {
question,
message_override: rendered_template.as_ref().and_then(|rendered_template| {
monitor_reason
.is_none()
.then_some(rendered_template.elicitation_message.clone())
}),
tool_params: rendered_template
.as_ref()
.and_then(|rendered_template| rendered_template.tool_params.clone())
.or_else(|| arguments.clone()),
tool_params_display,
})
}
pub(crate) fn mcp_tool_approval_elicitation_meta(
&self,
tool_params: Option<&Value>,
tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,
prompt_options: McpToolApprovalPromptOptions,
) -> Option<Value> {
let Self::McpToolCall {
server,
connector_id,
connector_name,
connector_description,
tool_title,
tool_description,
..
} = self
else {
return None;
};
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(tool_title) = 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) = 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"
&& (connector_id.is_some()
|| connector_name.is_some()
|| 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) = 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) = 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) = 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))
}
pub(crate) fn mcp_tool_approval_compat_response(
&self,
question: &RequestUserInputQuestion,
decision: ReviewDecision,
) -> Option<RequestUserInputResponse> {
let Self::McpToolCall { .. } = self else {
return None;
};
let selected_label = match decision {
ReviewDecision::ApprovedForSession => question
.options
.as_ref()
.and_then(|options| {
options
.iter()
.find(|option| option.label == MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION)
})
.map(|option| option.label.clone())
.unwrap_or_else(|| MCP_TOOL_APPROVAL_ACCEPT.to_string()),
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => {
MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()
}
};
Some(RequestUserInputResponse {
answers: HashMap::from([(
question.id.clone(),
RequestUserInputAnswer {
answers: vec![selected_label],
},
)]),
})
}
}
pub(crate) fn mcp_tool_approval_question_id(call_id: &str) -> String {
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}")
}
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('_'))
}
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" {
"this app".to_string()
} else {
format!("the {server} MCP server")
}
});
format!("Allow {actor} to run tool \"{tool_name}\"?")
}
fn mcp_tool_approval_question_text(question: String, monitor_reason: Option<&str>) -> String {
match monitor_reason.map(str::trim) {
Some(reason) if !reason.is_empty() => {
format!("Tool call needs your approval. Reason: {reason}")
}
_ => question,
}
}
fn build_mcp_tool_approval_display_params(
tool_params: Option<&Value>,
) -> Option<Vec<RenderedMcpToolApprovalParam>> {
let tool_params = tool_params?.as_object()?;
let mut display_params = tool_params
.iter()
.map(|(name, value)| 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)
}
#[derive(Debug, Clone, PartialEq, Serialize)]
@@ -959,7 +613,9 @@ pub(crate) fn guardian_approval_request_to_json(
}
}
pub(crate) fn guardian_assessment_action(action: &GuardianApprovalRequest) -> GuardianAssessmentAction {
pub(crate) fn guardian_assessment_action(
action: &GuardianApprovalRequest,
) -> GuardianAssessmentAction {
match action {
GuardianApprovalRequest::Shell { command, cwd, .. } => {
command_assessment_action(GuardianCommandSource::Shell, command, cwd)
@@ -980,10 +636,12 @@ pub(crate) fn guardian_assessment_action(action: &GuardianApprovalRequest) -> Gu
argv: argv.clone(),
cwd: cwd.clone(),
},
GuardianApprovalRequest::ApplyPatch { cwd, files, .. } => GuardianAssessmentAction::ApplyPatch {
cwd: cwd.clone(),
files: files.clone(),
},
GuardianApprovalRequest::ApplyPatch { cwd, files, .. } => {
GuardianAssessmentAction::ApplyPatch {
cwd: cwd.clone(),
files: files.clone(),
}
}
GuardianApprovalRequest::NetworkAccess {
id: _id,
turn_id: _turn_id,
@@ -1024,7 +682,9 @@ pub(crate) fn guardian_assessment_action(action: &GuardianApprovalRequest) -> Gu
}
}
pub(crate) fn guardian_reviewed_action(request: &GuardianApprovalRequest) -> GuardianReviewedAction {
pub(crate) fn guardian_reviewed_action(
request: &GuardianApprovalRequest,
) -> GuardianReviewedAction {
match request {
GuardianApprovalRequest::Shell {
sandbox_permissions,
@@ -1076,7 +736,9 @@ pub(crate) fn guardian_reviewed_action(request: &GuardianApprovalRequest) -> Gua
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
GuardianApprovalRequest::RequestPermissions { .. } => GuardianReviewedAction::RequestPermissions {},
GuardianApprovalRequest::RequestPermissions { .. } => {
GuardianReviewedAction::RequestPermissions {}
}
}
}
@@ -1124,7 +786,6 @@ pub(crate) fn format_guardian_action_pretty(
mod tests {
use super::*;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::ReviewDecision;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
@@ -1296,88 +957,4 @@ mod tests {
])
);
}
#[test]
fn mcp_tool_approval_compat_response_uses_session_label_when_present() {
let request = GuardianApprovalRequest::McpToolCall {
id: "call-1".to_string(),
server: "custom_server".to_string(),
tool_name: "dangerous_tool".to_string(),
hook_tool_name: "custom_server__dangerous_tool".to_string(),
arguments: None,
connector_id: None,
connector_name: None,
connector_description: None,
tool_title: None,
tool_description: None,
annotations: None,
};
let question = RequestUserInputQuestion {
id: "q-1".to_string(),
header: "Approve app tool call?".to_string(),
question: "Allow this app tool?".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![RequestUserInputQuestionOption {
label: MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string(),
description: "Remember until session ends".to_string(),
}]),
};
let response = request
.mcp_tool_approval_compat_response(&question, ReviewDecision::ApprovedForSession)
.expect("compat response");
assert_eq!(
response.answers.get("q-1"),
Some(&RequestUserInputAnswer {
answers: vec![MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string()],
})
);
}
#[test]
fn mcp_tool_approval_compat_response_uses_synthetic_decline_for_abort() {
let request = GuardianApprovalRequest::McpToolCall {
id: "call-1".to_string(),
server: "custom_server".to_string(),
tool_name: "dangerous_tool".to_string(),
hook_tool_name: "custom_server__dangerous_tool".to_string(),
arguments: None,
connector_id: None,
connector_name: None,
connector_description: None,
tool_title: None,
tool_description: None,
annotations: None,
};
let question = RequestUserInputQuestion {
id: "q-1".to_string(),
header: "Approve app tool call?".to_string(),
question: "Allow this app tool?".to_string(),
is_other: false,
is_secret: false,
options: None,
};
let response = request
.mcp_tool_approval_compat_response(&question, ReviewDecision::Abort)
.expect("compat response");
assert_eq!(
response.answers.get("q-1"),
Some(&RequestUserInputAnswer {
answers: vec![MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()],
})
);
}
#[test]
fn mcp_tool_approval_question_id_helpers_round_trip() {
let question_id = mcp_tool_approval_question_id("call-1");
assert_eq!(question_id, "mcp_tool_call_approval_call-1");
assert!(is_mcp_tool_approval_question_id(&question_id));
assert!(!is_mcp_tool_approval_question_id("other_question"));
}
}
-43
View File
@@ -26,50 +26,7 @@ use serde::Serialize;
pub(crate) use approval_request::GuardianApprovalRequest;
pub(crate) use approval_request::GuardianMcpAnnotations;
pub(crate) use approval_request::GuardianNetworkAccessTrigger;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_ACCEPT;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_CANCEL;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_KIND_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_PERSIST_ALWAYS;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_PERSIST_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_PERSIST_SESSION;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_SOURCE_CONNECTOR;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_SOURCE_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY;
#[allow(unused_imports)]
pub(crate) use approval_request::MCP_TOOL_APPROVAL_TOOL_TITLE_KEY;
pub(crate) use approval_request::McpToolApprovalPromptOptions;
pub(crate) use approval_request::guardian_approval_request_to_json;
pub(crate) use approval_request::is_mcp_tool_approval_question_id;
pub(crate) use approval_request::mcp_tool_approval_question_id;
pub(crate) use review::guardian_rejection_message;
pub(crate) use review::guardian_timeout_message;
pub(crate) use review::is_guardian_reviewer_source;
+348 -11
View File
@@ -19,24 +19,16 @@ use crate::config::load_global_mcp_servers;
use crate::connectors;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::GuardianMcpAnnotations;
use crate::guardian::MCP_TOOL_APPROVAL_ACCEPT;
use crate::guardian::MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER;
use crate::guardian::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
use crate::guardian::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
use crate::guardian::MCP_TOOL_APPROVAL_PERSIST_ALWAYS;
use crate::guardian::MCP_TOOL_APPROVAL_PERSIST_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_PERSIST_SESSION;
use crate::guardian::McpToolApprovalPromptOptions;
use crate::guardian::guardian_approval_request_to_json;
use crate::guardian::guardian_rejection_message;
use crate::guardian::guardian_timeout_message;
use crate::guardian::mcp_tool_approval_question_id;
use crate::guardian::new_guardian_review_id;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
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 codex_analytics::AppInvocation;
@@ -61,6 +53,7 @@ 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;
@@ -882,6 +875,45 @@ fn with_mcp_tool_call_thread_id_meta(
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct McpToolApprovalPromptOptions {
allow_session_remember: bool,
allow_persistent_approval: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct McpToolApprovalPrompt {
question: RequestUserInputQuestion,
message_override: Option<String>,
tool_params: Option<JsonValue>,
tool_params_display: Option<Vec<RenderedMcpToolApprovalParam>>,
}
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT: &str = "Allow";
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION: &str = "Allow for this session";
const MCP_TOOL_APPROVAL_ACCEPT_AND_REMEMBER: &str = "Allow and don't ask me again";
const MCP_TOOL_APPROVAL_CANCEL: &str = "Cancel";
// 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__";
pub(crate) const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX: &str = "mcp_tool_call_approval";
const MCP_TOOL_APPROVAL_KIND_KEY: &str = "codex_approval_kind";
const MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL: &str = "mcp_tool_call";
pub(crate) const MCP_TOOL_APPROVAL_PERSIST_KEY: &str = "persist";
pub(crate) const MCP_TOOL_APPROVAL_PERSIST_SESSION: &str = "session";
pub(crate) const MCP_TOOL_APPROVAL_PERSIST_ALWAYS: &str = "always";
const MCP_TOOL_APPROVAL_SOURCE_KEY: &str = "source";
const MCP_TOOL_APPROVAL_SOURCE_CONNECTOR: &str = "connector";
const MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: &str = "connector_id";
const MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: &str = "connector_name";
const MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: &str = "connector_description";
const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: &str = "tool_title";
const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: &str = "tool_description";
const MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
struct McpToolApprovalElicitationRequest<'a> {
server: &'a str,
approval_request: &'a GuardianApprovalRequest,
@@ -902,6 +934,309 @@ struct McpToolApprovalKey {
tool_name: String,
}
pub(crate) fn mcp_tool_approval_question_id(call_id: &str) -> String {
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}")
}
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('_'))
}
fn mcp_tool_approval_prompt(
approval_request: &GuardianApprovalRequest,
question_id: String,
prompt_options: McpToolApprovalPromptOptions,
monitor_reason: Option<&str>,
) -> Option<McpToolApprovalPrompt> {
let GuardianApprovalRequest::McpToolCall {
server,
tool_name,
arguments,
connector_id,
connector_name,
tool_title,
..
} = approval_request
else {
return None;
};
let rendered_template = render_mcp_tool_approval_template(
server,
connector_id.as_deref(),
connector_name.as_deref(),
tool_title.as_deref(),
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(arguments.as_ref()));
let question_override = rendered_template
.as_ref()
.map(|rendered_template| rendered_template.question.as_str());
let mut question = build_mcp_tool_approval_question(
question_id,
server,
tool_name,
connector_name.as_deref(),
prompt_options,
question_override,
);
question.question = mcp_tool_approval_question_text(question.question, monitor_reason);
Some(McpToolApprovalPrompt {
question,
message_override: rendered_template.as_ref().and_then(|rendered_template| {
monitor_reason
.is_none()
.then_some(rendered_template.elicitation_message.clone())
}),
tool_params: rendered_template
.as_ref()
.and_then(|rendered_template| rendered_template.tool_params.clone())
.or_else(|| arguments.clone()),
tool_params_display,
})
}
fn mcp_tool_approval_elicitation_meta(
approval_request: &GuardianApprovalRequest,
tool_params: Option<&JsonValue>,
tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,
prompt_options: McpToolApprovalPromptOptions,
) -> Option<JsonValue> {
let GuardianApprovalRequest::McpToolCall {
server,
connector_id,
connector_name,
connector_description,
tool_title,
tool_description,
..
} = approval_request
else {
return None;
};
let mut meta = serde_json::Map::new();
meta.insert(
MCP_TOOL_APPROVAL_KIND_KEY.to_string(),
JsonValue::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(),
JsonValue::String(MCP_TOOL_APPROVAL_PERSIST_SESSION.to_string()),
);
}
(false, true) => {
meta.insert(
MCP_TOOL_APPROVAL_PERSIST_KEY.to_string(),
JsonValue::String(MCP_TOOL_APPROVAL_PERSIST_ALWAYS.to_string()),
);
}
(false, false) => {}
}
if let Some(tool_title) = tool_title.as_ref() {
meta.insert(
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY.to_string(),
JsonValue::String(tool_title.clone()),
);
}
if let Some(tool_description) = tool_description.as_ref() {
meta.insert(
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY.to_string(),
JsonValue::String(tool_description.clone()),
);
}
if server == "codex_apps"
&& (connector_id.is_some() || connector_name.is_some() || connector_description.is_some())
{
meta.insert(
MCP_TOOL_APPROVAL_SOURCE_KEY.to_string(),
JsonValue::String(MCP_TOOL_APPROVAL_SOURCE_CONNECTOR.to_string()),
);
if let Some(connector_id) = connector_id.as_deref() {
meta.insert(
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY.to_string(),
JsonValue::String(connector_id.to_string()),
);
}
if let Some(connector_name) = connector_name.as_ref() {
meta.insert(
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY.to_string(),
JsonValue::String(connector_name.clone()),
);
}
if let Some(connector_description) = connector_description.as_ref() {
meta.insert(
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY.to_string(),
JsonValue::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(JsonValue::Object(meta))
}
pub(crate) fn mcp_tool_approval_compat_response(
approval_request: &GuardianApprovalRequest,
question: &RequestUserInputQuestion,
decision: ReviewDecision,
) -> Option<RequestUserInputResponse> {
let GuardianApprovalRequest::McpToolCall { .. } = approval_request else {
return None;
};
let selected_label = match decision {
ReviewDecision::ApprovedForSession => question
.options
.as_ref()
.and_then(|options| {
options
.iter()
.find(|option| option.label == MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION)
})
.map(|option| option.label.clone())
.unwrap_or_else(|| MCP_TOOL_APPROVAL_ACCEPT.to_string()),
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => {
MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()
}
};
Some(RequestUserInputResponse {
answers: HashMap::from([(
question.id.clone(),
RequestUserInputAnswer {
answers: vec![selected_label],
},
)]),
})
}
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" {
"this app".to_string()
} else {
format!("the {server} MCP server")
}
});
format!("Allow {actor} to run tool \\\"{tool_name}\\\"?")
}
fn mcp_tool_approval_question_text(question: String, monitor_reason: Option<&str>) -> String {
match monitor_reason.map(str::trim) {
Some(reason) if !reason.is_empty() => {
format!("Tool call needs your approval. Reason: {reason}")
}
_ => question,
}
}
fn build_mcp_tool_approval_display_params(
tool_params: Option<&JsonValue>,
) -> Option<Vec<RenderedMcpToolApprovalParam>> {
let tool_params = tool_params?.as_object()?;
let mut display_params = tool_params
.iter()
.map(|(name, value)| 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 mcp_tool_approval_prompt_options(
session_approval_key: Option<&McpToolApprovalKey>,
persistent_approval_key: Option<&McpToolApprovalKey>,
@@ -1029,7 +1364,8 @@ async fn maybe_request_mcp_tool_approval(
tool_call_mcp_elicitation_enabled,
);
let question_id = mcp_tool_approval_question_id(call_id);
let Some(prompt) = approval_request.mcp_tool_approval_prompt(
let Some(prompt) = mcp_tool_approval_prompt(
&approval_request,
question_id.clone(),
prompt_options,
monitor_reason.as_deref(),
@@ -1358,7 +1694,8 @@ fn build_mcp_tool_approval_elicitation_request(
turn_id: Some(turn_context.sub_id.clone()),
server_name: request.server.to_string(),
request: McpServerElicitationRequest::Form {
meta: request.approval_request.mcp_tool_approval_elicitation_meta(
meta: mcp_tool_approval_elicitation_meta(
request.approval_request,
request.tool_params,
request.tool_params_display,
request.prompt_options,
+132 -73
View File
@@ -2,19 +2,6 @@ use super::*;
use crate::config::ConfigBuilder;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::GuardianMcpAnnotations;
use crate::guardian::MCP_TOOL_APPROVAL_CANCEL;
use crate::guardian::MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
use crate::guardian::MCP_TOOL_APPROVAL_KIND_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL;
use crate::guardian::MCP_TOOL_APPROVAL_SOURCE_CONNECTOR;
use crate::guardian::MCP_TOOL_APPROVAL_SOURCE_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY;
use crate::guardian::MCP_TOOL_APPROVAL_TOOL_TITLE_KEY;
use crate::session::tests::make_session_and_context;
use crate::session::tests::make_session_and_context_with_rx;
use crate::state::ActiveTurn;
@@ -320,22 +307,90 @@ fn approval_question_text_prepends_safety_reason() {
/*metadata*/ None,
);
assert_eq!(
request
.mcp_tool_approval_prompt(
"q".to_string(),
prompt_options(
/*allow_session_remember*/ false,
/*allow_persistent_approval*/ false,
),
Some("This tool may contact an external system."),
)
.expect("mcp approval prompt")
.question
.question,
mcp_tool_approval_prompt(
&request,
"q".to_string(),
prompt_options(
/*allow_session_remember*/ false, /*allow_persistent_approval*/ false,
),
Some("This tool may contact an external system."),
)
.expect("mcp approval prompt")
.question
.question,
"Tool call needs your approval. Reason: This tool may contact an external system."
);
}
#[test]
fn mcp_tool_approval_compat_response_uses_session_label_when_present() {
let request = approval_prompt_request(
"custom_server",
"dangerous_tool",
/*arguments*/ None,
/*metadata*/ None,
);
let question = RequestUserInputQuestion {
id: "q-1".to_string(),
header: "Approve app tool call?".to_string(),
question: "Allow this app tool?".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![RequestUserInputQuestionOption {
label: MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string(),
description: "Remember until session ends".to_string(),
}]),
};
let response =
mcp_tool_approval_compat_response(&request, &question, ReviewDecision::ApprovedForSession)
.expect("compat response");
assert_eq!(
response.answers.get("q-1"),
Some(&RequestUserInputAnswer {
answers: vec![MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string()],
})
);
}
#[test]
fn mcp_tool_approval_compat_response_uses_synthetic_decline_for_abort() {
let request = approval_prompt_request(
"custom_server",
"dangerous_tool",
/*arguments*/ None,
/*metadata*/ None,
);
let question = RequestUserInputQuestion {
id: "q-1".to_string(),
header: "Approve app tool call?".to_string(),
question: "Allow this app tool?".to_string(),
is_other: false,
is_secret: false,
options: None,
};
let response = mcp_tool_approval_compat_response(&request, &question, ReviewDecision::Abort)
.expect("compat response");
assert_eq!(
response.answers.get("q-1"),
Some(&RequestUserInputAnswer {
answers: vec![MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()],
})
);
}
#[test]
fn mcp_tool_approval_question_id_helpers_round_trip() {
let question_id = mcp_tool_approval_question_id("call-1");
assert_eq!(question_id, "mcp_tool_call_approval_call-1");
assert!(is_mcp_tool_approval_question_id(&question_id));
assert!(!is_mcp_tool_approval_question_id("other_question"));
}
#[tokio::test]
async fn mcp_tool_call_span_records_expected_fields() {
let buffer: &'static std::sync::Mutex<Vec<u8>> =
@@ -543,15 +598,15 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
})),
Some(&metadata),
);
let prompt = approval_request
.mcp_tool_approval_prompt(
"q".to_string(),
prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
),
/*monitor_reason*/ None,
)
.expect("mcp approval prompt");
let prompt = mcp_tool_approval_prompt(
&approval_request,
"q".to_string(),
prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
),
/*monitor_reason*/ None,
)
.expect("mcp approval prompt");
let request = build_mcp_tool_approval_elicitation_request(
&session,
@@ -625,13 +680,13 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
#[test]
fn custom_mcp_tool_question_mentions_server_name() {
let question = approval_prompt_request(
"custom_server",
"run_action",
/*arguments*/ None,
/*metadata*/ None,
)
.mcp_tool_approval_prompt(
let question = mcp_tool_approval_prompt(
&approval_prompt_request(
"custom_server",
"run_action",
/*arguments*/ None,
/*metadata*/ None,
),
"q".to_string(),
prompt_options(
/*allow_session_remember*/ false, /*allow_persistent_approval*/ false,
@@ -658,13 +713,13 @@ fn custom_mcp_tool_question_mentions_server_name() {
#[test]
fn codex_apps_tool_question_uses_fallback_app_label() {
let question = approval_prompt_request(
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
/*arguments*/ None,
/*metadata*/ None,
)
.mcp_tool_approval_prompt(
let question = mcp_tool_approval_prompt(
&approval_prompt_request(
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
/*arguments*/ None,
/*metadata*/ None,
),
"q".to_string(),
prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
@@ -689,13 +744,13 @@ fn trusted_codex_apps_tool_question_offers_always_allow() {
/*tool_title*/ None,
/*tool_description*/ None,
);
let question = approval_prompt_request(
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
/*arguments*/ None,
Some(&metadata),
)
.mcp_tool_approval_prompt(
let question = mcp_tool_approval_prompt(
&approval_prompt_request(
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
/*arguments*/ None,
Some(&metadata),
),
"q".to_string(),
prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
@@ -743,13 +798,13 @@ fn codex_apps_tool_question_without_elicitation_omits_always_allow() {
/*tool_title*/ None,
/*tool_description*/ None,
);
let question = approval_prompt_request(
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
/*arguments*/ None,
Some(&metadata),
)
.mcp_tool_approval_prompt(
let question = mcp_tool_approval_prompt(
&approval_prompt_request(
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
/*arguments*/ None,
Some(&metadata),
),
"q".to_string(),
mcp_tool_approval_prompt_options(
Some(&session_key),
@@ -778,13 +833,13 @@ fn codex_apps_tool_question_without_elicitation_omits_always_allow() {
#[test]
fn custom_mcp_tool_question_offers_session_remember_and_always_allow() {
let question = approval_prompt_request(
"custom_server",
"run_action",
/*arguments*/ None,
/*metadata*/ None,
)
.mcp_tool_approval_prompt(
let question = mcp_tool_approval_prompt(
&approval_prompt_request(
"custom_server",
"run_action",
/*arguments*/ None,
/*metadata*/ None,
),
"q".to_string(),
prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
@@ -1181,7 +1236,8 @@ fn approval_elicitation_meta_marks_tool_approvals() {
/*metadata*/ None,
);
assert_eq!(
request.mcp_tool_approval_elicitation_meta(
mcp_tool_approval_elicitation_meta(
&request,
/*tool_params*/ None,
/*tool_params_display*/ None,
prompt_options(
@@ -1210,7 +1266,8 @@ fn approval_elicitation_meta_merges_session_and_always_persist_for_custom_server
Some(&metadata),
);
assert_eq!(
request.mcp_tool_approval_elicitation_meta(
mcp_tool_approval_elicitation_meta(
&request,
Some(&serde_json::json!({"id": 1})),
/*tool_params_display*/ None,
prompt_options(
@@ -1433,7 +1490,8 @@ fn approval_elicitation_meta_includes_connector_source_for_codex_apps() {
Some(&metadata),
);
assert_eq!(
request.mcp_tool_approval_elicitation_meta(
mcp_tool_approval_elicitation_meta(
&request,
Some(&serde_json::json!({
"calendar_id": "primary",
})),
@@ -1475,7 +1533,8 @@ fn approval_elicitation_meta_merges_session_and_always_persist_with_connector_so
Some(&metadata),
);
assert_eq!(
request.mcp_tool_approval_elicitation_meta(
mcp_tool_approval_elicitation_meta(
&request,
Some(&serde_json::json!({
"calendar_id": "primary",
})),