Files
codex/codex-rs/model-provider/src/auth.rs
T
Celia Chen 06afd63f4a feat: add Bedrock API key as a managed auth mode (#27443)
## Why

Codex needs to manage Amazon Bedrock API key credentials through the
existing auth lifecycle instead of introducing a separate auth manager
or provider-specific credential file. Treating Bedrock API key login as
a primary auth mode gives it the same persistence, keyring, reload, and
logout behavior as the existing OpenAI API key and ChatGPT modes.

The credential is valid only for the `amazon-bedrock` model provider.
OpenAI-compatible providers must reject this auth mode rather than
treating the Bedrock key as an OpenAI bearer token.

## What changed

- Added `bedrockApiKey` as an app-server `AuthMode` and
`CodexAuth::BedrockApiKey` as a primary `AuthManager` mode.
- Added `BedrockApiKeyAuth`, containing the API key and AWS region, to
the existing `AuthDotJson` payload stored in `$CODEX_HOME/auth.json` or
the configured keyring backend.
- Added `login_with_bedrock_api_key(...)`, parallel to
`login_with_api_key(...)`, which replaces the current stored login with
Bedrock credentials.
- Reused generic auth reload and logout behavior instead of adding a
Bedrock-specific auth manager or logout path.
- Updated login restrictions, status reporting, diagnostics, telemetry
classification, generated app-server schemas, and auth fixtures for the
new mode.
- Added explicit errors when Bedrock API key auth is selected with an
OpenAI-compatible model provider.

This PR establishes managed storage and auth-mode behavior. Routing the
managed key and region into Amazon Bedrock requests will be in follow-up
PRs.
2026-06-10 20:42:38 -07:00

169 lines
5.7 KiB
Rust

use std::sync::Arc;
use codex_agent_identity::AgentIdentityKey;
use codex_agent_identity::AgentTaskAuthorizationTarget;
use codex_agent_identity::authorization_header_for_agent_task;
use codex_api::AuthProvider;
use codex_api::SharedAuthProvider;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::error::CodexErr;
use http::HeaderMap;
use http::HeaderValue;
use crate::bearer_auth_provider::BearerAuthProvider;
const BEDROCK_API_KEY_UNSUPPORTED_MESSAGE: &str =
"Bedrock API key auth is only supported by the Amazon Bedrock model provider";
#[derive(Clone, Debug)]
struct AgentIdentityAuthProvider {
auth: codex_login::auth::AgentIdentityAuth,
}
impl AuthProvider for AgentIdentityAuthProvider {
fn add_auth_headers(&self, headers: &mut HeaderMap) {
let record = self.auth.record();
let header_value = authorization_header_for_agent_task(
AgentIdentityKey {
agent_runtime_id: &record.agent_runtime_id,
private_key_pkcs8_base64: &record.agent_private_key,
},
AgentTaskAuthorizationTarget {
agent_runtime_id: &record.agent_runtime_id,
task_id: self.auth.process_task_id(),
},
)
.map_err(std::io::Error::other);
if let Ok(header_value) = header_value
&& let Ok(header) = HeaderValue::from_str(&header_value)
{
let _ = headers.insert(http::header::AUTHORIZATION, header);
}
if let Ok(header) = HeaderValue::from_str(self.auth.account_id()) {
let _ = headers.insert("ChatGPT-Account-ID", header);
}
if self.auth.is_fedramp_account() {
let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true"));
}
}
}
// Some providers are meant to send no auth headers. Examples include local OSS
// providers and custom test providers with `requires_openai_auth = false`.
#[derive(Clone, Debug)]
struct UnauthenticatedAuthProvider;
impl AuthProvider for UnauthenticatedAuthProvider {
fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
}
pub fn unauthenticated_auth_provider() -> SharedAuthProvider {
Arc::new(UnauthenticatedAuthProvider)
}
/// Returns the provider-scoped auth manager when this provider uses command-backed auth.
///
/// Providers without custom auth continue using the caller-supplied base manager, when present.
pub(crate) fn auth_manager_for_provider(
auth_manager: Option<Arc<AuthManager>>,
provider: &ModelProviderInfo,
) -> Option<Arc<AuthManager>> {
match provider.auth.clone() {
Some(config) => Some(AuthManager::external_bearer_only(config)),
None => auth_manager,
}
}
pub(crate) fn resolve_provider_auth(
auth: Option<&CodexAuth>,
provider: &ModelProviderInfo,
) -> codex_protocol::error::Result<SharedAuthProvider> {
if matches!(auth, Some(CodexAuth::BedrockApiKey(_))) {
return Err(CodexErr::UnsupportedOperation(
BEDROCK_API_KEY_UNSUPPORTED_MESSAGE.to_string(),
));
}
if let Some(auth) = bearer_auth_for_provider(provider)? {
return Ok(Arc::new(auth));
}
Ok(match auth {
Some(auth) => auth_provider_from_auth(auth),
None => unauthenticated_auth_provider(),
})
}
fn bearer_auth_for_provider(
provider: &ModelProviderInfo,
) -> codex_protocol::error::Result<Option<BearerAuthProvider>> {
if let Some(api_key) = provider.api_key()? {
return Ok(Some(BearerAuthProvider::new(api_key)));
}
if let Some(token) = provider.experimental_bearer_token.clone() {
return Ok(Some(BearerAuthProvider::new(token)));
}
Ok(None)
}
/// Builds request-header auth for a first-party Codex auth snapshot.
pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
match auth {
CodexAuth::AgentIdentity(auth) => {
Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
}
CodexAuth::BedrockApiKey(_) => unreachable!("{BEDROCK_API_KEY_UNSUPPORTED_MESSAGE}"),
CodexAuth::ApiKey(_)
| CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::PersonalAccessToken(_) => Arc::new(BearerAuthProvider {
token: auth.get_token().ok(),
account_id: auth.get_account_id(),
is_fedramp_account: auth.is_fedramp_account(),
}),
}
}
#[cfg(test)]
mod tests {
use codex_login::auth::BedrockApiKeyAuth;
use codex_model_provider_info::WireApi;
use codex_model_provider_info::create_oss_provider_with_base_url;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn unauthenticated_auth_provider_adds_no_headers() {
let provider =
create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses);
let auth = resolve_provider_auth(/*auth*/ None, &provider).expect("auth should resolve");
assert!(auth.to_auth_headers().is_empty());
}
#[test]
fn openai_provider_rejects_bedrock_api_key_auth() {
let provider = ModelProviderInfo::create_openai_provider(/*base_url*/ None);
let auth = CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
api_key: "bedrock-api-key-test".to_string(),
region: "us-east-1".to_string(),
});
match resolve_provider_auth(Some(&auth), &provider) {
Err(CodexErr::UnsupportedOperation(message)) => {
assert_eq!(message, BEDROCK_API_KEY_UNSUPPORTED_MESSAGE);
}
Err(err) => panic!("unexpected auth error: {err:?}"),
Ok(_) => panic!("Bedrock API key auth should be rejected"),
}
}
}