Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions codex-rs/app-server/src/request_processors/account_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,16 @@ impl AccountRequestProcessor {
.set_auth_mode(self.auth_manager.get_api_auth_mode());
}

fn current_account_updated_notification(&self) -> AccountUpdatedNotification {
let auth = self.auth_manager.auth_cached();
async fn current_account_updated_notification(&self) -> AccountUpdatedNotification {
let provider = create_model_provider(
self.config.model_provider.clone(),
Some(self.auth_manager.clone()),
);
let auth = provider.auth().await;
AccountUpdatedNotification {
auth_mode: auth
.as_ref()
.map(CodexAuth::api_auth_mode)
.map(CodexAuth::auth_mode)
.map(auth_mode_to_api),
plan_type: auth.as_ref().and_then(CodexAuth::account_plan_type),
}
Expand Down Expand Up @@ -657,7 +661,7 @@ impl AccountRequestProcessor {

self.outgoing
.send_server_notification(ServerNotification::AccountUpdated(
self.current_account_updated_notification(),
self.current_account_updated_notification().await,
))
.await;
}
Expand Down
75 changes: 38 additions & 37 deletions codex-rs/cli/src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::Config;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CLIENT_ID;
use codex_login::CodexAuth;
Expand All @@ -19,6 +20,7 @@ use codex_login::login_with_api_key;
use codex_login::logout_with_revoke;
use codex_login::run_device_code_login;
use codex_login::run_login_server;
use codex_model_provider::create_model_provider;
use codex_protocol::auth::AuthMode;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_utils_cli::CliConfigOverrides;
Expand Down Expand Up @@ -423,51 +425,50 @@ pub async fn run_login_with_device_code_fallback_to_browser(

pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
let auth_route_config = config.auth_route_config();
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await;
let provider = create_model_provider(config.model_provider.clone(), Some(auth_manager));
let auth = provider.auth().await;
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);

if config.model_provider.is_amazon_bedrock() {
if auth_mode == Some(AuthMode::BedrockApiKey) {
eprintln!("Logged in using Amazon Bedrock API key");
} else {
eprintln!("Using Amazon Bedrock AWS SDK credential chain");
}
std::process::exit(0);
}

match CodexAuth::from_auth_storage(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
auth_route_config.as_ref(),
)
.await
{
Ok(Some(auth)) => match auth.auth_mode() {
AuthMode::ApiKey => match auth.get_token() {
Ok(api_key) => {
eprintln!("Logged in using an API key - {}", safe_format_key(&api_key));
std::process::exit(0);
}
Err(e) => {
eprintln!("Unexpected error retrieving API key: {e}");
std::process::exit(1);
}
},
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => {
eprintln!("Logged in using ChatGPT");
match (auth_mode, auth) {
(Some(AuthMode::ApiKey), Some(auth)) => match auth.get_token() {
Ok(api_key) => {
eprintln!("Logged in using an API key - {}", safe_format_key(&api_key));
std::process::exit(0);
}
AuthMode::AgentIdentity => {
eprintln!("Logged in using access token");
std::process::exit(0);
}
AuthMode::PersonalAccessToken => {
eprintln!("Logged in using personal access token");
std::process::exit(0);
}
AuthMode::BedrockApiKey => {
eprintln!("Logged in using Amazon Bedrock API key");
std::process::exit(0);
Err(e) => {
eprintln!("Unexpected error retrieving API key: {e}");
std::process::exit(1);
}
},
Ok(None) => {
(Some(AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens), Some(_)) => {
eprintln!("Logged in using ChatGPT");
std::process::exit(0);
}
(Some(AuthMode::AgentIdentity), Some(_)) => {
eprintln!("Logged in using access token");
std::process::exit(0);
}
(Some(AuthMode::PersonalAccessToken), Some(_)) => {
eprintln!("Logged in using personal access token");
std::process::exit(0);
}
(None, None) => {
eprintln!("Not logged in");
std::process::exit(1);
}
Err(e) => {
eprintln!("Error checking login status: {e}");
(auth_mode, _) => {
eprintln!("Unexpected provider auth state: mode={auth_mode:?}");
std::process::exit(1);
}
}
Expand Down
35 changes: 28 additions & 7 deletions codex-rs/login/src/auth/auth_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1596,26 +1596,47 @@ async fn personal_access_token_does_not_offer_unauthorized_recovery() {
#[serial(codex_auth_env)]
async fn load_auth_keeps_codex_api_key_env_precedence() {
let codex_home = tempdir().unwrap();
let bedrock_auth = BedrockApiKeyAuth {
api_key: "bedrock-api-key-test".to_string(),
region: "us-east-1".to_string(),
};
let record = agent_identity_record(WORKSPACE_ID_ALLOWED);
let agent_identity = fake_agent_identity_jwt(&record).expect("fake agent identity");
let _access_token_guard = EnvVarGuard::set(CODEX_ACCESS_TOKEN_ENV_VAR, &agent_identity);
let _api_key_guard = EnvVarGuard::set(CODEX_API_KEY_ENV_VAR, "sk-env");

let auth = super::load_auth(
codex_home.path(),
let auth_manager = AuthManager::new(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ true,
AuthCredentialsStoreMode::File,
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::Direct,
/*agent_identity_authapi_base_url*/ None,
/*auth_route_config*/ None,
)
.await
.expect("env auth should load")
.expect("env auth should be present");
.await;
assert_eq!(auth_manager.bedrock_api_key_auth_cached(), None);

assert_eq!(auth.api_key(), Some("sk-env"));
crate::auth::login_with_bedrock_api_key(
codex_home.path(),
&bedrock_auth.api_key,
&bedrock_auth.region,
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::Direct,
)
.expect("seed Bedrock auth");
assert!(auth_manager.reload().await);

assert_eq!(
auth_manager
.auth_cached()
.and_then(|auth| auth.api_key().map(str::to_string)),
Some("sk-env".to_string())
);
assert_eq!(
auth_manager.bedrock_api_key_auth_cached(),
Some(bedrock_auth)
);
}

#[tokio::test]
Expand Down
75 changes: 74 additions & 1 deletion codex-rs/login/src/auth/bedrock_api_key.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
use std::fmt;
use std::path::Path;

use codex_config::types::AuthCredentialsStoreMode;
use serde::Deserialize;
use serde::Serialize;

use super::manager::AuthManager;
use super::manager::load_auth_dot_json;
use super::manager::save_auth;
use super::storage::AuthDotJson;
use super::storage::AuthKeyringBackendKind;
use codex_protocol::auth::AuthMode;

/// Managed Amazon Bedrock API key persisted in `auth.json`.
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct BedrockApiKeyAuth {
pub api_key: String,
pub region: String,
}

impl fmt::Debug for BedrockApiKeyAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BedrockApiKeyAuth")
.field("api_key", &"<redacted>")
.field("region", &self.region)
.finish()
}
}

/// Writes an `auth.json` that contains only the Amazon Bedrock API key auth.
pub fn login_with_bedrock_api_key(
codex_home: &Path,
Expand Down Expand Up @@ -44,6 +56,67 @@ pub fn login_with_bedrock_api_key(
)
}

pub(super) fn load_stored_bedrock_api_key(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyringBackendKind,
) -> std::io::Result<Option<BedrockApiKeyAuth>> {
load_stored_bedrock_api_key_auth(
codex_home,
auth_credentials_store_mode,
keyring_backend_kind,
)
.map(|auth| auth.and_then(|auth| auth.bedrock_api_key))
}

fn load_stored_bedrock_api_key_auth(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyringBackendKind,
) -> std::io::Result<Option<AuthDotJson>> {
load_auth_dot_json(
codex_home,
auth_credentials_store_mode,
keyring_backend_kind,
)
.map(|auth| auth.filter(AuthDotJson::is_bedrock_api_key))
}

impl AuthManager {
pub fn has_stored_bedrock_api_key_auth(&self) -> std::io::Result<bool> {
load_stored_bedrock_api_key_auth(
&self.codex_home,
self.auth_credentials_store_mode,
self.keyring_backend_kind,
)
.map(|auth| auth.is_some())
}

/// Persists managed Bedrock auth without changing the in-memory auth snapshot.
///
/// The account coordinator publishes the provider-scoped cache update only
/// after the associated config mutation succeeds.
pub fn persist_bedrock_api_key_auth(&self, api_key: &str, region: &str) -> std::io::Result<()> {
login_with_bedrock_api_key(
&self.codex_home,
api_key,
region,
self.auth_credentials_store_mode,
self.keyring_backend_kind,
)
}

/// Reloads only managed Bedrock auth, preserving cached general Codex auth.
pub fn reload_bedrock_api_key_auth(&self) -> std::io::Result<bool> {
let bedrock_api_key = load_stored_bedrock_api_key(
&self.codex_home,
self.auth_credentials_store_mode,
self.keyring_backend_kind,
)?;
Ok(self.set_cached_bedrock_api_key_auth(bedrock_api_key))
}
}

#[cfg(test)]
#[path = "bedrock_api_key_tests.rs"]
mod tests;
Loading
Loading