mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
## 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
164 lines
5.3 KiB
Rust
164 lines
5.3 KiB
Rust
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn user_verification_request_preserves_signed_bytes_and_display_text() {
|
|
assert_eq!(
|
|
parse_request(CustomRequest::new(
|
|
"openai/elicitation/create",
|
|
Some(json!({
|
|
"mode": MODE,
|
|
"title": "Approve purchase",
|
|
"description": "Pay $200 to Example Store",
|
|
"challenge": "AQID",
|
|
})),
|
|
))
|
|
.unwrap(),
|
|
Elicitation::UserVerification {
|
|
meta: None,
|
|
title: "Approve purchase".to_string(),
|
|
description: "Pay $200 to Example Store".to_string(),
|
|
challenge: "AQID".to_string(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn user_verification_request_metadata_matches_mcp_elicitation() {
|
|
for meta in [
|
|
None,
|
|
Some(json!(null)),
|
|
Some(json!({})),
|
|
Some(json!({"example/display": {"label": "Operation", "details": [1, true, null]}})),
|
|
Some(json!(true)),
|
|
Some(json!(7)),
|
|
Some(json!("scalar")),
|
|
Some(json!([])),
|
|
] {
|
|
let mut params = json!({
|
|
"mode": MODE,
|
|
"title": "Approve",
|
|
"description": "Review operation",
|
|
"challenge": "AQID",
|
|
});
|
|
let mut mcp_params = json!({
|
|
"mode": "form",
|
|
"message": "Approve",
|
|
"requestedSchema": {"type": "object", "properties": {}},
|
|
});
|
|
if let Some(meta) = &meta {
|
|
params["_meta"] = meta.clone();
|
|
mcp_params["_meta"] = meta.clone();
|
|
}
|
|
let mcp_request = serde_json::from_value::<rmcp::model::ElicitRequestParams>(mcp_params);
|
|
let expected = if mcp_request.is_ok() {
|
|
Ok(Elicitation::UserVerification {
|
|
meta: meta.filter(serde_json::Value::is_object),
|
|
title: "Approve".into(),
|
|
description: "Review operation".into(),
|
|
challenge: "AQID".into(),
|
|
})
|
|
} else {
|
|
Err(invalid_request())
|
|
};
|
|
assert_eq!(
|
|
parse_request(CustomRequest::new(
|
|
"openai/elicitation/create",
|
|
Some(params),
|
|
)),
|
|
expected,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn user_verification_rejects_invalid_or_unbounded_requests_without_echoing_values() {
|
|
for (field, value) in [
|
|
("mode", json!("unknown")),
|
|
("title", json!("")),
|
|
("title", json!("x".repeat(257))),
|
|
("description", json!("x".repeat(4097))),
|
|
("challenge", json!("")),
|
|
("challenge", json!("not base64url!")),
|
|
("challenge", json!(URL_SAFE_NO_PAD.encode(vec![0; 4097]))),
|
|
("unexpected", json!("secret")),
|
|
] {
|
|
let mut params = json!({
|
|
"mode": MODE, "title": "Approve", "description": "", "challenge": "AQID"
|
|
});
|
|
params[field] = value;
|
|
assert_eq!(
|
|
parse_request(CustomRequest::new(
|
|
"openai/elicitation/create",
|
|
Some(params)
|
|
)),
|
|
Err(invalid_request()),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn user_verification_acceptance_returns_proof_only_in_content() {
|
|
let content = Some(json!({"credentialId": "credential:用户/1=", "signature": "BAUG"}));
|
|
assert_eq!(
|
|
validate_response(ElicitationResponse {
|
|
action: ElicitationAction::Accept,
|
|
content: content.clone(),
|
|
meta: Some(json!({"untrusted": "ignored"})),
|
|
}),
|
|
ElicitationResponse {
|
|
action: ElicitationAction::Accept,
|
|
content,
|
|
meta: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn user_verification_cancels_acceptance_without_a_valid_bounded_proof() {
|
|
for content in [
|
|
None,
|
|
Some(json!({})),
|
|
Some(json!({"credentialId": "AQID", "signature": ""})),
|
|
Some(json!({"credentialId": "AQID", "signature": "BAUG="})),
|
|
Some(json!({"credentialId": "é".repeat(513), "signature": "BAUG"})),
|
|
Some(json!({"credentialId": "", "signature": "BAUG"})),
|
|
Some(json!({"credentialId": "AQID", "signature": URL_SAFE_NO_PAD.encode(vec![0; 129])})),
|
|
Some(json!({"credentialId": "AQID", "signature": "BAUG", "extra": true})),
|
|
] {
|
|
assert_eq!(
|
|
validate_response(ElicitationResponse {
|
|
action: ElicitationAction::Accept,
|
|
content,
|
|
meta: Some(json!({"openai/userVerificationReason": "userCancelled"})),
|
|
}),
|
|
ElicitationResponse {
|
|
action: ElicitationAction::Cancel,
|
|
content: None,
|
|
meta: Some(json!({"openai/userVerificationReason": "invalidProof"})),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn user_verification_decline_and_cancel_discard_proof_material() {
|
|
for action in [ElicitationAction::Decline, ElicitationAction::Cancel] {
|
|
assert_eq!(
|
|
validate_response(ElicitationResponse {
|
|
action: action.clone(),
|
|
content: Some(json!({"credentialId": "AQID", "signature": "BAUG"})),
|
|
meta: Some(
|
|
json!({"secret": "discarded", "openai/userVerificationReason": "timeout"})
|
|
),
|
|
}),
|
|
ElicitationResponse {
|
|
action,
|
|
content: None,
|
|
meta: Some(json!({"openai/userVerificationReason": "timeout"}))
|
|
}
|
|
);
|
|
}
|
|
}
|