//! MCP elicitation request tracking and policy handling. //! //! RMCP clients call into this module when a server asks Codex to elicit data //! from the user. It decides whether the request can be automatically accepted, //! must be declined by policy, or should be surfaced as a Codex protocol event //! and later resolved through the stored responder. use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex as StdMutex; use crate::mcp::McpPermissionPromptAutoApproveContext; use crate::mcp::mcp_permission_prompt_is_auto_approved; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use async_channel::Sender; use codex_protocol::approvals::ElicitationRequest; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::mcp::RequestId as ProtocolRequestId; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::SendElicitation; use futures::future::BoxFuture; use futures::future::FutureExt; use rmcp::model::CreateElicitationRequestParams; use rmcp::model::ElicitationAction; use rmcp::model::RequestId; use tokio::sync::Mutex; use tokio::sync::oneshot; #[derive(Debug, Clone)] pub struct ElicitationReviewRequest { pub server_name: String, pub request_id: RequestId, pub elicitation: CreateElicitationRequestParams, } pub trait ElicitationReviewer: Send + Sync { fn review( &self, request: ElicitationReviewRequest, ) -> BoxFuture<'static, Result>>; } pub type ElicitationReviewerHandle = Arc; #[derive(Clone)] pub(crate) struct ElicitationRequestManager { requests: Arc>, pub(crate) approval_policy: Arc>, pub(crate) permission_profile: Arc>, auto_deny: Arc>, reviewer: Option, } impl ElicitationRequestManager { pub(crate) fn new( approval_policy: AskForApproval, permission_profile: PermissionProfile, reviewer: Option, ) -> Self { Self { requests: Arc::new(Mutex::new(HashMap::new())), approval_policy: Arc::new(StdMutex::new(approval_policy)), permission_profile: Arc::new(StdMutex::new(permission_profile)), auto_deny: Arc::new(StdMutex::new(false)), reviewer, } } pub(crate) fn auto_deny(&self) -> bool { self.auto_deny .lock() .map(|auto_deny| *auto_deny) .unwrap_or(false) } pub(crate) fn set_auto_deny(&self, auto_deny: bool) { if let Ok(mut current) = self.auto_deny.lock() { *current = auto_deny; } } pub(crate) async fn resolve( &self, server_name: String, id: RequestId, response: ElicitationResponse, ) -> Result<()> { self.requests .lock() .await .remove(&(server_name, id)) .ok_or_else(|| anyhow!("elicitation request not found"))? .send(response) .map_err(|e| anyhow!("failed to send elicitation response: {e:?}")) } pub(crate) fn make_sender( &self, server_name: String, tx_event: Sender, ) -> SendElicitation { let elicitation_requests = self.requests.clone(); let approval_policy = self.approval_policy.clone(); let permission_profile = self.permission_profile.clone(); let auto_deny = self.auto_deny.clone(); let reviewer = self.reviewer.clone(); Box::new(move |id, elicitation| { let elicitation_requests = elicitation_requests.clone(); let tx_event = tx_event.clone(); let server_name = server_name.clone(); let approval_policy = approval_policy.clone(); let permission_profile = permission_profile.clone(); let auto_deny = auto_deny.clone(); let reviewer = reviewer.clone(); async move { let auto_deny = auto_deny .lock() .map(|auto_deny| *auto_deny) .unwrap_or(false); if auto_deny { return Ok(ElicitationResponse { action: ElicitationAction::Decline, content: None, meta: None, }); } let approval_policy = approval_policy .lock() .map(|policy| *policy) .unwrap_or(AskForApproval::Never); let permission_profile = permission_profile .lock() .map(|profile| profile.clone()) .unwrap_or_default(); if mcp_permission_prompt_is_auto_approved( approval_policy, &permission_profile, McpPermissionPromptAutoApproveContext::default(), ) && can_auto_accept_elicitation(&elicitation) { return Ok(ElicitationResponse { action: ElicitationAction::Accept, content: Some(serde_json::json!({})), meta: None, }); } if elicitation_is_rejected_by_policy(approval_policy) { return Ok(ElicitationResponse { action: ElicitationAction::Decline, content: None, meta: None, }); } if let Some(reviewer) = reviewer.as_ref() { let request = ElicitationReviewRequest { server_name: server_name.clone(), request_id: id.clone(), elicitation: elicitation.clone(), }; if let Some(response) = reviewer.review(request).await? { return Ok(response); } } let request = match elicitation { CreateElicitationRequestParams::FormElicitationParams { meta, message, requested_schema, } => ElicitationRequest::Form { meta: meta .map(serde_json::to_value) .transpose() .context("failed to serialize MCP elicitation metadata")?, message, requested_schema: serde_json::to_value(requested_schema) .context("failed to serialize MCP elicitation schema")?, }, CreateElicitationRequestParams::UrlElicitationParams { meta, message, url, elicitation_id, } => ElicitationRequest::Url { meta: meta .map(serde_json::to_value) .transpose() .context("failed to serialize MCP elicitation metadata")?, message, url, elicitation_id, }, }; let (tx, rx) = oneshot::channel(); { let mut lock = elicitation_requests.lock().await; lock.insert((server_name.clone(), id.clone()), tx); } let _ = tx_event .send(Event { id: "mcp_elicitation_request".to_string(), msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { turn_id: None, server_name, id: match id.clone() { rmcp::model::NumberOrString::String(value) => { ProtocolRequestId::String(value.to_string()) } rmcp::model::NumberOrString::Number(value) => { ProtocolRequestId::Integer(value) } }, request, }), }) .await; rx.await .context("elicitation request channel closed unexpectedly") } .boxed() }) } } pub(crate) fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool { match approval_policy { AskForApproval::Never => true, AskForApproval::OnFailure => false, AskForApproval::OnRequest => false, AskForApproval::UnlessTrusted => false, AskForApproval::Granular(granular_config) => !granular_config.allows_mcp_elicitations(), } } type ResponderMap = HashMap<(String, RequestId), oneshot::Sender>; fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) -> bool { match elicitation { CreateElicitationRequestParams::FormElicitationParams { requested_schema, .. } => { // Auto-accept confirm/approval elicitations without schema requirements. requested_schema.properties.is_empty() } CreateElicitationRequestParams::UrlElicitationParams { .. } => false, } }