From 5802e4868f5e8d83f43ce4b5190a759b9b756fd3 Mon Sep 17 00:00:00 2001 From: mikhail-oai Date: Sun, 15 Feb 2026 20:34:52 -0500 Subject: [PATCH] Default CLI auth credentials store to auto --- codex-rs/Cargo.toml | 1 + codex-rs/app-server/tests/suite/v2/account.rs | 31 +++++++++++++++- codex-rs/core/Cargo.toml | 2 +- codex-rs/core/config.schema.json | 16 ++++---- codex-rs/core/src/auth/storage.rs | 37 +++++++++++++++++-- codex-rs/core/src/config/mod.rs | 12 +++--- 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 75b3d18d3e..1c91f992d7 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -339,6 +339,7 @@ unwrap_used = "deny" # silence the false positive here instead of deleting a real dependency. [workspace.metadata.cargo-shear] ignored = [ + "askama", "icu_provider", "openssl-sys", "codex-utils-readiness", diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index 0c96800958..190c9e6b73 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -804,7 +804,13 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> { #[tokio::test] async fn login_account_api_key_succeeds_and_notifies() -> Result<()> { let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + create_config_toml( + codex_home.path(), + CreateConfigTomlParams { + requires_openai_auth: Some(true), + ..Default::default() + }, + )?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -844,7 +850,28 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> { }; pretty_assertions::assert_eq!(payload.auth_mode, Some(AuthMode::ApiKey)); - assert!(codex_home.path().join("auth.json").exists()); + drop(mcp); + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + 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)?; + let expected = GetAccountResponse { + account: Some(Account::ApiKey {}), + requires_openai_auth: true, + }; + assert_eq!(account, expected); + Ok(()) } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 208ba1c3a6..fe562f0b12 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -169,4 +169,4 @@ wiremock = { workspace = true } zstd = { workspace = true } [package.metadata.cargo-shear] -ignored = ["openssl-sys"] +ignored = ["askama", "openssl-sys"] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index c9da5c688b..861c8e03f5 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -125,6 +125,13 @@ "AuthCredentialsStoreMode": { "description": "Determine where Codex should store CLI auth credentials.", "oneOf": [ + { + "description": "Use keyring when available; otherwise, fall back to a file in CODEX_HOME.", + "enum": [ + "auto" + ], + "type": "string" + }, { "description": "Persist credentials in CODEX_HOME/auth.json.", "enum": [ @@ -139,13 +146,6 @@ ], "type": "string" }, - { - "description": "Use keyring when available; otherwise, fall back to a file in CODEX_HOME.", - "enum": [ - "auto" - ], - "type": "string" - }, { "description": "Store credentials in memory only for the current process.", "enum": [ @@ -1286,7 +1286,7 @@ } ], "default": null, - "description": "Preferred backend for storing CLI auth credentials. file (default): Use a file in the Codex home directory. keyring: Use an OS-specific keyring service. auto: Use the keyring if available, otherwise use a file." + "description": "Preferred backend for storing CLI auth credentials. auto (default): Use the keyring if available, otherwise use a file. file: Use a file in the Codex home directory. keyring: Use an OS-specific keyring service." }, "compact_prompt": { "description": "Compact prompt used for history compaction.", diff --git a/codex-rs/core/src/auth/storage.rs b/codex-rs/core/src/auth/storage.rs index 81d17e4e1e..c0bebc9ca3 100644 --- a/codex-rs/core/src/auth/storage.rs +++ b/codex-rs/core/src/auth/storage.rs @@ -29,13 +29,13 @@ use once_cell::sync::Lazy; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum AuthCredentialsStoreMode { + /// Use keyring when available; otherwise, fall back to a file in CODEX_HOME. #[default] + Auto, /// Persist credentials in CODEX_HOME/auth.json. File, /// Persist credentials in the keyring. Fail if unavailable. Keyring, - /// Use keyring when available; otherwise, fall back to a file in CODEX_HOME. - Auto, /// Store credentials in memory only for the current process. Ephemeral, } @@ -260,8 +260,13 @@ impl AuthStorageBackend for AutoAuthStorage { } fn delete(&self) -> std::io::Result { - // Keyring storage will delete from disk as well - self.keyring_storage.delete() + match self.keyring_storage.delete() { + Ok(removed) => Ok(removed), + Err(err) => { + warn!("failed to delete auth from keyring, falling back to file deletion: {err}"); + self.file_storage.delete() + } + } } } @@ -755,4 +760,28 @@ mod tests { ); Ok(()) } + + #[test] + fn auto_auth_storage_delete_falls_back_when_keyring_errors() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + ); + let key = compute_store_key(codex_home.path())?; + mock_keyring.set_error(&key, KeyringError::Invalid("error".into(), "delete".into())); + + let auth_file = get_auth_file(codex_home.path()); + std::fs::write(&auth_file, "stale")?; + + let removed = storage.delete()?; + + assert!(removed, "fallback deletion should report removal"); + assert!( + !auth_file.exists(), + "fallback auth.json should be removed when keyring delete fails" + ); + Ok(()) + } } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b83a17592c..e6d57cf406 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -261,9 +261,9 @@ pub struct Config { pub cwd: PathBuf, /// Preferred store for CLI auth credentials. - /// file (default): Use a file in the Codex home directory. + /// auto (default): Use the OS-specific keyring service if available, otherwise use a file. + /// file: Use a file in the Codex home directory. /// keyring: Use an OS-specific keyring service. - /// auto: Use the OS-specific keyring service if available, otherwise use a file. pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode, /// Definition for MCP servers that Codex can reach out to for tool calls. @@ -923,9 +923,9 @@ pub struct ConfigToml { pub forced_login_method: Option, /// Preferred backend for storing CLI auth credentials. - /// file (default): Use a file in the Codex home directory. + /// auto (default): Use the keyring if available, otherwise use a file. + /// file: Use a file in the Codex home directory. /// keyring: Use an OS-specific keyring service. - /// auto: Use the keyring if available, otherwise use a file. #[serde(default)] pub cli_auth_credentials_store: Option, @@ -2465,7 +2465,7 @@ trust_level = "trusted" } #[test] - fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { + fn config_defaults_to_auto_cli_auth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml::default(); @@ -2477,7 +2477,7 @@ trust_level = "trusted" assert_eq!( config.cli_auth_credentials_store_mode, - AuthCredentialsStoreMode::File, + AuthCredentialsStoreMode::Auto, ); Ok(())