mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why First-party async traits should expose their `Send` contracts explicitly without requiring `async_trait`. This completes the migration pattern established in #27303 and #27304. ## What changed - Replaced the remaining first-party `async_trait` traits with native return-position `impl Future + Send` where statically dispatched and explicit boxed `Send` futures where object safety is required. - Kept implementations behavior-preserving, outlining existing async bodies into inherent methods where that keeps the diff reviewable. - Removed all direct first-party `async-trait` dependencies and the workspace dependency declaration. - Added a cargo-deny policy that permits `async-trait` only through the remaining transitive wrapper crates. - Updated `rand` from 0.8.5 to 0.8.6 to resolve RUSTSEC-2026-0097 and keep the full cargo-deny check passing. ## Validation - `just test -p codex-exec-server`: 216 passed, 2 skipped. - `just test -p codex-model-provider`: 39 passed. - `just test -p codex-core` and `just test`: changed tests passed; remaining failures are environment-sensitive suites unrelated to this migration. - `cargo deny check` - `just fix` - `just fmt` - `cargo shear` - `just bazel-lock-check`
297 lines
9.6 KiB
Rust
297 lines
9.6 KiB
Rust
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<BedrockAuthMethod> {
|
|
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<SharedAuthProvider> {
|
|
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<String, std::env::VarError>,
|
|
) -> Option<String> {
|
|
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<String, std::env::VarError> + Copy,
|
|
) -> Result<String> {
|
|
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::<Vec<_>>();
|
|
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<Request, AuthError> {
|
|
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<String, std::env::VarError> {
|
|
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")
|
|
);
|
|
}
|
|
}
|