Files
codex/codex-rs/rmcp-client/src/user_verification_tests.rs
T
riley-oai 3cd6004dc4 Add capability-gated MCP user-verification handling (#43289)
## What changed

- Handle `openai/userVerification` requests through `openai/elicitation/create` when the client advertises `userVerification` support.
- Validate request fields, size limits, and base64url encoding. Require a correctly shaped, bounded proof for acceptance; cancel invalid acceptance responses and discard proof material on decline or cancellation.
- Handle request and service cancellation, including cancellation received before request dispatch. Bound early-cancellation storage and cancel new verifications when capacity is exceeded.
- Keep verification inactive in Codex: cancel requests without prompting until the typed app-server and UI path is available.

## Testing

Add tests for request and proof validation, malformed modes, cancellation races and capacity limits, timeout-pause cleanup, and independent concurrent requests. Add integration coverage showing that verification does not prompt or prevent tool and turn completion before activation.

GitOrigin-RevId: 6313c1be40fae0b33cc3ce0f8fe025b4124ed2ef
2026-09-06 21:41:16 +00:00

113 lines
3.6 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 {
title: "Approve purchase".to_string(),
description: "Pay $200 to Example Store".to_string(),
challenge: "AQID".to_string(),
}
);
}
#[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: None,
}),
ElicitationResponse {
action: ElicitationAction::Cancel,
content: None,
meta: None,
}
);
}
}
#[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"})),
}),
ElicitationResponse {
action,
content: None,
meta: None
}
);
}
}