From db4e9c635dd2860b97d903306795d7ae009f10e8 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 17 Jun 2026 11:36:03 -0700 Subject: [PATCH 1/3] Enable auth elicitation by default --- codex-rs/features/src/lib.rs | 4 ++-- codex-rs/features/src/tests.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index b887237a18cb..4cb8c9791eff 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -1188,8 +1188,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::AuthElicitation, key: "auth_elicitation", - stage: Stage::UnderDevelopment, - default_enabled: false, + stage: Stage::Stable, + default_enabled: true, }, FeatureSpec { id: Feature::Personality, diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 2e9d3203c019..aecc200442ab 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -316,9 +316,9 @@ fn tool_call_mcp_elicitation_is_stable_and_enabled_by_default() { } #[test] -fn auth_elicitation_is_under_development() { - assert_eq!(Feature::AuthElicitation.stage(), Stage::UnderDevelopment); - assert_eq!(Feature::AuthElicitation.default_enabled(), false); +fn auth_elicitation_is_stable_and_enabled_by_default() { + assert_eq!(Feature::AuthElicitation.stage(), Stage::Stable); + assert_eq!(Feature::AuthElicitation.default_enabled(), true); assert_eq!( feature_for_key("auth_elicitation"), Some(Feature::AuthElicitation) From d3f1e2032632f0428bf7ff5822b53f6bb5b6c777 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 17 Jun 2026 15:20:57 -0700 Subject: [PATCH 2/3] Add default auth elicitation coverage --- codex-rs/core/src/config/config_tests.rs | 12 +- codex-rs/core/src/mcp_tool_call_tests.rs | 16 +- .../core/tests/suite/mcp_auth_elicitation.rs | 216 ++++++++++++++++++ codex-rs/core/tests/suite/mod.rs | 1 + 4 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 codex-rs/core/tests/suite/mcp_auth_elicitation.rs diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 47aa8910dacc..cb5e8adf5d1b 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5624,17 +5624,17 @@ async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std:: let mcp_config = config.to_mcp_config(&plugins_manager).await; assert_eq!( mcp_config.client_elicitation_capability, - ElicitationCapability::default() + ElicitationCapability { + form: Some(FormElicitationCapability::default()), + url: Some(UrlElicitationCapability::default()), + } ); - let _ = config.features.enable(Feature::AuthElicitation); + let _ = config.features.disable(Feature::AuthElicitation); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert_eq!( mcp_config.client_elicitation_capability, - ElicitationCapability { - form: Some(FormElicitationCapability::default()), - url: Some(UrlElicitationCapability::default()), - } + ElicitationCapability::default() ); Ok(()) diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 56d1ff9b6cd2..15ffdf90ae74 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1292,7 +1292,11 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: #[tokio::test] async fn codex_apps_auth_elicitation_feature_disabled_returns_original_result() { - let (session, turn_context, rx_event) = make_session_and_context_with_rx().await; + let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await; + let mut features = Features::with_defaults(); + features.disable(Feature::AuthElicitation); + let mutable_turn_context = Arc::get_mut(&mut turn_context).expect("single turn context ref"); + Arc::make_mut(&mut mutable_turn_context.config).features = ManagedFeatures::from(features); install_host_owned_codex_apps_manager(&session, &turn_context).await; let result = codex_apps_auth_failure_result(); let metadata = codex_apps_auth_failure_metadata(); @@ -1400,16 +1404,10 @@ async fn codex_apps_auth_elicitation_granular_mcp_disabled_returns_original_resu } #[tokio::test] -async fn codex_apps_auth_elicitation_feature_enabled_requests_elicitation() { - let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await; +async fn codex_apps_auth_elicitation_enabled_by_default_requests_elicitation() { + let (session, turn_context, rx_event) = make_session_and_context_with_rx().await; install_host_owned_codex_apps_manager(&session, &turn_context).await; *session.active_turn.lock().await = Some(ActiveTurn::default()); - let mut features = Features::with_defaults(); - features.enable(Feature::AuthElicitation); - { - let turn_context = Arc::get_mut(&mut turn_context).expect("single turn context ref"); - Arc::make_mut(&mut turn_context.config).features = ManagedFeatures::from(features); - } let result = codex_apps_auth_failure_result(); let metadata = codex_apps_auth_failure_metadata(); diff --git a/codex-rs/core/tests/suite/mcp_auth_elicitation.rs b/codex-rs/core/tests/suite/mcp_auth_elicitation.rs new file mode 100644 index 000000000000..72c14f0d9e69 --- /dev/null +++ b/codex-rs/core/tests/suite/mcp_auth_elicitation.rs @@ -0,0 +1,216 @@ +#![cfg(not(target_os = "windows"))] +#![allow(clippy::unwrap_used)] + +use anyhow::Result; +use codex_core::config::Constrained; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_protocol::approvals::ElicitationRequest; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::ElicitationAction; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::user_input::UserInput; +use core_test_support::PathExt; +use core_test_support::apps_test_server::AppsTestServer; +use core_test_support::apps_test_server::SEARCH_CALENDAR_CREATE_TOOL; +use core_test_support::apps_test_server::SEARCH_CALENDAR_NAMESPACE; +use core_test_support::apps_test_server::search_capable_apps_builder; +use core_test_support::responses::ev_assistant_message; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_function_call_with_namespace; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_sse_sequence; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::skip_if_no_network; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use serde_json::Value; +use serde_json::json; +use wiremock::Mock; +use wiremock::Request; +use wiremock::Respond; +use wiremock::ResponseTemplate; +use wiremock::matchers::body_partial_json; +use wiremock::matchers::method; +use wiremock::matchers::path_regex; + +#[derive(Clone, Copy)] +struct AuthFailureResponder; + +impl Respond for AuthFailureResponder { + fn respond(&self, request: &Request) -> ResponseTemplate { + let body: Value = + serde_json::from_slice(&request.body).expect("tools/call request should be valid JSON"); + let id = body.get("id").cloned().unwrap_or(Value::Null); + + ResponseTemplate::new(/*status*/ 200).set_body_json(json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "content": [{ + "type": "text", + "text": "Connector reauthentication required", + }], + "isError": true, + "_meta": { + "_codex_apps": { + "connector_auth_failure": { + "is_auth_failure": true, + "auth_reason": "reauthentication_required", + "connector_id": "calendar", + "link_id": "link_123", + "error_code": "UNAUTHORIZED", + "error_http_status_code": 401, + "error_action": "TRIGGER_REAUTHENTICATION", + }, + }, + }, + }, + })) + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn codex_apps_auth_failure_requests_elicitation_by_default() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let apps_server = AppsTestServer::mount_searchable(&server).await?; + Mock::given(method("POST")) + .and(path_regex("^/api/codex/apps/?$")) + .and(body_partial_json(json!({ + "method": "tools/call", + "params": { + "name": "calendar_create_event", + }, + }))) + .respond_with(AuthFailureResponder) + .with_priority(/*p*/ 1) + .mount(&server) + .await; + + let call_id = "calendar-auth-call"; + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_function_call_with_namespace( + call_id, + SEARCH_CALENDAR_NAMESPACE, + SEARCH_CALENDAR_CREATE_TOOL, + &json!({ + "title": "Lunch", + "starts_at": "2026-06-18T12:00:00Z", + }) + .to_string(), + ), + ev_completed("resp-1"), + ]), + sse(vec![ + ev_response_created("resp-2"), + ev_assistant_message("msg-1", "done"), + ev_completed("resp-2"), + ]), + ], + ) + .await; + + let mut builder = + search_capable_apps_builder(apps_server.chatgpt_base_url).with_config(|config| { + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + let user_config_path = config.codex_home.join("config.toml").abs(); + let user_config = toml::from_str( + r#" +[apps.calendar] +default_tools_approval_mode = "auto" +"#, + ) + .expect("apps config should parse"); + config.config_layer_stack = config + .config_layer_stack + .with_user_config(&user_config_path, user_config); + }); + let test = builder.build(&server).await?; + + test.codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "Use [$calendar](app://calendar) to create a calendar event.".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + + let EventMsg::ElicitationRequest(request) = wait_for_event(&test.codex, |event| { + matches!( + event, + EventMsg::ElicitationRequest(_) | EventMsg::TurnComplete(_) + ) + }) + .await + else { + panic!("default auth elicitation should prompt before completing the turn"); + }; + + assert_eq!(request.server_name, CODEX_APPS_MCP_SERVER_NAME); + assert_eq!( + request.id, + codex_protocol::mcp::RequestId::String(format!("codex_apps_auth_{call_id}")) + ); + assert_eq!( + request.request, + ElicitationRequest::Url { + meta: Some(json!({ + "_codex_apps": { + "connector_auth_failure": { + "is_auth_failure": true, + "connector_id": "calendar", + "connector_name": "Calendar", + "install_url": "https://chatgpt.com/apps/calendar/calendar", + "auth_reason": "reauthentication_required", + "link_id": "link_123", + "error_code": "UNAUTHORIZED", + "error_http_status_code": 401, + "error_action": "TRIGGER_REAUTHENTICATION", + }, + }, + })), + message: "Reconnect Calendar on ChatGPT to restore access for this request." + .to_string(), + url: "https://chatgpt.com/apps/calendar/calendar".to_string(), + elicitation_id: format!("codex_apps_auth_{call_id}"), + } + ); + + test.codex + .submit(Op::ResolveElicitation { + server_name: request.server_name, + request_id: request.id, + decision: ElicitationAction::Accept, + content: None, + meta: None, + }) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + let requests = responses.requests(); + assert_eq!(requests.len(), 2); + let output = requests[1] + .function_call_output_text(call_id) + .expect("follow-up request should contain the auth elicitation result"); + assert!(output.contains( + "Authentication for Calendar was requested and accepted. Retry this tool call now." + )); + assert!(!output.contains("Connector reauthentication required")); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 87dc0ffce3d5..a23f49a279be 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -65,6 +65,7 @@ mod image_rollout; mod items; mod json_result; mod live_cli; +mod mcp_auth_elicitation; mod mcp_turn_metadata; mod model_overrides; mod model_runtime_selectors; From bf8196d9d386d44db58e82cece7295c6a46dca73 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Tue, 7 Jul 2026 10:55:44 -0700 Subject: [PATCH 3/3] Run auth elicitation integration test on Windows --- codex-rs/core/tests/suite/mcp_auth_elicitation.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/codex-rs/core/tests/suite/mcp_auth_elicitation.rs b/codex-rs/core/tests/suite/mcp_auth_elicitation.rs index 72c14f0d9e69..735006d49524 100644 --- a/codex-rs/core/tests/suite/mcp_auth_elicitation.rs +++ b/codex-rs/core/tests/suite/mcp_auth_elicitation.rs @@ -1,4 +1,3 @@ -#![cfg(not(target_os = "windows"))] #![allow(clippy::unwrap_used)] use anyhow::Result;