Support Codex Apps auth elicitations (#19193)

## Summary

- request URL-mode MCP elicitations when Codex Apps tool calls fail with
connector auth metadata
- route Codex Apps auth URL elicitations into the TUI app-link flow

## Test plan

- `just fmt`
- `cargo test -p codex-core mcp_tool_call::tests`
- `cargo test -p codex-mcp`
- `cargo test -p codex-tui bottom_pane::app_link_view::tests`
- `just fix -p codex-core`
- `just fix -p codex-mcp`
- `just fix -p codex-tui`

Also attempted broader local runs:

- `cargo test -p codex-core` fails in unrelated
config/request-permission/proxy-sensitive tests under the current Codex
Desktop environment.
- `cargo test -p codex-tui` fails in unrelated status
snapshots/trust-default tests because the ambient environment renders
workspace-write/network permission defaults.
This commit is contained in:
Matthew Zeng authored and GitHub committed 2026-05-06 07:18:00 +00:00
1 parent 22326e263c
commit f9a907aebe
25 files changed
+1748 -78

No files matched your search

+80
View File
@@ -37,6 +37,8 @@ use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::NetworkApprovalContext as AppServerNetworkApprovalContext;
@@ -2755,6 +2757,84 @@ async fn inactive_thread_permissions_approval_preserves_file_system_permissions(
);
}
#[tokio::test]
async fn inactive_thread_url_elicitation_routes_to_app_link() {
let app = make_test_app().await;
let thread_id = ThreadId::new();
let request = ServerRequest::McpServerElicitationRequest {
request_id: AppServerRequestId::Integer(9),
params: McpServerElicitationRequestParams {
thread_id: thread_id.to_string(),
turn_id: Some("turn-auth".to_string()),
server_name: "payments".to_string(),
request: McpServerElicitationRequest::Url {
meta: None,
message: "Review the payment details to continue.".to_string(),
url: "https://payments.example/checkout/123".to_string(),
elicitation_id: "payment-123".to_string(),
},
},
};
let Some(ThreadInteractiveRequest::AppLink(params)) = app
.interactive_request_for_thread_request(thread_id, &request)
.await
else {
panic!("expected app link request");
};
assert_eq!(params.title, "Action required");
assert_eq!(params.description, Some("Server: payments".to_string()));
assert_eq!(params.url, "https://payments.example/checkout/123");
assert_eq!(
params.elicitation_target,
Some(crate::bottom_pane::AppLinkElicitationTarget {
thread_id,
server_name: "payments".to_string(),
request_id: AppServerRequestId::Integer(9),
})
);
}
#[tokio::test]
async fn inactive_thread_invalid_url_elicitation_is_declined() {
let (app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
let thread_id = ThreadId::new();
let request = ServerRequest::McpServerElicitationRequest {
request_id: AppServerRequestId::Integer(10),
params: McpServerElicitationRequestParams {
thread_id: thread_id.to_string(),
turn_id: Some("turn-auth".to_string()),
server_name: "payments".to_string(),
request: McpServerElicitationRequest::Url {
meta: None,
message: "Review the payment details to continue.".to_string(),
url: "http://payments.example/checkout/123".to_string(),
elicitation_id: "payment-123".to_string(),
},
},
};
assert!(
app.interactive_request_for_thread_request(thread_id, &request)
.await
.is_none()
);
assert_matches!(
app_event_rx.try_recv(),
Ok(AppEvent::SubmitThreadOp {
thread_id: op_thread_id,
op: Op::ResolveElicitation {
server_name,
request_id: AppServerRequestId::Integer(10),
decision: codex_app_server_protocol::McpServerElicitationAction::Decline,
content: None,
meta: None,
},
}) if op_thread_id == thread_id && server_name == "payments"
);
}
#[tokio::test]
async fn inactive_thread_approval_badge_clears_after_turn_completion_notification() -> Result<()> {
let mut app = make_test_app().await;
+38 -19
View File
@@ -262,31 +262,47 @@ impl App {
}),
),
ServerRequest::McpServerElicitationRequest { request_id, params } => {
if let Some(request) = McpServerElicitationFormRequest::from_app_server_request(
if let Some(params) = AppLinkViewParams::from_url_app_server_request(
thread_id,
&params.server_name,
request_id.clone(),
params.clone(),
&params.request,
) {
Some(ThreadInteractiveRequest::AppLink(params))
} else if let Some(request) =
McpServerElicitationFormRequest::from_app_server_request(
thread_id,
request_id.clone(),
params.clone(),
)
{
Some(ThreadInteractiveRequest::McpServerElicitation(request))
} else {
Some(ThreadInteractiveRequest::Approval(
ApprovalRequest::McpElicitation {
thread_id,
thread_label,
server_name: params.server_name.clone(),
request_id: request_id.clone(),
message: match &params.request {
codex_app_server_protocol::McpServerElicitationRequest::Form {
message,
..
}
| codex_app_server_protocol::McpServerElicitationRequest::Url {
message,
..
} => message.clone(),
match &params.request {
codex_app_server_protocol::McpServerElicitationRequest::Form {
message,
..
} => Some(ThreadInteractiveRequest::Approval(
ApprovalRequest::McpElicitation {
thread_id,
thread_label,
server_name: params.server_name.clone(),
request_id: request_id.clone(),
message: message.clone(),
},
},
))
)),
codex_app_server_protocol::McpServerElicitationRequest::Url { .. } => {
self.app_event_tx.resolve_elicitation(
thread_id,
params.server_name.clone(),
request_id.clone(),
codex_app_server_protocol::McpServerElicitationAction::Decline,
/*content*/ None,
/*meta*/ None,
);
None
}
}
}
}
ServerRequest::PermissionsRequestApproval { params, .. } => Some(
@@ -304,6 +320,9 @@ impl App {
pub(super) fn push_thread_interactive_request(&mut self, request: ThreadInteractiveRequest) {
match request {
ThreadInteractiveRequest::AppLink(params) => {
self.chat_widget.open_app_link_view(params);
}
ThreadInteractiveRequest::Approval(request) => {
self.render_inactive_patch_preview(&request);
self.chat_widget.push_approval_request(request);