From 07d66eccd904cd15cd754c43d3f74a72d63a1980 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Wed, 10 Jun 2026 17:56:21 -0700 Subject: [PATCH 1/2] Add secret auth storage config --- codex-rs/config/src/types.rs | 20 +++++++ codex-rs/core/config.schema.json | 6 ++ codex-rs/core/src/config/config_tests.rs | 74 ++++++++++++++++++++++++ codex-rs/core/src/config/mod.rs | 74 +++++++++++++++++++++++- codex-rs/features/src/lib.rs | 8 +++ codex-rs/features/src/tests.rs | 10 ++++ 6 files changed, 191 insertions(+), 1 deletion(-) diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 7cf8ac66a4cc..044d09ca85b4 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -113,6 +113,26 @@ pub enum OAuthCredentialsStoreMode { Keyring, } +/// Determine how auth credentials should use keyring-backed storage. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum AuthKeyringBackendKind { + /// Store the serialized auth payload directly in the OS keyring. + Direct, + /// Store auth payloads in the local encrypted secrets file, with the file key in the OS keyring. + Secrets, +} + +impl Default for AuthKeyringBackendKind { + fn default() -> Self { + if cfg!(windows) { + Self::Secrets + } else { + Self::Direct + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum WindowsSandboxModeToml { diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 068f482c7a55..31daf2feb428 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -599,6 +599,9 @@ "search_tool": { "type": "boolean" }, + "secret_auth_storage": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, @@ -4751,6 +4754,9 @@ "search_tool": { "type": "boolean" }, + "secret_auth_storage": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index f422591d89c5..e890f73efdf0 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4,8 +4,13 @@ use crate::config::edit::apply_blocking; use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerEntry; +use codex_config::ConfigLayerStack; +use codex_config::ConfigRequirements; +use codex_config::ConfigRequirementsToml; +use codex_config::FeatureRequirementsToml; use codex_config::ProfileV2Name; use codex_config::RequirementSource; +use codex_config::Sourced; use codex_config::config_toml::AgentRoleToml; use codex_config::config_toml::AgentsToml; use codex_config::config_toml::AutoReviewToml; @@ -33,6 +38,7 @@ use codex_config::permissions_toml::PermissionsToml; use codex_config::permissions_toml::WorkspaceRootsToml; use codex_config::types::AppToolApproval; use codex_config::types::ApprovalsReviewer; +use codex_config::types::AuthKeyringBackendKind; use codex_config::types::BundledSkillsConfig; use codex_config::types::FeedbackConfigToml; use codex_config::types::HistoryPersistence; @@ -5056,6 +5062,74 @@ async fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { Ok(()) } +#[test] +fn resolve_bootstrap_auth_keyring_backend_kind_uses_secret_auth_storage_feature() +-> std::io::Result<()> { + let config_toml = ConfigToml { + features: Some(FeaturesToml::from(BTreeMap::from([( + "secret_auth_storage".to_string(), + true, + )]))), + ..Default::default() + }; + assert_eq!( + resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( + config_toml, + /*feature_requirements*/ None, + )?)?, + AuthKeyringBackendKind::Secrets + ); + + let config_toml = ConfigToml { + features: Some(FeaturesToml::from(BTreeMap::from([( + "secret_auth_storage".to_string(), + false, + )]))), + ..Default::default() + }; + assert_eq!( + resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( + config_toml.clone(), + /*feature_requirements*/ None, + )?)?, + AuthKeyringBackendKind::Direct + ); + + let requirements = Sourced::new( + FeatureRequirementsToml { + entries: BTreeMap::from([("secret_auth_storage".to_string(), true)]), + }, + RequirementSource::Unknown, + ); + assert_eq!( + resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( + config_toml, + Some(requirements), + )?)?, + AuthKeyringBackendKind::Secrets + ); + + Ok(()) +} + +fn config_toml_load_result( + config_toml: ConfigToml, + feature_requirements: Option>, +) -> std::io::Result { + let requirements = ConfigRequirements { + feature_requirements, + ..Default::default() + }; + Ok(ConfigTomlLoadResult { + config_toml, + config_layer_stack: ConfigLayerStack::new( + Vec::new(), + requirements, + ConfigRequirementsToml::default(), + )?, + }) +} + #[tokio::test] async fn legacy_toggles_map_to_features() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f292891c435a..312456f8f09f 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -36,6 +36,7 @@ use codex_config::permissions_toml::PermissionsToml; use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; use codex_config::types::AuthCredentialsStoreMode; +use codex_config::types::AuthKeyringBackendKind; use codex_config::types::History; use codex_config::types::McpServerConfig; use codex_config::types::McpServerDisabledReason; @@ -1367,6 +1368,12 @@ impl Config { workspace_roots } + pub fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind { + auth_keyring_backend_kind_from_secret_auth_storage( + self.features.enabled(Feature::SecretAuthStorage), + ) + } + pub fn to_models_manager_config(&self) -> ModelsManagerConfig { ModelsManagerConfig { model_context_window: self.model_context_window, @@ -1647,6 +1654,29 @@ pub async fn load_config_as_toml_with_cli_and_load_options( cli_overrides: Vec<(String, TomlValue)>, options: impl Into, ) -> std::io::Result { + load_config_toml_with_layer_stack(codex_home, cwd, cli_overrides, options) + .await + .map(|result| result.config_toml) +} + +/// Partially loaded config plus the layer stack used to derive it. +/// +/// This is intended for startup paths that must inspect raw config before a +/// full [`Config`] can be constructed, but still need access to managed +/// requirements loaded with the config layers. +pub struct ConfigTomlLoadResult { + pub config_toml: ConfigToml, + pub config_layer_stack: ConfigLayerStack, +} + +/// Loads the partially merged config together with the layer stack used to +/// derive it, before constructing a full [`Config`]. +pub async fn load_config_toml_with_layer_stack( + codex_home: &Path, + cwd: Option<&AbsolutePathBuf>, + cli_overrides: Vec<(String, TomlValue)>, + options: impl Into, +) -> std::io::Result { let config_layer_stack = load_config_layers_state( LOCAL_FS.as_ref(), codex_home, @@ -1663,7 +1693,49 @@ pub async fn load_config_as_toml_with_cli_and_load_options( e })?; - Ok(cfg) + Ok(ConfigTomlLoadResult { + config_toml: cfg, + config_layer_stack, + }) +} + +/// Resolve the auth keyring backend from a partially loaded bootstrap config. +/// +/// This is intended for startup paths that must read auth before managed cloud +/// requirements can be loaded and before a full [`Config`] exists. +pub fn resolve_bootstrap_auth_keyring_backend_kind( + bootstrap_config: &ConfigTomlLoadResult, +) -> std::io::Result { + let config_toml = &bootstrap_config.config_toml; + let features = Features::from_sources( + FeatureConfigSource { + features: config_toml.features.as_ref(), + experimental_use_unified_exec_tool: config_toml.experimental_use_unified_exec_tool, + }, + FeatureConfigSource::default(), + FeatureOverrides::default(), + ); + let managed_features = ManagedFeatures::from_configured( + features, + bootstrap_config + .config_layer_stack + .requirements() + .feature_requirements + .clone(), + )?; + Ok(auth_keyring_backend_kind_from_secret_auth_storage( + managed_features.enabled(Feature::SecretAuthStorage), + )) +} + +fn auth_keyring_backend_kind_from_secret_auth_storage( + secret_auth_storage_enabled: bool, +) -> AuthKeyringBackendKind { + if secret_auth_storage_enabled { + AuthKeyringBackendKind::Secrets + } else { + AuthKeyringBackendKind::Direct + } } pub fn deserialize_config_toml_with_base( diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 2f8e91d709fb..65c1c3b28b61 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -81,6 +81,8 @@ pub enum Feature { ShellTool, /// Enable Claude-style lifecycle hooks loaded from hooks.json files. CodexHooks, + /// Store CLI auth in the encrypted local secrets backend when keyring storage is selected. + SecretAuthStorage, // Experimental /// Enable JavaScript code mode backed by the in-process V8 runtime. @@ -748,6 +750,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::SecretAuthStorage, + key: "secret_auth_storage", + stage: Stage::Stable, + default_enabled: cfg!(windows), + }, FeatureSpec { id: Feature::UnifiedExec, key: "unified_exec", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 0a4f925f4f70..db56819889c4 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -189,6 +189,16 @@ fn tool_search_is_removed_and_disabled_by_default() { assert_eq!(feature_for_key("tool_search"), Some(Feature::ToolSearch)); } +#[test] +fn secret_auth_storage_defaults_to_windows_only() { + assert_eq!(Feature::SecretAuthStorage.stage(), Stage::Stable); + assert_eq!(Feature::SecretAuthStorage.default_enabled(), cfg!(windows)); + assert_eq!( + feature_for_key("secret_auth_storage"), + Some(Feature::SecretAuthStorage) + ); +} + #[test] fn browser_controls_are_stable_and_enabled_by_default() { assert_eq!(Feature::InAppBrowser.stage(), Stage::Stable); From 3e3379b56006b9e877f5db4ba65009b4ad41c49c Mon Sep 17 00:00:00 2001 From: celia-oai Date: Fri, 12 Jun 2026 11:51:10 -0700 Subject: [PATCH 2/2] refactor --- codex-rs/core/src/config/auth_keyring.rs | 59 ++++++++++++++ .../core/src/config/auth_keyring_tests.rs | 79 +++++++++++++++++++ codex-rs/core/src/config/config_tests.rs | 72 ----------------- codex-rs/core/src/config/mod.rs | 48 +---------- 4 files changed, 140 insertions(+), 118 deletions(-) create mode 100644 codex-rs/core/src/config/auth_keyring.rs create mode 100644 codex-rs/core/src/config/auth_keyring_tests.rs diff --git a/codex-rs/core/src/config/auth_keyring.rs b/codex-rs/core/src/config/auth_keyring.rs new file mode 100644 index 000000000000..e6a1a94f5cb3 --- /dev/null +++ b/codex-rs/core/src/config/auth_keyring.rs @@ -0,0 +1,59 @@ +use super::Config; +use super::ConfigTomlLoadResult; +use super::ManagedFeatures; +use codex_config::types::AuthKeyringBackendKind; +use codex_features::Feature; +use codex_features::FeatureConfigSource; +use codex_features::FeatureOverrides; +use codex_features::Features; + +impl Config { + pub fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind { + auth_keyring_backend_kind_from_secret_auth_storage( + self.features.enabled(Feature::SecretAuthStorage), + ) + } +} + +/// Resolve the auth keyring backend from a partially loaded bootstrap config. +/// +/// This is intended for startup paths that must read auth before managed cloud +/// requirements can be loaded and before a full [`Config`] exists. +pub fn resolve_bootstrap_auth_keyring_backend_kind( + bootstrap_config: &ConfigTomlLoadResult, +) -> std::io::Result { + let config_toml = &bootstrap_config.config_toml; + let features = Features::from_sources( + FeatureConfigSource { + features: config_toml.features.as_ref(), + experimental_use_unified_exec_tool: config_toml.experimental_use_unified_exec_tool, + }, + FeatureConfigSource::default(), + FeatureOverrides::default(), + ); + let managed_features = ManagedFeatures::from_configured( + features, + bootstrap_config + .config_layer_stack + .requirements() + .feature_requirements + .clone(), + )?; + Ok(auth_keyring_backend_kind_from_secret_auth_storage( + managed_features.enabled(Feature::SecretAuthStorage), + )) +} + +fn auth_keyring_backend_kind_from_secret_auth_storage( + secret_auth_storage_enabled: bool, +) -> AuthKeyringBackendKind { + if secret_auth_storage_enabled { + AuthKeyringBackendKind::Secrets + } else { + AuthKeyringBackendKind::Direct + } +} + +#[cfg(test)] +#[path = "auth_keyring_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/config/auth_keyring_tests.rs b/codex-rs/core/src/config/auth_keyring_tests.rs new file mode 100644 index 000000000000..3ec78650b0a1 --- /dev/null +++ b/codex-rs/core/src/config/auth_keyring_tests.rs @@ -0,0 +1,79 @@ +use super::*; +use codex_config::ConfigLayerStack; +use codex_config::ConfigRequirements; +use codex_config::ConfigRequirementsToml; +use codex_config::FeatureRequirementsToml; +use codex_config::RequirementSource; +use codex_config::Sourced; +use codex_config::config_toml::ConfigToml; +use codex_features::FeaturesToml; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; + +#[test] +fn resolve_bootstrap_auth_keyring_backend_kind_uses_secret_auth_storage_feature() +-> std::io::Result<()> { + let config_toml = ConfigToml { + features: Some(FeaturesToml::from(BTreeMap::from([( + "secret_auth_storage".to_string(), + true, + )]))), + ..Default::default() + }; + assert_eq!( + resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( + config_toml, + /*feature_requirements*/ None, + )?)?, + AuthKeyringBackendKind::Secrets + ); + + let config_toml = ConfigToml { + features: Some(FeaturesToml::from(BTreeMap::from([( + "secret_auth_storage".to_string(), + false, + )]))), + ..Default::default() + }; + assert_eq!( + resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( + config_toml.clone(), + /*feature_requirements*/ None, + )?)?, + AuthKeyringBackendKind::Direct + ); + + let requirements = Sourced::new( + FeatureRequirementsToml { + entries: BTreeMap::from([("secret_auth_storage".to_string(), true)]), + }, + RequirementSource::Unknown, + ); + assert_eq!( + resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( + config_toml, + Some(requirements), + )?)?, + AuthKeyringBackendKind::Secrets + ); + + Ok(()) +} + +fn config_toml_load_result( + config_toml: ConfigToml, + feature_requirements: Option>, +) -> std::io::Result { + let requirements = ConfigRequirements { + feature_requirements, + ..Default::default() + }; + Ok(ConfigTomlLoadResult { + config_toml, + config_layer_stack: ConfigLayerStack::new( + Vec::new(), + requirements, + ConfigRequirementsToml::default(), + )?, + }) +} diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index e890f73efdf0..c4c24d34d90d 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5,9 +5,6 @@ use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerStack; -use codex_config::ConfigRequirements; -use codex_config::ConfigRequirementsToml; -use codex_config::FeatureRequirementsToml; use codex_config::ProfileV2Name; use codex_config::RequirementSource; use codex_config::Sourced; @@ -38,7 +35,6 @@ use codex_config::permissions_toml::PermissionsToml; use codex_config::permissions_toml::WorkspaceRootsToml; use codex_config::types::AppToolApproval; use codex_config::types::ApprovalsReviewer; -use codex_config::types::AuthKeyringBackendKind; use codex_config::types::BundledSkillsConfig; use codex_config::types::FeedbackConfigToml; use codex_config::types::HistoryPersistence; @@ -5062,74 +5058,6 @@ async fn feature_table_overrides_legacy_flags() -> std::io::Result<()> { Ok(()) } -#[test] -fn resolve_bootstrap_auth_keyring_backend_kind_uses_secret_auth_storage_feature() --> std::io::Result<()> { - let config_toml = ConfigToml { - features: Some(FeaturesToml::from(BTreeMap::from([( - "secret_auth_storage".to_string(), - true, - )]))), - ..Default::default() - }; - assert_eq!( - resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( - config_toml, - /*feature_requirements*/ None, - )?)?, - AuthKeyringBackendKind::Secrets - ); - - let config_toml = ConfigToml { - features: Some(FeaturesToml::from(BTreeMap::from([( - "secret_auth_storage".to_string(), - false, - )]))), - ..Default::default() - }; - assert_eq!( - resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( - config_toml.clone(), - /*feature_requirements*/ None, - )?)?, - AuthKeyringBackendKind::Direct - ); - - let requirements = Sourced::new( - FeatureRequirementsToml { - entries: BTreeMap::from([("secret_auth_storage".to_string(), true)]), - }, - RequirementSource::Unknown, - ); - assert_eq!( - resolve_bootstrap_auth_keyring_backend_kind(&config_toml_load_result( - config_toml, - Some(requirements), - )?)?, - AuthKeyringBackendKind::Secrets - ); - - Ok(()) -} - -fn config_toml_load_result( - config_toml: ConfigToml, - feature_requirements: Option>, -) -> std::io::Result { - let requirements = ConfigRequirements { - feature_requirements, - ..Default::default() - }; - Ok(ConfigTomlLoadResult { - config_toml, - config_layer_stack: ConfigLayerStack::new( - Vec::new(), - requirements, - ConfigRequirementsToml::default(), - )?, - }) -} - #[tokio::test] async fn legacy_toggles_map_to_features() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 312456f8f09f..a96f005cb600 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -36,7 +36,6 @@ use codex_config::permissions_toml::PermissionsToml; use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; use codex_config::types::AuthCredentialsStoreMode; -use codex_config::types::AuthKeyringBackendKind; use codex_config::types::History; use codex_config::types::McpServerConfig; use codex_config::types::McpServerDisabledReason; @@ -138,6 +137,7 @@ use toml::Value as TomlValue; use toml_edit::DocumentMut; pub(crate) mod agent_roles; +mod auth_keyring; pub mod edit; mod managed_features; mod network_proxy_spec; @@ -146,6 +146,7 @@ mod permissions; mod resolved_permission_profile; #[cfg(test)] mod schema; +pub use auth_keyring::resolve_bootstrap_auth_keyring_backend_kind; pub use codex_config::ConfigLoadOptions; pub use codex_config::Constrained; pub use codex_config::ConstraintError; @@ -1368,12 +1369,6 @@ impl Config { workspace_roots } - pub fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind { - auth_keyring_backend_kind_from_secret_auth_storage( - self.features.enabled(Feature::SecretAuthStorage), - ) - } - pub fn to_models_manager_config(&self) -> ModelsManagerConfig { ModelsManagerConfig { model_context_window: self.model_context_window, @@ -1699,45 +1694,6 @@ pub async fn load_config_toml_with_layer_stack( }) } -/// Resolve the auth keyring backend from a partially loaded bootstrap config. -/// -/// This is intended for startup paths that must read auth before managed cloud -/// requirements can be loaded and before a full [`Config`] exists. -pub fn resolve_bootstrap_auth_keyring_backend_kind( - bootstrap_config: &ConfigTomlLoadResult, -) -> std::io::Result { - let config_toml = &bootstrap_config.config_toml; - let features = Features::from_sources( - FeatureConfigSource { - features: config_toml.features.as_ref(), - experimental_use_unified_exec_tool: config_toml.experimental_use_unified_exec_tool, - }, - FeatureConfigSource::default(), - FeatureOverrides::default(), - ); - let managed_features = ManagedFeatures::from_configured( - features, - bootstrap_config - .config_layer_stack - .requirements() - .feature_requirements - .clone(), - )?; - Ok(auth_keyring_backend_kind_from_secret_auth_storage( - managed_features.enabled(Feature::SecretAuthStorage), - )) -} - -fn auth_keyring_backend_kind_from_secret_auth_storage( - secret_auth_storage_enabled: bool, -) -> AuthKeyringBackendKind { - if secret_auth_storage_enabled { - AuthKeyringBackendKind::Secrets - } else { - AuthKeyringBackendKind::Direct - } -} - pub fn deserialize_config_toml_with_base( root_value: TomlValue, config_base_dir: &Path,