mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## What changed - Classify token-source failures by credential kind and exchange stage, with sign-in or administrator guidance that omits provider error text. Treat resource authorization failures separately from enterprise identity failures when deciding whether authentication is required. - Add policy-bound request authority checks that return `RequestNotSent` when policy is updating or unavailable, while allowing admitted responses to finish. Support child authorities that isolate connection revocation and share response draining. - Recheck account validity after acquiring the credential invalidation lock and check cancellation before deleting a rejected credential. ## Testing Extend regression coverage for policy suspension during token renewal, completion of admitted requests, recovery with fresh authority, and transport retry classification. Expand provider-error tests to cover `unauthorized_client` and `invalid_client` while checking that credential text is not exposed. GitOrigin-RevId: c0097adc35cb5fdffde1466e4bc2b5ec78114a4b
122 lines
4.5 KiB
Rust
122 lines
4.5 KiB
Rust
use std::any::TypeId;
|
|
|
|
use codex_exec_server::ExecServerError;
|
|
use pretty_assertions::assert_eq;
|
|
use rmcp::transport::DynamicTransportError;
|
|
use rmcp::transport::streamable_http_client::AuthRequiredError;
|
|
use rmcp::transport::streamable_http_client::StreamableHttpError;
|
|
|
|
use crate::http_client_adapter::StreamableHttpClientAdapterError;
|
|
use crate::rmcp_client::ClientOperationError;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn retryable_initialize_error_includes_discovery_and_initialized_notification_context() {
|
|
let contexts = [
|
|
"send discover request",
|
|
"send initialize request",
|
|
"send initialized notification",
|
|
"receive initialize response",
|
|
];
|
|
|
|
assert_eq!(
|
|
contexts.map(|context| {
|
|
RmcpClient::is_retryable_client_initialize_error(&retryable_initialize_error(context))
|
|
}),
|
|
[true, true, true, false],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn streamable_http_retry_classifies_transport_and_auth_errors() {
|
|
let errors = [
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::RequestNotSent("enterprise policy is updating".to_string()),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::HttpRequest("error sending request for url".to_string()),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::Server {
|
|
code: JSON_RPC_INTERNAL_ERROR_CODE,
|
|
message: "http/request failed: error sending request for url".to_string(),
|
|
},
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::Protocol(
|
|
"http response stream `http-1` failed: exec-server transport disconnected"
|
|
.to_string(),
|
|
),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::Protocol(
|
|
"http response stream `http-1` received seq 2, expected 1".to_string(),
|
|
),
|
|
)),
|
|
StreamableHttpError::UnexpectedServerResponse("HTTP 502: upstream failure".into()),
|
|
StreamableHttpError::UnexpectedServerResponse("HTTP 400: bad request".into()),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::Auth(
|
|
codex_api::AuthError::Transient("credential refresh unavailable".to_string()),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::Auth(
|
|
codex_api::AuthError::Build("invalid authentication".to_string()),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::Auth(
|
|
codex_api::AuthError::Policy(codex_http_client::NetworkPolicyDenied::Revoked),
|
|
)),
|
|
];
|
|
|
|
assert_eq!(
|
|
errors.map(|error| RmcpClient::is_retryable_streamable_http_error(&error)),
|
|
[
|
|
false, true, true, true, false, true, false, true, false, false
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn startup_http_authentication_challenges_require_reauthorization() {
|
|
let transport_error = || {
|
|
DynamicTransportError::from_parts(
|
|
"streamable_http",
|
|
TypeId::of::<()>(),
|
|
Box::new(
|
|
StreamableHttpError::<StreamableHttpClientAdapterError>::AuthRequired(
|
|
AuthRequiredError::new("Bearer error=\"invalid_token\"".to_string()),
|
|
),
|
|
),
|
|
)
|
|
};
|
|
let errors = [
|
|
anyhow::Error::new(rmcp::service::ClientInitializeError::TransportError {
|
|
error: transport_error(),
|
|
context: "send initialize request".into(),
|
|
}),
|
|
anyhow::Error::new(ClientOperationError::from(
|
|
rmcp::service::ServiceError::TransportSend(transport_error()),
|
|
)),
|
|
];
|
|
|
|
for error in errors {
|
|
assert!(crate::startup_error::is_authentication_required_error(
|
|
&error
|
|
));
|
|
}
|
|
}
|
|
|
|
fn retryable_initialize_error(context: &'static str) -> rmcp::service::ClientInitializeError {
|
|
rmcp::service::ClientInitializeError::TransportError {
|
|
error: DynamicTransportError::from_parts(
|
|
"streamable_http",
|
|
TypeId::of::<()>(),
|
|
Box::new(StreamableHttpError::Client(
|
|
StreamableHttpClientAdapterError::HttpRequest(ExecServerError::HttpRequest(
|
|
"error sending request for url".to_string(),
|
|
)),
|
|
)),
|
|
),
|
|
context: context.into(),
|
|
}
|
|
}
|