mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +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:
1 parent
f158b31db5
commit
ff06ab7172
6 files changed
+235
-21
No files matched your search
@@ -6039,13 +6039,6 @@ async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std::
|
|||||||
.await?;
|
.await?;
|
||||||
let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf());
|
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;
|
let mcp_config = config.to_mcp_config(&plugins_manager).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mcp_config.client_elicitation_capability,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1472,7 +1472,11 @@ async fn host_owned_codex_apps_manager(
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn codex_apps_auth_elicitation_feature_disabled_returns_original_result() {
|
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 manager = host_owned_codex_apps_manager(&session, &turn_context).await;
|
||||||
let result = codex_apps_auth_failure_result();
|
let result = codex_apps_auth_failure_result();
|
||||||
let metadata = codex_apps_auth_failure_metadata();
|
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]
|
#[tokio::test]
|
||||||
async fn codex_apps_auth_elicitation_feature_enabled_requests_elicitation() {
|
async fn codex_apps_auth_elicitation_enabled_by_default_requests_elicitation() {
|
||||||
let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await;
|
let (session, turn_context, rx_event) = make_session_and_context_with_rx().await;
|
||||||
let manager = host_owned_codex_apps_manager(&session, &turn_context).await;
|
let manager = host_owned_codex_apps_manager(&session, &turn_context).await;
|
||||||
*session.active_turn.lock().await = Some(ActiveTurn::default());
|
*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 result = codex_apps_auth_failure_result();
|
||||||
let metadata = codex_apps_auth_failure_metadata();
|
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 items;
|
||||||
mod json_result;
|
mod json_result;
|
||||||
mod live_cli;
|
mod live_cli;
|
||||||
|
mod mcp_auth_elicitation;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod mcp_refresh_cleanup;
|
mod mcp_refresh_cleanup;
|
||||||
mod mcp_tool_exposure;
|
mod mcp_tool_exposure;
|
||||||
|
|||||||
@@ -1273,8 +1273,8 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::AuthElicitation,
|
id: Feature::AuthElicitation,
|
||||||
key: "auth_elicitation",
|
key: "auth_elicitation",
|
||||||
stage: Stage::UnderDevelopment,
|
stage: Stage::Stable,
|
||||||
default_enabled: false,
|
default_enabled: true,
|
||||||
},
|
},
|
||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::Personality,
|
id: Feature::Personality,
|
||||||
|
|||||||
@@ -316,9 +316,9 @@ fn tool_call_mcp_elicitation_is_stable_and_enabled_by_default() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auth_elicitation_is_under_development() {
|
fn auth_elicitation_is_stable_and_enabled_by_default() {
|
||||||
assert_eq!(Feature::AuthElicitation.stage(), Stage::UnderDevelopment);
|
assert_eq!(Feature::AuthElicitation.stage(), Stage::Stable);
|
||||||
assert_eq!(Feature::AuthElicitation.default_enabled(), false);
|
assert_eq!(Feature::AuthElicitation.default_enabled(), true);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
feature_for_key("auth_elicitation"),
|
feature_for_key("auth_elicitation"),
|
||||||
Some(Feature::AuthElicitation)
|
Some(Feature::AuthElicitation)
|
||||||
|
|||||||
Reference in new issue
Block a user