This commit is contained in:
celia-oai
2026-06-02 16:17:09 -07:00
parent 7f3d9ca324
commit c002d7ab26
37 changed files with 432 additions and 110 deletions
+5
View File
@@ -1447,6 +1447,7 @@ mod tests {
result: serde_json::to_value(GetAccountResponse {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
})
.expect("response should serialize"),
}),
@@ -1500,6 +1501,7 @@ mod tests {
result: serde_json::to_value(GetAccountResponse {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
})
.expect("response should serialize"),
}),
@@ -1575,6 +1577,7 @@ mod tests {
GetAccountResponse {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
}
);
@@ -1676,6 +1679,7 @@ mod tests {
result: serde_json::to_value(GetAccountResponse {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
})
.expect("response should serialize"),
}),
@@ -1729,6 +1733,7 @@ mod tests {
GetAccountResponse {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
}
);
@@ -288,8 +288,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -326,4 +326,4 @@
],
"title": "PermissionsRequestApprovalParams",
"type": "object"
}
}
+3 -3
View File
@@ -1593,8 +1593,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -2005,4 +2005,4 @@
}
],
"title": "ServerRequest"
}
}
@@ -3786,8 +3786,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -5684,6 +5684,9 @@
},
{
"properties": {
"authSource": {
"$ref": "#/definitions/v2/AmazonBedrockAuthSource"
},
"type": {
"enum": [
"amazonBedrock"
@@ -5693,6 +5696,7 @@
}
},
"required": [
"authSource",
"type"
],
"title": "AmazonBedrockAccount",
@@ -5903,6 +5907,13 @@
"AgentPath": {
"type": "string"
},
"AmazonBedrockAuthSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"AnalyticsConfig": {
"additionalProperties": true,
"properties": {
@@ -7702,6 +7713,31 @@
"title": "ProjectConfigLayerSource",
"type": "object"
},
{
"description": "Local Codex-owned provider state, such as a provider activated by managed auth.",
"properties": {
"file": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"provider": {
"type": "string"
},
"type": {
"enum": [
"providerState"
],
"title": "ProviderStateConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"provider",
"type"
],
"title": "ProviderStateConfigLayerSource",
"type": "object"
},
{
"description": "Session-layer overrides supplied via `-c`/`--config`.",
"properties": {
@@ -19279,4 +19315,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
@@ -49,6 +49,9 @@
},
{
"properties": {
"authSource": {
"$ref": "#/definitions/AmazonBedrockAuthSource"
},
"type": {
"enum": [
"amazonBedrock"
@@ -58,6 +61,7 @@
}
},
"required": [
"authSource",
"type"
],
"title": "AmazonBedrockAccount",
@@ -268,6 +272,13 @@
"AgentPath": {
"type": "string"
},
"AmazonBedrockAuthSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"AnalyticsConfig": {
"additionalProperties": true,
"properties": {
@@ -4064,6 +4075,31 @@
"title": "ProjectConfigLayerSource",
"type": "object"
},
{
"description": "Local Codex-owned provider state, such as a provider activated by managed auth.",
"properties": {
"file": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"provider": {
"type": "string"
},
"type": {
"enum": [
"providerState"
],
"title": "ProviderStateConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"provider",
"type"
],
"title": "ProviderStateConfigLayerSource",
"type": "object"
},
{
"description": "Session-layer overrides supplied via `-c`/`--config`.",
"properties": {
@@ -589,6 +589,31 @@
"title": "ProjectConfigLayerSource",
"type": "object"
},
{
"description": "Local Codex-owned provider state, such as a provider activated by managed auth.",
"properties": {
"file": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"provider": {
"type": "string"
},
"type": {
"enum": [
"providerState"
],
"title": "ProviderStateConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"provider",
"type"
],
"title": "ProviderStateConfigLayerSource",
"type": "object"
},
{
"description": "Session-layer overrides supplied via `-c`/`--config`.",
"properties": {
@@ -154,6 +154,31 @@
"title": "ProjectConfigLayerSource",
"type": "object"
},
{
"description": "Local Codex-owned provider state, such as a provider activated by managed auth.",
"properties": {
"file": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"provider": {
"type": "string"
},
"type": {
"enum": [
"providerState"
],
"title": "ProviderStateConfigLayerSourceType",
"type": "string"
}
},
"required": [
"file",
"provider",
"type"
],
"title": "ProviderStateConfigLayerSource",
"type": "object"
},
{
"description": "Session-layer overrides supplied via `-c`/`--config`.",
"properties": {
@@ -45,6 +45,9 @@
},
{
"properties": {
"authSource": {
"$ref": "#/definitions/AmazonBedrockAuthSource"
},
"type": {
"enum": [
"amazonBedrock"
@@ -54,6 +57,7 @@
}
},
"required": [
"authSource",
"type"
],
"title": "AmazonBedrockAccount",
@@ -61,6 +65,13 @@
}
]
},
"AmazonBedrockAuthSource": {
"enum": [
"codexManaged",
"awsManaged"
],
"type": "string"
},
"PlanType": {
"enum": [
"free",
@@ -2,5 +2,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PlanType } from "../PlanType";
import type { AmazonBedrockAuthSource } from "./AmazonBedrockAuthSource";
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", authSource: AmazonBedrockAuthSource, };
@@ -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 AmazonBedrockAuthSource = "codexManaged" | "awsManaged";
@@ -28,4 +28,4 @@ file: AbsolutePathBuf,
* Name of the selected profile-v2 config layered on top of the base
* user config, when this layer represents one.
*/
profile: string | null, } | { "type": "project", dotCodexFolder: AbsolutePathBuf, } | { "type": "sessionFlags" } | { "type": "legacyManagedConfigTomlFromFile", file: AbsolutePathBuf, } | { "type": "legacyManagedConfigTomlFromMdm" };
profile: string | null, } | { "type": "project", dotCodexFolder: AbsolutePathBuf, } | { "type": "providerState", provider: string, file: AbsolutePathBuf, } | { "type": "sessionFlags" } | { "type": "legacyManagedConfigTomlFromFile", file: AbsolutePathBuf, } | { "type": "legacyManagedConfigTomlFromMdm" };
@@ -13,6 +13,7 @@ export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPerm
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
export type { AmazonBedrockAuthSource } from "./AmazonBedrockAuthSource";
export type { AnalyticsConfig } from "./AnalyticsConfig";
export type { AppBranding } from "./AppBranding";
export type { AppInfo } from "./AppInfo";
@@ -241,6 +241,7 @@ pub struct GetAccountParams {
pub struct GetAccountResponse {
pub account: Option<Account>,
pub requires_openai_auth: bool,
pub codex_managed_auth: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -80,6 +80,15 @@ pub enum ConfigLayerSource {
dot_codex_folder: AbsolutePathBuf,
},
/// Local Codex-owned provider state, such as a provider activated by
/// managed auth.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
ProviderState {
provider: String,
file: AbsolutePathBuf,
},
/// Session-layer overrides supplied via `-c`/`--config`.
SessionFlags,
@@ -112,6 +121,7 @@ impl ConfigLayerSource {
}
}
ConfigLayerSource::Project { .. } => 25,
ConfigLayerSource::ProviderState { .. } => 28,
ConfigLayerSource::SessionFlags => 30,
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40,
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => 50,
+6 -5
View File
@@ -1773,7 +1773,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
- `account/login/start` — begin login (`apiKey`, `chatgpt`, `chatgptDeviceCode`).
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
- `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`.
- `account/logout` — sign out; triggers `account/updated`.
- `account/logout` — sign out; triggers `account/updated` when Codex-managed auth changes.
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
- `account/rateLimits/read` — fetch ChatGPT rate limits and an optional effective monthly credit limit; updates arrive via `account/rateLimits/updated` (notify).
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. This is a sparse rolling update; merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot.
@@ -1792,16 +1792,17 @@ Request:
Response examples:
```json
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
{ "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": null, "requiresOpenaiAuth": false, "codexManagedAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true, "codexManagedAuth": false } } // OpenAI auth required (typical for OpenAI-hosted models)
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true, "codexManagedAuth": true } }
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true, "codexManagedAuth": true } }
```
Field notes:
- `refreshToken` (bool): set `true` to force a token refresh.
- `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials.
- `codexManagedAuth` is `true` when `account/logout` can clear Codex-owned persisted auth for the current account. When `false`, logout is a successful no-op for auth managed outside Codex.
### 2) Log in with an API key
@@ -631,6 +631,12 @@ fn override_message(layer: &ConfigLayerSource) -> String {
"Overridden by project config: {}/{CONFIG_TOML_FILE}",
dot_codex_folder.display(),
),
ConfigLayerSource::ProviderState { provider, file } => {
format!(
"Overridden by provider state ({provider}): {}",
file.display()
)
}
ConfigLayerSource::SessionFlags => "Overridden by session flags".to_string(),
ConfigLayerSource::User { file, .. } => {
format!("Overridden by user config: {}", file.display())
@@ -47,8 +47,6 @@ use codex_app_server_protocol::CommandExecParams;
use codex_app_server_protocol::CommandExecResizeParams;
use codex_app_server_protocol::CommandExecTerminateParams;
use codex_app_server_protocol::CommandExecWriteParams;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit as ConfigWriteEdit;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::ConversationGitInfo;
use codex_app_server_protocol::ConversationSummary;
@@ -102,7 +100,6 @@ use codex_app_server_protocol::McpServerStatusDetail;
use codex_app_server_protocol::McpServerToolCallParams;
use codex_app_server_protocol::McpServerToolCallResponse;
use codex_app_server_protocol::MemoryResetResponse;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::MockExperimentalMethodParams;
use codex_app_server_protocol::MockExperimentalMethodResponse;
use codex_app_server_protocol::ModelListParams;
@@ -365,6 +362,7 @@ use codex_model_provider::is_supported_amazon_bedrock_region;
use codex_model_provider::save_amazon_bedrock_auth;
use codex_models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets;
use codex_protocol::ThreadId;
use codex_protocol::account::ProviderAccount;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::Personality;
@@ -331,31 +331,6 @@ impl AccountRequestProcessor {
save_amazon_bedrock_auth(&self.config.codex_home, key, region)
.map_err(|err| internal_error(format!("failed to save Amazon Bedrock auth: {err}")))?;
self.config_manager
.batch_write(ConfigBatchWriteParams {
edits: vec![
ConfigWriteEdit {
key_path: "model_provider".to_string(),
value: serde_json::json!(AMAZON_BEDROCK_PROVIDER_ID),
merge_strategy: MergeStrategy::Upsert,
},
ConfigWriteEdit {
key_path: format!(
"model_providers.{AMAZON_BEDROCK_PROVIDER_ID}.aws.region"
),
value: serde_json::json!(region),
merge_strategy: MergeStrategy::Upsert,
},
],
file_path: None,
expected_version: None,
reload_user_config: false,
})
.await
.map_err(|err| {
internal_error(format!("failed to update Amazon Bedrock config: {err}"))
})?;
let next_config = self
.config_manager
.load_latest_config(/*fallback_cwd*/ None)
@@ -796,54 +771,63 @@ impl AccountRequestProcessor {
}
}
match self.auth_manager.logout_with_revoke().await {
Ok(_) => {}
Err(err) => {
return Err(internal_error(format!("logout failed: {err}")));
let current_config = self
.config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
if current_config.model_provider_id == AMAZON_BEDROCK_PROVIDER_ID {
let provider = create_model_provider(
current_config.model_provider.clone(),
Some(self.auth_manager.clone()),
Some(current_config.codex_home.to_path_buf()),
);
let account_state = provider.account_state().map_err(|err| match err {
ProviderAccountError::MissingChatgptAccountDetails => {
invalid_request("email and plan type are required for chatgpt authentication")
}
})?;
let codex_managed_auth = account_state.codex_managed_auth;
let Some(ProviderAccount::AmazonBedrock) = account_state.account else {
return Err(invalid_request(
"active Amazon Bedrock provider did not report an Amazon Bedrock account",
));
};
if !codex_managed_auth {
return Ok(None);
}
}
let removed_bedrock_auth =
delete_amazon_bedrock_auth(&self.config.codex_home).map_err(|err| {
internal_error(format!("failed to remove Amazon Bedrock auth: {err}"))
})?;
if removed_bedrock_auth {
let current_config = self
let next_config = self
.config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
if current_config.model_provider_id == AMAZON_BEDROCK_PROVIDER_ID {
self.config_manager
.batch_write(ConfigBatchWriteParams {
edits: vec![ConfigWriteEdit {
key_path: "model_provider".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
}],
file_path: None,
expected_version: None,
reload_user_config: false,
})
.await
.map_err(|err| {
internal_error(format!("failed to update Amazon Bedrock config: {err}"))
})?;
for thread_id in self.thread_manager.list_thread_ids().await {
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
continue;
};
thread.refresh_runtime_config(next_config.clone()).await;
}
Self::spawn_effective_plugins_changed_task(
Arc::clone(&self.thread_manager),
self.config_manager.clone(),
);
Self::maybe_refresh_remote_installed_plugins_cache_for_current_config(
&self.config_manager,
&self.thread_manager,
self.auth_manager.auth_cached(),
)
.await;
return Ok(None);
}
let next_config = self
.config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
for thread_id in self.thread_manager.list_thread_ids().await {
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
continue;
};
thread.refresh_runtime_config(next_config.clone()).await;
}
Self::spawn_effective_plugins_changed_task(
Arc::clone(&self.thread_manager),
self.config_manager.clone(),
);
match self.auth_manager.logout_with_revoke().await {
Ok(_) => {}
Err(err) => {
return Err(internal_error(format!("logout failed: {err}")));
}
}
@@ -997,6 +981,7 @@ impl AccountRequestProcessor {
Ok(GetAccountResponse {
account,
requires_openai_auth: account_state.requires_openai_auth,
codex_managed_auth: account_state.codex_managed_auth,
})
}
@@ -1180,6 +1180,20 @@ impl TestAppServer {
self.send_request("account/login/start", Some(params)).await
}
/// Send an `account/login/start` JSON-RPC request for Amazon Bedrock login.
pub async fn send_login_account_amazon_bedrock_request(
&mut self,
key: &str,
region: &str,
) -> anyhow::Result<i64> {
let params = serde_json::json!({
"type": "amazonBedrock",
"key": key,
"region": region,
});
self.send_request("account/login/start", Some(params)).await
}
/// Send an `account/login/cancel` JSON-RPC request.
pub async fn send_cancel_login_account_request(
&mut self,
+71 -21
View File
@@ -250,26 +250,34 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
}
#[tokio::test]
async fn logout_account_removes_bedrock_model_provider_after_managed_auth_removed() -> Result<()> {
async fn logout_account_removes_managed_bedrock_provider_state() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
model_provider_id: Some("amazon-bedrock".to_string()),
extra_provider_config: Some(
r#"[model_providers.amazon-bedrock.aws]
region = "us-west-2"
"#
.to_string(),
),
..Default::default()
},
)?;
save_amazon_bedrock_auth(codex_home.path(), "bedrock-key", "us-west-2")?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let account_id = mcp
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let account_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(account_id)),
)
.await??;
let account: GetAccountResponse = to_response(account_resp)?;
assert_eq!(account.account, Some(Account::AmazonBedrock {}));
assert!(account.codex_managed_auth);
let id = mcp.send_logout_account_request().await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
@@ -289,22 +297,14 @@ region = "us-west-2"
);
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
assert!(
!config.contains("model_provider = \"amazon-bedrock\""),
"active Bedrock model provider should be removed after managed Bedrock logout"
);
assert!(
config.contains("[model_providers.amazon-bedrock.aws]"),
"Bedrock provider config should be preserved"
);
assert!(
config.contains("region = \"us-west-2\""),
"Bedrock region should be preserved"
config.contains("model_provider = \"mock_provider\""),
"managed Bedrock login should not mutate user model provider config"
);
Ok(())
}
#[tokio::test]
async fn logout_account_keeps_bedrock_model_provider_without_managed_auth() -> Result<()> {
async fn logout_account_noops_for_aws_managed_bedrock_auth() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
@@ -320,7 +320,8 @@ region = "us-west-2"
},
)?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let id = mcp.send_logout_account_request().await?;
@@ -339,6 +340,47 @@ region = "us-west-2"
Ok(())
}
#[tokio::test]
async fn login_account_amazon_bedrock_activates_provider_without_user_config_write() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let login_id = mcp
.send_login_account_amazon_bedrock_request("bedrock-key", "us-west-2")
.await?;
let login_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(login_id)),
)
.await??;
let login: LoginAccountResponse = to_response(login_resp)?;
assert_eq!(login, LoginAccountResponse::AmazonBedrock {});
let get_id = mcp
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let get_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(get_id)),
)
.await??;
let account: GetAccountResponse = to_response(get_resp)?;
assert_eq!(account.account, Some(Account::AmazonBedrock {}));
assert!(account.codex_managed_auth);
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
assert!(
config.contains("model_provider = \"mock_provider\""),
"Amazon Bedrock login should not mutate user model provider config"
);
Ok(())
}
#[tokio::test]
async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -410,6 +452,7 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
plan_type: AccountPlanType::Pro,
}),
requires_openai_auth: true,
codex_managed_auth: true,
}
);
@@ -478,6 +521,7 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
plan_type: AccountPlanType::Pro,
}),
requires_openai_auth: true,
codex_managed_auth: true,
}
);
@@ -1688,6 +1732,7 @@ async fn get_account_with_api_key() -> Result<()> {
let expected = GetAccountResponse {
account: Some(Account::ApiKey {}),
requires_openai_auth: true,
codex_managed_auth: true,
};
assert_eq!(received, expected);
Ok(())
@@ -1722,6 +1767,7 @@ async fn get_account_when_auth_not_required() -> Result<()> {
let expected = GetAccountResponse {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
};
assert_eq!(received, expected);
Ok(())
@@ -1763,6 +1809,7 @@ region = "us-west-2"
let expected = GetAccountResponse {
account: Some(Account::AmazonBedrock {}),
requires_openai_auth: false,
codex_managed_auth: false,
};
assert_eq!(received, expected);
Ok(())
@@ -1808,6 +1855,7 @@ async fn get_account_with_chatgpt() -> Result<()> {
plan_type: AccountPlanType::Pro,
}),
requires_openai_auth: true,
codex_managed_auth: true,
};
assert_eq!(received, expected);
Ok(())
@@ -1891,6 +1939,7 @@ async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<(
GetAccountResponse {
account: None,
requires_openai_auth: true,
codex_managed_auth: false,
}
);
server.verify().await;
@@ -1935,6 +1984,7 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
plan_type: AccountPlanType::Unknown,
}),
requires_openai_auth: true,
codex_managed_auth: true,
};
assert_eq!(received, expected);
Ok(())
@@ -4,7 +4,7 @@ use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::TestAppServer;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::ItemCompletedNotification;
@@ -76,7 +76,8 @@ async fn standalone_image_generation_persists_image_and_returns_it_to_model() ->
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
@@ -157,7 +158,7 @@ async fn standalone_image_generation_persists_image_and_returns_it_to_model() ->
}
async fn wait_for_image_generation_completed(
mcp: &mut McpProcess,
mcp: &mut TestAppServer,
) -> Result<ItemCompletedNotification> {
loop {
let notification = mcp
@@ -20,6 +20,9 @@ pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file:
dot_codex_folder.as_path().display()
)
}
ConfigLayerSource::ProviderState { provider, file } => {
format!("provider state ({provider}, {})", file.as_path().display())
}
ConfigLayerSource::SessionFlags => "session-flags".to_string(),
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => {
format!("legacy managed_config.toml ({})", file.as_path().display())
+1
View File
@@ -253,6 +253,7 @@ fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Op
ConfigLayerSource::Project { dot_codex_folder } => {
Some(dot_codex_folder.as_path().join(config_toml_file))
}
ConfigLayerSource::ProviderState { file, .. } => Some(file.to_path_buf()),
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()),
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
+1
View File
@@ -19,6 +19,7 @@ pub mod permissions_toml;
mod plugin_edit;
pub mod profile_toml;
mod project_root_markers;
mod provider_state;
mod requirements_exec_policy;
mod requirements_layers;
pub mod schema;
+5
View File
@@ -22,6 +22,7 @@ use crate::merge::merge_toml_values;
use crate::overrides::build_cli_overrides_layer;
use crate::project_root_markers::default_project_root_markers;
use crate::project_root_markers::project_root_markers_from_config;
use crate::provider_state::load_provider_state_layer;
use crate::state::ConfigLayerEntry;
use crate::state::ConfigLayerStack;
use crate::state::ConfigLoadOptions;
@@ -348,6 +349,10 @@ pub async fn load_config_layers_state(
startup_warnings = Some(project_layers.startup_warnings);
}
if let Some(provider_state_layer) = load_provider_state_layer(fs, codex_home).await? {
layers.push(provider_state_layer);
}
// Add a layer for runtime overrides from the CLI or UI, if any exist.
if let Some(cli_overrides_layer) = cli_overrides_layer {
layers.push(ConfigLayerEntry::new(
+73
View File
@@ -0,0 +1,73 @@
use codex_app_server_protocol::ConfigLayerSource;
use codex_file_system::ExecutorFileSystem;
use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use std::io;
use std::path::Path;
use toml::Value as TomlValue;
use crate::ConfigLayerEntry;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AmazonBedrockAuthState {
#[serde(default = "default_activates_provider")]
activates_provider: bool,
}
pub(crate) async fn load_provider_state_layer(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
) -> io::Result<Option<ConfigLayerEntry>> {
let auth_file = amazon_bedrock_auth_file(codex_home);
let contents = match fs.read_file_text(&auth_file, /*sandbox*/ None).await {
Ok(contents) => contents,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(io::Error::new(
err.kind(),
format!(
"Failed to read provider state file {}: {err}",
auth_file.as_path().display()
),
));
}
};
let state: AmazonBedrockAuthState = serde_json::from_str(&contents).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Failed to parse provider state file {}: {err}",
auth_file.as_path().display()
),
)
})?;
if !state.activates_provider {
return Ok(None);
}
let mut root = toml::map::Map::new();
root.insert(
"model_provider".to_string(),
TomlValue::String(AMAZON_BEDROCK_PROVIDER_ID.to_string()),
);
Ok(Some(ConfigLayerEntry::new(
ConfigLayerSource::ProviderState {
provider: AMAZON_BEDROCK_PROVIDER_ID.to_string(),
file: auth_file,
},
TomlValue::Table(root),
)))
}
pub(crate) fn amazon_bedrock_auth_file(codex_home: &Path) -> AbsolutePathBuf {
AbsolutePathBuf::resolve_path_against_base(
"model-providers/amazon-bedrock/auth.json",
codex_home,
)
}
fn default_activates_provider() -> bool {
true
}
+1
View File
@@ -207,6 +207,7 @@ impl ConfigLayerEntry {
ConfigLayerSource::EnterpriseManaged { .. } => None,
ConfigLayerSource::User { file, .. } => file.parent(),
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()),
ConfigLayerSource::ProviderState { .. } => None,
ConfigLayerSource::SessionFlags => None,
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => None,
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None,
+1
View File
@@ -351,6 +351,7 @@ fn skill_roots_from_layer_stack_inner(
}
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::ProviderState { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {}
+4
View File
@@ -371,6 +371,9 @@ fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf {
ConfigLayerSource::EnterpriseManaged { id, name } => synthetic_layer_path(&format!(
"<enterprise-managed:{name}:{id}>/{CONFIG_TOML_FILE}"
)),
ConfigLayerSource::ProviderState { provider, .. } => {
synthetic_layer_path(&format!("<provider-state:{provider}>/{CONFIG_TOML_FILE}"))
}
ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {
synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
}
@@ -612,6 +615,7 @@ fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSou
ConfigLayerSource::Project { .. } => (HookSource::Project, false),
ConfigLayerSource::Mdm { .. } => (HookSource::Mdm, true),
ConfigLayerSource::EnterpriseManaged { .. } => (HookSource::CloudManagedConfig, true),
ConfigLayerSource::ProviderState { .. } => (HookSource::Unknown, false),
ConfigLayerSource::SessionFlags => (HookSource::SessionFlags, false),
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => {
(HookSource::LegacyManagedConfigFile, true)
@@ -217,6 +217,7 @@ mod tests {
let auth = AmazonBedrockAuth {
bearer_token: "stored-bedrock-key".to_string(),
region: "eu-west-1".to_string(),
activates_provider: true,
};
let stored_auth: StoredAmazonBedrockAuth = Ok(Some(auth));
@@ -90,9 +90,11 @@ impl ModelProvider for AmazonBedrockModelProvider {
}
fn account_state(&self) -> ProviderAccountResult {
let codex_managed_auth = matches!(&self.stored_auth, Ok(Some(_)));
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock),
requires_openai_auth: false,
codex_managed_auth,
})
}
@@ -11,6 +11,8 @@ use serde::Serialize;
pub struct AmazonBedrockAuth {
pub bearer_token: String,
pub region: String,
#[serde(default = "default_activates_provider")]
pub activates_provider: bool,
}
pub(super) type StoredAmazonBedrockAuth = std::result::Result<Option<AmazonBedrockAuth>, String>;
@@ -23,6 +25,7 @@ pub fn save_amazon_bedrock_auth(
let auth = AmazonBedrockAuth {
bearer_token: bearer_token.to_string(),
region: region.to_string(),
activates_provider: true,
};
let auth_path = amazon_bedrock_auth_file(codex_home);
if let Some(parent) = auth_path.parent() {
@@ -97,6 +100,7 @@ mod tests {
Some(AmazonBedrockAuth {
bearer_token: "bedrock-key".to_string(),
region: "us-east-1".to_string(),
activates_provider: true,
})
);
assert!(delete_amazon_bedrock_auth(&codex_home).expect("delete auth"));
@@ -108,3 +112,7 @@ mod tests {
let _ = fs::remove_dir_all(&codex_home);
}
}
fn default_activates_provider() -> bool {
true
}
+6
View File
@@ -45,6 +45,7 @@ impl Default for ProviderCapabilities {
pub struct ProviderAccountState {
pub account: Option<ProviderAccount>,
pub requires_openai_auth: bool,
pub codex_managed_auth: bool,
}
/// Error returned when a provider cannot construct its app-visible account state.
@@ -231,6 +232,7 @@ impl ModelProvider for ConfiguredModelProvider {
};
Ok(ProviderAccountState {
codex_managed_auth: account.is_some(),
account,
requires_openai_auth: self.info.requires_openai_auth,
})
@@ -438,6 +440,7 @@ mod tests {
Ok(ProviderAccountState {
account: None,
requires_openai_auth: true,
codex_managed_auth: false,
})
);
}
@@ -457,6 +460,7 @@ mod tests {
Ok(ProviderAccountState {
account: Some(ProviderAccount::ApiKey),
requires_openai_auth: true,
codex_managed_auth: true,
})
);
}
@@ -480,6 +484,7 @@ mod tests {
Ok(ProviderAccountState {
account: None,
requires_openai_auth: false,
codex_managed_auth: false,
})
);
}
@@ -497,6 +502,7 @@ mod tests {
Ok(ProviderAccountState {
account: Some(ProviderAccount::AmazonBedrock),
requires_openai_auth: false,
codex_managed_auth: false,
})
);
}
+4 -1
View File
@@ -34,7 +34,10 @@ pub enum PlanType {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderAccount {
ApiKey,
Chatgpt { email: String, plan_type: PlanType },
Chatgpt {
email: String,
plan_type: PlanType,
},
AmazonBedrock,
}
+1 -1
View File
@@ -308,7 +308,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),
+2
View File
@@ -271,6 +271,7 @@ fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>>
ConfigLayerSource::SessionFlags => render_session_flag_details(&layer.config),
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::ProviderState { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => render_non_file_layer_value(layer),
ConfigLayerSource::System { .. }
| ConfigLayerSource::User { .. }
@@ -336,6 +337,7 @@ fn non_file_layer_value_label(source: &ConfigLayerSource) -> &'static str {
}
ConfigLayerSource::EnterpriseManaged { .. } => "Enterprise-managed config value",
ConfigLayerSource::SessionFlags
| ConfigLayerSource::ProviderState { .. }
| ConfigLayerSource::System { .. }
| ConfigLayerSource::User { .. }
| ConfigLayerSource::Project { .. }
+1 -1
View File
@@ -1901,7 +1901,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,
})
}