mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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.
This commit is contained in:
committed by
GitHub
Unverified
parent
87ab01834a
commit
06afd63f4a
@@ -532,6 +532,13 @@
|
||||
"personalAccessToken"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Amazon Bedrock bearer token managed by Codex.",
|
||||
"enum": [
|
||||
"bedrockApiKey"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+7
@@ -6724,6 +6724,13 @@
|
||||
"personalAccessToken"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Amazon Bedrock bearer token managed by Codex.",
|
||||
"enum": [
|
||||
"bedrockApiKey"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+7
@@ -1002,6 +1002,13 @@
|
||||
"personalAccessToken"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Amazon Bedrock bearer token managed by Codex.",
|
||||
"enum": [
|
||||
"bedrockApiKey"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -38,6 +38,13 @@
|
||||
"personalAccessToken"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Amazon Bedrock bearer token managed by Codex.",
|
||||
"enum": [
|
||||
"bedrockApiKey"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* Authentication mode for OpenAI-backed providers.
|
||||
*/
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken";
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
|
||||
@@ -41,6 +41,11 @@ pub enum AuthMode {
|
||||
#[ts(rename = "personalAccessToken")]
|
||||
#[strum(serialize = "personalAccessToken")]
|
||||
PersonalAccessToken,
|
||||
/// Amazon Bedrock bearer token managed by Codex.
|
||||
#[serde(rename = "bedrockApiKey")]
|
||||
#[ts(rename = "bedrockApiKey")]
|
||||
#[strum(serialize = "bedrockApiKey")]
|
||||
BedrockApiKey,
|
||||
}
|
||||
|
||||
impl AuthMode {
|
||||
@@ -48,7 +53,7 @@ impl AuthMode {
|
||||
pub fn has_chatgpt_account(self) -> bool {
|
||||
match self {
|
||||
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::AgentIdentity => false,
|
||||
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ fn remote_control_auth_dot_json(account_id: Option<&str>) -> AuthDotJson {
|
||||
last_refresh: Some(chrono::Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1842,6 +1842,7 @@ mod tests {
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ pub fn write_chatgpt_auth(
|
||||
last_refresh,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
save_auth(codex_home, &auth, cli_auth_credentials_store_mode).context("write auth.json")
|
||||
|
||||
@@ -119,6 +119,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
@@ -1324,6 +1324,7 @@ fn stored_auth_mode(auth: &codex_login::AuthDotJson) -> &'static str {
|
||||
codex_app_server_protocol::AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
|
||||
codex_app_server_protocol::AuthMode::AgentIdentity => "agent_identity",
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken => "personal_access_token",
|
||||
codex_app_server_protocol::AuthMode::BedrockApiKey => "bedrock_api_key",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1331,10 +1332,12 @@ fn stored_auth_mode_value(auth: &AuthDotJson) -> codex_app_server_protocol::Auth
|
||||
if let Some(mode) = auth.auth_mode {
|
||||
return mode;
|
||||
}
|
||||
if auth.openai_api_key.is_some() {
|
||||
codex_app_server_protocol::AuthMode::ApiKey
|
||||
} else if auth.personal_access_token.is_some() {
|
||||
if auth.personal_access_token.is_some() {
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken
|
||||
} else if auth.bedrock_api_key.is_some() {
|
||||
codex_app_server_protocol::AuthMode::BedrockApiKey
|
||||
} else if auth.openai_api_key.is_some() {
|
||||
codex_app_server_protocol::AuthMode::ApiKey
|
||||
} else {
|
||||
codex_app_server_protocol::AuthMode::Chatgpt
|
||||
}
|
||||
@@ -1407,6 +1410,11 @@ fn stored_auth_issues(
|
||||
issues.push("personal access token auth is missing a personal access token");
|
||||
}
|
||||
}
|
||||
codex_app_server_protocol::AuthMode::BedrockApiKey => {
|
||||
if auth.bedrock_api_key.is_none() {
|
||||
issues.push("Bedrock API key auth is missing a Bedrock API key");
|
||||
}
|
||||
}
|
||||
}
|
||||
issues
|
||||
}
|
||||
@@ -2437,6 +2445,7 @@ fn auth_mode_name(auth: &CodexAuth) -> &'static str {
|
||||
codex_app_server_protocol::AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
|
||||
codex_app_server_protocol::AuthMode::AgentIdentity => "agent_identity",
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken => "personal_access_token",
|
||||
codex_app_server_protocol::AuthMode::BedrockApiKey => "bedrock_api_key",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2566,7 +2575,10 @@ fn provider_auth_reachability_mode_from_auth(
|
||||
return ProviderAuthReachabilityMode::Chatgpt;
|
||||
}
|
||||
match stored_auth.map(stored_auth_mode_value) {
|
||||
Some(codex_app_server_protocol::AuthMode::ApiKey) => ProviderAuthReachabilityMode::ApiKey,
|
||||
Some(
|
||||
codex_app_server_protocol::AuthMode::ApiKey
|
||||
| codex_app_server_protocol::AuthMode::BedrockApiKey,
|
||||
) => ProviderAuthReachabilityMode::ApiKey,
|
||||
Some(
|
||||
codex_app_server_protocol::AuthMode::Chatgpt
|
||||
| codex_app_server_protocol::AuthMode::ChatgptAuthTokens
|
||||
@@ -3471,6 +3483,7 @@ mod tests {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -3489,6 +3502,7 @@ mod tests {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -3509,6 +3523,7 @@ mod tests {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: Some("at-test".to_string()),
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
assert_eq!(stored_auth_mode(&auth), "personal_access_token");
|
||||
@@ -3531,6 +3546,7 @@ mod tests {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -395,6 +395,10 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
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);
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
eprintln!("Not logged in");
|
||||
|
||||
@@ -1985,7 +1985,7 @@ impl AuthRequestTelemetryContext {
|
||||
let auth_telemetry = auth_header_telemetry(api_auth);
|
||||
Self {
|
||||
auth_mode: auth_mode.map(|mode| match mode {
|
||||
AuthMode::ApiKey => "ApiKey",
|
||||
AuthMode::ApiKey | AuthMode::BedrockApiKey => "ApiKey",
|
||||
AuthMode::Chatgpt
|
||||
| AuthMode::ChatgptAuthTokens
|
||||
| AuthMode::AgentIdentity
|
||||
|
||||
@@ -166,6 +166,7 @@ async fn login_with_access_token_writes_only_personal_access_token() {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: Some("at-login-test".to_string()),
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(auth.resolved_mode(), AuthMode::PersonalAccessToken);
|
||||
@@ -325,6 +326,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
last_refresh: Some(last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
},
|
||||
auth_dot_json
|
||||
);
|
||||
@@ -367,6 +369,7 @@ fn logout_removes_auth_file() -> Result<(), std::io::Error> {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
super::save_auth(dir.path(), &auth_dot_json, AuthCredentialsStoreMode::File)?;
|
||||
let auth_file = get_auth_file(dir.path());
|
||||
@@ -1112,6 +1115,7 @@ async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismat
|
||||
last_refresh: None,
|
||||
agent_identity: Some(agent_identity),
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use std::path::Path;
|
||||
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::manager::save_auth;
|
||||
use super::storage::AuthDotJson;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
|
||||
/// Managed Amazon Bedrock API key persisted in `auth.json`.
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BedrockApiKeyAuth {
|
||||
pub api_key: String,
|
||||
pub region: String,
|
||||
}
|
||||
|
||||
/// Writes an `auth.json` that contains only the Amazon Bedrock API key auth.
|
||||
pub fn login_with_bedrock_api_key(
|
||||
codex_home: &Path,
|
||||
api_key: &str,
|
||||
region: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::BedrockApiKey),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: Some(BedrockApiKeyAuth {
|
||||
api_key: api_key.to_string(),
|
||||
region: region.to_string(),
|
||||
}),
|
||||
};
|
||||
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "bedrock_api_key_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,162 @@
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::auth::AuthManager;
|
||||
use crate::auth::CodexAuth;
|
||||
use crate::auth::storage::AuthStorageBackend;
|
||||
use crate::auth::storage::FileAuthStorage;
|
||||
|
||||
fn api_key_auth() -> AuthDotJson {
|
||||
AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("sk-test-key".to_string()),
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bedrock_only_auth() -> AuthDotJson {
|
||||
AuthDotJson {
|
||||
auth_mode: None,
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: Some(bedrock_auth()),
|
||||
}
|
||||
}
|
||||
|
||||
fn bedrock_auth() -> BedrockApiKeyAuth {
|
||||
BedrockApiKeyAuth {
|
||||
api_key: "bedrock-api-key-test".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
|
||||
storage.save(&api_key_auth())?;
|
||||
login_with_bedrock_api_key(
|
||||
codex_home.path(),
|
||||
"bedrock-api-key-test",
|
||||
"us-east-1",
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let auth_manager = AuthManager::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let loaded = storage.load()?.expect("auth should be stored");
|
||||
let expected = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::BedrockApiKey),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: Some(bedrock_auth()),
|
||||
};
|
||||
assert_eq!(loaded, expected);
|
||||
assert_eq!(auth_manager.auth_mode(), Some(AuthMode::BedrockApiKey));
|
||||
assert_eq!(
|
||||
auth_manager.auth_cached().and_then(|auth| match auth {
|
||||
CodexAuth::BedrockApiKey(auth) => Some(auth),
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::Chatgpt(_)
|
||||
| CodexAuth::ChatgptAuthTokens(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_) => None,
|
||||
}),
|
||||
Some(bedrock_auth())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_removes_bedrock_auth() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
|
||||
login_with_bedrock_api_key(
|
||||
codex_home.path(),
|
||||
"bedrock-api-key-test",
|
||||
"us-east-1",
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
let auth_manager = AuthManager::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(auth_manager.logout().await?);
|
||||
|
||||
assert_eq!(storage.load()?, None);
|
||||
assert_eq!(auth_manager.auth_cached(), None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bedrock_only_auth_storage_creates_primary_auth() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
|
||||
storage.save(&bedrock_only_auth())?;
|
||||
|
||||
let auth_manager = AuthManager::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(auth_manager.auth_mode(), Some(AuthMode::BedrockApiKey));
|
||||
assert_eq!(
|
||||
auth_manager.auth_cached().and_then(|auth| match auth {
|
||||
CodexAuth::BedrockApiKey(auth) => Some(auth),
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::Chatgpt(_)
|
||||
| CodexAuth::ChatgptAuthTokens(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_) => None,
|
||||
}),
|
||||
Some(bedrock_auth())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_with_api_key_clears_bedrock_api_key() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
|
||||
login_with_bedrock_api_key(
|
||||
codex_home.path(),
|
||||
"bedrock-api-key-test",
|
||||
"us-east-1",
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
crate::auth::login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test-key",
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
assert_eq!(storage.load()?, Some(api_key_auth()));
|
||||
Ok(())
|
||||
}
|
||||
@@ -29,6 +29,7 @@ use super::access_token::classify_codex_access_token;
|
||||
use super::external_bearer::BearerTokenRefresher;
|
||||
use super::revoke::revoke_auth_tokens;
|
||||
pub use crate::auth::agent_identity::AgentIdentityAuth;
|
||||
pub use crate::auth::bedrock_api_key::BedrockApiKeyAuth;
|
||||
pub use crate::auth::personal_access_token::PersonalAccessTokenAuth;
|
||||
pub use crate::auth::storage::AgentIdentityAuthRecord;
|
||||
pub use crate::auth::storage::AuthDotJson;
|
||||
@@ -57,12 +58,14 @@ pub enum CodexAuth {
|
||||
ChatgptAuthTokens(ChatgptAuthTokens),
|
||||
AgentIdentity(AgentIdentityAuth),
|
||||
PersonalAccessToken(PersonalAccessTokenAuth),
|
||||
BedrockApiKey(BedrockApiKeyAuth),
|
||||
}
|
||||
|
||||
impl PartialEq for CodexAuth {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::PersonalAccessToken(a), Self::PersonalAccessToken(b)) => a == b,
|
||||
(Self::BedrockApiKey(a), Self::BedrockApiKey(b)) => a == b,
|
||||
_ => self.api_auth_mode() == other.api_auth_mode(),
|
||||
}
|
||||
}
|
||||
@@ -235,6 +238,14 @@ impl CodexAuth {
|
||||
};
|
||||
return Self::from_personal_access_token(personal_access_token).await;
|
||||
}
|
||||
if auth_mode == ApiAuthMode::BedrockApiKey {
|
||||
let Some(auth) = auth_dot_json.bedrock_api_key else {
|
||||
return Err(std::io::Error::other(
|
||||
"Bedrock API key auth is missing a Bedrock API key.",
|
||||
));
|
||||
};
|
||||
return Ok(Self::BedrockApiKey(auth));
|
||||
}
|
||||
|
||||
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
|
||||
let state = ChatgptAuthState {
|
||||
@@ -255,6 +266,7 @@ impl CodexAuth {
|
||||
ApiAuthMode::PersonalAccessToken => {
|
||||
unreachable!("personal access token mode is handled above")
|
||||
}
|
||||
ApiAuthMode::BedrockApiKey => unreachable!("bedrock api key mode is handled above"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +308,7 @@ impl CodexAuth {
|
||||
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => AuthMode::Chatgpt,
|
||||
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
|
||||
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
|
||||
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +319,7 @@ impl CodexAuth {
|
||||
Self::ChatgptAuthTokens(_) => ApiAuthMode::ChatgptAuthTokens,
|
||||
Self::AgentIdentity(_) => ApiAuthMode::AgentIdentity,
|
||||
Self::PersonalAccessToken(_) => ApiAuthMode::PersonalAccessToken,
|
||||
Self::BedrockApiKey(_) => ApiAuthMode::BedrockApiKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +360,8 @@ impl CodexAuth {
|
||||
Self::Chatgpt(_)
|
||||
| Self::ChatgptAuthTokens(_)
|
||||
| Self::AgentIdentity(_)
|
||||
| Self::PersonalAccessToken(_) => None,
|
||||
| Self::PersonalAccessToken(_)
|
||||
| Self::BedrockApiKey(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +390,9 @@ impl CodexAuth {
|
||||
"agent identity auth does not expose a bearer token",
|
||||
)),
|
||||
Self::PersonalAccessToken(auth) => Ok(auth.access_token().to_string()),
|
||||
Self::BedrockApiKey(_) => Err(std::io::Error::other(
|
||||
"Bedrock API key auth does not expose a Codex bearer token",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +465,10 @@ impl CodexAuth {
|
||||
let state = match self {
|
||||
Self::Chatgpt(auth) => &auth.state,
|
||||
Self::ChatgptAuthTokens(auth) => &auth.state,
|
||||
Self::ApiKey(_) | Self::AgentIdentity(_) | Self::PersonalAccessToken(_) => return None,
|
||||
Self::ApiKey(_)
|
||||
| Self::AgentIdentity(_)
|
||||
| Self::PersonalAccessToken(_)
|
||||
| Self::BedrockApiKey(_) => return None,
|
||||
};
|
||||
#[expect(clippy::unwrap_used)]
|
||||
state.auth_dot_json.lock().unwrap().clone()
|
||||
@@ -472,6 +493,7 @@ impl CodexAuth {
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
let client = create_client();
|
||||
@@ -589,6 +611,7 @@ pub fn login_with_api_key(
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)
|
||||
}
|
||||
@@ -612,6 +635,7 @@ pub async fn login_with_access_token(
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: Some(access_token.to_string()),
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => {
|
||||
@@ -627,6 +651,7 @@ pub async fn login_with_access_token(
|
||||
last_refresh: None,
|
||||
agent_identity: Some(jwt.to_string()),
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -698,7 +723,8 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
|
||||
if let Some(required_method) = config.forced_login_method {
|
||||
let method_violation = match (required_method, auth.auth_mode()) {
|
||||
(ForcedLoginMethod::Api, AuthMode::ApiKey) => None,
|
||||
(ForcedLoginMethod::Api, AuthMode::ApiKey)
|
||||
| (ForcedLoginMethod::Api, AuthMode::BedrockApiKey) => None,
|
||||
(ForcedLoginMethod::Chatgpt, AuthMode::Chatgpt)
|
||||
| (ForcedLoginMethod::Chatgpt, AuthMode::ChatgptAuthTokens)
|
||||
| (ForcedLoginMethod::Chatgpt, AuthMode::AgentIdentity)
|
||||
@@ -710,7 +736,8 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
"API key login is required, but ChatGPT is currently being used. Logging out."
|
||||
.to_string(),
|
||||
),
|
||||
(ForcedLoginMethod::Chatgpt, AuthMode::ApiKey) => Some(
|
||||
(ForcedLoginMethod::Chatgpt, AuthMode::ApiKey)
|
||||
| (ForcedLoginMethod::Chatgpt, AuthMode::BedrockApiKey) => Some(
|
||||
"ChatGPT login is required, but an API key is currently being used. Logging out."
|
||||
.to_string(),
|
||||
),
|
||||
@@ -727,7 +754,9 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
|
||||
if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
|
||||
let chatgpt_account_id = match &auth {
|
||||
CodexAuth::ApiKey(_) | CodexAuth::PersonalAccessToken(_) => return Ok(()),
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::PersonalAccessToken(_)
|
||||
| CodexAuth::BedrockApiKey(_) => return Ok(()),
|
||||
CodexAuth::AgentIdentity(_) => auth.get_account_id(),
|
||||
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
|
||||
let token_data = match auth.get_token_data() {
|
||||
@@ -1043,6 +1072,7 @@ impl AuthDotJson {
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1059,13 +1089,16 @@ impl AuthDotJson {
|
||||
Self::from_external_tokens(&external)
|
||||
}
|
||||
|
||||
fn resolved_mode(&self) -> ApiAuthMode {
|
||||
pub(super) fn resolved_mode(&self) -> ApiAuthMode {
|
||||
if let Some(mode) = self.auth_mode {
|
||||
return mode;
|
||||
}
|
||||
if self.personal_access_token.is_some() {
|
||||
return ApiAuthMode::PersonalAccessToken;
|
||||
}
|
||||
if self.bedrock_api_key.is_some() {
|
||||
return ApiAuthMode::BedrockApiKey;
|
||||
}
|
||||
if self.openai_api_key.is_some() {
|
||||
return ApiAuthMode::ApiKey;
|
||||
}
|
||||
@@ -1590,6 +1623,7 @@ impl AuthManager {
|
||||
_ => false,
|
||||
},
|
||||
(ApiAuthMode::PersonalAccessToken, ApiAuthMode::PersonalAccessToken) => a == b,
|
||||
(ApiAuthMode::BedrockApiKey, ApiAuthMode::BedrockApiKey) => a == b,
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
@@ -1851,7 +1885,8 @@ impl AuthManager {
|
||||
}
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_) => Ok(()),
|
||||
| CodexAuth::PersonalAccessToken(_)
|
||||
| CodexAuth::BedrockApiKey(_) => Ok(()),
|
||||
};
|
||||
if let Err(RefreshTokenError::Permanent(error)) = &result {
|
||||
self.record_permanent_refresh_failure_if_unchanged(&attempted_auth, error);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod access_token;
|
||||
mod agent_identity;
|
||||
mod bedrock_api_key;
|
||||
pub mod default_client;
|
||||
pub mod error;
|
||||
mod personal_access_token;
|
||||
@@ -10,6 +11,8 @@ mod external_bearer;
|
||||
mod manager;
|
||||
mod revoke;
|
||||
|
||||
pub use bedrock_api_key::BedrockApiKeyAuth;
|
||||
pub use bedrock_api_key::login_with_bedrock_api_key;
|
||||
pub use error::RefreshTokenFailedError;
|
||||
pub use error::RefreshTokenFailedReason;
|
||||
pub use manager::*;
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tracing::warn;
|
||||
|
||||
use super::BedrockApiKeyAuth;
|
||||
use crate::token_data::TokenData;
|
||||
use codex_agent_identity::AgentIdentityJwtClaims;
|
||||
use codex_agent_identity::decode_agent_identity_jwt;
|
||||
@@ -48,6 +49,9 @@ pub struct AuthDotJson {
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub personal_access_token: Option<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bedrock_api_key: Option<BedrockApiKeyAuth>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -20,6 +20,7 @@ async fn file_storage_load_returns_auth_dot_json() -> anyhow::Result<()> {
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
storage
|
||||
@@ -42,6 +43,7 @@ async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()> {
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
let file = get_auth_file(codex_home.path());
|
||||
@@ -76,6 +78,7 @@ async fn file_storage_round_trips_agent_identity_auth() -> anyhow::Result<()> {
|
||||
last_refresh: None,
|
||||
agent_identity: Some(agent_identity),
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
storage.save(&auth_dot_json)?;
|
||||
@@ -96,6 +99,7 @@ async fn file_storage_round_trips_personal_access_token_auth() -> anyhow::Result
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: Some("at-example".to_string()),
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
storage.save(&auth_dot_json)?;
|
||||
@@ -146,6 +150,7 @@ fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
let storage = create_auth_storage(dir.path().to_path_buf(), AuthCredentialsStoreMode::File);
|
||||
storage.save(&auth_dot_json)?;
|
||||
@@ -171,6 +176,7 @@ fn ephemeral_storage_save_load_delete_is_in_memory_only() -> anyhow::Result<()>
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
storage.save(&auth_dot_json)?;
|
||||
@@ -271,6 +277,7 @@ fn auth_with_prefix(prefix: &str) -> AuthDotJson {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +304,7 @@ fn keyring_auth_storage_load_returns_deserialized_auth() -> anyhow::Result<()> {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
seed_keyring_with_auth(
|
||||
&mock_keyring,
|
||||
@@ -341,6 +349,7 @@ fn keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Res
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
storage.save(&auth)?;
|
||||
|
||||
@@ -40,6 +40,7 @@ pub use auth::enforce_login_restrictions;
|
||||
pub use auth::load_auth_dot_json;
|
||||
pub use auth::login_with_access_token;
|
||||
pub use auth::login_with_api_key;
|
||||
pub use auth::login_with_bedrock_api_key;
|
||||
pub use auth::logout;
|
||||
pub use auth::logout_with_revoke;
|
||||
pub use auth::read_codex_access_token_from_env;
|
||||
|
||||
@@ -823,6 +823,7 @@ pub(crate) async fn persist_tokens_async(
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(&codex_home, &auth, auth_credentials_store_mode)?;
|
||||
Ok::<_, io::Error>((previous_auth, auth))
|
||||
@@ -1327,6 +1328,7 @@ mod tests {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ async fn refresh_token_succeeds_updates_storage() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -121,6 +122,7 @@ async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -187,6 +189,7 @@ async fn auth_refreshes_when_access_token_is_near_expiry() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -238,6 +241,7 @@ async fn auth_skips_access_token_outside_refresh_window() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -275,6 +279,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -286,6 +291,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -342,6 +348,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -354,6 +361,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -414,6 +422,7 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
|
||||
last_refresh: Some(stale_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -463,6 +472,7 @@ async fn refreshes_token_when_access_token_is_expired() -> Result<()> {
|
||||
last_refresh: Some(fresh_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -514,6 +524,7 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
|
||||
last_refresh: Some(stale_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -526,6 +537,7 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
|
||||
last_refresh: Some(fresh_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -579,6 +591,7 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul
|
||||
last_refresh: Some(stale_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -591,6 +604,7 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul
|
||||
last_refresh: Some(fresh_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -642,6 +656,7 @@ async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Re
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -696,6 +711,7 @@ async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -764,6 +780,7 @@ async fn refresh_token_does_not_retry_after_bad_request_reused_failure() -> Resu
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -832,6 +849,7 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -855,6 +873,7 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<
|
||||
last_refresh: Some(fresh_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -915,6 +934,7 @@ async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()>
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -969,6 +989,7 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -980,6 +1001,7 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -1065,6 +1087,7 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
@@ -1077,6 +1100,7 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(
|
||||
ctx.codex_home.path(),
|
||||
@@ -1136,6 +1160,7 @@ async fn unauthorized_recovery_requires_chatgpt_auth() -> Result<()> {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
ctx.write_auth(&auth).await?;
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ fn chatgpt_auth_with_refresh_token(refresh_token: &str) -> AuthDotJson {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,15 @@ 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,
|
||||
@@ -79,6 +83,12 @@ 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));
|
||||
}
|
||||
@@ -109,6 +119,7 @@ pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
|
||||
CodexAuth::AgentIdentity(auth) => {
|
||||
Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
|
||||
}
|
||||
CodexAuth::BedrockApiKey(_) => unreachable!("{BEDROCK_API_KEY_UNSUPPORTED_MESSAGE}"),
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::Chatgpt(_)
|
||||
| CodexAuth::ChatgptAuthTokens(_)
|
||||
@@ -122,8 +133,10 @@ pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
|
||||
|
||||
#[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::*;
|
||||
|
||||
@@ -135,4 +148,21 @@ mod tests {
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct ProviderAccountState {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProviderAccountError {
|
||||
MissingChatgptAccountDetails,
|
||||
UnsupportedBedrockApiKeyAuth,
|
||||
}
|
||||
|
||||
impl fmt::Display for ProviderAccountError {
|
||||
@@ -62,6 +63,12 @@ impl fmt::Display for ProviderAccountError {
|
||||
"email and plan type are required for chatgpt authentication"
|
||||
)
|
||||
}
|
||||
Self::UnsupportedBedrockApiKeyAuth => {
|
||||
write!(
|
||||
f,
|
||||
"Bedrock API key auth is only supported by the Amazon Bedrock model provider"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +239,9 @@ impl ModelProvider for ConfiguredModelProvider {
|
||||
})
|
||||
.map(|auth| match &auth {
|
||||
CodexAuth::ApiKey(_) => Ok(ProviderAccount::ApiKey),
|
||||
CodexAuth::BedrockApiKey(_) => {
|
||||
Err(ProviderAccountError::UnsupportedBedrockApiKeyAuth)
|
||||
}
|
||||
CodexAuth::Chatgpt(_)
|
||||
| CodexAuth::ChatgptAuthTokens(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
@@ -287,6 +297,7 @@ impl ModelProvider for ConfiguredModelProvider {
|
||||
mod tests {
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use codex_login::auth::BedrockApiKeyAuth;
|
||||
use codex_model_provider_info::ModelProviderAwsAuthInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
@@ -374,6 +385,13 @@ mod tests {
|
||||
.expect("valid model")
|
||||
}
|
||||
|
||||
fn bedrock_api_key_auth() -> CodexAuth {
|
||||
CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
|
||||
api_key: "bedrock-api-key-test".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_provider_uses_default_capabilities() {
|
||||
let provider = create_model_provider(
|
||||
@@ -491,6 +509,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_provider_rejects_bedrock_api_key_account_state() {
|
||||
let provider = create_model_provider(
|
||||
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
|
||||
Some(AuthManager::from_auth_for_testing(bedrock_api_key_auth())),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
provider.account_state(),
|
||||
Err(ProviderAccountError::UnsupportedBedrockApiKeyAuth)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_non_openai_provider_returns_no_account_state() {
|
||||
let provider = create_model_provider(
|
||||
|
||||
@@ -212,6 +212,7 @@ c2ln",
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
std::fs::create_dir_all(codex_home).expect("codex home should be created");
|
||||
std::fs::write(
|
||||
|
||||
@@ -54,7 +54,8 @@ pub enum TelemetryAuthMode {
|
||||
impl From<codex_app_server_protocol::AuthMode> for TelemetryAuthMode {
|
||||
fn from(mode: codex_app_server_protocol::AuthMode) -> Self {
|
||||
match mode {
|
||||
codex_app_server_protocol::AuthMode::ApiKey => Self::ApiKey,
|
||||
codex_app_server_protocol::AuthMode::ApiKey
|
||||
| codex_app_server_protocol::AuthMode::BedrockApiKey => Self::ApiKey,
|
||||
codex_app_server_protocol::AuthMode::Chatgpt
|
||||
| codex_app_server_protocol::AuthMode::ChatgptAuthTokens
|
||||
| codex_app_server_protocol::AuthMode::AgentIdentity
|
||||
|
||||
@@ -1253,6 +1253,7 @@ pub(crate) fn status_account_display_from_auth_mode(
|
||||
email: None,
|
||||
plan: plan_type.map(plan_type_display_name),
|
||||
}),
|
||||
Some(AuthMode::BedrockApiKey) => None,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ mod tests {
|
||||
last_refresh: Some(Utc::now()),
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(codex_home, &auth, AuthCredentialsStoreMode::File)
|
||||
.expect("chatgpt auth should save");
|
||||
@@ -158,6 +159,7 @@ mod tests {
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user