[codex] Enable auth elicitation by default (#28772)

## Summary

- enable `auth_elicitation` by default
- promote the feature to `Stable`, as default-enabled features must be
stable
- update the feature regression test to cover the new lifecycle state
and default

## Impact

Auth elicitation is now available without requiring users or clients to
opt in through configuration.

## Testing

- `just test -p codex-features` (52 passed)
This commit is contained in:
Matthew Zeng
2026-07-07 11:48:27 -07:00
committed by GitHub
parent f158b31db5
commit ff06ab7172
6 changed files with 235 additions and 21 deletions
+7 -7
View File
@@ -6039,13 +6039,6 @@ async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std::
.await?;
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
mcp_config.client_elicitation_capability,
ElicitationCapability::default()
);
let _ = config.features.enable(Feature::AuthElicitation);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
mcp_config.client_elicitation_capability,
@@ -6055,6 +6048,13 @@ async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std::
}
);
let _ = config.features.disable(Feature::AuthElicitation);
let mcp_config = config.to_mcp_config(&plugins_manager).await;
assert_eq!(
mcp_config.client_elicitation_capability,
ElicitationCapability::default()
);
Ok(())
}
+7 -9
View File
@@ -1472,7 +1472,11 @@ async fn host_owned_codex_apps_manager(
#[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);
let manager = host_owned_codex_apps_manager(&session, &turn_context).await;
let result = codex_apps_auth_failure_result();
let metadata = codex_apps_auth_failure_metadata();
@@ -1585,16 +1589,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;
let manager = 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();
@@ -0,0 +1,215 @@
#![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(())
}
+1
View File
@@ -65,6 +65,7 @@ mod image_rollout;
mod items;
mod json_result;
mod live_cli;
mod mcp_auth_elicitation;
#[cfg(unix)]
mod mcp_refresh_cleanup;
mod mcp_tool_exposure;
+2 -2
View File
@@ -1273,8 +1273,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,
+3 -3
View File
@@ -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)