1use std::collections::HashMap;
9use std::sync::Arc;
10use std::sync::Mutex as StdMutex;
11use std::sync::atomic::AtomicU64;
12use std::sync::atomic::Ordering;
13
14use crate::mcp::McpPermissionPromptAutoApproveContext;
15use crate::mcp::mcp_permission_prompt_is_auto_approved;
16use anyhow::Context;
17use anyhow::Result;
18use anyhow::anyhow;
19use async_channel::Sender;
20use codex_protocol::approvals::ElicitationRequest;
21use codex_protocol::approvals::ElicitationRequestEvent;
22use codex_protocol::mcp::RequestId as ProtocolRequestId;
23use codex_protocol::models::PermissionProfile;
24use codex_protocol::protocol::AskForApproval;
25use codex_protocol::protocol::Event;
26use codex_protocol::protocol::EventMsg;
27use codex_rmcp_client::Elicitation;
28use codex_rmcp_client::ElicitationResponse;
29use codex_rmcp_client::SendElicitation;
30use futures::future::BoxFuture;
31use futures::future::FutureExt;
32use rmcp::model::ElicitationAction;
33use rmcp::model::RequestId;
34use tokio::sync::Mutex;
35use tokio::sync::oneshot;
36
37static NEXT_ELICITATION_REQUEST_ID: AtomicU64 = AtomicU64::new(0);
38
39#[derive(Debug, Clone)]
40pub struct ElicitationReviewRequest {
41 pub server_name: String,
42 pub request_id: RequestId,
43 pub elicitation: Elicitation,
44}
45
46pub trait ElicitationReviewer: Send + Sync {
47 fn review(
48 &self,
49 request: ElicitationReviewRequest,
50 ) -> BoxFuture<'static, Result<Option<ElicitationResponse>>>;
51}
52
53pub type ElicitationReviewerHandle = Arc<dyn ElicitationReviewer>;
54
55#[derive(Clone)]
57pub struct ElicitationLifecycle {
58 register: Arc<dyn Fn() -> Box<dyn Send + Sync> + Send + Sync>,
59}
60
61impl ElicitationLifecycle {
62 pub fn new<T>(register: impl Fn() -> T + Send + Sync + 'static) -> Self
63 where
64 T: Send + Sync + 'static,
65 {
66 Self {
67 register: Arc::new(move || Box::new(register())),
68 }
69 }
70
71 fn start(&self) -> ActiveElicitation {
72 ActiveElicitation {
73 _registration: (self.register)(),
74 }
75 }
76}
77
78struct ActiveElicitation {
79 _registration: Box<dyn Send + Sync>,
80}
81
82#[derive(Clone, Default)]
88pub struct ElicitationRequestRouter {
89 requests: Arc<Mutex<ResponderMap>>,
90}
91
92impl ElicitationRequestRouter {
93 async fn resolve(
94 &self,
95 server_name: String,
96 id: RequestId,
97 response: ElicitationResponse,
98 ) -> Result<()> {
99 self.requests
100 .lock()
101 .await
102 .remove(&(server_name, id))
103 .ok_or_else(|| anyhow!("elicitation request not found"))?
104 .send(response)
105 .map_err(|e| anyhow!("failed to send elicitation response: {e:?}"))
106 }
107}
108
109#[derive(Clone)]
110pub(crate) struct ElicitationRequestManager {
111 router: ElicitationRequestRouter,
112 pub(crate) approval_policy: Arc<StdMutex<AskForApproval>>,
113 pub(crate) permission_profile: Arc<StdMutex<PermissionProfile>>,
114 auto_deny: Arc<StdMutex<bool>>,
115 reviewer: Option<ElicitationReviewerHandle>,
116 lifecycle: Option<ElicitationLifecycle>,
117}
118
119impl ElicitationRequestManager {
120 pub(crate) fn new(
121 approval_policy: AskForApproval,
122 permission_profile: PermissionProfile,
123 reviewer: Option<ElicitationReviewerHandle>,
124 lifecycle: Option<ElicitationLifecycle>,
125 router: ElicitationRequestRouter,
126 ) -> Self {
127 Self {
128 router,
129 approval_policy: Arc::new(StdMutex::new(approval_policy)),
130 permission_profile: Arc::new(StdMutex::new(permission_profile)),
131 auto_deny: Arc::new(StdMutex::new(false)),
132 reviewer,
133 lifecycle,
134 }
135 }
136
137 pub(crate) fn auto_deny(&self) -> bool {
138 self.auto_deny
139 .lock()
140 .map(|auto_deny| *auto_deny)
141 .unwrap_or(false)
142 }
143
144 pub(crate) fn set_auto_deny(&self, auto_deny: bool) {
145 if let Ok(mut current) = self.auto_deny.lock() {
146 *current = auto_deny;
147 }
148 }
149
150 pub(crate) async fn resolve(
151 &self,
152 server_name: String,
153 id: RequestId,
154 response: ElicitationResponse,
155 ) -> Result<()> {
156 self.router.resolve(server_name, id, response).await
157 }
158
159 pub(crate) fn router(&self) -> ElicitationRequestRouter {
160 self.router.clone()
161 }
162
163 pub(crate) fn make_sender(
164 &self,
165 server_name: String,
166 tx_event: Option<Sender<Event>>,
167 ) -> SendElicitation {
168 let router = self.router.clone();
169 let approval_policy = self.approval_policy.clone();
170 let permission_profile = self.permission_profile.clone();
171 let auto_deny = self.auto_deny.clone();
172 let reviewer = self.reviewer.clone();
173 let lifecycle = self.lifecycle.clone();
174 Box::new(move |id, elicitation| {
175 let router = router.clone();
176 let tx_event = tx_event.clone();
177 let server_name = server_name.clone();
178 let approval_policy = approval_policy.clone();
179 let permission_profile = permission_profile.clone();
180 let auto_deny = auto_deny.clone();
181 let reviewer = reviewer.clone();
182 let lifecycle = lifecycle.clone();
183 async move {
184 let auto_deny = auto_deny
185 .lock()
186 .map(|auto_deny| *auto_deny)
187 .unwrap_or(false);
188 if auto_deny {
189 return Ok(ElicitationResponse {
190 action: ElicitationAction::Decline,
191 content: None,
192 meta: None,
193 });
194 }
195
196 let approval_policy = approval_policy
197 .lock()
198 .map(|policy| *policy)
199 .unwrap_or(AskForApproval::Never);
200 let permission_profile = permission_profile
201 .lock()
202 .map(|profile| profile.clone())
203 .unwrap_or_default();
204 if mcp_permission_prompt_is_auto_approved(
205 approval_policy,
206 &permission_profile,
207 McpPermissionPromptAutoApproveContext::default(),
208 ) && can_auto_accept_elicitation(&elicitation)
209 {
210 return Ok(ElicitationResponse {
211 action: ElicitationAction::Accept,
212 content: Some(serde_json::json!({})),
213 meta: None,
214 });
215 }
216
217 if elicitation_is_rejected_by_policy(approval_policy) {
218 return Ok(ElicitationResponse {
219 action: ElicitationAction::Decline,
220 content: None,
221 meta: None,
222 });
223 }
224
225 if let Some(reviewer) = reviewer.as_ref() {
226 let request = ElicitationReviewRequest {
227 server_name: server_name.clone(),
228 request_id: id.clone(),
229 elicitation: elicitation.clone(),
230 };
231 if let Some(response) = reviewer.review(request).await? {
232 return Ok(response);
233 }
234 }
235
236 let Some(tx_event) = tx_event else {
237 return Ok(ElicitationResponse {
238 action: ElicitationAction::Decline,
239 content: None,
240 meta: None,
241 });
242 };
243
244 let public_request_id = format!(
245 "codex-mcp-elicitation-{}",
246 NEXT_ELICITATION_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
247 );
248 let routed_request_id = RequestId::String(public_request_id.clone().into());
249 let request = match elicitation {
250 Elicitation::Mcp(
251 rmcp::model::CreateElicitationRequestParams::FormElicitationParams {
252 meta,
253 message,
254 requested_schema,
255 },
256 ) => ElicitationRequest::Form {
257 meta: meta
258 .map(serde_json::to_value)
259 .transpose()
260 .context("failed to serialize MCP elicitation metadata")?,
261 message,
262 requested_schema: serde_json::to_value(requested_schema)
263 .context("failed to serialize MCP elicitation schema")?,
264 },
265 Elicitation::Mcp(
266 rmcp::model::CreateElicitationRequestParams::UrlElicitationParams {
267 meta,
268 message,
269 url,
270 elicitation_id,
271 },
272 ) => ElicitationRequest::Url {
273 meta: meta
274 .map(serde_json::to_value)
275 .transpose()
276 .context("failed to serialize MCP elicitation metadata")?,
277 message,
278 url,
279 elicitation_id,
280 },
281 Elicitation::OpenAiForm {
282 meta,
283 message,
284 requested_schema,
285 } => ElicitationRequest::OpenAiForm {
286 meta,
287 message,
288 requested_schema,
289 },
290 };
291 let (tx, rx) = oneshot::channel();
292 let _active_elicitation = lifecycle.as_ref().map(ElicitationLifecycle::start);
293 {
294 let mut lock = router.requests.lock().await;
295 lock.insert((server_name.clone(), routed_request_id), tx);
296 }
297 let _ = tx_event
298 .send(Event {
299 id: "mcp_elicitation_request".to_string(),
300 msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
301 turn_id: None,
302 server_name,
303 id: ProtocolRequestId::String(public_request_id),
304 request,
305 }),
306 })
307 .await;
308 rx.await
309 .context("elicitation request channel closed unexpectedly")
310 }
311 .boxed()
312 })
313 }
314}
315
316pub(crate) fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool {
317 match approval_policy {
318 AskForApproval::Never => true,
319 AskForApproval::OnRequest => false,
320 AskForApproval::UnlessTrusted => false,
321 AskForApproval::Granular(granular_config) => !granular_config.allows_mcp_elicitations(),
322 }
323}
324
325type ResponderMap = HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>;
326
327fn can_auto_accept_elicitation(elicitation: &Elicitation) -> bool {
328 match elicitation {
329 Elicitation::Mcp(rmcp::model::CreateElicitationRequestParams::FormElicitationParams {
330 requested_schema,
331 ..
332 }) => {
333 requested_schema.properties.is_empty()
335 }
336 Elicitation::Mcp(rmcp::model::CreateElicitationRequestParams::UrlElicitationParams {
337 ..
338 })
339 | Elicitation::OpenAiForm { .. } => false,
340 }
341}