mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
[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:
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user