mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Implement Amazon Bedrock setup in the app server (#40007)
## What changed - Implement `account/bedrock/discover` to report AWS profiles and available environment credentials. - Implement `account/bedrock/setup` for validated AWS profiles and environment credentials, persisting the selected region and profile in the Bedrock provider configuration. - Resolve Bedrock authentication according to the selected credential source, and clear stale profile configuration when using a Codex-managed Bedrock API key. ## Testing - Cover profile and environment discovery, setup validation and persistence, conflicting credentials, and authentication-source precedence. GitOrigin-RevId: 16d700cb0843df768c2fdb6d0c8d26c3402e93df
This commit is contained in:
Generated
+1
@@ -1995,6 +1995,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-app-server-transport",
|
||||
"codex-arg0",
|
||||
"codex-aws-auth",
|
||||
"codex-backend-client",
|
||||
"codex-chatgpt",
|
||||
"codex-cloud-config",
|
||||
|
||||
BIN
Binary file not shown.
@@ -53,10 +53,7 @@ pub enum BedrockSetupParams {
|
||||
Profile { profile: String, region: String },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Environment {
|
||||
credential_type: AwsCredentialType,
|
||||
region: String,
|
||||
},
|
||||
Environment { region: String },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
AccessKeys {
|
||||
|
||||
@@ -36,6 +36,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
codex-analytics = { workspace = true }
|
||||
codex-agent-extension = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-aws-auth = { workspace = true }
|
||||
codex-cloud-config = { workspace = true }
|
||||
codex-code-mode = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
|
||||
@@ -1541,9 +1541,12 @@ impl MessageProcessor {
|
||||
.login_account(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::BedrockDiscover { .. } | ClientRequest::BedrockSetup { .. } => Err(
|
||||
crate::error_code::method_not_found("Amazon Bedrock setup is not implemented"),
|
||||
),
|
||||
ClientRequest::BedrockDiscover { params, .. } => {
|
||||
self.account_processor.bedrock_discover(params).await
|
||||
}
|
||||
ClientRequest::BedrockSetup { params, .. } => {
|
||||
self.account_processor.bedrock_setup(params).await
|
||||
}
|
||||
ClientRequest::LogoutAccount { .. } => {
|
||||
self.account_processor
|
||||
.logout_account(request_id.clone())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::bedrock_auth::clear_user_model_provider_if_bedrock;
|
||||
use super::bedrock_auth::set_user_model_provider_to_bedrock;
|
||||
use super::bedrock_auth::configure_bedrock_for_managed_auth;
|
||||
use super::*;
|
||||
use crate::auth_mode::auth_mode_to_api;
|
||||
use crate::external_auth::ExternalAuthBridge;
|
||||
@@ -8,6 +8,7 @@ use codex_app_server_protocol::DesktopOnboardingEntrypoint;
|
||||
use codex_login::LoginOnboardingEntrypoint;
|
||||
use codex_model_provider::is_supported_amazon_bedrock_region;
|
||||
|
||||
mod bedrock_setup;
|
||||
mod rate_limit_resets;
|
||||
|
||||
// Duration before a browser ChatGPT login attempt is abandoned.
|
||||
@@ -344,6 +345,24 @@ impl AccountRequestProcessor {
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_bedrock_login_allowed(&self) -> Result<(), JSONRPCErrorError> {
|
||||
if self.auth_manager.is_workload_identity_selected() {
|
||||
return Err(self.configured_auth_owned_by_host_error());
|
||||
}
|
||||
if self.auth_manager.is_external_chatgpt_auth_active() {
|
||||
return Err(self.external_auth_active_error());
|
||||
}
|
||||
if !self
|
||||
.auth_manager
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"Amazon Bedrock login is disabled. Use ChatGPT login instead.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn login_api_key_common(
|
||||
&self,
|
||||
params: &LoginApiKeyParams,
|
||||
@@ -405,17 +424,7 @@ impl AccountRequestProcessor {
|
||||
region: String,
|
||||
) {
|
||||
let result = async {
|
||||
if self.auth_manager.is_external_chatgpt_auth_active() {
|
||||
return Err(self.external_auth_active_error());
|
||||
}
|
||||
if !self
|
||||
.auth_manager
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"Amazon Bedrock login is disabled. Use ChatGPT login instead.",
|
||||
));
|
||||
}
|
||||
self.ensure_bedrock_login_allowed()?;
|
||||
|
||||
let api_key = api_key.trim();
|
||||
if api_key.is_empty() {
|
||||
@@ -435,7 +444,7 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
set_user_model_provider_to_bedrock(&self.config_manager).await?;
|
||||
configure_bedrock_for_managed_auth(&self.config_manager).await?;
|
||||
login_with_bedrock_api_key(
|
||||
&self.config.codex_home,
|
||||
api_key,
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
use super::super::bedrock_auth::ensure_user_model_provider_can_be_bedrock;
|
||||
use super::super::config_processor::map_error as map_config_error;
|
||||
use super::AccountRequestProcessor;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use codex_app_server_protocol::AwsCredentialType;
|
||||
use codex_app_server_protocol::BedrockAwsProfile;
|
||||
use codex_app_server_protocol::BedrockDiscoverParams;
|
||||
use codex_app_server_protocol::BedrockDiscoverResponse;
|
||||
use codex_app_server_protocol::BedrockEnvironmentCredential;
|
||||
use codex_app_server_protocol::BedrockSetupParams;
|
||||
use codex_app_server_protocol::BedrockSetupResponse;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigEdit;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID;
|
||||
use codex_model_provider::is_supported_amazon_bedrock_region;
|
||||
|
||||
const AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID";
|
||||
const AWS_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY";
|
||||
const AWS_BEARER_TOKEN_BEDROCK: &str = "AWS_BEARER_TOKEN_BEDROCK";
|
||||
|
||||
impl AccountRequestProcessor {
|
||||
pub(crate) async fn bedrock_discover(
|
||||
&self,
|
||||
_params: BedrockDiscoverParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.ensure_bedrock_login_allowed()?;
|
||||
let profiles = codex_aws_auth::discover_aws_profiles()
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to discover AWS profiles: {err}")))?
|
||||
.into_iter()
|
||||
.map(|profile| BedrockAwsProfile {
|
||||
name: profile.name,
|
||||
region: profile.region,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let region = non_empty_env_var("AWS_REGION");
|
||||
let environment_credentials = [
|
||||
(
|
||||
AwsCredentialType::AccessKeys,
|
||||
non_empty_env_var(AWS_ACCESS_KEY_ID).is_some()
|
||||
&& non_empty_env_var(AWS_SECRET_ACCESS_KEY).is_some(),
|
||||
),
|
||||
(
|
||||
AwsCredentialType::BedrockApiKey,
|
||||
non_empty_env_var(AWS_BEARER_TOKEN_BEDROCK).is_some(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(_, available)| *available)
|
||||
.map(|(credential_type, _)| BedrockEnvironmentCredential {
|
||||
credential_type,
|
||||
region: region.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Some(
|
||||
BedrockDiscoverResponse {
|
||||
profiles,
|
||||
environment_credentials,
|
||||
}
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn bedrock_setup(
|
||||
&self,
|
||||
params: BedrockSetupParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.ensure_bedrock_login_allowed()?;
|
||||
ensure_user_model_provider_can_be_bedrock(&self.config_manager).await?;
|
||||
if matches!(¶ms, BedrockSetupParams::Environment { .. })
|
||||
&& matches!(
|
||||
self.auth_manager.auth_cached(),
|
||||
Some(CodexAuth::BedrockApiKey(_))
|
||||
)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"A Bedrock API key is already configured and takes priority over AWS credentials. Run `codex logout` and try again.",
|
||||
));
|
||||
}
|
||||
|
||||
let region = match ¶ms {
|
||||
BedrockSetupParams::Profile { region, .. }
|
||||
| BedrockSetupParams::Environment { region, .. }
|
||||
| BedrockSetupParams::AccessKeys { region, .. } => region.trim(),
|
||||
};
|
||||
if !is_supported_amazon_bedrock_region(region) {
|
||||
return Err(invalid_request(format!(
|
||||
"Amazon Bedrock Mantle does not support region `{region}`"
|
||||
)));
|
||||
}
|
||||
|
||||
let profile = match ¶ms {
|
||||
BedrockSetupParams::Profile { profile, .. } => {
|
||||
let profile = profile.trim();
|
||||
if profile.is_empty() {
|
||||
return Err(invalid_request("AWS profile name must not be empty."));
|
||||
}
|
||||
codex_aws_auth::validate_aws_profile(profile, region)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
invalid_request(format!(
|
||||
"failed to load credentials for AWS profile `{profile}`: {err}"
|
||||
))
|
||||
})?;
|
||||
Some(profile.to_string())
|
||||
}
|
||||
BedrockSetupParams::Environment { .. } => {
|
||||
let has_bedrock_api_key = non_empty_env_var(AWS_BEARER_TOKEN_BEDROCK).is_some();
|
||||
let has_access_keys = non_empty_env_var(AWS_ACCESS_KEY_ID).is_some()
|
||||
&& non_empty_env_var(AWS_SECRET_ACCESS_KEY).is_some();
|
||||
if !has_bedrock_api_key && !has_access_keys {
|
||||
return Err(invalid_request(
|
||||
"No AWS credentials found. Please Configure AWS credentials or complete AWS sign-in, then try again.",
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
BedrockSetupParams::AccessKeys { .. } => {
|
||||
// TODO: Support direct AWS access key setup with Codex-managed credential storage.
|
||||
return Err(invalid_request(
|
||||
"Direct AWS access key setup is not supported yet.",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Validate the selected credentials against Bedrock ListModels once supported.
|
||||
self.cancel_active_login().await;
|
||||
self.config_manager
|
||||
.batch_write(ConfigBatchWriteParams {
|
||||
edits: [
|
||||
(
|
||||
"model_provider",
|
||||
serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
|
||||
),
|
||||
(
|
||||
"model_providers.amazon-bedrock.aws.region",
|
||||
serde_json::json!(region),
|
||||
),
|
||||
(
|
||||
"model_providers.amazon-bedrock.aws.profile",
|
||||
serde_json::json!(profile),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(key_path, value)| ConfigEdit {
|
||||
key_path: key_path.to_string(),
|
||||
value,
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
})
|
||||
.collect(),
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: false,
|
||||
})
|
||||
.await
|
||||
.map_err(map_config_error)?;
|
||||
|
||||
Ok(Some(BedrockSetupResponse {}.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_env_var(name: &str) -> Option<String> {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
@@ -2,7 +2,8 @@ use super::config_processor::map_error as map_config_error;
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use codex_app_server_protocol::ConfigValueWriteParams;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigEdit;
|
||||
use codex_app_server_protocol::ConfigWriteErrorCode;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
@@ -11,7 +12,40 @@ use codex_config::ConfigLayerSource;
|
||||
use codex_config::format_config_layer_source;
|
||||
use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID;
|
||||
|
||||
pub(super) async fn set_user_model_provider_to_bedrock(
|
||||
pub(super) async fn configure_bedrock_for_managed_auth(
|
||||
config_manager: &ConfigManager,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
ensure_user_model_provider_can_be_bedrock(config_manager).await?;
|
||||
|
||||
config_manager
|
||||
.batch_write(ConfigBatchWriteParams {
|
||||
edits: [
|
||||
(
|
||||
"model_provider",
|
||||
serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
|
||||
),
|
||||
(
|
||||
"model_providers.amazon-bedrock.aws.profile",
|
||||
serde_json::Value::Null,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(key_path, value)| ConfigEdit {
|
||||
key_path: key_path.to_string(),
|
||||
value,
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
})
|
||||
.collect(),
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: false,
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(map_config_error)
|
||||
}
|
||||
|
||||
pub(super) async fn ensure_user_model_provider_can_be_bedrock(
|
||||
config_manager: &ConfigManager,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let layers = config_manager
|
||||
@@ -45,17 +79,7 @@ pub(super) async fn set_user_model_provider_to_bedrock(
|
||||
)));
|
||||
}
|
||||
|
||||
config_manager
|
||||
.write_value(ConfigValueWriteParams {
|
||||
key_path: "model_provider".to_string(),
|
||||
value: serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(map_config_error)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn clear_user_model_provider_if_bedrock(
|
||||
|
||||
@@ -1099,6 +1099,18 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
|
||||
async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let original_config = std::fs::read_to_string(&config_path)?;
|
||||
std::fs::write(
|
||||
&config_path,
|
||||
format!(
|
||||
"{original_config}\n[model_providers.amazon-bedrock]\n\
|
||||
http_headers = {{ X-Existing = \"preserved\" }}\n\
|
||||
[model_providers.amazon-bedrock.aws]\n\
|
||||
profile = \"stale-profile\"\n\
|
||||
region = \"us-east-1\"\n"
|
||||
),
|
||||
)?;
|
||||
login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test-key",
|
||||
@@ -1119,6 +1131,10 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> R
|
||||
"model_provider".to_string(),
|
||||
toml::Value::String("amazon-bedrock".to_string()),
|
||||
);
|
||||
expected_config["model_providers"]["amazon-bedrock"]["aws"]
|
||||
.as_table_mut()
|
||||
.expect("AWS configuration should be a table")
|
||||
.remove("profile");
|
||||
let request_id = mcp
|
||||
.send_login_account_amazon_bedrock_request(" managed-bedrock-api-key ", " us-west-2 ")
|
||||
.await?;
|
||||
@@ -1167,6 +1183,15 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> R
|
||||
}
|
||||
);
|
||||
assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?;
|
||||
assert_eq!(
|
||||
read_account(&mut mcp).await?,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::AmazonBedrock {
|
||||
uses_codex_managed_credentials: true,
|
||||
}),
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use codex_app_server_protocol::BedrockDiscoverParams;
|
||||
use codex_app_server_protocol::BedrockDiscoverResponse;
|
||||
use codex_app_server_protocol::BedrockSetupParams;
|
||||
use codex_app_server_protocol::BedrockSetupResponse;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::login_with_bedrock_api_key;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
|
||||
|
||||
async fn bedrock_app_server(
|
||||
codex_home: &Path,
|
||||
environment: &[(&str, Option<&str>)],
|
||||
) -> Result<TestAppServer> {
|
||||
let aws_config = codex_home.join("aws-config");
|
||||
let aws_credentials = codex_home.join("aws-credentials");
|
||||
std::fs::write(&aws_config, "[profile engineering]\nregion = us-west-2\n")?;
|
||||
std::fs::write(
|
||||
&aws_credentials,
|
||||
"[engineering]\naws_access_key_id = engineering-id\naws_secret_access_key = engineering-secret\n\
|
||||
[finance]\naws_access_key_id = finance-id\naws_secret_access_key = finance-secret\n",
|
||||
)?;
|
||||
let aws_config_path = aws_config.to_string_lossy();
|
||||
let aws_credentials_path = aws_credentials.to_string_lossy();
|
||||
TestAppServer::builder()
|
||||
.with_codex_home(codex_home)
|
||||
.with_env_overrides(&[
|
||||
("AWS_CONFIG_FILE", Some(aws_config_path.as_ref())),
|
||||
(
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
Some(aws_credentials_path.as_ref()),
|
||||
),
|
||||
("AWS_PROFILE", None),
|
||||
("AWS_ACCESS_KEY_ID", None),
|
||||
("AWS_SECRET_ACCESS_KEY", None),
|
||||
("AWS_SESSION_TOKEN", None),
|
||||
("AWS_BEARER_TOKEN_BEDROCK", None),
|
||||
("AWS_REGION", None),
|
||||
("AWS_DEFAULT_REGION", None),
|
||||
("AWS_EC2_METADATA_DISABLED", Some("true")),
|
||||
])
|
||||
.with_env_overrides(environment)
|
||||
.build_initialized_with_timeout(READ_TIMEOUT)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_bedrock_profiles_and_environment_credentials() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = bedrock_app_server(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("AWS_PROFILE", Some("engineering")),
|
||||
("AWS_ACCESS_KEY_ID", Some("environment-id")),
|
||||
("AWS_SECRET_ACCESS_KEY", Some("environment-secret")),
|
||||
("AWS_SESSION_TOKEN", Some("environment-token")),
|
||||
("AWS_BEARER_TOKEN_BEDROCK", Some("environment-bedrock-key")),
|
||||
("AWS_DEFAULT_REGION", Some("us-west-2")),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response: BedrockDiscoverResponse = app_server
|
||||
.request(|request_id| ClientRequest::BedrockDiscover {
|
||||
request_id,
|
||||
params: BedrockDiscoverParams {},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
serde_json::to_value(response)?,
|
||||
json!({
|
||||
"profiles": [
|
||||
{"name": "engineering", "region": "us-west-2"},
|
||||
{"name": "finance", "region": null}
|
||||
],
|
||||
"environmentCredentials": [
|
||||
{"type": "accessKeys", "region": null},
|
||||
{"type": "bedrockApiKey", "region": null}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
let response: BedrockSetupResponse = app_server
|
||||
.request(|request_id| ClientRequest::BedrockSetup {
|
||||
request_id,
|
||||
params: BedrockSetupParams::Environment {
|
||||
region: " us-east-2 ".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(response, BedrockSetupResponse {});
|
||||
|
||||
let config: toml::Value = toml::from_str(&std::fs::read_to_string(
|
||||
codex_home.path().join("config.toml"),
|
||||
)?)?;
|
||||
assert_eq!(
|
||||
config,
|
||||
toml::toml! {
|
||||
model_provider = "amazon-bedrock"
|
||||
[model_providers.amazon-bedrock.aws]
|
||||
region = "us-east-2"
|
||||
}
|
||||
.into()
|
||||
);
|
||||
assert!(!codex_home.path().join(".env").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn setup_bedrock_profile_and_environment() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let dotenv_path = codex_home.path().join(".env");
|
||||
std::fs::write(
|
||||
&config_path,
|
||||
"[model_providers.amazon-bedrock]\nhttp_headers = { X-Existing = \"preserved\" }\n\
|
||||
[model_providers.amazon-bedrock.aws]\nprofile = \"old\"\nregion = \"us-east-1\"\n",
|
||||
)?;
|
||||
let existing_dotenv = "# existing configuration\nUNRELATED=value\nAWS_ACCESS_KEY_ID=old-id\n\
|
||||
export AWS_SECRET_ACCESS_KEY=old-secret\nAWS_SESSION_TOKEN=stale-token\n";
|
||||
std::fs::write(&dotenv_path, existing_dotenv)?;
|
||||
let mut app_server = bedrock_app_server(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("AWS_ACCESS_KEY_ID", Some("environment-id")),
|
||||
("AWS_SECRET_ACCESS_KEY", Some("environment-secret")),
|
||||
("AWS_DEFAULT_REGION", Some("us-east-1")),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response: BedrockSetupResponse = app_server
|
||||
.request(|request_id| ClientRequest::BedrockSetup {
|
||||
request_id,
|
||||
params: BedrockSetupParams::Profile {
|
||||
profile: " engineering ".to_string(),
|
||||
region: " us-west-2 ".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(response, BedrockSetupResponse {});
|
||||
let config: toml::Value = toml::from_str(&std::fs::read_to_string(&config_path)?)?;
|
||||
assert_eq!(
|
||||
serde_json::to_value(&config["model_providers"]["amazon-bedrock"]["aws"])?,
|
||||
json!({"profile": "engineering", "region": "us-west-2"})
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(&dotenv_path)?, existing_dotenv);
|
||||
|
||||
let response: BedrockSetupResponse = app_server
|
||||
.request(|request_id| ClientRequest::BedrockSetup {
|
||||
request_id,
|
||||
params: BedrockSetupParams::Environment {
|
||||
region: "us-east-1".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(response, BedrockSetupResponse {});
|
||||
assert_eq!(std::fs::read_to_string(&dotenv_path)?, existing_dotenv);
|
||||
let config: toml::Value = toml::from_str(&std::fs::read_to_string(&config_path)?)?;
|
||||
assert_eq!(
|
||||
serde_json::to_value(&config["model_providers"]["amazon-bedrock"]["aws"])?,
|
||||
json!({"region": "us-east-1"})
|
||||
);
|
||||
|
||||
let config: toml::Value = toml::from_str(&std::fs::read_to_string(config_path)?)?;
|
||||
assert_eq!(
|
||||
config,
|
||||
toml::toml! {
|
||||
model_provider = "amazon-bedrock"
|
||||
|
||||
[model_providers.amazon-bedrock]
|
||||
http_headers = { X-Existing = "preserved" }
|
||||
|
||||
[model_providers.amazon-bedrock.aws]
|
||||
region = "us-east-1"
|
||||
}
|
||||
.into()
|
||||
);
|
||||
|
||||
let managed_home = TempDir::new()?;
|
||||
let managed_config_path = managed_home.path().join("config.toml");
|
||||
std::fs::write(&managed_config_path, "")?;
|
||||
login_with_bedrock_api_key(
|
||||
managed_home.path(),
|
||||
"managed-bedrock-api-key",
|
||||
"us-east-1",
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
let auth_path = managed_home.path().join("auth.json");
|
||||
let expected_auth = std::fs::read_to_string(&auth_path)?;
|
||||
let mut app_server = bedrock_app_server(
|
||||
managed_home.path(),
|
||||
&[(
|
||||
"AWS_BEARER_TOKEN_BEDROCK",
|
||||
Some("environment-bedrock-api-key"),
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
let mut expected_config: toml::Value =
|
||||
toml::from_str(&std::fs::read_to_string(&managed_config_path)?)?;
|
||||
expected_config
|
||||
.as_table_mut()
|
||||
.expect("config should be a table")
|
||||
.extend(toml::toml! {
|
||||
model_provider = "amazon-bedrock"
|
||||
[model_providers.amazon-bedrock.aws]
|
||||
profile = "engineering"
|
||||
region = "us-west-2"
|
||||
});
|
||||
let response: BedrockSetupResponse = app_server
|
||||
.request(|request_id| ClientRequest::BedrockSetup {
|
||||
request_id,
|
||||
params: BedrockSetupParams::Profile {
|
||||
profile: "engineering".to_string(),
|
||||
region: "us-west-2".to_string(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(response, BedrockSetupResponse {});
|
||||
assert_eq!(
|
||||
toml::from_str::<toml::Value>(&std::fs::read_to_string(&managed_config_path)?)?,
|
||||
expected_config
|
||||
);
|
||||
|
||||
let request_id = app_server
|
||||
.send_raw_request(
|
||||
"account/bedrock/setup",
|
||||
Some(json!({"type": "environment", "region": "us-east-1"})),
|
||||
)
|
||||
.await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"A Bedrock API key is already configured and takes priority over AWS credentials. Run `codex logout` and try again."
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(&auth_path)?, expected_auth);
|
||||
assert_eq!(
|
||||
toml::from_str::<toml::Value>(&std::fs::read_to_string(&managed_config_path)?)?,
|
||||
expected_config
|
||||
);
|
||||
assert!(!managed_home.path().join(".env").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn setup_bedrock_rejects_invalid_or_conflicting_credentials() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = bedrock_app_server(codex_home.path(), &[]).await?;
|
||||
|
||||
for (params, expected_error) in [
|
||||
(
|
||||
json!({"type": "profile", "profile": "engineering", "region": "us-west-1"}),
|
||||
"Amazon Bedrock Mantle does not support region `us-west-1`",
|
||||
),
|
||||
(
|
||||
json!({"type": "profile", "profile": " ", "region": "us-west-2"}),
|
||||
"AWS profile name must not be empty.",
|
||||
),
|
||||
(
|
||||
json!({"type": "profile", "profile": "missing", "region": "us-west-2"}),
|
||||
"failed to load credentials for AWS profile `missing`:",
|
||||
),
|
||||
(
|
||||
json!({"type": "environment", "region": "us-west-2"}),
|
||||
"No AWS credentials found. Please Configure AWS credentials or complete AWS sign-in, then try again.",
|
||||
),
|
||||
(
|
||||
json!({
|
||||
"type": "accessKeys",
|
||||
"accessKeyId": "test-id",
|
||||
"secretAccessKey": "test-secret",
|
||||
"region": "us-west-2",
|
||||
}),
|
||||
"Direct AWS access key setup is not supported yet.",
|
||||
),
|
||||
] {
|
||||
let request_id = app_server
|
||||
.send_raw_request("account/bedrock/setup", Some(params))
|
||||
.await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert!(
|
||||
error.error.message.contains(expected_error),
|
||||
"expected {expected_error:?}, got {:?}",
|
||||
error.error.message
|
||||
);
|
||||
}
|
||||
assert!(!codex_home.path().join(".env").exists());
|
||||
assert!(!codex_home.path().join("config.toml").exists());
|
||||
|
||||
let home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
"forced_login_method = \"chatgpt\"\n",
|
||||
)?;
|
||||
let mut app_server = bedrock_app_server(home.path(), &[]).await?;
|
||||
let request_id = app_server
|
||||
.send_raw_request("account/bedrock/discover", Some(json!({})))
|
||||
.await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"Amazon Bedrock login is disabled. Use ChatGPT login instead."
|
||||
);
|
||||
assert!(!home.path().join(".env").exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod app_list;
|
||||
mod app_read;
|
||||
mod attestation;
|
||||
mod auto_env;
|
||||
mod bedrock_setup;
|
||||
mod client_metadata;
|
||||
mod code_mode_host;
|
||||
mod collaboration_mode_list;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use aws_config::default_provider::region::DefaultRegionChain;
|
||||
use aws_config::profile::ProfileFileCredentialsProvider;
|
||||
use aws_config::provider_config::ProviderConfig;
|
||||
use aws_credential_types::provider::ProvideCredentials;
|
||||
use aws_types::os_shim_internal::Env;
|
||||
use aws_types::os_shim_internal::Fs;
|
||||
use aws_types::region::Region;
|
||||
|
||||
use crate::AwsAuthError;
|
||||
|
||||
/// A named AWS profile and the region configured directly on that profile.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AwsProfile {
|
||||
pub name: String,
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
/// Discovers AWS profiles using the SDK's shared config and credentials files.
|
||||
pub async fn discover_aws_profiles() -> Result<Vec<AwsProfile>, AwsAuthError> {
|
||||
let profiles = aws_config::profile::load(
|
||||
&Fs::real(),
|
||||
&Env::real(),
|
||||
&Default::default(),
|
||||
/*selected_profile_override*/ None,
|
||||
)
|
||||
.await?;
|
||||
let selected_profile = profiles.selected_profile();
|
||||
let mut discovered = profiles
|
||||
.profiles()
|
||||
.filter_map(|name| {
|
||||
profiles.get_profile(name).map(|profile| AwsProfile {
|
||||
name: name.to_string(),
|
||||
region: profile.get("region").map(str::to_string),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
discovered.sort_by(|left, right| {
|
||||
(left.name != selected_profile, &left.name)
|
||||
.cmp(&(right.name != selected_profile, &right.name))
|
||||
});
|
||||
|
||||
Ok(discovered)
|
||||
}
|
||||
|
||||
/// Resolves credentials only from the selected AWS profile.
|
||||
pub async fn validate_aws_profile(profile: &str, region: &str) -> Result<(), AwsAuthError> {
|
||||
profile_credentials_provider(profile, Some(region))
|
||||
.await
|
||||
.provide_credentials()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn profile_credentials_provider(
|
||||
profile: &str,
|
||||
region: Option<&str>,
|
||||
) -> ProfileFileCredentialsProvider {
|
||||
let region = match region {
|
||||
Some(region) => Some(Region::new(region.to_string())),
|
||||
None => {
|
||||
DefaultRegionChain::builder()
|
||||
.profile_name(profile)
|
||||
.build()
|
||||
.region()
|
||||
.await
|
||||
}
|
||||
};
|
||||
let provider_config = ProviderConfig::without_region().with_region(region);
|
||||
|
||||
ProfileFileCredentialsProvider::builder()
|
||||
.configure(&provider_config)
|
||||
.profile_name(profile)
|
||||
.build()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod config;
|
||||
mod discovery;
|
||||
mod signing;
|
||||
|
||||
use std::time::SystemTime;
|
||||
@@ -10,6 +11,10 @@ use http::HeaderMap;
|
||||
use http::Method;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use discovery::AwsProfile;
|
||||
pub use discovery::discover_aws_profiles;
|
||||
pub use discovery::validate_aws_profile;
|
||||
|
||||
/// AWS auth configuration used to resolve credentials and sign requests.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AwsAuthConfig {
|
||||
@@ -39,10 +44,14 @@ pub struct AwsSignedRequest {
|
||||
pub enum AwsAuthError {
|
||||
#[error("AWS service name must not be empty")]
|
||||
EmptyService,
|
||||
#[error("AWS profile must be configured")]
|
||||
MissingProfile,
|
||||
#[error("AWS SDK config did not resolve a credentials provider")]
|
||||
MissingCredentialsProvider,
|
||||
#[error("AWS SDK config did not resolve a region")]
|
||||
MissingRegion,
|
||||
#[error("failed to load AWS profiles: {0}")]
|
||||
ProfileLoad(#[from] aws_config::profile::ProfileFileLoadError),
|
||||
#[error("failed to load AWS credentials: {0}")]
|
||||
Credentials(#[from] aws_credential_types::provider::error::CredentialsError),
|
||||
#[error("request URL is not a valid URI: {0}")]
|
||||
@@ -89,6 +98,19 @@ impl AwsAuthContext {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_profile(config: AwsAuthConfig) -> Result<Self, AwsAuthError> {
|
||||
let profile = config
|
||||
.profile
|
||||
.as_deref()
|
||||
.ok_or(AwsAuthError::MissingProfile)?;
|
||||
let credentials_provider = SharedCredentialsProvider::new(
|
||||
discovery::profile_credentials_provider(profile, config.region.as_deref()).await,
|
||||
);
|
||||
let mut context = Self::load(config).await?;
|
||||
context.credentials_provider = credentials_provider;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
pub fn region(&self) -> &str {
|
||||
&self.region
|
||||
}
|
||||
@@ -121,8 +143,10 @@ impl AwsAuthError {
|
||||
| aws_credential_types::provider::error::CredentialsError::ProviderError(_)
|
||||
),
|
||||
AwsAuthError::EmptyService
|
||||
| AwsAuthError::MissingProfile
|
||||
| AwsAuthError::MissingCredentialsProvider
|
||||
| AwsAuthError::MissingRegion
|
||||
| AwsAuthError::ProfileLoad(_)
|
||||
| AwsAuthError::InvalidUri(_)
|
||||
| AwsAuthError::BuildHttpRequest(_)
|
||||
| AwsAuthError::InvalidHeaderValue(_)
|
||||
@@ -217,6 +241,7 @@ mod tests {
|
||||
#[test]
|
||||
fn deterministic_aws_auth_errors_are_not_retryable() {
|
||||
assert!(!AwsAuthError::EmptyService.is_retryable());
|
||||
assert!(!AwsAuthError::MissingProfile.is_retryable());
|
||||
assert!(
|
||||
!AwsAuthError::Credentials(CredentialsError::not_loaded_no_source()).is_retryable()
|
||||
);
|
||||
@@ -247,7 +272,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_rejects_empty_service_name() {
|
||||
async fn load_rejects_invalid_configuration() {
|
||||
let err = AwsAuthContext::load(AwsAuthConfig {
|
||||
profile: None,
|
||||
region: None,
|
||||
@@ -257,5 +282,15 @@ mod tests {
|
||||
.expect_err("empty service should be rejected");
|
||||
|
||||
assert_eq!(err.to_string(), "AWS service name must not be empty");
|
||||
|
||||
let err = AwsAuthContext::load_profile(AwsAuthConfig {
|
||||
profile: None,
|
||||
region: Some("us-east-1".to_string()),
|
||||
service: "bedrock".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect_err("profile auth should require a configured profile");
|
||||
|
||||
assert_eq!(err.to_string(), "AWS profile must be configured");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const AWS_DEFAULT_REGION_ENV_VAR: &str = "AWS_DEFAULT_REGION";
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum BedrockAuthSource {
|
||||
CommandBearerToken,
|
||||
ConfiguredAwsProfile,
|
||||
ManagedBearerToken,
|
||||
EnvBearerToken,
|
||||
EnvAwsCredentials,
|
||||
@@ -53,6 +54,12 @@ pub(super) fn auth_source(
|
||||
) -> BedrockAuthSource {
|
||||
if provider_info.has_command_auth() {
|
||||
BedrockAuthSource::CommandBearerToken
|
||||
} else if provider_info
|
||||
.aws
|
||||
.as_ref()
|
||||
.is_some_and(|aws| aws.profile.is_some())
|
||||
{
|
||||
BedrockAuthSource::ConfiguredAwsProfile
|
||||
} else if matches!(
|
||||
auth_manager.and_then(AuthManager::auth_cached),
|
||||
Some(CodexAuth::BedrockApiKey(_))
|
||||
@@ -70,38 +77,69 @@ pub(super) fn auth_source(
|
||||
}
|
||||
|
||||
pub(super) async fn resolve_auth_method(
|
||||
source: BedrockAuthSource,
|
||||
managed_auth: Option<&BedrockApiKeyAuth>,
|
||||
aws: &ModelProviderAwsAuthInfo,
|
||||
endpoint: BedrockEndpoint,
|
||||
) -> Result<BedrockAuthMethod> {
|
||||
if let Some(managed_auth) = managed_auth {
|
||||
return Ok(BedrockAuthMethod::ManagedBearerToken {
|
||||
token: managed_auth.api_key.clone(),
|
||||
region: managed_auth.region.clone(),
|
||||
});
|
||||
match source {
|
||||
BedrockAuthSource::CommandBearerToken => Err(CodexErr::Fatal(
|
||||
"Amazon Bedrock command authentication must be resolved by the model provider"
|
||||
.to_string(),
|
||||
)),
|
||||
BedrockAuthSource::ManagedBearerToken => {
|
||||
let managed_auth = managed_auth.ok_or_else(|| {
|
||||
CodexErr::Fatal(
|
||||
"selected Codex-managed Amazon Bedrock API key is no longer available"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(BedrockAuthMethod::ManagedBearerToken {
|
||||
token: managed_auth.api_key.clone(),
|
||||
region: managed_auth.region.clone(),
|
||||
})
|
||||
}
|
||||
BedrockAuthSource::EnvBearerToken => {
|
||||
let token = non_empty_env_var_from(AWS_BEARER_TOKEN_BEDROCK_ENV_VAR, std::env::var)
|
||||
.ok_or_else(|| {
|
||||
CodexErr::Fatal(
|
||||
"selected `AWS_BEARER_TOKEN_BEDROCK` credential is no longer available"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
let region = bearer_token_region(aws, std::env::var)?;
|
||||
Ok(BedrockAuthMethod::EnvBearerToken { token, region })
|
||||
}
|
||||
BedrockAuthSource::ConfiguredAwsProfile => {
|
||||
let config = match endpoint {
|
||||
BedrockEndpoint::Mantle => aws_auth_config(aws),
|
||||
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
|
||||
};
|
||||
let context = AwsAuthContext::load_profile(config)
|
||||
.await
|
||||
.map_err(aws_auth_error_to_codex_error)?;
|
||||
Ok(BedrockAuthMethod::AwsSdkAuth { context })
|
||||
}
|
||||
BedrockAuthSource::EnvAwsCredentials | BedrockAuthSource::AwsSdk => {
|
||||
let config = match endpoint {
|
||||
BedrockEndpoint::Mantle => aws_auth_config(aws),
|
||||
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
|
||||
};
|
||||
let context = AwsAuthContext::load(config)
|
||||
.await
|
||||
.map_err(aws_auth_error_to_codex_error)?;
|
||||
Ok(BedrockAuthMethod::AwsSdkAuth { context })
|
||||
}
|
||||
}
|
||||
|
||||
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 = match endpoint {
|
||||
BedrockEndpoint::Mantle => aws_auth_config(aws),
|
||||
BedrockEndpoint::Runtime => runtime::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(
|
||||
source: BedrockAuthSource,
|
||||
managed_auth: Option<&BedrockApiKeyAuth>,
|
||||
aws: &ModelProviderAwsAuthInfo,
|
||||
endpoint: BedrockEndpoint,
|
||||
) -> Result<SharedAuthProvider> {
|
||||
match resolve_auth_method(managed_auth, aws, endpoint).await? {
|
||||
match resolve_auth_method(source, managed_auth, aws, endpoint).await? {
|
||||
BedrockAuthMethod::ManagedBearerToken { token, .. }
|
||||
| BedrockAuthMethod::EnvBearerToken { token, .. } => Ok(Arc::new(BearerAuthProvider {
|
||||
token: Some(token),
|
||||
@@ -115,11 +153,23 @@ pub(super) async fn resolve_provider_auth(
|
||||
}
|
||||
|
||||
pub(super) async fn resolve_region(
|
||||
source: BedrockAuthSource,
|
||||
managed_auth: Option<&BedrockApiKeyAuth>,
|
||||
aws: &ModelProviderAwsAuthInfo,
|
||||
endpoint: BedrockEndpoint,
|
||||
) -> Result<String> {
|
||||
match resolve_auth_method(managed_auth, aws, endpoint).await? {
|
||||
if source == BedrockAuthSource::CommandBearerToken {
|
||||
let config = match endpoint {
|
||||
BedrockEndpoint::Mantle => aws_auth_config(aws),
|
||||
BedrockEndpoint::Runtime => runtime::aws_auth_config(aws),
|
||||
};
|
||||
let context = AwsAuthContext::load(config)
|
||||
.await
|
||||
.map_err(aws_auth_error_to_codex_error)?;
|
||||
return Ok(context.region().to_string());
|
||||
}
|
||||
|
||||
match resolve_auth_method(source, managed_auth, aws, endpoint).await? {
|
||||
BedrockAuthMethod::ManagedBearerToken { region, .. }
|
||||
| BedrockAuthMethod::EnvBearerToken { region, .. } => Ok(region),
|
||||
BedrockAuthMethod::AwsSdkAuth { context } => Ok(context.region().to_string()),
|
||||
@@ -239,20 +289,71 @@ mod tests {
|
||||
#[test]
|
||||
fn bedrock_auth_source_distinguishes_static_environment_credentials() {
|
||||
let provider = ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None);
|
||||
let cases: &[(&[&str], BedrockAuthSource)] = &[
|
||||
let configured_profile_provider =
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
|
||||
profile: Some("configured-profile".to_string()),
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: None,
|
||||
}));
|
||||
let managed_auth =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
|
||||
api_key: "managed-bedrock-api-key".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
}));
|
||||
let cases: &[(
|
||||
&ModelProviderInfo,
|
||||
Option<&AuthManager>,
|
||||
&[&str],
|
||||
BedrockAuthSource,
|
||||
)] = &[
|
||||
(
|
||||
&configured_profile_provider,
|
||||
Some(managed_auth.as_ref()),
|
||||
&[
|
||||
AWS_BEARER_TOKEN_BEDROCK_ENV_VAR,
|
||||
AWS_ACCESS_KEY_ID_ENV_VAR,
|
||||
AWS_SECRET_ACCESS_KEY_ENV_VAR,
|
||||
],
|
||||
BedrockAuthSource::ConfiguredAwsProfile,
|
||||
),
|
||||
(
|
||||
&provider,
|
||||
Some(managed_auth.as_ref()),
|
||||
&[AWS_BEARER_TOKEN_BEDROCK_ENV_VAR],
|
||||
BedrockAuthSource::ManagedBearerToken,
|
||||
),
|
||||
(
|
||||
&provider,
|
||||
None,
|
||||
&[
|
||||
AWS_BEARER_TOKEN_BEDROCK_ENV_VAR,
|
||||
AWS_ACCESS_KEY_ID_ENV_VAR,
|
||||
AWS_SECRET_ACCESS_KEY_ENV_VAR,
|
||||
],
|
||||
BedrockAuthSource::EnvBearerToken,
|
||||
),
|
||||
(
|
||||
&provider,
|
||||
None,
|
||||
&[AWS_ACCESS_KEY_ID_ENV_VAR, AWS_SECRET_ACCESS_KEY_ENV_VAR],
|
||||
BedrockAuthSource::EnvAwsCredentials,
|
||||
),
|
||||
(&[AWS_ACCESS_KEY_ID_ENV_VAR], BedrockAuthSource::AwsSdk),
|
||||
(
|
||||
&provider,
|
||||
None,
|
||||
&[AWS_ACCESS_KEY_ID_ENV_VAR],
|
||||
BedrockAuthSource::AwsSdk,
|
||||
),
|
||||
(
|
||||
&provider,
|
||||
None,
|
||||
&["AWS_PROFILE", AWS_REGION_ENV_VAR],
|
||||
BedrockAuthSource::AwsSdk,
|
||||
),
|
||||
];
|
||||
|
||||
for (variables, expected) in cases {
|
||||
let actual = auth_source(&provider, /*auth_manager*/ None, |name| {
|
||||
for (provider, auth_manager, variables, expected) in cases {
|
||||
let actual = auth_source(provider, *auth_manager, |name| {
|
||||
variables
|
||||
.contains(&name)
|
||||
.then(|| "configured".to_string())
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
|
||||
use super::BedrockEndpoint;
|
||||
use super::auth::BedrockAuthSource;
|
||||
use super::auth::resolve_region;
|
||||
|
||||
const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle";
|
||||
@@ -55,10 +56,11 @@ pub(super) fn base_url(region: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
pub(super) async fn bedrock_mantle_runtime_base_url(
|
||||
source: BedrockAuthSource,
|
||||
managed_auth: Option<&BedrockApiKeyAuth>,
|
||||
aws: &ModelProviderAwsAuthInfo,
|
||||
) -> Result<String> {
|
||||
let region = resolve_region(managed_auth, aws, BedrockEndpoint::Mantle).await?;
|
||||
let region = resolve_region(source, managed_auth, aws, BedrockEndpoint::Mantle).await?;
|
||||
base_url(®ion)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,9 +82,10 @@ impl AmazonBedrockModelProvider {
|
||||
region: None,
|
||||
auth_refresh: None,
|
||||
});
|
||||
let uses_aws_sdk_auth =
|
||||
auth::auth_source(&provider_info, auth_manager.as_deref(), std::env::var)
|
||||
== auth::BedrockAuthSource::AwsSdk;
|
||||
let uses_aws_sdk_auth = matches!(
|
||||
auth::auth_source(&provider_info, auth_manager.as_deref(), std::env::var),
|
||||
auth::BedrockAuthSource::ConfiguredAwsProfile | auth::BedrockAuthSource::AwsSdk
|
||||
);
|
||||
let auth_recovery = if uses_aws_sdk_auth && aws.auth_refresh.is_some() {
|
||||
process_shared_state().aws_auth_recovery(&aws)
|
||||
} else {
|
||||
@@ -100,7 +101,14 @@ impl AmazonBedrockModelProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_source(&self) -> auth::BedrockAuthSource {
|
||||
auth::auth_source(&self.info, self.auth_manager.as_deref(), std::env::var)
|
||||
}
|
||||
|
||||
fn managed_auth(&self) -> Option<BedrockApiKeyAuth> {
|
||||
if self.auth_source() != auth::BedrockAuthSource::ManagedBearerToken {
|
||||
return None;
|
||||
}
|
||||
self.auth_manager
|
||||
.as_ref()
|
||||
.and_then(|auth_manager| auth_manager.auth_cached())
|
||||
@@ -117,18 +125,25 @@ impl AmazonBedrockModelProvider {
|
||||
|
||||
fn uses_aws_auth_recovery(&self) -> bool {
|
||||
self.auth_recovery.is_some()
|
||||
&& auth::auth_source(&self.info, self.auth_manager.as_deref(), std::env::var)
|
||||
== auth::BedrockAuthSource::AwsSdk
|
||||
&& matches!(
|
||||
self.auth_source(),
|
||||
auth::BedrockAuthSource::ConfiguredAwsProfile | auth::BedrockAuthSource::AwsSdk
|
||||
)
|
||||
}
|
||||
|
||||
async fn auth(&self) -> Option<CodexAuth> {
|
||||
if self.info.has_command_auth() {
|
||||
match self.auth_manager.as_ref() {
|
||||
match self.auth_source() {
|
||||
auth::BedrockAuthSource::CommandBearerToken => match self.auth_manager.as_ref() {
|
||||
Some(auth_manager) => auth_manager.auth().await,
|
||||
None => None,
|
||||
},
|
||||
auth::BedrockAuthSource::ManagedBearerToken => {
|
||||
self.managed_auth().map(CodexAuth::BedrockApiKey)
|
||||
}
|
||||
} else {
|
||||
self.managed_auth().map(CodexAuth::BedrockApiKey)
|
||||
auth::BedrockAuthSource::ConfiguredAwsProfile
|
||||
| auth::BedrockAuthSource::EnvBearerToken
|
||||
| auth::BedrockAuthSource::EnvAwsCredentials
|
||||
| auth::BedrockAuthSource::AwsSdk => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,25 +157,29 @@ impl AmazonBedrockModelProvider {
|
||||
if let Some(base_url) = self.info.base_url.clone() {
|
||||
return Ok(Some(base_url));
|
||||
}
|
||||
let auth_source = self.auth_source();
|
||||
let managed_auth = self.managed_auth();
|
||||
let base_url = match self.endpoint {
|
||||
BedrockEndpoint::Mantle => {
|
||||
bedrock_mantle_runtime_base_url(managed_auth.as_ref(), &self.aws).await?
|
||||
bedrock_mantle_runtime_base_url(auth_source, managed_auth.as_ref(), &self.aws)
|
||||
.await?
|
||||
}
|
||||
BedrockEndpoint::Runtime => {
|
||||
bedrock_runtime_base_url(managed_auth.as_ref(), &self.aws).await?
|
||||
bedrock_runtime_base_url(auth_source, managed_auth.as_ref(), &self.aws).await?
|
||||
}
|
||||
};
|
||||
Ok(Some(base_url))
|
||||
}
|
||||
|
||||
async fn api_auth(&self) -> Result<SharedAuthProvider> {
|
||||
if self.info.has_command_auth() {
|
||||
let source = self.auth_source();
|
||||
if source == auth::BedrockAuthSource::CommandBearerToken {
|
||||
let auth = self.auth().await;
|
||||
return resolve_configured_provider_auth(auth.as_ref(), &self.info);
|
||||
}
|
||||
|
||||
let managed_auth = self.managed_auth();
|
||||
resolve_bedrock_provider_auth(managed_auth.as_ref(), &self.aws, self.endpoint).await
|
||||
resolve_bedrock_provider_auth(source, managed_auth.as_ref(), &self.aws, self.endpoint).await
|
||||
}
|
||||
|
||||
fn default_model_catalog(&self) -> ModelsResponse {
|
||||
@@ -208,10 +227,13 @@ impl ModelProvider for AmazonBedrockModelProvider {
|
||||
}
|
||||
|
||||
fn auth_manager(&self) -> Option<Arc<AuthManager>> {
|
||||
if self.info.has_command_auth() || self.managed_auth().is_some() {
|
||||
self.auth_manager.clone()
|
||||
} else {
|
||||
None
|
||||
match self.auth_source() {
|
||||
auth::BedrockAuthSource::CommandBearerToken
|
||||
| auth::BedrockAuthSource::ManagedBearerToken => self.auth_manager.clone(),
|
||||
auth::BedrockAuthSource::ConfiguredAwsProfile
|
||||
| auth::BedrockAuthSource::EnvBearerToken
|
||||
| auth::BedrockAuthSource::EnvAwsCredentials
|
||||
| auth::BedrockAuthSource::AwsSdk => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +274,8 @@ impl ModelProvider for AmazonBedrockModelProvider {
|
||||
fn account_state(&self) -> ProviderAccountResult {
|
||||
Ok(ProviderAccountState {
|
||||
account: Some(ProviderAccount::AmazonBedrock {
|
||||
uses_codex_managed_credentials: self.managed_auth().is_some(),
|
||||
uses_codex_managed_credentials: self.auth_source()
|
||||
== auth::BedrockAuthSource::ManagedBearerToken,
|
||||
}),
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
@@ -346,7 +369,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_auth_uses_configured_base_url_without_resolving_aws() {
|
||||
async fn command_auth_resolves_configured_and_regional_base_urls() {
|
||||
let mut provider_info = command_auth_provider(Some("https://proxy.example.com/v1"));
|
||||
provider_info.aws = Some(ModelProviderAwsAuthInfo {
|
||||
profile: Some("aws-profile-that-should-not-be-loaded".to_string()),
|
||||
@@ -381,26 +404,44 @@ mod tests {
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
);
|
||||
|
||||
let mut regional_provider_info = command_auth_provider(/*base_url*/ None);
|
||||
regional_provider_info.aws = Some(ModelProviderAwsAuthInfo {
|
||||
profile: None,
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: None,
|
||||
});
|
||||
let regional_provider =
|
||||
AmazonBedrockModelProvider::new(regional_provider_info, /*auth_manager*/ None);
|
||||
|
||||
assert_eq!(
|
||||
regional_provider
|
||||
.runtime_base_url()
|
||||
.await
|
||||
.expect("configured AWS region should resolve for command auth"),
|
||||
Some("https://bedrock-mantle.us-west-2.api.aws/openai/v1".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_auth_takes_precedence_over_aws_auth() {
|
||||
async fn configured_profile_takes_precedence_over_managed_auth() {
|
||||
let managed_auth = BedrockApiKeyAuth {
|
||||
api_key: "managed-bedrock-api-key".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
};
|
||||
let auth_manager =
|
||||
AuthManager::from_auth_for_testing(CodexAuth::BedrockApiKey(managed_auth.clone()));
|
||||
let aws = ModelProviderAwsAuthInfo {
|
||||
profile: None,
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: Some(AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: vec!["login".into()],
|
||||
timeout_ms: NonZeroU64::new(1_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
};
|
||||
let provider = AmazonBedrockModelProvider::new(
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
|
||||
profile: Some("aws-profile-that-should-not-be-loaded".to_string()),
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: Some(AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: vec!["login".into()],
|
||||
timeout_ms: NonZeroU64::new(1_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
})),
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(Some(aws.clone())),
|
||||
Some(auth_manager.clone()),
|
||||
);
|
||||
|
||||
@@ -439,6 +480,44 @@ mod tests {
|
||||
.get(http::header::AUTHORIZATION),
|
||||
Some(&HeaderValue::from_static("Bearer managed-bedrock-api-key"))
|
||||
);
|
||||
assert!(!provider.uses_aws_auth_recovery());
|
||||
|
||||
let configured_profile_provider = AmazonBedrockModelProvider::new(
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(Some(ModelProviderAwsAuthInfo {
|
||||
profile: Some("configured-aws-profile".to_string()),
|
||||
..aws
|
||||
})),
|
||||
Some(auth_manager),
|
||||
);
|
||||
|
||||
assert!(configured_profile_provider.auth_manager().is_none());
|
||||
assert_eq!(configured_profile_provider.auth().await, None);
|
||||
assert_eq!(
|
||||
configured_profile_provider.account_state(),
|
||||
Ok(ProviderAccountState {
|
||||
account: Some(ProviderAccount::AmazonBedrock {
|
||||
uses_codex_managed_credentials: false,
|
||||
}),
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
configured_profile_provider
|
||||
.runtime_base_url()
|
||||
.await
|
||||
.expect("configured AWS profile region should resolve"),
|
||||
Some("https://bedrock-mantle.us-west-2.api.aws/openai/v1".to_string())
|
||||
);
|
||||
assert!(
|
||||
configured_profile_provider
|
||||
.api_auth()
|
||||
.await
|
||||
.expect("configured AWS profile auth should resolve")
|
||||
.to_auth_headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.is_none()
|
||||
);
|
||||
assert!(configured_profile_provider.uses_aws_auth_recovery());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_model_provider_info::ModelProviderAwsAuthInfo;
|
||||
use codex_protocol::error::Result;
|
||||
|
||||
use super::BedrockEndpoint;
|
||||
use super::auth::BedrockAuthSource;
|
||||
use super::auth::resolve_region;
|
||||
use super::mantle::region_from_config;
|
||||
|
||||
@@ -22,10 +23,11 @@ pub(super) fn base_url(region: &str) -> String {
|
||||
}
|
||||
|
||||
pub(super) async fn bedrock_runtime_base_url(
|
||||
source: BedrockAuthSource,
|
||||
managed_auth: Option<&BedrockApiKeyAuth>,
|
||||
aws: &ModelProviderAwsAuthInfo,
|
||||
) -> Result<String> {
|
||||
let region = resolve_region(managed_auth, aws, BedrockEndpoint::Runtime).await?;
|
||||
let region = resolve_region(source, managed_auth, aws, BedrockEndpoint::Runtime).await?;
|
||||
Ok(base_url(®ion))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user