From ee335f4b228cea103a4aa0eddb5a1211e0d2b1cf Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Wed, 19 Nov 2025 15:01:05 -0800 Subject: [PATCH 01/10] elicitations --- codex-rs/core/src/codex.rs | 15 ++ codex-rs/core/src/mcp_connection_manager.rs | 136 ++++++++++++++---- codex-rs/core/src/rollout/policy.rs | 3 +- codex-rs/protocol/src/approvals.rs | 9 ++ codex-rs/protocol/src/protocol.rs | 3 + codex-rs/rmcp-client/src/lib.rs | 2 + .../rmcp-client/src/logging_client_handler.rs | 32 +++-- codex-rs/rmcp-client/src/rmcp_client.rs | 16 ++- 8 files changed, 169 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index e5b4e4c316..ad44aaca51 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -34,6 +34,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TaskStartedEvent; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; +use codex_rmcp_client::ElicitationResponse; use futures::future::BoxFuture; use futures::prelude::*; use futures::stream::FuturesOrdered; @@ -44,6 +45,7 @@ use mcp_types::ListResourcesRequestParams; use mcp_types::ListResourcesResult; use mcp_types::ReadResourceRequestParams; use mcp_types::ReadResourceResult; +use mcp_types::RequestId; use serde_json; use serde_json::Value; use tokio::sync::Mutex; @@ -938,6 +940,19 @@ impl Session { } } + pub async fn resolve_elicitation( + &self, + server_name: String, + id: RequestId, + response: ElicitationResponse, + ) -> anyhow::Result<()> { + self.services + .mcp_connection_manager + .read() + .await + .resolve_elicitation(server_name, id, response) + } + /// Records input items: always append to conversation history and /// persist these response items to rollout. pub(crate) async fn record_conversation_items( diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index d8869e5e9f..f87de8cda3 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -10,7 +10,10 @@ use std::collections::HashMap; use std::collections::HashSet; use std::env; use std::ffi::OsString; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use crate::mcp::auth::McpAuthStatusEntry; @@ -20,12 +23,15 @@ use anyhow::anyhow; use async_channel::Sender; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; +use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpStartupCompleteEvent; use codex_protocol::protocol::McpStartupFailure; use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; +use codex_rmcp_client::Elicitation; +use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rmcp_client::RmcpClient; use futures::future::BoxFuture; @@ -39,6 +45,7 @@ use mcp_types::ListResourcesRequestParams; use mcp_types::ListResourcesResult; use mcp_types::ReadResourceRequestParams; use mcp_types::ReadResourceResult; +use mcp_types::RequestId; use mcp_types::Resource; use mcp_types::ResourceTemplate; use mcp_types::Tool; @@ -46,6 +53,7 @@ use mcp_types::Tool; use serde_json::json; use sha1::Digest; use sha1::Sha1; +use tokio::sync::oneshot; use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; use tracing::warn; @@ -129,6 +137,10 @@ impl AsyncManagedClient { config: McpServerConfig, store_mode: OAuthCredentialsStoreMode, cancel_token: CancellationToken, + tx_event: Sender, + elicitation_requests: Arc< + Mutex>>, + >, ) -> Self { let tool_filter = ToolFilter::from_config(&config); let fut = start_server_task( @@ -141,6 +153,8 @@ impl AsyncManagedClient { config.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT), tool_filter, cancel_token, + tx_event, + elicitation_requests, ); Self { client: fut.boxed().shared(), @@ -156,6 +170,8 @@ impl AsyncManagedClient { #[derive(Default)] pub(crate) struct McpConnectionManager { clients: HashMap, + elicitation_requests: + Arc>>>, } impl McpConnectionManager { @@ -172,6 +188,7 @@ impl McpConnectionManager { } let mut clients = HashMap::new(); let mut join_set = JoinSet::new(); + let elicitation_requests = Arc::new(Mutex::new(HashMap::new())); for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) { let cancel_token = cancel_token.child_token(); let _ = emit_update( @@ -182,8 +199,14 @@ impl McpConnectionManager { }, ) .await; - let async_managed_client = - AsyncManagedClient::new(server_name.clone(), cfg, store_mode, cancel_token.clone()); + let async_managed_client = AsyncManagedClient::new( + server_name.clone(), + cfg, + store_mode, + cancel_token.clone(), + tx_event.clone(), + elicitation_requests.clone(), + ); clients.insert(server_name.clone(), async_managed_client.clone()); let tx_event = tx_event.clone(); let auth_entry = auth_entries.get(&server_name).cloned(); @@ -217,6 +240,7 @@ impl McpConnectionManager { }); } self.clients = clients; + self.elicitation_requests = elicitation_requests; tokio::spawn(async move { let outcomes = join_set.join_all().await; let mut summary = McpStartupCompleteEvent::default(); @@ -250,6 +274,21 @@ impl McpConnectionManager { .context("failed to get client") } + pub fn resolve_elicitation( + &self, + server_name: String, + id: RequestId, + response: ElicitationResponse, + ) -> Result<()> { + self.elicitation_requests + .lock() + .expect("elicitation_requests lock") + .remove(&(server_name, id)) + .ok_or_else(|| anyhow!("elicitation request not found"))? + .send(response) + .map_err(|e| anyhow!("failed to send elicitation response: {e:?}")) + } + /// Returns a single map that contains all tools. Each key is the /// fully-qualified name for the tool. pub async fn list_all_tools(&self) -> HashMap { @@ -578,6 +617,45 @@ impl From for StartupOutcomeError { } } +#[allow(clippy::type_complexity)] +fn make_elicitation_sender( + server_name: String, + tx_event: Sender, + elicitation_requests: Arc< + Mutex>>, + >, +) -> Box< + dyn Fn(RequestId, Elicitation) -> Pin + Send>> + + Send + + Sync, +> { + Box::new(move |id: RequestId, elicitation: Elicitation| { + let elicitation_requests = elicitation_requests.clone(); + let tx_event = tx_event.clone(); + let server_name = server_name.clone(); + Box::pin(async move { + let (tx, rx) = oneshot::channel::(); + { + let mut elicitation_requests_lock = elicitation_requests + .lock() + .expect("elicitation_requests lock"); + elicitation_requests_lock.insert((server_name.clone(), id.clone()), tx); + } + let _ = tx_event + .send(Event { + id: "mcp_elicitation_request".to_string(), + msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { + server_name, + id, + message: elicitation.message, + }), + }) + .await; + rx.await.unwrap() + }) + }) +} + async fn start_server_task( server_name: String, transport: McpServerTransportConfig, @@ -586,6 +664,10 @@ async fn start_server_task( tool_timeout: Duration, tool_filter: ToolFilter, cancel_token: CancellationToken, + tx_event: Sender, + elicitation_requests: Arc< + Mutex>>, + >, ) -> Result { if cancel_token.is_cancelled() { return Err(StartupOutcomeError::Cancelled); @@ -594,6 +676,9 @@ async fn start_server_task( return Err(error.into()); } + let send_elicitation = + make_elicitation_sender(server_name.clone(), tx_event, elicitation_requests); + match start_server_work( server_name, transport, @@ -601,6 +686,7 @@ async fn start_server_task( startup_timeout, tool_timeout, tool_filter, + send_elicitation, ) .or_cancel(&cancel_token) .await @@ -617,6 +703,11 @@ async fn start_server_work( startup_timeout: Duration, tool_timeout: Duration, tool_filter: ToolFilter, + send_elicitation: Box< + dyn Fn(RequestId, Elicitation) -> Pin + Send>> + + Send + + Sync, + >, ) -> Result { let params = mcp_types::InitializeRequestParams { capabilities: ClientCapabilities { @@ -639,7 +730,7 @@ async fn start_server_work( protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(), }; - let client_result = match transport { + let client = match transport { McpServerTransportConfig::Stdio { command, args, @@ -649,16 +740,9 @@ async fn start_server_work( } => { let command_os: OsString = command.into(); let args_os: Vec = args.into_iter().map(Into::into).collect(); - match RmcpClient::new_stdio_client(command_os, args_os, env, &env_vars, cwd).await { - Ok(client) => { - let client = Arc::new(client); - client - .initialize(params.clone(), Some(startup_timeout)) - .await - .map(|_| client) - } - Err(err) => Err(err.into()), - } + RmcpClient::new_stdio_client(command_os, args_os, env, &env_vars, cwd) + .await + .map_err(|err| StartupOutcomeError::from(anyhow!(err))) } McpServerTransportConfig::StreamableHttp { url, @@ -671,7 +755,7 @@ async fn start_server_work( Ok(token) => token, Err(error) => return Err(error.into()), }; - match RmcpClient::new_streamable_http_client( + RmcpClient::new_streamable_http_client( &server_name, &url, resolved_bearer_token, @@ -680,25 +764,15 @@ async fn start_server_work( store_mode, ) .await - { - Ok(client) => { - let client = Arc::new(client); - client - .initialize(params.clone(), Some(startup_timeout)) - .await - .map(|_| client) - } - Err(err) => Err(err), - } + .map_err(StartupOutcomeError::from) } - }; + }?; - let client = match client_result { - Ok(client) => client, - Err(error) => { - return Err(error.into()); - } - }; + let client = Arc::new(client); + client + .initialize(params.clone(), Some(startup_timeout), send_elicitation) + .await + .map_err(StartupOutcomeError::from)?; let tools = match list_tools_for_client(&server_name, &client, startup_timeout).await { Ok(tools) => tools, diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 9e0e308362..05e4294a41 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -84,6 +84,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::ItemCompleted(_) | EventMsg::AgentMessageContentDelta(_) | EventMsg::ReasoningContentDelta(_) - | EventMsg::ReasoningRawContentDelta(_) => false, + | EventMsg::ReasoningRawContentDelta(_) + | EventMsg::ElicitationRequest(_) => false, } } diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index f7c5fc6049..731cf918ad 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; +use mcp_types::RequestId; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -53,6 +54,14 @@ pub struct ExecApprovalRequestEvent { pub parsed_cmd: Vec, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +pub struct ElicitationRequestEvent { + pub server_name: String, + pub id: RequestId, + pub message: String, + // pub requested_schema: ElicitRequestParamsRequestedSchema, +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ApplyPatchApprovalRequestEvent { /// Responses API call id for the associated patch apply call, if available. diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e3bc76199a..2cfbfdc5ec 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -11,6 +11,7 @@ use std::str::FromStr; use std::time::Duration; use crate::ConversationId; +use crate::approvals::ElicitationRequestEvent; use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::custom_prompts::CustomPrompt; @@ -505,6 +506,8 @@ pub enum EventMsg { ExecApprovalRequest(ExecApprovalRequestEvent), + ElicitationRequest(ElicitationRequestEvent), + ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent), /// Notification advising the user that something they are using has been diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 87ce86b464..5c38833850 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -17,4 +17,6 @@ pub use oauth::delete_oauth_tokens; pub(crate) use oauth::load_oauth_tokens; pub use oauth::save_oauth_tokens; pub use perform_oauth_login::perform_oauth_login; +pub use rmcp_client::Elicitation; +pub use rmcp_client::ElicitationResponse; pub use rmcp_client::RmcpClient; diff --git a/codex-rs/rmcp-client/src/logging_client_handler.rs b/codex-rs/rmcp-client/src/logging_client_handler.rs index 85d237b0e9..008875f938 100644 --- a/codex-rs/rmcp-client/src/logging_client_handler.rs +++ b/codex-rs/rmcp-client/src/logging_client_handler.rs @@ -1,13 +1,15 @@ +use std::sync::Arc; + use rmcp::ClientHandler; use rmcp::RoleClient; use rmcp::model::CancelledNotificationParam; use rmcp::model::ClientInfo; use rmcp::model::CreateElicitationRequestParam; use rmcp::model::CreateElicitationResult; -use rmcp::model::ElicitationAction; use rmcp::model::LoggingLevel; use rmcp::model::LoggingMessageNotificationParam; use rmcp::model::ProgressNotificationParam; +use rmcp::model::RequestId; use rmcp::model::ResourceUpdatedNotificationParam; use rmcp::service::NotificationContext; use rmcp::service::RequestContext; @@ -16,32 +18,34 @@ use tracing::error; use tracing::info; use tracing::warn; -#[derive(Debug, Clone)] +use crate::rmcp_client::SendElicitation; + +#[derive(Clone)] pub(crate) struct LoggingClientHandler { client_info: ClientInfo, + send_elicitation: Arc, } impl LoggingClientHandler { - pub(crate) fn new(client_info: ClientInfo) -> Self { - Self { client_info } + pub(crate) fn new(client_info: ClientInfo, send_elicitation: SendElicitation) -> Self { + Self { + client_info, + send_elicitation: Arc::new(send_elicitation), + } } } impl ClientHandler for LoggingClientHandler { - // TODO (CODEX-3571): support elicitations. async fn create_elicitation( &self, request: CreateElicitationRequestParam, - _context: RequestContext, + context: RequestContext, ) -> Result { - info!( - "MCP server requested elicitation ({}). Elicitations are not supported yet. Declining.", - request.message - ); - Ok(CreateElicitationResult { - action: ElicitationAction::Decline, - content: None, - }) + let id = match context.id { + RequestId::String(id) => mcp_types::RequestId::String(id.to_string()), + RequestId::Number(id) => mcp_types::RequestId::Integer(id), + }; + Ok((self.send_elicitation)(id, request).await) } async fn on_cancelled( diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index d7d3477b00..25b910dbbe 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::ffi::OsString; use std::io; use std::path::PathBuf; +use std::pin::Pin; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; @@ -21,8 +22,11 @@ use mcp_types::ListToolsRequestParams; use mcp_types::ListToolsResult; use mcp_types::ReadResourceRequestParams; use mcp_types::ReadResourceResult; +use mcp_types::RequestId; use reqwest::header::HeaderMap; use rmcp::model::CallToolRequestParam; +use rmcp::model::CreateElicitationRequestParam; +use rmcp::model::CreateElicitationResult; use rmcp::model::InitializeRequestParam; use rmcp::model::PaginatedRequestParam; use rmcp::model::ReadResourceRequestParam; @@ -77,6 +81,15 @@ enum ClientState { }, } +pub type Elicitation = CreateElicitationRequestParam; +pub type ElicitationResponse = CreateElicitationResult; + +pub type SendElicitation = Box< + dyn Fn(RequestId, Elicitation) -> Pin + Send>> + + Send + + Sync, +>; + /// MCP client implemented on top of the official `rmcp` SDK. /// https://github.com/modelcontextprotocol/rust-sdk pub struct RmcpClient { @@ -200,9 +213,10 @@ impl RmcpClient { &self, params: InitializeRequestParams, timeout: Option, + send_elicitation: SendElicitation, ) -> Result { let rmcp_params: InitializeRequestParam = convert_to_rmcp(params.clone())?; - let client_handler = LoggingClientHandler::new(rmcp_params); + let client_handler = LoggingClientHandler::new(rmcp_params, send_elicitation); let (transport, oauth_persistor) = { let mut guard = self.state.lock().await; From cc175c3978cb2f4eda185c7c9e1cc5c6f9c64dca Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Wed, 12 Nov 2025 15:23:55 -0800 Subject: [PATCH 02/10] elicitations-ui --- codex-rs/core/src/codex.rs | 37 +++++ codex-rs/core/src/rollout/policy.rs | 1 + .../src/event_processor_with_human_output.rs | 3 + codex-rs/mcp-server/src/codex_tool_runner.rs | 3 + codex-rs/protocol/src/approvals.rs | 8 ++ codex-rs/protocol/src/protocol.rs | 12 ++ codex-rs/rmcp-client/src/lib.rs | 1 + codex-rs/tui/src/app.rs | 19 +++ .../tui/src/bottom_pane/approval_overlay.rs | 131 ++++++++++++++++-- codex-rs/tui/src/chatwidget.rs | 47 ++++++- codex-rs/tui/src/chatwidget/interrupts.rs | 7 + 11 files changed, 255 insertions(+), 14 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ad44aaca51..2b5b86d4fd 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1421,6 +1421,13 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv ) .await; } + Op::ResolveElicitation { + server_name, + request_id, + decision, + } => { + handlers::resolve_elicitation(&sess, server_name, request_id, decision).await; + } Op::Shutdown => { if handlers::shutdown(&sess, sub.id.clone()).await { break; @@ -1448,6 +1455,7 @@ mod handlers { use crate::tasks::RegularTask; use crate::tasks::UndoTask; use crate::tasks::UserShellCommandTask; + use codex_protocol::approvals::ElicitationDecision; use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; @@ -1459,6 +1467,9 @@ mod handlers { use codex_protocol::protocol::TurnAbortReason; use codex_protocol::user_input::UserInput; + use codex_rmcp_client::ElicitationAction; + use codex_rmcp_client::ElicitationResponse; + use mcp_types::RequestId; use std::sync::Arc; use tracing::info; use tracing::warn; @@ -1542,6 +1553,32 @@ mod handlers { *previous_context = Some(turn_context); } + pub async fn resolve_elicitation( + sess: &Arc, + server_name: String, + request_id: RequestId, + decision: ElicitationDecision, + ) { + let action = match decision { + ElicitationDecision::Accept => ElicitationAction::Accept, + ElicitationDecision::Decline => ElicitationAction::Decline, + ElicitationDecision::Cancel => ElicitationAction::Cancel, + }; + let response = ElicitationResponse { + action, + content: None, + }; + if let Err(err) = sess + .resolve_elicitation(server_name, request_id, response) + .await + { + warn!( + error = %err, + "failed to resolve elicitation request in session" + ); + } + } + pub async fn exec_approval(sess: &Arc, id: String, decision: ReviewDecision) { match decision { ReviewDecision::Abort => { diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 05e4294a41..6270071b0a 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -64,6 +64,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::ExecCommandOutputDelta(_) | EventMsg::ExecCommandEnd(_) | EventMsg::ExecApprovalRequest(_) + | EventMsg::ElicitationRequest(_) | EventMsg::ApplyPatchApprovalRequest(_) | EventMsg::BackgroundEvent(_) | EventMsg::StreamError(_) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 2d550fea46..f459cdbe06 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -227,6 +227,9 @@ impl EventProcessor for EventProcessorWithHumanOutput { EventMsg::TaskStarted(_) => { // Ignore. } + EventMsg::ElicitationRequest(_) => { + // Currently unused in exec mode; ignore. + } EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { let last_message = last_agent_message.as_deref(); if let Some(output_file) = self.last_message_path.as_deref() { diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 93dc7764dc..c194a99399 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -208,6 +208,9 @@ async fn run_codex_tool_session_inner( EventMsg::Warning(_) => { continue; } + EventMsg::ElicitationRequest(_) => { + continue; + } EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id, reason, diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 731cf918ad..767b0cbcad 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -62,6 +62,14 @@ pub struct ElicitationRequestEvent { // pub requested_schema: ElicitRequestParamsRequestedSchema, } +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +pub enum ElicitationDecision { + Accept, + Decline, + Cancel, +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ApplyPatchApprovalRequestEvent { /// Responses API call id for the associated patch apply call, if available. diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2cfbfdc5ec..ed4589179d 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -24,6 +24,7 @@ use crate::parse_command::ParsedCommand; use crate::plan_tool::UpdatePlanArgs; use crate::user_input::UserInput; use mcp_types::CallToolResult; +use mcp_types::RequestId; use mcp_types::Resource as McpResource; use mcp_types::ResourceTemplate as McpResourceTemplate; use mcp_types::Tool as McpTool; @@ -36,6 +37,7 @@ use strum_macros::Display; use ts_rs::TS; pub use crate::approvals::ApplyPatchApprovalRequestEvent; +pub use crate::approvals::ElicitationDecision; pub use crate::approvals::ExecApprovalRequestEvent; pub use crate::approvals::SandboxCommandAssessment; pub use crate::approvals::SandboxRiskLevel; @@ -154,6 +156,16 @@ pub enum Op { decision: ReviewDecision, }, + /// Resolve an MCP elicitation request. + ResolveElicitation { + /// Name of the MCP server that issued the request. + server_name: String, + /// Request identifier from the MCP server. + request_id: RequestId, + /// User's decision for the request. + decision: ElicitationDecision, + }, + /// Append an entry to the persistent cross-session message history. /// /// Note the entry is not guaranteed to be logged if the user has diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 5c38833850..2e24b52ef6 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -17,6 +17,7 @@ pub use oauth::delete_oauth_tokens; pub(crate) use oauth::load_oauth_tokens; pub use oauth::save_oauth_tokens; pub use perform_oauth_login::perform_oauth_login; +pub use rmcp::model::ElicitationAction; pub use rmcp_client::Elicitation; pub use rmcp_client::ElicitationResponse; pub use rmcp_client::RmcpClient; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 7c86dd3b6e..893908a0a7 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -44,6 +44,8 @@ use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::style::Stylize; use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::Wrap; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -821,6 +823,23 @@ impl App { "E X E C".to_string(), )); } + ApprovalRequest::Elicitation { + server_name, + message, + .. + } => { + let _ = tui.enter_alt_screen(); + let paragraph = Paragraph::new(vec![ + Line::from(vec!["Server: ".into(), server_name.clone().bold()]), + Line::from(""), + Line::from(message.clone()), + ]) + .wrap(Wrap { trim: false }); + self.overlay = Some(Overlay::new_static_with_renderables( + vec![Box::new(paragraph)], + "E L I C I T".to_string(), + )); + } }, } Ok(true) diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index ef709f0051..8dd46b9ff4 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -16,6 +16,7 @@ use crate::key_hint::KeyBinding; use crate::render::highlight::highlight_bash_to_lines; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; +use codex_core::protocol::ElicitationDecision; use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; @@ -25,6 +26,7 @@ use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; +use mcp_types::RequestId; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; @@ -48,6 +50,11 @@ pub(crate) enum ApprovalRequest { cwd: PathBuf, changes: HashMap, }, + Elicitation { + server_name: String, + request_id: RequestId, + message: String, + }, } /// Modal overlay asking the user to approve or deny one or more requests. @@ -105,6 +112,10 @@ impl ApprovalOverlay { patch_options(), "Would you like to make the following edits?".to_string(), ), + ApprovalVariant::Elicitation { server_name, .. } => ( + elicitation_options(), + format!("{server_name} needs your approval."), + ), }; let header = Box::new(ColumnRenderable::with([ @@ -149,13 +160,23 @@ impl ApprovalOverlay { return; }; if let Some(variant) = self.current_variant.as_ref() { - match (&variant, option.decision) { - (ApprovalVariant::Exec { id, command }, decision) => { - self.handle_exec_decision(id, command, decision); + match (&variant, &option.decision) { + (ApprovalVariant::Exec { id, command }, ApprovalDecision::Review(decision)) => { + self.handle_exec_decision(id, command, *decision); + } + (ApprovalVariant::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => { + self.handle_patch_decision(id, *decision); } - (ApprovalVariant::ApplyPatch { id, .. }, decision) => { - self.handle_patch_decision(id, decision); + ( + ApprovalVariant::Elicitation { + server_name, + request_id, + }, + ApprovalDecision::Elicitation(decision), + ) => { + self.handle_elicitation_decision(server_name, request_id, *decision); } + _ => {} } } @@ -179,6 +200,20 @@ impl ApprovalOverlay { })); } + fn handle_elicitation_decision( + &self, + server_name: &str, + request_id: &RequestId, + decision: ElicitationDecision, + ) { + self.app_event_tx + .send(AppEvent::CodexOp(Op::ResolveElicitation { + server_name: server_name.to_string(), + request_id: request_id.clone(), + decision, + })); + } + fn advance_queue(&mut self) { if let Some(next) = self.queue.pop() { self.set_current(next); @@ -244,6 +279,16 @@ impl BottomPaneView for ApprovalOverlay { ApprovalVariant::ApplyPatch { id, .. } => { self.handle_patch_decision(id, ReviewDecision::Abort); } + ApprovalVariant::Elicitation { + server_name, + request_id, + } => { + self.handle_elicitation_decision( + server_name, + request_id, + ElicitationDecision::Cancel, + ); + } } } self.queue.clear(); @@ -336,6 +381,28 @@ impl From for ApprovalRequestState { header: Box::new(ColumnRenderable::with(header)), } } + ApprovalRequest::Elicitation { + server_name, + request_id, + message, + } => { + let mut header: Vec> = Vec::new(); + header.push(Box::new(Line::from(vec![ + "Server: ".into(), + server_name.clone().bold(), + ]))); + header.push(Box::new(Line::from(""))); + header.push(Box::new( + Paragraph::new(Line::from(message.clone())).wrap(Wrap { trim: false }), + )); + Self { + variant: ApprovalVariant::Elicitation { + server_name, + request_id, + }, + header: Box::new(ColumnRenderable::with(header)), + } + } } } } @@ -364,14 +431,29 @@ fn render_risk_lines(risk: &SandboxCommandAssessment) -> Vec> { #[derive(Clone)] enum ApprovalVariant { - Exec { id: String, command: Vec }, - ApplyPatch { id: String }, + Exec { + id: String, + command: Vec, + }, + ApplyPatch { + id: String, + }, + Elicitation { + server_name: String, + request_id: RequestId, + }, +} + +#[derive(Clone, Copy)] +enum ApprovalDecision { + Review(ReviewDecision), + Elicitation(ElicitationDecision), } #[derive(Clone)] struct ApprovalOption { label: String, - decision: ReviewDecision, + decision: ApprovalDecision, display_shortcut: Option, additional_shortcuts: Vec, } @@ -388,19 +470,19 @@ fn exec_options() -> Vec { vec![ ApprovalOption { label: "Yes, proceed".to_string(), - decision: ReviewDecision::Approved, + decision: ApprovalDecision::Review(ReviewDecision::Approved), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], }, ApprovalOption { label: "Yes, and don't ask again for this command".to_string(), - decision: ReviewDecision::ApprovedForSession, + decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))], }, ApprovalOption { label: "No, and tell Codex what to do differently".to_string(), - decision: ReviewDecision::Abort, + decision: ApprovalDecision::Review(ReviewDecision::Abort), display_shortcut: Some(key_hint::plain(KeyCode::Esc)), additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], }, @@ -411,19 +493,42 @@ fn patch_options() -> Vec { vec![ ApprovalOption { label: "Yes, proceed".to_string(), - decision: ReviewDecision::Approved, + decision: ApprovalDecision::Review(ReviewDecision::Approved), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], }, ApprovalOption { label: "No, and tell Codex what to do differently".to_string(), - decision: ReviewDecision::Abort, + decision: ApprovalDecision::Review(ReviewDecision::Abort), display_shortcut: Some(key_hint::plain(KeyCode::Esc)), additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], }, ] } +fn elicitation_options() -> Vec { + vec![ + ApprovalOption { + label: "Yes, provide the requested info".to_string(), + decision: ApprovalDecision::Elicitation(ElicitationDecision::Accept), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], + }, + ApprovalOption { + label: "No, but continue without it".to_string(), + decision: ApprovalDecision::Elicitation(ElicitationDecision::Decline), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], + }, + ApprovalOption { + label: "Cancel this request".to_string(), + decision: ApprovalDecision::Elicitation(ElicitationDecision::Cancel), + display_shortcut: Some(key_hint::plain(KeyCode::Esc)), + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('c'))], + }, + ] +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9429ce143e..3cec2ebf55 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -53,6 +53,7 @@ use codex_core::protocol::WarningEvent; use codex_core::protocol::WebSearchBeginEvent; use codex_core::protocol::WebSearchEndEvent; use codex_protocol::ConversationId; +use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::user_input::UserInput; use crossterm::event::KeyCode; @@ -708,6 +709,15 @@ impl ChatWidget { ); } + fn on_elicitation_request(&mut self, id: String, ev: ElicitationRequestEvent) { + let id2 = id.clone(); + let ev2 = ev.clone(); + self.defer_or_handle( + |q| q.push_elicitation(id, ev), + |s| s.handle_elicitation_request_now(id2, ev2), + ); + } + fn on_exec_command_begin(&mut self, ev: ExecCommandBeginEvent) { self.flush_answer_stream_with_separator(); let ev2 = ev.clone(); @@ -1013,6 +1023,33 @@ impl ChatWidget { }); } + pub(crate) fn handle_elicitation_request_now( + &mut self, + id: String, + ev: ElicitationRequestEvent, + ) { + self.flush_answer_stream_with_separator(); + if self.conversation_id.is_none() { + tracing::warn!("received elicitation request without a conversation id: {id}"); + self.add_error_message( + "Received an elicitation request before the session was ready.".to_string(), + ); + return; + } + + self.notify(Notification::ElicitationRequested { + server_name: ev.server_name.clone(), + }); + + let request = ApprovalRequest::Elicitation { + server_name: ev.server_name, + request_id: ev.id, + message: ev.message, + }; + self.bottom_pane.push_approval_request(request); + self.request_redraw(); + } + pub(crate) fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) { // Ensure the status indicator is visible while the command runs. self.running_commands.insert( @@ -1649,6 +1686,9 @@ impl ChatWidget { EventMsg::ApplyPatchApprovalRequest(ev) => { self.on_apply_patch_approval_request(id.unwrap_or_default(), ev) } + EventMsg::ElicitationRequest(ev) => { + self.on_elicitation_request(id.unwrap_or_default(), ev); + } EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev), EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta), EventMsg::PatchApplyBegin(ev) => self.on_patch_apply_begin(ev), @@ -2938,6 +2978,7 @@ enum Notification { AgentTurnComplete { response: String }, ExecApprovalRequested { command: String }, EditApprovalRequested { cwd: PathBuf, changes: Vec }, + ElicitationRequested { server_name: String }, } impl Notification { @@ -2961,6 +3002,9 @@ impl Notification { } ) } + Notification::ElicitationRequested { server_name } => { + format!("Approval requested by {server_name}") + } } } @@ -2968,7 +3012,8 @@ impl Notification { match self { Notification::AgentTurnComplete { .. } => "agent-turn-complete", Notification::ExecApprovalRequested { .. } - | Notification::EditApprovalRequested { .. } => "approval-requested", + | Notification::EditApprovalRequested { .. } + | Notification::ElicitationRequested { .. } => "approval-requested", } } diff --git a/codex-rs/tui/src/chatwidget/interrupts.rs b/codex-rs/tui/src/chatwidget/interrupts.rs index 531de3e646..5c9d3d8c1b 100644 --- a/codex-rs/tui/src/chatwidget/interrupts.rs +++ b/codex-rs/tui/src/chatwidget/interrupts.rs @@ -7,6 +7,7 @@ use codex_core::protocol::ExecCommandEndEvent; use codex_core::protocol::McpToolCallBeginEvent; use codex_core::protocol::McpToolCallEndEvent; use codex_core::protocol::PatchApplyEndEvent; +use codex_protocol::approvals::ElicitationRequestEvent; use super::ChatWidget; @@ -14,6 +15,7 @@ use super::ChatWidget; pub(crate) enum QueuedInterrupt { ExecApproval(String, ExecApprovalRequestEvent), ApplyPatchApproval(String, ApplyPatchApprovalRequestEvent), + Elicitation(String, ElicitationRequestEvent), ExecBegin(ExecCommandBeginEvent), ExecEnd(ExecCommandEndEvent), McpBegin(McpToolCallBeginEvent), @@ -51,6 +53,10 @@ impl InterruptManager { .push_back(QueuedInterrupt::ApplyPatchApproval(id, ev)); } + pub(crate) fn push_elicitation(&mut self, id: String, ev: ElicitationRequestEvent) { + self.queue.push_back(QueuedInterrupt::Elicitation(id, ev)); + } + pub(crate) fn push_exec_begin(&mut self, ev: ExecCommandBeginEvent) { self.queue.push_back(QueuedInterrupt::ExecBegin(ev)); } @@ -78,6 +84,7 @@ impl InterruptManager { QueuedInterrupt::ApplyPatchApproval(id, ev) => { chat.handle_apply_patch_approval_now(id, ev) } + QueuedInterrupt::Elicitation(id, ev) => chat.handle_elicitation_request_now(id, ev), QueuedInterrupt::ExecBegin(ev) => chat.handle_exec_begin_now(ev), QueuedInterrupt::ExecEnd(ev) => chat.handle_exec_end_now(ev), QueuedInterrupt::McpBegin(ev) => chat.handle_mcp_begin_now(ev), From fde295548e2a7c5ddc574f5fb9f1507afe935486 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Thu, 20 Nov 2025 17:10:09 -0800 Subject: [PATCH 03/10] refactor --- codex-rs/core/src/mcp_connection_manager.rs | 262 ++++++++---------- .../src/event_processor_with_human_output.rs | 14 +- codex-rs/exec/src/lib.rs | 15 + codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/protocol/src/approvals.rs | 2 + codex-rs/rmcp-client/src/lib.rs | 1 + codex-rs/tui/src/app.rs | 6 +- .../tui/src/bottom_pane/approval_overlay.rs | 19 +- 8 files changed, 160 insertions(+), 160 deletions(-) diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index f87de8cda3..004a3dda47 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -10,8 +10,6 @@ use std::collections::HashMap; use std::collections::HashSet; use std::env; use std::ffi::OsString; -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -30,10 +28,10 @@ use codex_protocol::protocol::McpStartupCompleteEvent; use codex_protocol::protocol::McpStartupFailure; use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; -use codex_rmcp_client::Elicitation; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rmcp_client::RmcpClient; +use codex_rmcp_client::SendElicitation; use futures::future::BoxFuture; use futures::future::FutureExt; use futures::future::Shared; @@ -118,6 +116,57 @@ pub(crate) struct ToolInfo { pub(crate) tool: Tool, } +type ResponderMap = HashMap<(String, RequestId), oneshot::Sender>; + +#[derive(Clone, Default)] +struct ElicitationRequestManager { + requests: Arc>, +} + +impl ElicitationRequestManager { + fn resolve( + &self, + server_name: String, + id: RequestId, + response: ElicitationResponse, + ) -> Result<()> { + self.requests + .lock() + .map_err(|e| anyhow!("failed to lock elicitation requests: {e:?}"))? + .remove(&(server_name, id)) + .ok_or_else(|| anyhow!("elicitation request not found"))? + .send(response) + .map_err(|e| anyhow!("failed to send elicitation response: {e:?}")) + } + + fn make_sender(&self, server_name: String, tx_event: Sender) -> SendElicitation { + let elicitation_requests = self.requests.clone(); + Box::new(move |id, elicitation| { + let elicitation_requests = elicitation_requests.clone(); + let tx_event = tx_event.clone(); + let server_name = server_name.clone(); + Box::pin(async move { + let (tx, rx) = oneshot::channel(); + if let Ok(mut lock) = elicitation_requests.lock() { + lock.insert((server_name.clone(), id.clone()), tx); + } + let _ = tx_event + .send(Event { + id: "mcp_elicitation_request".to_string(), + msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { + server_name, + id, + message: elicitation.message, + }), + }) + .await; + #[expect(clippy::unwrap_used)] + rx.await.unwrap() + }) + }) + } +} + #[derive(Clone)] struct ManagedClient { client: Arc, @@ -138,24 +187,32 @@ impl AsyncManagedClient { store_mode: OAuthCredentialsStoreMode, cancel_token: CancellationToken, tx_event: Sender, - elicitation_requests: Arc< - Mutex>>, - >, + elicitation_requests: ElicitationRequestManager, ) -> Self { let tool_filter = ToolFilter::from_config(&config); - let fut = start_server_task( - server_name, - config.transport, - store_mode, - config - .startup_timeout_sec - .unwrap_or(DEFAULT_STARTUP_TIMEOUT), - config.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT), - tool_filter, - cancel_token, - tx_event, - elicitation_requests, - ); + let fut = async move { + if let Err(error) = validate_mcp_server_name(&server_name) { + return Err(error.into()); + } + + let client = + Arc::new(make_rmcp_client(&server_name, config.transport, store_mode).await?); + match start_server_task( + server_name, + client, + config.startup_timeout_sec.or(Some(DEFAULT_STARTUP_TIMEOUT)), + config.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT), + tool_filter, + tx_event, + elicitation_requests, + ) + .or_cancel(&cancel_token) + .await + { + Ok(result) => result, + Err(CancelErr::Cancelled) => Err(StartupOutcomeError::Cancelled), + } + }; Self { client: fut.boxed().shared(), } @@ -170,8 +227,7 @@ impl AsyncManagedClient { #[derive(Default)] pub(crate) struct McpConnectionManager { clients: HashMap, - elicitation_requests: - Arc>>>, + elicitation_requests: ElicitationRequestManager, } impl McpConnectionManager { @@ -188,7 +244,7 @@ impl McpConnectionManager { } let mut clients = HashMap::new(); let mut join_set = JoinSet::new(); - let elicitation_requests = Arc::new(Mutex::new(HashMap::new())); + let elicitation_requests = ElicitationRequestManager::default(); for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) { let cancel_token = cancel_token.child_token(); let _ = emit_update( @@ -240,7 +296,7 @@ impl McpConnectionManager { }); } self.clients = clients; - self.elicitation_requests = elicitation_requests; + self.elicitation_requests = elicitation_requests.clone(); tokio::spawn(async move { let outcomes = join_set.join_all().await; let mut summary = McpStartupCompleteEvent::default(); @@ -280,13 +336,7 @@ impl McpConnectionManager { id: RequestId, response: ElicitationResponse, ) -> Result<()> { - self.elicitation_requests - .lock() - .expect("elicitation_requests lock") - .remove(&(server_name, id)) - .ok_or_else(|| anyhow!("elicitation request not found"))? - .send(response) - .map_err(|e| anyhow!("failed to send elicitation response: {e:?}")) + self.elicitation_requests.resolve(server_name, id, response) } /// Returns a single map that contains all tools. Each key is the @@ -617,97 +667,14 @@ impl From for StartupOutcomeError { } } -#[allow(clippy::type_complexity)] -fn make_elicitation_sender( - server_name: String, - tx_event: Sender, - elicitation_requests: Arc< - Mutex>>, - >, -) -> Box< - dyn Fn(RequestId, Elicitation) -> Pin + Send>> - + Send - + Sync, -> { - Box::new(move |id: RequestId, elicitation: Elicitation| { - let elicitation_requests = elicitation_requests.clone(); - let tx_event = tx_event.clone(); - let server_name = server_name.clone(); - Box::pin(async move { - let (tx, rx) = oneshot::channel::(); - { - let mut elicitation_requests_lock = elicitation_requests - .lock() - .expect("elicitation_requests lock"); - elicitation_requests_lock.insert((server_name.clone(), id.clone()), tx); - } - let _ = tx_event - .send(Event { - id: "mcp_elicitation_request".to_string(), - msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { - server_name, - id, - message: elicitation.message, - }), - }) - .await; - rx.await.unwrap() - }) - }) -} - async fn start_server_task( server_name: String, - transport: McpServerTransportConfig, - store_mode: OAuthCredentialsStoreMode, - startup_timeout: Duration, // TODO: cancel_token should handle this. + client: Arc, + startup_timeout: Option, // TODO: cancel_token should handle this. tool_timeout: Duration, tool_filter: ToolFilter, - cancel_token: CancellationToken, tx_event: Sender, - elicitation_requests: Arc< - Mutex>>, - >, -) -> Result { - if cancel_token.is_cancelled() { - return Err(StartupOutcomeError::Cancelled); - } - if let Err(error) = validate_mcp_server_name(&server_name) { - return Err(error.into()); - } - - let send_elicitation = - make_elicitation_sender(server_name.clone(), tx_event, elicitation_requests); - - match start_server_work( - server_name, - transport, - store_mode, - startup_timeout, - tool_timeout, - tool_filter, - send_elicitation, - ) - .or_cancel(&cancel_token) - .await - { - Ok(result) => result, - Err(CancelErr::Cancelled) => Err(StartupOutcomeError::Cancelled), - } -} - -async fn start_server_work( - server_name: String, - transport: McpServerTransportConfig, - store_mode: OAuthCredentialsStoreMode, - startup_timeout: Duration, - tool_timeout: Duration, - tool_filter: ToolFilter, - send_elicitation: Box< - dyn Fn(RequestId, Elicitation) -> Pin + Send>> - + Send - + Sync, - >, + elicitation_requests: ElicitationRequestManager, ) -> Result { let params = mcp_types::InitializeRequestParams { capabilities: ClientCapabilities { @@ -730,7 +697,36 @@ async fn start_server_work( protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(), }; - let client = match transport { + let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event); + + client + .initialize(params, startup_timeout, send_elicitation) + .await + .map_err(StartupOutcomeError::from)?; + + let tools = match list_tools_for_client(&server_name, &client, startup_timeout).await { + Ok(tools) => tools, + Err(error) => { + return Err(error.into()); + } + }; + + let managed = ManagedClient { + client: Arc::clone(&client), + tools, + tool_timeout: Some(tool_timeout), + tool_filter, + }; + + Ok(managed) +} + +async fn make_rmcp_client( + server_name: &str, + transport: McpServerTransportConfig, + store_mode: OAuthCredentialsStoreMode, +) -> Result { + match transport { McpServerTransportConfig::Stdio { command, args, @@ -751,12 +747,12 @@ async fn start_server_work( bearer_token_env_var, } => { let resolved_bearer_token = - match resolve_bearer_token(&server_name, bearer_token_env_var.as_deref()) { + match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) { Ok(token) => token, Err(error) => return Err(error.into()), }; RmcpClient::new_streamable_http_client( - &server_name, + server_name, &url, resolved_bearer_token, http_headers, @@ -766,37 +762,15 @@ async fn start_server_work( .await .map_err(StartupOutcomeError::from) } - }?; - - let client = Arc::new(client); - client - .initialize(params.clone(), Some(startup_timeout), send_elicitation) - .await - .map_err(StartupOutcomeError::from)?; - - let tools = match list_tools_for_client(&server_name, &client, startup_timeout).await { - Ok(tools) => tools, - Err(error) => { - return Err(error.into()); - } - }; - - let managed = ManagedClient { - client: Arc::clone(&client), - tools, - tool_timeout: Some(tool_timeout), - tool_filter, - }; - - Ok(managed) + } } async fn list_tools_for_client( server_name: &str, client: &Arc, - timeout: Duration, + timeout: Option, ) -> Result> { - let resp = client.list_tools(None, Some(timeout)).await?; + let resp = client.list_tools(None, timeout).await?; Ok(resp .tools .into_iter() diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index f459cdbe06..e28b726cab 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -227,8 +227,18 @@ impl EventProcessor for EventProcessorWithHumanOutput { EventMsg::TaskStarted(_) => { // Ignore. } - EventMsg::ElicitationRequest(_) => { - // Currently unused in exec mode; ignore. + EventMsg::ElicitationRequest(ev) => { + ts_msg!( + self, + "{} {}", + "elicitation request".style(self.magenta), + ev.server_name.style(self.dimmed) + ); + ts_msg!( + self, + "{}", + "auto-cancelling (not supported in exec mode)".style(self.dimmed) + ); } EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { let last_message = last_agent_message.as_deref(); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a003b4ff21..90ab673b14 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -30,6 +30,7 @@ use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; +use codex_protocol::approvals::ElicitationDecision; use codex_protocol::config_types::SandboxMode; use codex_protocol::user_input::UserInput; use event_processor_with_human_output::EventProcessorWithHumanOutput; @@ -401,9 +402,23 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // exit with a non-zero status for automation-friendly signaling. let mut error_seen = false; while let Some(event) = rx.recv().await { + let elicitation = match &event.msg { + EventMsg::ElicitationRequest(ev) => Some((ev.server_name.clone(), ev.id.clone())), + _ => None, + }; if matches!(event.msg, EventMsg::Error(_)) { error_seen = true; } + if let Some((server_name, request_id)) = elicitation { + // Automatically cancel elicitation requests in exec mode. + conversation + .submit(Op::ResolveElicitation { + server_name, + request_id, + decision: ElicitationDecision::Cancel, + }) + .await?; + } let shutdown: CodexStatus = event_processor.process_event(event); match shutdown { CodexStatus::Running => continue, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index c194a99399..2eee3b853e 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -209,6 +209,7 @@ async fn run_codex_tool_session_inner( continue; } EventMsg::ElicitationRequest(_) => { + // TODO: forward elicitation requests to the client? continue; } EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 767b0cbcad..e876c73837 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -59,6 +59,8 @@ pub struct ElicitationRequestEvent { pub server_name: String, pub id: RequestId, pub message: String, + // TODO: MCP servers can request we fill out a schema for the elicitation. We don't support + // this yet. // pub requested_schema: ElicitRequestParamsRequestedSchema, } diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 2e24b52ef6..ac617f3d29 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -21,3 +21,4 @@ pub use rmcp::model::ElicitationAction; pub use rmcp_client::Elicitation; pub use rmcp_client::ElicitationResponse; pub use rmcp_client::RmcpClient; +pub use rmcp_client::SendElicitation; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 893908a0a7..599335e44f 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -830,14 +830,14 @@ impl App { } => { let _ = tui.enter_alt_screen(); let paragraph = Paragraph::new(vec![ - Line::from(vec!["Server: ".into(), server_name.clone().bold()]), + Line::from(vec!["Server: ".into(), server_name.bold()]), Line::from(""), - Line::from(message.clone()), + Line::from(message), ]) .wrap(Wrap { trim: false }); self.overlay = Some(Overlay::new_static_with_renderables( vec![Box::new(paragraph)], - "E L I C I T".to_string(), + "E L I C I T A T I O N".to_string(), )); } }, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 8dd46b9ff4..77232719d8 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -386,21 +386,18 @@ impl From for ApprovalRequestState { request_id, message, } => { - let mut header: Vec> = Vec::new(); - header.push(Box::new(Line::from(vec![ - "Server: ".into(), - server_name.clone().bold(), - ]))); - header.push(Box::new(Line::from(""))); - header.push(Box::new( - Paragraph::new(Line::from(message.clone())).wrap(Wrap { trim: false }), - )); + let header = Paragraph::new(vec![ + Line::from(vec!["Server: ".into(), server_name.clone().bold()]), + Line::from(""), + Line::from(message), + ]) + .wrap(Wrap { trim: false }); Self { variant: ApprovalVariant::Elicitation { server_name, request_id, }, - header: Box::new(ColumnRenderable::with(header)), + header: Box::new(header), } } } @@ -444,7 +441,7 @@ enum ApprovalVariant { }, } -#[derive(Clone, Copy)] +#[derive(Clone)] enum ApprovalDecision { Review(ReviewDecision), Elicitation(ElicitationDecision), From 5ca8939f4b0e1277bdd5e6f31dfe83b5519758fc Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Thu, 20 Nov 2025 17:11:44 -0800 Subject: [PATCH 04/10] tests --- codex-rs/rmcp-client/tests/resources.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/codex-rs/rmcp-client/tests/resources.rs b/codex-rs/rmcp-client/tests/resources.rs index 2117f9b14c..b564064b79 100644 --- a/codex-rs/rmcp-client/tests/resources.rs +++ b/codex-rs/rmcp-client/tests/resources.rs @@ -2,6 +2,8 @@ use std::ffi::OsString; use std::path::PathBuf; use std::time::Duration; +use codex_rmcp_client::ElicitationAction; +use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::RmcpClient; use escargot::CargoBuild; use mcp_types::ClientCapabilities; @@ -55,7 +57,18 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> { .await?; client - .initialize(init_params(), Some(Duration::from_secs(5))) + .initialize( + init_params(), + Some(Duration::from_secs(5)), + Box::new(|_, _| { + Box::pin(async { + ElicitationResponse { + action: ElicitationAction::Accept, + content: Some(json!({})), + } + }) + }), + ) .await?; let list = client From a152b61e0a06e1ef40545f0a1d43762c5a8bc715 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Thu, 20 Nov 2025 17:14:04 -0800 Subject: [PATCH 05/10] fix --- codex-rs/core/src/rollout/policy.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 6270071b0a..4d5f709d25 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -85,7 +85,6 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::ItemCompleted(_) | EventMsg::AgentMessageContentDelta(_) | EventMsg::ReasoningContentDelta(_) - | EventMsg::ReasoningRawContentDelta(_) - | EventMsg::ElicitationRequest(_) => false, + | EventMsg::ReasoningRawContentDelta(_) => false, } } From 1ff8bce1d764db140dbfcc1fe8115bbde05710d0 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 10:41:24 -0800 Subject: [PATCH 06/10] simplify SendElicitation --- codex-rs/core/src/mcp_connection_manager.rs | 9 +++++---- codex-rs/rmcp-client/src/logging_client_handler.rs | 4 +++- codex-rs/rmcp-client/src/rmcp_client.rs | 7 +++---- codex-rs/rmcp-client/tests/resources.rs | 10 ++++++---- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 004a3dda47..6dc4f6a835 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -145,7 +145,7 @@ impl ElicitationRequestManager { let elicitation_requests = elicitation_requests.clone(); let tx_event = tx_event.clone(); let server_name = server_name.clone(); - Box::pin(async move { + async move { let (tx, rx) = oneshot::channel(); if let Ok(mut lock) = elicitation_requests.lock() { lock.insert((server_name.clone(), id.clone()), tx); @@ -160,9 +160,10 @@ impl ElicitationRequestManager { }), }) .await; - #[expect(clippy::unwrap_used)] - rx.await.unwrap() - }) + rx.await + .context("elicitation request channel closed unexpectedly") + } + .boxed() }) } } diff --git a/codex-rs/rmcp-client/src/logging_client_handler.rs b/codex-rs/rmcp-client/src/logging_client_handler.rs index 008875f938..0d2c3aaa97 100644 --- a/codex-rs/rmcp-client/src/logging_client_handler.rs +++ b/codex-rs/rmcp-client/src/logging_client_handler.rs @@ -45,7 +45,9 @@ impl ClientHandler for LoggingClientHandler { RequestId::String(id) => mcp_types::RequestId::String(id.to_string()), RequestId::Number(id) => mcp_types::RequestId::Integer(id), }; - Ok((self.send_elicitation)(id, request).await) + (self.send_elicitation)(id, request) + .await + .map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None)) } async fn on_cancelled( diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index 25b910dbbe..fe9f48d04e 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::ffi::OsString; use std::io; use std::path::PathBuf; -use std::pin::Pin; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; @@ -10,6 +9,7 @@ use std::time::Duration; use anyhow::Result; use anyhow::anyhow; use futures::FutureExt; +use futures::future::BoxFuture; use mcp_types::CallToolRequestParams; use mcp_types::CallToolResult; use mcp_types::InitializeRequestParams; @@ -84,10 +84,9 @@ enum ClientState { pub type Elicitation = CreateElicitationRequestParam; pub type ElicitationResponse = CreateElicitationResult; +/// Interface for sending elicitation requests to the UI and awaiting a response. pub type SendElicitation = Box< - dyn Fn(RequestId, Elicitation) -> Pin + Send>> - + Send - + Sync, + dyn Fn(RequestId, Elicitation) -> BoxFuture<'static, Result> + Send + Sync, >; /// MCP client implemented on top of the official `rmcp` SDK. diff --git a/codex-rs/rmcp-client/tests/resources.rs b/codex-rs/rmcp-client/tests/resources.rs index b564064b79..fda21d14e2 100644 --- a/codex-rs/rmcp-client/tests/resources.rs +++ b/codex-rs/rmcp-client/tests/resources.rs @@ -6,6 +6,7 @@ use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::RmcpClient; use escargot::CargoBuild; +use futures::FutureExt as _; use mcp_types::ClientCapabilities; use mcp_types::Implementation; use mcp_types::InitializeRequestParams; @@ -61,12 +62,13 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> { init_params(), Some(Duration::from_secs(5)), Box::new(|_, _| { - Box::pin(async { - ElicitationResponse { + async { + Ok(ElicitationResponse { action: ElicitationAction::Accept, content: Some(json!({})), - } - }) + }) + } + .boxed() }), ) .await?; From 39597f298de16dc21318abd892cef0b3e2f8574f Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 10:42:10 -0800 Subject: [PATCH 07/10] map_err --- codex-rs/core/src/mcp_connection_manager.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 6dc4f6a835..e1b05cef48 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -705,12 +705,9 @@ async fn start_server_task( .await .map_err(StartupOutcomeError::from)?; - let tools = match list_tools_for_client(&server_name, &client, startup_timeout).await { - Ok(tools) => tools, - Err(error) => { - return Err(error.into()); - } - }; + let tools = list_tools_for_client(&server_name, &client, startup_timeout) + .await + .map_err(StartupOutcomeError::from)?; let managed = ManagedClient { client: Arc::clone(&client), From 6860fcc70cfea237235373a6c4ab6cef6e8636ca Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 10:50:09 -0800 Subject: [PATCH 08/10] fix --- codex-rs/tui/src/chatwidget.rs | 22 +++++----------------- codex-rs/tui/src/chatwidget/interrupts.rs | 8 ++++---- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3cec2ebf55..9a2d9085c1 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -709,12 +709,11 @@ impl ChatWidget { ); } - fn on_elicitation_request(&mut self, id: String, ev: ElicitationRequestEvent) { - let id2 = id.clone(); + fn on_elicitation_request(&mut self, ev: ElicitationRequestEvent) { let ev2 = ev.clone(); self.defer_or_handle( - |q| q.push_elicitation(id, ev), - |s| s.handle_elicitation_request_now(id2, ev2), + |q| q.push_elicitation(ev), + |s| s.handle_elicitation_request_now(ev2), ); } @@ -1023,19 +1022,8 @@ impl ChatWidget { }); } - pub(crate) fn handle_elicitation_request_now( - &mut self, - id: String, - ev: ElicitationRequestEvent, - ) { + pub(crate) fn handle_elicitation_request_now(&mut self, ev: ElicitationRequestEvent) { self.flush_answer_stream_with_separator(); - if self.conversation_id.is_none() { - tracing::warn!("received elicitation request without a conversation id: {id}"); - self.add_error_message( - "Received an elicitation request before the session was ready.".to_string(), - ); - return; - } self.notify(Notification::ElicitationRequested { server_name: ev.server_name.clone(), @@ -1687,7 +1675,7 @@ impl ChatWidget { self.on_apply_patch_approval_request(id.unwrap_or_default(), ev) } EventMsg::ElicitationRequest(ev) => { - self.on_elicitation_request(id.unwrap_or_default(), ev); + self.on_elicitation_request(ev); } EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev), EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta), diff --git a/codex-rs/tui/src/chatwidget/interrupts.rs b/codex-rs/tui/src/chatwidget/interrupts.rs index 5c9d3d8c1b..dc1e683ea5 100644 --- a/codex-rs/tui/src/chatwidget/interrupts.rs +++ b/codex-rs/tui/src/chatwidget/interrupts.rs @@ -15,7 +15,7 @@ use super::ChatWidget; pub(crate) enum QueuedInterrupt { ExecApproval(String, ExecApprovalRequestEvent), ApplyPatchApproval(String, ApplyPatchApprovalRequestEvent), - Elicitation(String, ElicitationRequestEvent), + Elicitation(ElicitationRequestEvent), ExecBegin(ExecCommandBeginEvent), ExecEnd(ExecCommandEndEvent), McpBegin(McpToolCallBeginEvent), @@ -53,8 +53,8 @@ impl InterruptManager { .push_back(QueuedInterrupt::ApplyPatchApproval(id, ev)); } - pub(crate) fn push_elicitation(&mut self, id: String, ev: ElicitationRequestEvent) { - self.queue.push_back(QueuedInterrupt::Elicitation(id, ev)); + pub(crate) fn push_elicitation(&mut self, ev: ElicitationRequestEvent) { + self.queue.push_back(QueuedInterrupt::Elicitation(ev)); } pub(crate) fn push_exec_begin(&mut self, ev: ExecCommandBeginEvent) { @@ -84,7 +84,7 @@ impl InterruptManager { QueuedInterrupt::ApplyPatchApproval(id, ev) => { chat.handle_apply_patch_approval_now(id, ev) } - QueuedInterrupt::Elicitation(id, ev) => chat.handle_elicitation_request_now(id, ev), + QueuedInterrupt::Elicitation(ev) => chat.handle_elicitation_request_now(ev), QueuedInterrupt::ExecBegin(ev) => chat.handle_exec_begin_now(ev), QueuedInterrupt::ExecEnd(ev) => chat.handle_exec_end_now(ev), QueuedInterrupt::McpBegin(ev) => chat.handle_mcp_begin_now(ev), From 916c8aa2117864fd46d648c54c2de9c3c3bf5f99 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 12:13:33 -0800 Subject: [PATCH 09/10] simplify --- codex-rs/exec/src/lib.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 90ab673b14..07498875ce 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -402,23 +402,19 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // exit with a non-zero status for automation-friendly signaling. let mut error_seen = false; while let Some(event) = rx.recv().await { - let elicitation = match &event.msg { - EventMsg::ElicitationRequest(ev) => Some((ev.server_name.clone(), ev.id.clone())), - _ => None, - }; - if matches!(event.msg, EventMsg::Error(_)) { - error_seen = true; - } - if let Some((server_name, request_id)) = elicitation { + if let EventMsg::ElicitationRequest(ev) = &event.msg { // Automatically cancel elicitation requests in exec mode. conversation .submit(Op::ResolveElicitation { - server_name, - request_id, + server_name: ev.server_name.clone(), + request_id: ev.id.clone(), decision: ElicitationDecision::Cancel, }) .await?; } + if matches!(event.msg, EventMsg::Error(_)) { + error_seen = true; + } let shutdown: CodexStatus = event_processor.process_event(event); match shutdown { CodexStatus::Running => continue, From 4577cec931f97295ca35164e362cbf6b830d1c89 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Fri, 21 Nov 2025 14:29:13 -0800 Subject: [PATCH 10/10] review --- codex-rs/core/src/codex.rs | 9 +++--- codex-rs/exec/src/lib.rs | 4 +-- codex-rs/protocol/src/approvals.rs | 2 +- codex-rs/protocol/src/protocol.rs | 4 +-- codex-rs/tui/src/app.rs | 2 +- .../tui/src/bottom_pane/approval_overlay.rs | 30 +++++++++---------- codex-rs/tui/src/chatwidget.rs | 2 +- 7 files changed, 26 insertions(+), 27 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 2b5b86d4fd..4019da5493 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1455,7 +1455,6 @@ mod handlers { use crate::tasks::RegularTask; use crate::tasks::UndoTask; use crate::tasks::UserShellCommandTask; - use codex_protocol::approvals::ElicitationDecision; use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; @@ -1557,12 +1556,12 @@ mod handlers { sess: &Arc, server_name: String, request_id: RequestId, - decision: ElicitationDecision, + decision: codex_protocol::approvals::ElicitationAction, ) { let action = match decision { - ElicitationDecision::Accept => ElicitationAction::Accept, - ElicitationDecision::Decline => ElicitationAction::Decline, - ElicitationDecision::Cancel => ElicitationAction::Cancel, + codex_protocol::approvals::ElicitationAction::Accept => ElicitationAction::Accept, + codex_protocol::approvals::ElicitationAction::Decline => ElicitationAction::Decline, + codex_protocol::approvals::ElicitationAction::Cancel => ElicitationAction::Cancel, }; let response = ElicitationResponse { action, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 07498875ce..eb013b8280 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -30,7 +30,7 @@ use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; -use codex_protocol::approvals::ElicitationDecision; +use codex_protocol::approvals::ElicitationAction; use codex_protocol::config_types::SandboxMode; use codex_protocol::user_input::UserInput; use event_processor_with_human_output::EventProcessorWithHumanOutput; @@ -408,7 +408,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any .submit(Op::ResolveElicitation { server_name: ev.server_name.clone(), request_id: ev.id.clone(), - decision: ElicitationDecision::Cancel, + decision: ElicitationAction::Cancel, }) .await?; } diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index e876c73837..17d6c08734 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -66,7 +66,7 @@ pub struct ElicitationRequestEvent { #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "lowercase")] -pub enum ElicitationDecision { +pub enum ElicitationAction { Accept, Decline, Cancel, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index ed4589179d..8b328f93ef 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -37,7 +37,7 @@ use strum_macros::Display; use ts_rs::TS; pub use crate::approvals::ApplyPatchApprovalRequestEvent; -pub use crate::approvals::ElicitationDecision; +pub use crate::approvals::ElicitationAction; pub use crate::approvals::ExecApprovalRequestEvent; pub use crate::approvals::SandboxCommandAssessment; pub use crate::approvals::SandboxRiskLevel; @@ -163,7 +163,7 @@ pub enum Op { /// Request identifier from the MCP server. request_id: RequestId, /// User's decision for the request. - decision: ElicitationDecision, + decision: ElicitationAction, }, /// Append an entry to the persistent cross-session message history. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 599335e44f..6d3feca229 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -823,7 +823,7 @@ impl App { "E X E C".to_string(), )); } - ApprovalRequest::Elicitation { + ApprovalRequest::McpElicitation { server_name, message, .. diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 77232719d8..c6006a9ae7 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -16,7 +16,7 @@ use crate::key_hint::KeyBinding; use crate::render::highlight::highlight_bash_to_lines; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; -use codex_core::protocol::ElicitationDecision; +use codex_core::protocol::ElicitationAction; use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; @@ -50,7 +50,7 @@ pub(crate) enum ApprovalRequest { cwd: PathBuf, changes: HashMap, }, - Elicitation { + McpElicitation { server_name: String, request_id: RequestId, message: String, @@ -112,7 +112,7 @@ impl ApprovalOverlay { patch_options(), "Would you like to make the following edits?".to_string(), ), - ApprovalVariant::Elicitation { server_name, .. } => ( + ApprovalVariant::McpElicitation { server_name, .. } => ( elicitation_options(), format!("{server_name} needs your approval."), ), @@ -168,11 +168,11 @@ impl ApprovalOverlay { self.handle_patch_decision(id, *decision); } ( - ApprovalVariant::Elicitation { + ApprovalVariant::McpElicitation { server_name, request_id, }, - ApprovalDecision::Elicitation(decision), + ApprovalDecision::McpElicitation(decision), ) => { self.handle_elicitation_decision(server_name, request_id, *decision); } @@ -204,7 +204,7 @@ impl ApprovalOverlay { &self, server_name: &str, request_id: &RequestId, - decision: ElicitationDecision, + decision: ElicitationAction, ) { self.app_event_tx .send(AppEvent::CodexOp(Op::ResolveElicitation { @@ -279,14 +279,14 @@ impl BottomPaneView for ApprovalOverlay { ApprovalVariant::ApplyPatch { id, .. } => { self.handle_patch_decision(id, ReviewDecision::Abort); } - ApprovalVariant::Elicitation { + ApprovalVariant::McpElicitation { server_name, request_id, } => { self.handle_elicitation_decision( server_name, request_id, - ElicitationDecision::Cancel, + ElicitationAction::Cancel, ); } } @@ -381,7 +381,7 @@ impl From for ApprovalRequestState { header: Box::new(ColumnRenderable::with(header)), } } - ApprovalRequest::Elicitation { + ApprovalRequest::McpElicitation { server_name, request_id, message, @@ -393,7 +393,7 @@ impl From for ApprovalRequestState { ]) .wrap(Wrap { trim: false }); Self { - variant: ApprovalVariant::Elicitation { + variant: ApprovalVariant::McpElicitation { server_name, request_id, }, @@ -435,7 +435,7 @@ enum ApprovalVariant { ApplyPatch { id: String, }, - Elicitation { + McpElicitation { server_name: String, request_id: RequestId, }, @@ -444,7 +444,7 @@ enum ApprovalVariant { #[derive(Clone)] enum ApprovalDecision { Review(ReviewDecision), - Elicitation(ElicitationDecision), + McpElicitation(ElicitationAction), } #[derive(Clone)] @@ -507,19 +507,19 @@ fn elicitation_options() -> Vec { vec![ ApprovalOption { label: "Yes, provide the requested info".to_string(), - decision: ApprovalDecision::Elicitation(ElicitationDecision::Accept), + decision: ApprovalDecision::McpElicitation(ElicitationAction::Accept), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], }, ApprovalOption { label: "No, but continue without it".to_string(), - decision: ApprovalDecision::Elicitation(ElicitationDecision::Decline), + decision: ApprovalDecision::McpElicitation(ElicitationAction::Decline), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], }, ApprovalOption { label: "Cancel this request".to_string(), - decision: ApprovalDecision::Elicitation(ElicitationDecision::Cancel), + decision: ApprovalDecision::McpElicitation(ElicitationAction::Cancel), display_shortcut: Some(key_hint::plain(KeyCode::Esc)), additional_shortcuts: vec![key_hint::plain(KeyCode::Char('c'))], }, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9a2d9085c1..a9560e7242 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1029,7 +1029,7 @@ impl ChatWidget { server_name: ev.server_name.clone(), }); - let request = ApprovalRequest::Elicitation { + let request = ApprovalRequest::McpElicitation { server_name: ev.server_name, request_id: ev.id, message: ev.message,