From ff8f62e2a087dbd0a4c02e8ea5d3275c2a9322b2 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Wed, 10 Jun 2026 17:56:21 -0700 Subject: [PATCH] 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 7b634a5188..b0b0a8b63f 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 9ed02fdd17..ba03ec51fb 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -588,6 +588,9 @@ "search_tool": { "type": "boolean" }, + "secret_auth_storage": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, @@ -4720,6 +4723,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 c19fe76fb5..ee9747b27d 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -6,8 +6,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; @@ -34,6 +39,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; @@ -5106,6 +5112,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 cadb797212..22b0d88ca8 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -38,6 +38,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; @@ -1351,6 +1352,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, @@ -1626,6 +1633,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, @@ -1642,7 +1672,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 b30c255165..d398144a08 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. @@ -744,6 +746,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 b7d158ca36..869054e4b6 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -199,6 +199,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);