use std::sync::Arc; use codex_api::AuthError; use codex_api::AuthProvider; use codex_api::SharedAuthProvider; use codex_aws_auth::AwsAuthContext; use codex_aws_auth::AwsAuthError; use codex_aws_auth::AwsRequestToSign; use codex_client::Request; use codex_client::RequestBody; use codex_client::RequestCompression; use codex_login::auth::BedrockApiKeyAuth; use codex_model_provider_info::ModelProviderAwsAuthInfo; use codex_protocol::error::CodexErr; use codex_protocol::error::Result; use http::HeaderMap; use crate::BearerAuthProvider; use super::mantle::aws_auth_config; use super::mantle::region_from_config; const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK"; const AWS_REGION_ENV_VAR: &str = "AWS_REGION"; const AWS_DEFAULT_REGION_ENV_VAR: &str = "AWS_DEFAULT_REGION"; pub(super) enum BedrockAuthMethod { ManagedBearerToken { token: String, region: String }, EnvBearerToken { token: String, region: String }, AwsSdkAuth { context: AwsAuthContext }, } pub(super) async fn resolve_auth_method( managed_auth: Option<&BedrockApiKeyAuth>, aws: &ModelProviderAwsAuthInfo, ) -> Result { if let Some(managed_auth) = managed_auth { return Ok(BedrockAuthMethod::ManagedBearerToken { token: managed_auth.api_key.clone(), region: managed_auth.region.clone(), }); } if let Some(token) = non_empty_env_var_from(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR, std::env::var) { let region = bearer_token_region(aws, std::env::var)?; return Ok(BedrockAuthMethod::EnvBearerToken { token, region }); } let config = aws_auth_config(aws); let context = AwsAuthContext::load(config) .await .map_err(aws_auth_error_to_codex_error)?; Ok(BedrockAuthMethod::AwsSdkAuth { context }) } pub(super) async fn resolve_provider_auth( managed_auth: Option<&BedrockApiKeyAuth>, aws: &ModelProviderAwsAuthInfo, ) -> Result { match resolve_auth_method(managed_auth, aws).await? { BedrockAuthMethod::ManagedBearerToken { token, .. } | BedrockAuthMethod::EnvBearerToken { token, .. } => Ok(Arc::new(BearerAuthProvider { token: Some(token), account_id: None, is_fedramp_account: false, })), BedrockAuthMethod::AwsSdkAuth { context } => { Ok(Arc::new(BedrockMantleSigV4AuthProvider::new(context))) } } } fn non_empty_env_var_from( name: &'static str, env_var: impl Fn(&'static str) -> std::result::Result, ) -> Option { env_var(name) .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) } fn bearer_token_region( aws: &ModelProviderAwsAuthInfo, env_var: impl Fn(&'static str) -> std::result::Result + Copy, ) -> Result { region_from_config(aws) .or_else(|| non_empty_env_var_from(AWS_REGION_ENV_VAR, env_var)) .or_else(|| non_empty_env_var_from(AWS_DEFAULT_REGION_ENV_VAR, env_var)) .ok_or_else(|| { CodexErr::Fatal( "Amazon Bedrock bearer token auth requires \ `model_providers.amazon-bedrock.aws.region`, `AWS_REGION`, or `AWS_DEFAULT_REGION`" .to_string(), ) }) } fn aws_auth_error_to_codex_error(error: AwsAuthError) -> CodexErr { CodexErr::Fatal(format!("failed to resolve Amazon Bedrock auth: {error}")) } fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError { if error.is_retryable() { AuthError::Transient(error.to_string()) } else { AuthError::Build(error.to_string()) } } fn remove_headers_not_preserved_by_bedrock_mantle(headers: &mut HeaderMap) { // The Bedrock Mantle front door does not preserve legacy OpenAI // compatibility headers that use snake_case, such as `session_id` and // `thread_id`, before SigV4 verification. Signing that header class makes // richer Codex agent requests fail even though raw Responses requests work. let headers_to_remove = headers .keys() .filter(|name| name.as_str().contains('_')) .cloned() .collect::>(); for name in headers_to_remove { headers.remove(name); } } /// AWS SigV4 auth provider for Bedrock Mantle OpenAI-compatible requests. #[derive(Debug)] struct BedrockMantleSigV4AuthProvider { context: AwsAuthContext, } impl BedrockMantleSigV4AuthProvider { fn new(context: AwsAuthContext) -> Self { Self { context } } async fn apply_auth(&self, request: Request) -> std::result::Result { let mut request = request; remove_headers_not_preserved_by_bedrock_mantle(&mut request.headers); let prepared = request.prepare_body_for_send().map_err(AuthError::Build)?; let signed = self .context .sign(AwsRequestToSign { method: request.method.clone(), url: request.url.clone(), headers: prepared.headers.clone(), body: prepared.body_bytes(), }) .await .map_err(aws_auth_error_to_auth_error)?; request.url = signed.url; request.headers = signed.headers; request.body = prepared.body.map(RequestBody::Raw); request.compression = RequestCompression::None; Ok(request) } } impl AuthProvider for BedrockMantleSigV4AuthProvider { fn add_auth_headers(&self, _headers: &mut HeaderMap) {} fn apply_auth(&self, request: Request) -> codex_api::AuthProviderFuture<'_> { Box::pin(BedrockMantleSigV4AuthProvider::apply_auth(self, request)) } } #[cfg(test)] mod tests { use codex_api::AuthProvider; use http::HeaderValue; use pretty_assertions::assert_eq; use super::*; fn missing_env_var(_: &'static str) -> std::result::Result { Err(std::env::VarError::NotPresent) } #[test] fn bedrock_bearer_auth_prefers_configured_region_and_uses_header() { let token = "bedrock-api-key-test".to_string(); let region = bearer_token_region( &ModelProviderAwsAuthInfo { profile: None, region: Some(" us-west-2 ".to_string()), }, |name| match name { AWS_REGION_ENV_VAR => Ok("eu-west-1".to_string()), _ => Err(std::env::VarError::NotPresent), }, ) .expect("configured region should resolve"); let provider = BearerAuthProvider { token: Some(token), account_id: None, is_fedramp_account: false, }; let mut headers = http::HeaderMap::new(); provider.add_auth_headers(&mut headers); assert_eq!(region, "us-west-2"); assert!( headers .get(http::header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) .is_some_and(|value| value.starts_with("Bearer bedrock-api-key-")) ); } #[test] fn bedrock_bearer_auth_uses_aws_region_env() { let region = bearer_token_region( &ModelProviderAwsAuthInfo { profile: None, region: None, }, |name| match name { AWS_REGION_ENV_VAR => Ok(" eu-central-1 ".to_string()), _ => Err(std::env::VarError::NotPresent), }, ) .expect("AWS_REGION should resolve"); assert_eq!(region, "eu-central-1"); } #[test] fn bedrock_bearer_auth_uses_aws_default_region_env() { let region = bearer_token_region( &ModelProviderAwsAuthInfo { profile: None, region: None, }, |name| match name { AWS_DEFAULT_REGION_ENV_VAR => Ok("ap-northeast-1".to_string()), _ => Err(std::env::VarError::NotPresent), }, ) .expect("AWS_DEFAULT_REGION should resolve"); assert_eq!(region, "ap-northeast-1"); } #[test] fn bedrock_bearer_auth_rejects_missing_configured_region() { let err = bearer_token_region( &ModelProviderAwsAuthInfo { profile: None, region: None, }, missing_env_var, ) .expect_err("missing region should fail"); assert_eq!( err.to_string(), "Fatal error: Amazon Bedrock bearer token auth requires \ `model_providers.amazon-bedrock.aws.region`, `AWS_REGION`, or `AWS_DEFAULT_REGION`" ); } #[test] fn bedrock_mantle_sigv4_strips_headers_not_preserved_by_mantle() { let mut headers = HeaderMap::new(); headers.insert( "session_id", HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"), ); headers.insert( "thread_id", HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"), ); headers.insert( "future_identity_header", HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"), ); headers.insert( "x-client-request-id", HeaderValue::from_static("request-id"), ); remove_headers_not_preserved_by_bedrock_mantle(&mut headers); assert!(!headers.contains_key("session_id")); assert!(!headers.contains_key("thread_id")); assert!(!headers.contains_key("future_identity_header")); assert_eq!( headers .get("x-client-request-id") .and_then(|value| value.to_str().ok()), Some("request-id") ); } }