Files
codex/codex-rs/rmcp-client/src/startup_error.rs
T
jif ec8edd576c Refine enterprise MCP auth errors and request authority checks (#55691)
## 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
2026-10-20 22:33:14 +00:00

97 lines
4.1 KiB
Rust

use anyhow::Error;
use codex_exec_server::ExecServerError;
use rmcp::service::ClientInitializeError;
use rmcp::service::ServiceError;
use rmcp::transport::DynamicTransportError;
use rmcp::transport::auth::AuthError;
use rmcp::transport::streamable_http_client::StreamableHttpError;
use crate::ema_auth_policy::EmaAuthFailure;
use crate::ema_auth_policy::EmaInvalidGrantSource;
use crate::http_client_adapter::StreamableHttpClientAdapterError;
use crate::rmcp_client::ClientOperationError;
/// Returns whether an RMCP client error indicates that authentication is required.
///
/// This does not distinguish first-time login from reauthentication.
/// Streamable HTTP initialization errors are stored inside RMCP's dynamic
/// transport error, which is not part of the standard error source chain.
pub fn is_authentication_required_error(error: &Error) -> bool {
error.chain().any(|source| {
if let Some(error) = source.downcast_ref::<crate::EmaTokenSourceError>() {
return error.is_authentication_required();
}
source
.downcast_ref::<AuthError>()
.is_some_and(auth_error_requires_authentication)
|| source
.downcast_ref::<EmaAuthFailure>()
.is_some_and(|error| match error {
EmaAuthFailure::InvalidGrant {
grant_source: EmaInvalidGrantSource::EnterpriseIdentity,
}
| EmaAuthFailure::InsufficientUserAuthentication {
grant_source: EmaInvalidGrantSource::EnterpriseIdentity,
}
| EmaAuthFailure::IdentityClientRejected
| EmaAuthFailure::ReauthenticationRequired => true,
// Rejecting an ID-JAG does not invalidate the enterprise login itself.
EmaAuthFailure::InvalidGrant {
grant_source: EmaInvalidGrantSource::ResourceAuthorization,
}
| EmaAuthFailure::InsufficientUserAuthentication {
grant_source: EmaInvalidGrantSource::ResourceAuthorization,
} => false,
})
|| source
.downcast_ref::<ExecServerError>()
.is_some_and(exec_server_error_requires_authentication)
|| source
.downcast_ref::<ClientInitializeError>()
.is_some_and(|mut error| {
while let ClientInitializeError::LegacyFallbackFailed { fallback, .. } = error {
error = fallback;
}
matches!(
error,
ClientInitializeError::TransportError { error, .. }
if transport_error_requires_authentication(error)
)
})
|| source
.downcast_ref::<ClientOperationError>()
.is_some_and(|error| {
matches!(
error,
ClientOperationError::Service(ServiceError::TransportSend(error))
if transport_error_requires_authentication(error)
)
})
})
}
fn transport_error_requires_authentication(error: &DynamicTransportError) -> bool {
error
.error
.downcast_ref::<StreamableHttpError<StreamableHttpClientAdapterError>>()
.is_some_and(|error| match error {
StreamableHttpError::AuthRequired(_) => true,
StreamableHttpError::Auth(auth_error) => auth_error_requires_authentication(auth_error),
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(error)) => {
exec_server_error_requires_authentication(error)
}
_ => false,
})
}
fn exec_server_error_requires_authentication(error: &ExecServerError) -> bool {
matches!(error, ExecServerError::AuthenticationRequired(_))
}
fn auth_error_requires_authentication(error: &AuthError) -> bool {
matches!(
error,
AuthError::AuthorizationRequired | AuthError::TokenExpired
)
}