mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## 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
92 lines
2.9 KiB
Rust
92 lines
2.9 KiB
Rust
//! Validation for the user-verification elicitation extension.
|
|
|
|
use base64::Engine as _;
|
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
use rmcp::model::CustomRequest;
|
|
use rmcp::model::ElicitationAction;
|
|
use serde::Deserialize;
|
|
|
|
use crate::rmcp_client::Elicitation;
|
|
use crate::rmcp_client::ElicitationResponse;
|
|
|
|
pub(crate) const MODE: &str = "openai/userVerification";
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct RequestParams {
|
|
mode: String,
|
|
title: String,
|
|
description: String,
|
|
challenge: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct Proof<'a> {
|
|
credential_id: &'a str,
|
|
signature: &'a str,
|
|
}
|
|
|
|
pub(crate) fn parse_request(request: CustomRequest) -> Result<Elicitation, rmcp::ErrorData> {
|
|
let params = request
|
|
.params_as::<RequestParams>()
|
|
.ok()
|
|
.flatten()
|
|
.ok_or_else(invalid_request)?;
|
|
if params.mode != MODE
|
|
|| params.title.is_empty()
|
|
|| params.title.len() > 256
|
|
|| params.description.len() > 4096
|
|
|| !valid_bytes(¶ms.challenge, /*max_decoded_bytes*/ 4096)
|
|
{
|
|
return Err(invalid_request());
|
|
}
|
|
Ok(Elicitation::UserVerification {
|
|
title: params.title,
|
|
description: params.description,
|
|
challenge: params.challenge,
|
|
})
|
|
}
|
|
|
|
/// Accept only a bounded proof, and never return proof material for cancellation or rejection.
|
|
pub(crate) fn validate_response(mut response: ElicitationResponse) -> ElicitationResponse {
|
|
response.meta = None;
|
|
match response.action {
|
|
ElicitationAction::Accept => {
|
|
let proof = response
|
|
.content
|
|
.as_ref()
|
|
.and_then(|content| Proof::deserialize(content).ok());
|
|
if proof.is_some_and(|proof| {
|
|
!proof.credential_id.is_empty()
|
|
&& proof.credential_id.len() <= 1024
|
|
&& valid_bytes(proof.signature, /*max_decoded_bytes*/ 128)
|
|
}) {
|
|
return response;
|
|
}
|
|
tracing::warn!("user-verification acceptance omitted a valid proof; cancelling");
|
|
response.action = ElicitationAction::Cancel;
|
|
}
|
|
ElicitationAction::Decline | ElicitationAction::Cancel => {}
|
|
_ => response.action = ElicitationAction::Cancel,
|
|
}
|
|
response.content = None;
|
|
response
|
|
}
|
|
|
|
fn valid_bytes(encoded: &str, max_decoded_bytes: usize) -> bool {
|
|
!encoded.is_empty()
|
|
&& encoded.len() <= max_decoded_bytes.div_ceil(3) * 4
|
|
&& URL_SAFE_NO_PAD
|
|
.decode(encoded)
|
|
.is_ok_and(|bytes| !bytes.is_empty() && bytes.len() <= max_decoded_bytes)
|
|
}
|
|
|
|
fn invalid_request() -> rmcp::ErrorData {
|
|
rmcp::ErrorData::invalid_params("invalid user-verification request", /*data*/ None)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "user_verification_tests.rs"]
|
|
mod tests;
|