Files
codex/codex-rs/app-server/src/user_verification_response.rs
berkdeniz-oai b38942ce7c Preserve diagnostic reasons for MCP user verification failures (#51808)
## Why

User-verification cancellations previously discarded failure reasons, leaving MCP servers unable to distinguish user cancellation from unavailable approval, interruption, or invalid proof.

## What changed

- Carry a bounded `openai/userVerificationReason` value in response `_meta` for verification failures, declines, and cancellations across the TUI, app server, and MCP client.
- Preserve recognized reasons and classify invalid responses, missing or invalid proofs, response errors, closed response channels, interruptions, and unavailable approval.
- Discard unknown reasons and unrelated metadata. Keep proof material out of decline and cancellation responses, and keep display messages and raw error diagnostics out of response metadata.

## Testing

Add and update tests for typed failure propagation, metadata filtering, invalid proofs, response-channel failures, interrupted requests, and unavailable approval, including TUI RPC and native MCP response coverage.

GitOrigin-RevId: 6b7f855ce8eb13e759b5b86d0dc2b1afdfcff7b3
2026-10-07 19:28:14 +00:00

64 lines
2.3 KiB
Rust

//! Device proofs travel only in elicitation content, never in diagnostic metadata.
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::McpServerElicitationRequestResponse;
use codex_app_server_protocol::UserVerificationProof;
use codex_rmcp_client::UserVerificationReason;
use serde::Deserialize;
use tokio::sync::oneshot;
use crate::outgoing_message::ClientRequestResult;
use crate::server_request_error::is_turn_transition_server_request_error;
pub(crate) fn from_client_result(
result: Result<ClientRequestResult, oneshot::error::RecvError>,
) -> McpServerElicitationRequestResponse {
let mut reason = UserVerificationReason::InvalidResponse;
let response = match result {
Ok(Ok(value)) => serde_json::from_value::<McpServerElicitationRequestResponse>(value).ok(),
Ok(Err(error)) => {
reason = if is_turn_transition_server_request_error(&error) {
UserVerificationReason::Interrupted
} else {
UserVerificationReason::ResponseError
};
None
}
Err(_) => {
reason = UserVerificationReason::ResponseChannelClosed;
None
}
};
if let Some(mut response) = response {
let client_reason = UserVerificationReason::from_meta(response.meta.as_ref());
response.meta = None;
match response.action {
McpServerElicitationAction::Accept => {
if response
.content
.as_ref()
.is_some_and(|content| UserVerificationProof::deserialize(content).is_ok())
{
// The MCP boundary validates the encoded proof and size limits.
return response;
}
reason = UserVerificationReason::InvalidProof;
}
McpServerElicitationAction::Decline | McpServerElicitationAction::Cancel => {
response.content = None;
response.meta = client_reason.map(UserVerificationReason::into_meta);
return response;
}
}
}
McpServerElicitationRequestResponse {
action: McpServerElicitationAction::Cancel,
content: None,
meta: Some(reason.into_meta()),
}
}
#[cfg(test)]
#[path = "user_verification_response_tests.rs"]
mod tests;