[codex] expose Bedrock credential source in account/read (#27751)

## Why

`account/read` currently reports only `type: "amazonBedrock"`, so
clients cannot distinguish a Codex-managed Bedrock API key from
credentials supplied by AWS. The app UI needs that distinction to render
the appropriate account state without duplicating provider-auth logic.

Credential-source selection belongs to the Bedrock model provider
because it already owns the precedence between managed Bedrock auth and
the external AWS credential path. This builds on #27443 and #27689.

## What changed

- Added `AmazonBedrockCredentialSource` with `codexManaged` and
`awsManaged` values.
- Included the selected credential source in
`ProviderAccount::AmazonBedrock` and the app-server `Account` response.
- Made `AmazonBedrockModelProvider::account_state()` classify the source
from its managed-auth state.
- Regenerated the app-server JSON and TypeScript schemas.
- Updated app-server account documentation and downstream TUI matches.

`codexManaged` means the provider found a managed Bedrock API key.
`awsManaged` identifies the provider's external AWS credential path; it
does not assert that the AWS credential chain has been validated.

## Testing

- Added model-provider coverage for Codex-managed precedence and
AWS-managed fallback.
- Added app-server protocol serialization coverage for both wire values.
- Added app-server integration coverage for both `account/read`
responses.
- `just test -p codex-protocol -p codex-model-provider -p
codex-app-server-protocol` (497 tests passed).

After rebasing onto #27711, the `codex-app-server` test target compiled
past the image-generation `PathUri` migration. Local linking was then
interrupted by disk exhaustion (`No space left on device`).
This commit is contained in:
Celia Chen
2026-06-16 00:14:53 -07:00
committed by GitHub
Unverified
parent 314fa3d25b
commit 12aaeb7bf8
15 changed files with 200 additions and 10 deletions
@@ -5847,6 +5847,14 @@
},
{
"properties": {
"credentialSource": {
"allOf": [
{
"$ref": "#/definitions/v2/AmazonBedrockCredentialSource"
}
],
"default": "awsManaged"
},
"type": {
"enum": [
"amazonBedrock"
@@ -6166,6 +6174,13 @@
"AgentPath": {
"type": "string"
},
"AmazonBedrockCredentialSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"AnalyticsConfig": {
"additionalProperties": true,
"properties": {
@@ -49,6 +49,14 @@
},
{
"properties": {
"credentialSource": {
"allOf": [
{
"$ref": "#/definitions/AmazonBedrockCredentialSource"
}
],
"default": "awsManaged"
},
"type": {
"enum": [
"amazonBedrock"
@@ -368,6 +376,13 @@
"AgentPath": {
"type": "string"
},
"AmazonBedrockCredentialSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"AnalyticsConfig": {
"additionalProperties": true,
"properties": {
@@ -45,6 +45,14 @@
},
{
"properties": {
"credentialSource": {
"allOf": [
{
"$ref": "#/definitions/AmazonBedrockCredentialSource"
}
],
"default": "awsManaged"
},
"type": {
"enum": [
"amazonBedrock"
@@ -61,6 +69,13 @@
}
]
},
"AmazonBedrockCredentialSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"PlanType": {
"enum": [
"free",
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AmazonBedrockCredentialSource = "codexManaged" | "awsManaged";
+1
View File
@@ -3,6 +3,7 @@
export type { AbsolutePathBuf } from "./AbsolutePathBuf";
export type { AgentMessageInputContent } from "./AgentMessageInputContent";
export type { AgentPath } from "./AgentPath";
export type { AmazonBedrockCredentialSource } from "./AmazonBedrockCredentialSource";
export type { ApiPathString } from "./ApiPathString";
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AmazonBedrockCredentialSource } from "../AmazonBedrockCredentialSource";
import type { PlanType } from "../PlanType";
export type Account = { "type": "apiKey", } | { "type": "chatgpt", email: string, planType: PlanType, } | { "type": "amazonBedrock", };
export type Account = { "type": "apiKey", } | { "type": "chatgpt", email: string, planType: PlanType, } | { "type": "amazonBedrock", credentialSource: AmazonBedrockCredentialSource, };
@@ -1674,6 +1674,7 @@ mod tests {
use super::*;
use anyhow::Result;
use codex_protocol::ThreadId;
use codex_protocol::account::AmazonBedrockCredentialSource;
use codex_protocol::account::PlanType;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::parse_command::ParsedCommand;
@@ -2777,6 +2778,41 @@ mod tests {
serde_json::to_value(&chatgpt)?,
);
let codex_managed_bedrock = v2::Account::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::CodexManaged,
};
assert_eq!(
json!({
"type": "amazonBedrock",
"credentialSource": "codexManaged",
}),
serde_json::to_value(&codex_managed_bedrock)?,
);
let aws_managed_bedrock = v2::Account::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::AwsManaged,
};
assert_eq!(
json!({
"type": "amazonBedrock",
"credentialSource": "awsManaged",
}),
serde_json::to_value(&aws_managed_bedrock)?,
);
Ok(())
}
#[test]
fn account_defaults_legacy_bedrock_credential_source() -> Result<()> {
assert_eq!(
v2::Account::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::AwsManaged,
},
serde_json::from_value(json!({
"type": "amazonBedrock",
}))?,
);
Ok(())
}
@@ -1,5 +1,6 @@
use crate::protocol::common::AuthMode;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::account::AmazonBedrockCredentialSource;
use codex_protocol::account::PlanType;
use codex_protocol::account::ProviderAccount;
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
@@ -28,7 +29,14 @@ pub enum Account {
#[serde(rename = "amazonBedrock", rename_all = "camelCase")]
#[ts(rename = "amazonBedrock", rename_all = "camelCase")]
AmazonBedrock {},
AmazonBedrock {
#[serde(default = "default_bedrock_credential_source")]
credential_source: AmazonBedrockCredentialSource,
},
}
fn default_bedrock_credential_source() -> AmazonBedrockCredentialSource {
AmazonBedrockCredentialSource::AwsManaged
}
impl From<ProviderAccount> for Account {
@@ -36,7 +44,9 @@ impl From<ProviderAccount> for Account {
match account {
ProviderAccount::ApiKey => Self::ApiKey {},
ProviderAccount::Chatgpt { email, plan_type } => Self::Chatgpt { email, plan_type },
ProviderAccount::AmazonBedrock => Self::AmazonBedrock {},
ProviderAccount::AmazonBedrock { credential_source } => {
Self::AmazonBedrock { credential_source }
}
}
}
}
+3
View File
@@ -1902,12 +1902,15 @@ Response examples:
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true } }
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true } }
{ "id": 1, "result": { "account": { "type": "amazonBedrock", "credentialSource": "codexManaged" }, "requiresOpenaiAuth": false } }
{ "id": 1, "result": { "account": { "type": "amazonBedrock", "credentialSource": "awsManaged" }, "requiresOpenaiAuth": false } }
```
Field notes:
- `refreshToken` (bool): set `true` to force a token refresh.
- `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials.
- Amazon Bedrock reports `credentialSource: "codexManaged"` when it uses a Bedrock API key managed by Codex. Otherwise it reports `credentialSource: "awsManaged"` for the external AWS credential path. This identifies the selected credential source; it does not validate that the AWS credential chain can resolve credentials.
### 2) Log in with an API key
+50 -1
View File
@@ -37,6 +37,8 @@ use codex_login::AuthKeyringBackendKind;
use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_login::login_with_api_key;
use codex_login::login_with_bedrock_api_key;
use codex_protocol::account::AmazonBedrockCredentialSource;
use codex_protocol::account::PlanType as AccountPlanType;
use core_test_support::responses;
use pretty_assertions::assert_eq;
@@ -1725,13 +1727,60 @@ region = "us-west-2"
let received: GetAccountResponse = to_response(resp)?;
let expected = GetAccountResponse {
account: Some(Account::AmazonBedrock {}),
account: Some(Account::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::AwsManaged,
}),
requires_openai_auth: false,
};
assert_eq!(received, expected);
Ok(())
}
#[tokio::test]
async fn get_account_with_managed_bedrock_provider() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
model_provider_id: Some("amazon-bedrock".to_string()),
..Default::default()
},
)?;
login_with_bedrock_api_key(
codex_home.path(),
"managed-bedrock-api-key",
"us-west-2",
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
)?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: GetAccountResponse = to_response(resp)?;
assert_eq!(
received,
GetAccountResponse {
account: Some(Account::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::CodexManaged,
}),
requires_openai_auth: false,
}
);
Ok(())
}
#[tokio::test]
async fn get_account_with_chatgpt() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -15,6 +15,7 @@ use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::manager::SharedModelsManager;
use codex_models_manager::manager::StaticModelsManager;
use codex_protocol::account::AmazonBedrockCredentialSource;
use codex_protocol::account::ProviderAccount;
use codex_protocol::error::Result;
use codex_protocol::openai_models::ModelsResponse;
@@ -130,8 +131,13 @@ impl ModelProvider for AmazonBedrockModelProvider {
}
fn account_state(&self) -> ProviderAccountResult {
let credential_source = if self.managed_auth().is_some() {
AmazonBedrockCredentialSource::CodexManaged
} else {
AmazonBedrockCredentialSource::AwsManaged
};
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock),
account: Some(ProviderAccount::AmazonBedrock { credential_source }),
requires_openai_auth: false,
})
}
@@ -209,6 +215,15 @@ mod tests {
provider.auth().await,
Some(CodexAuth::BedrockApiKey(managed_auth))
);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::CodexManaged,
}),
requires_openai_auth: false,
})
);
assert_eq!(
provider
.runtime_base_url()
@@ -238,6 +253,15 @@ mod tests {
assert!(provider.auth_manager().is_none());
assert_eq!(provider.auth().await, None);
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock {
credential_source: AmazonBedrockCredentialSource::AwsManaged,
}),
requires_openai_auth: false,
})
);
}
#[test]
+4 -1
View File
@@ -577,7 +577,10 @@ mod tests {
assert_eq!(
provider.account_state(),
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock),
account: Some(ProviderAccount::AmazonBedrock {
credential_source:
codex_protocol::account::AmazonBedrockCredentialSource::AwsManaged,
}),
requires_openai_auth: false,
})
);
+15 -2
View File
@@ -34,8 +34,21 @@ pub enum PlanType {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderAccount {
ApiKey,
Chatgpt { email: String, plan_type: PlanType },
AmazonBedrock,
Chatgpt {
email: String,
plan_type: PlanType,
},
AmazonBedrock {
credential_source: AmazonBedrockCredentialSource,
},
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum AmazonBedrockCredentialSource {
CodexManaged,
AwsManaged,
}
impl PlanType {
+1 -1
View File
@@ -327,7 +327,7 @@ impl AppServerSession {
true,
)
}
Some(Account::AmazonBedrock {}) => {
Some(Account::AmazonBedrock { .. }) => {
(None, None, None, None, FeedbackAudience::External, false)
}
None => (None, None, None, None, FeedbackAudience::External, false),
+1 -1
View File
@@ -1874,7 +1874,7 @@ async fn get_login_status(
Ok(match account.account {
Some(AppServerAccount::ApiKey {}) => LoginStatus::AuthMode(AppServerAuthMode::ApiKey),
Some(AppServerAccount::Chatgpt { .. }) => LoginStatus::AuthMode(AppServerAuthMode::Chatgpt),
Some(AppServerAccount::AmazonBedrock {}) => LoginStatus::NotAuthenticated,
Some(AppServerAccount::AmazonBedrock { .. }) => LoginStatus::NotAuthenticated,
None => LoginStatus::NotAuthenticated,
})
}