diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 976485c5c..d1b1e5eb9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -408,6 +408,7 @@ dependencies = [ "base64 0.22.1", "chrono", "codex-app-server-protocol", + "codex-config", "codex-core", "codex-features", "codex-login", @@ -1606,6 +1607,7 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "codex-config", "codex-connectors", "codex-core", "codex-git-utils", @@ -1708,6 +1710,7 @@ dependencies = [ "base64 0.22.1", "chrono", "codex-backend-client", + "codex-config", "codex-core", "codex-login", "codex-otel", @@ -1805,8 +1808,13 @@ dependencies = [ "anyhow", "codex-app-server-protocol", "codex-execpolicy", + "codex-features", + "codex-git-utils", + "codex-model-provider-info", + "codex-network-proxy", "codex-protocol", "codex-utils-absolute-path", + "dunce", "futures", "multimap", "pretty_assertions", @@ -1926,7 +1934,6 @@ dependencies = [ "regex-lite", "reqwest", "rmcp", - "schemars 0.8.22", "serde", "serde_json", "serial_test", @@ -2116,7 +2123,6 @@ dependencies = [ name = "codex-features" version = "0.0.0" dependencies = [ - "codex-login", "codex-otel", "codex-protocol", "pretty_assertions", @@ -2272,7 +2278,6 @@ dependencies = [ "rand 0.9.2", "regex-lite", "reqwest", - "schemars 0.8.22", "serde", "serde_json", "serial_test", @@ -2375,6 +2380,7 @@ dependencies = [ "codex-api", "codex-app-server-protocol", "codex-collaboration-mode-templates", + "codex-config", "codex-feedback", "codex-login", "codex-model-provider-info", @@ -2570,6 +2576,7 @@ dependencies = [ "anyhow", "axum", "codex-client", + "codex-config", "codex-keyring-store", "codex-protocol", "codex-utils-cargo-bin", @@ -2581,7 +2588,6 @@ dependencies = [ "pretty_assertions", "reqwest", "rmcp", - "schemars 0.8.22", "serde", "serde_json", "serial_test", diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index cdbc4cb2c..95265baf5 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -5628,7 +5628,11 @@ impl CodexMessageProcessor { .set_enabled(Feature::Apps, thread.enabled(Feature::Apps)); } - if !config.features.apps_enabled(Some(&self.auth_manager)).await { + let auth = self.auth_manager.auth().await; + if !config + .features + .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth)) + { self.outgoing .send_response( request_id, @@ -6296,9 +6300,11 @@ impl CodexMessageProcessor { } let plugin_apps = load_plugin_apps(result.installed_path.as_path()); + let auth = self.auth_manager.auth().await; let apps_needing_auth = if plugin_apps.is_empty() - || !config.features.apps_enabled(Some(&self.auth_manager)).await - { + || !config.features.apps_enabled_for_auth( + auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth), + ) { Vec::new() } else { let (all_connectors_result, accessible_connectors_result) = tokio::join!( diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 4bcce5c7c..f8beec08d 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -921,7 +921,11 @@ impl MessageProcessor { return; } }; - if !config.features.apps_enabled(Some(&self.auth_manager)).await { + let auth = self.auth_manager.auth().await; + if !config.features.apps_enabled_for_auth( + auth.as_ref() + .is_some_and(codex_login::CodexAuth::is_chatgpt_auth), + ) { return; } diff --git a/codex-rs/app-server/src/transport/remote_control/tests.rs b/codex-rs/app-server/src/transport/remote_control/tests.rs index 280949adf..b403fac48 100644 --- a/codex-rs/app-server/src/transport/remote_control/tests.rs +++ b/codex-rs/app-server/src/transport/remote_control/tests.rs @@ -17,9 +17,9 @@ use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::ServerNotification; +use codex_config::types::AuthCredentialsStoreMode; use codex_core::test_support::auth_manager_from_auth; use codex_core::test_support::auth_manager_from_auth_with_home; -use codex_login::AuthCredentialsStoreMode; use codex_login::AuthDotJson; use codex_login::AuthManager; use codex_login::CodexAuth; diff --git a/codex-rs/app-server/src/transport/remote_control/websocket.rs b/codex-rs/app-server/src/transport/remote_control/websocket.rs index 56bc88cc6..3f42a7bbb 100644 --- a/codex-rs/app-server/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server/src/transport/remote_control/websocket.rs @@ -836,8 +836,8 @@ mod tests { use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::ServerNotification; + use codex_config::types::AuthCredentialsStoreMode; use codex_core::test_support::auth_manager_from_auth; - use codex_login::AuthCredentialsStoreMode; use codex_login::AuthDotJson; use codex_login::CodexAuth; use codex_login::save_auth; diff --git a/codex-rs/app-server/tests/common/Cargo.toml b/codex-rs/app-server/tests/common/Cargo.toml index 6eb1a2c58..aef2f58df 100644 --- a/codex-rs/app-server/tests/common/Cargo.toml +++ b/codex-rs/app-server/tests/common/Cargo.toml @@ -15,6 +15,7 @@ anyhow = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } codex-app-server-protocol = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } codex-features = { workspace = true } codex-login = { workspace = true } diff --git a/codex-rs/app-server/tests/common/auth_fixtures.rs b/codex-rs/app-server/tests/common/auth_fixtures.rs index dfda24725..99334f077 100644 --- a/codex-rs/app-server/tests/common/auth_fixtures.rs +++ b/codex-rs/app-server/tests/common/auth_fixtures.rs @@ -7,7 +7,7 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD; use chrono::DateTime; use chrono::Utc; use codex_app_server_protocol::AuthMode; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthDotJson; use codex_login::save_auth; use codex_login::token_data::TokenData; diff --git a/codex-rs/app-server/tests/suite/auth.rs b/codex-rs/app-server/tests/suite/auth.rs index 78572d8ce..e6134e480 100644 --- a/codex-rs/app-server/tests/suite/auth.rs +++ b/codex-rs/app-server/tests/suite/auth.rs @@ -12,7 +12,7 @@ use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::RequestId; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; use pretty_assertions::assert_eq; use std::path::Path; diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index 0755c3755..3c88bcb7a 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -28,7 +28,7 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStatus; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::login_with_api_key; use codex_protocol::account::PlanType as AccountPlanType; use core_test_support::responses; diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs index 8e8e328a8..a4d7a7f34 100644 --- a/codex-rs/app-server/tests/suite/v2/analytics.rs +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -2,10 +2,10 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; use app_test_support::DEFAULT_CLIENT_NAME; use app_test_support::write_chatgpt_auth; +use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::OtelExporterKind; use codex_config::types::OtelHttpProtocol; use codex_core::config::ConfigBuilder; -use codex_login::AuthCredentialsStoreMode; use pretty_assertions::assert_eq; use serde_json::Value; use std::collections::HashMap; diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index d8cedf2a4..57a27961a 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -35,7 +35,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthDotJson; use codex_login::save_auth; use pretty_assertions::assert_eq; diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 8849b39ab..e7661546a 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -28,7 +28,7 @@ use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use core_test_support::responses; diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs index 7a50092b9..632667725 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs @@ -31,7 +31,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput as V2UserInput; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use core_test_support::responses; use pretty_assertions::assert_eq; use rmcp::handler::server::ServerHandler; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 95630079c..a3bea5317 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -25,7 +25,7 @@ use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; use codex_app_server_protocol::RequestId; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use rmcp::handler::server::ServerHandler; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index d1ee1aa20..8bc4a8598 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -15,8 +15,8 @@ use codex_app_server_protocol::PluginMarketplaceEntry; use codex_app_server_protocol::PluginSource; use codex_app_server_protocol::PluginSummary; use codex_app_server_protocol::RequestId; +use codex_config::types::AuthCredentialsStoreMode; use codex_core::config::set_project_trust_level; -use codex_login::AuthCredentialsStoreMode; use codex_protocol::config_types::TrustLevel; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 32e799468..115a04f0f 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -23,7 +23,7 @@ use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginReadParams; use codex_app_server_protocol::PluginReadResponse; use codex_app_server_protocol::RequestId; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use rmcp::handler::server::ServerHandler; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs index d0122c62f..00fabe483 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs @@ -11,7 +11,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::PluginUninstallParams; use codex_app_server_protocol::PluginUninstallResponse; use codex_app_server_protocol::RequestId; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use pretty_assertions::assert_eq; use serde_json::json; use tempfile::TempDir; diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index 64c155081..203d66494 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -10,7 +10,7 @@ use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::RateLimitSnapshot; use codex_app_server_protocol::RateLimitWindow; use codex_app_server_protocol::RequestId; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_protocol::account::PlanType as AccountPlanType; use pretty_assertions::assert_eq; use serde_json::json; diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index 0849fe9b3..9907fc4b1 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -24,7 +24,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; use pretty_assertions::assert_eq; use serde_json::Value; diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 2b1d6a8dd..80ae75688 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -38,7 +38,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 7907e621b..50c373ac9 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -18,9 +18,9 @@ use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadStatusChangedNotification; +use codex_config::types::AuthCredentialsStoreMode; use codex_core::config::set_project_trust_level; use codex_git_utils::resolve_root_git_project_for_trust; -use codex_login::AuthCredentialsStoreMode; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index b99d1cb73..59591dd0d 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -43,7 +43,7 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput as V2UserInput; -use codex_core::config::ConfigToml; +use codex_config::config_toml::ConfigToml; use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME; use codex_features::FEATURES; use codex_features::Feature; diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 84c793b53..381b4fc87 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -11,15 +11,16 @@ workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-connectors = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } +codex-git-utils = { workspace = true } codex-login = { workspace = true } codex-utils-cli = { workspace = true } -codex-utils-cargo-bin = { workspace = true } serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } -codex-git-utils = { workspace = true } [dev-dependencies] +codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } +serde_json = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/chatgpt/src/chatgpt_token.rs b/codex-rs/chatgpt/src/chatgpt_token.rs index 4f6c492a4..d20a7e57c 100644 --- a/codex-rs/chatgpt/src/chatgpt_token.rs +++ b/codex-rs/chatgpt/src/chatgpt_token.rs @@ -1,4 +1,4 @@ -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthManager; use codex_login::token_data::TokenData; use std::path::Path; diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 8ba12b346..1ea293f97 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -1,5 +1,6 @@ use codex_core::config::Config; use codex_login::AuthManager; +use codex_login::CodexAuth; use codex_login::token_data::TokenData; use std::collections::HashSet; use std::time::Duration; @@ -32,7 +33,10 @@ async fn apps_enabled(config: &Config) -> bool { /*enable_codex_api_key_env*/ false, config.cli_auth_credentials_store_mode, ); - config.features.apps_enabled(Some(&auth_manager)).await + let auth = auth_manager.auth().await; + config + .features + .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth)) } pub async fn list_connectors(config: &Config) -> anyhow::Result> { if !apps_enabled(config).await { diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index ced9d4931..9fa7dc450 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -8,8 +8,8 @@ //! support can request from users. use codex_app_server_protocol::AuthMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_core::config::Config; -use codex_login::AuthCredentialsStoreMode; use codex_login::CLIENT_ID; use codex_login::CodexAuth; use codex_login::ServerOptions; diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 144316b50..027a88a7c 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -194,7 +194,7 @@ impl McpCli { async fn perform_oauth_login_retry_without_scopes( name: &str, url: &str, - store_mode: codex_rmcp_client::OAuthCredentialsStoreMode, + store_mode: codex_config::types::OAuthCredentialsStoreMode, http_headers: Option>, env_http_headers: Option>, resolved_scopes: &ResolvedMcpOAuthScopes, diff --git a/codex-rs/cloud-requirements/Cargo.toml b/codex-rs/cloud-requirements/Cargo.toml index 9eda7a7c3..59f8741cd 100644 --- a/codex-rs/cloud-requirements/Cargo.toml +++ b/codex-rs/cloud-requirements/Cargo.toml @@ -12,6 +12,7 @@ async-trait = { workspace = true } base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-backend-client = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } codex-login = { workspace = true } codex-otel = { workspace = true } diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index f2916c2e9..04463a7dd 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -15,12 +15,12 @@ use chrono::DateTime; use chrono::Duration as ChronoDuration; use chrono::Utc; use codex_backend_client::Client as BackendClient; +use codex_config::types::AuthCredentialsStoreMode; use codex_core::config_loader::CloudRequirementsLoadError; use codex_core::config_loader::CloudRequirementsLoadErrorCode; use codex_core::config_loader::CloudRequirementsLoader; use codex_core::config_loader::ConfigRequirementsToml; use codex_core::util::backoff; -use codex_login::AuthCredentialsStoreMode; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::RefreshTokenError; @@ -820,7 +820,7 @@ mod tests { use super::*; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; - use codex_login::AuthCredentialsStoreMode; + use codex_config::types::AuthCredentialsStoreMode; use codex_protocol::protocol::AskForApproval; use pretty_assertions::assert_eq; use serde_json::json; diff --git a/codex-rs/codex-mcp/src/mcp/auth.rs b/codex-rs/codex-mcp/src/mcp/auth.rs index a4e14e42f..01dc5e9b4 100644 --- a/codex-rs/codex-mcp/src/mcp/auth.rs +++ b/codex-rs/codex-mcp/src/mcp/auth.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use anyhow::Result; +use codex_config::types::OAuthCredentialsStoreMode; use codex_protocol::protocol::McpAuthStatus; -use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rmcp_client::OAuthProviderError; use codex_rmcp_client::determine_streamable_http_auth_status; use codex_rmcp_client::discover_streamable_http_oauth; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 1e197372a..faa8ecc2f 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -12,6 +12,7 @@ use async_channel::unbounded; use codex_config::Constrained; use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; +use codex_config::types::OAuthCredentialsStoreMode; use codex_login::CodexAuth; use codex_plugin::PluginCapabilitySummary; use codex_protocol::mcp::Resource; @@ -20,7 +21,6 @@ use codex_protocol::mcp::Tool; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; -use codex_rmcp_client::OAuthCredentialsStoreMode; use serde_json::Value; use crate::mcp::auth::compute_auth_statuses; diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index 9a995a5c7..b4de70ee9 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -34,6 +34,7 @@ use async_channel::Sender; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; use codex_config::Constrained; +use codex_config::types::OAuthCredentialsStoreMode; use codex_protocol::approvals::ElicitationRequest; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::mcp::CallToolResult; @@ -47,7 +48,6 @@ use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; use codex_protocol::protocol::SandboxPolicy; use codex_rmcp_client::ElicitationResponse; -use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rmcp_client::RmcpClient; use codex_rmcp_client::SendElicitation; use futures::future::BoxFuture; diff --git a/codex-rs/config/Cargo.toml b/codex-rs/config/Cargo.toml index 0b440263e..9532d74d0 100644 --- a/codex-rs/config/Cargo.toml +++ b/codex-rs/config/Cargo.toml @@ -8,10 +8,16 @@ license.workspace = true workspace = true [dependencies] +anyhow = { workspace = true } codex-app-server-protocol = { workspace = true } codex-execpolicy = { workspace = true } +codex-features = { workspace = true } +codex-git-utils = { workspace = true } +codex-model-provider-info = { workspace = true } +codex-network-proxy = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } +dunce = { workspace = true } futures = { workspace = true, features = ["alloc", "std"] } multimap = { workspace = true } schemars = { workspace = true } @@ -27,7 +33,6 @@ tracing = { workspace = true } wildmatch = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs new file mode 100644 index 000000000..caf9d25b8 --- /dev/null +++ b/codex-rs/config/src/config_toml.rs @@ -0,0 +1,778 @@ +//! Schema-heavy configuration TOML types used by Codex. + +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::path::Path; + +use crate::permissions_toml::PermissionsToml; +use crate::profile_toml::ConfigProfile; +use crate::types::AnalyticsConfigToml; +use crate::types::ApprovalsReviewer; +use crate::types::AppsConfigToml; +use crate::types::AuthCredentialsStoreMode; +use crate::types::FeedbackConfigToml; +use crate::types::History; +use crate::types::McpServerConfig; +use crate::types::MemoriesToml; +use crate::types::Notice; +use crate::types::OAuthCredentialsStoreMode; +use crate::types::OtelConfigToml; +use crate::types::PluginConfig; +use crate::types::SandboxWorkspaceWrite; +use crate::types::ShellEnvironmentPolicyToml; +use crate::types::SkillsConfig; +use crate::types::ToolSuggestConfig; +use crate::types::Tui; +use crate::types::UriBasedFileOpener; +use crate::types::WindowsToml; +use codex_app_server_protocol::Tools; +use codex_app_server_protocol::UserSavedConfig; +use codex_features::FeaturesToml; +use codex_git_utils::resolve_root_git_project_for_trust; +use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; +use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; +use codex_model_provider_info::ModelProviderInfo; +use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; +use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; +use codex_model_provider_info::OPENAI_PROVIDER_ID; +use codex_protocol::config_types::ForcedLoginMethod; +use codex_protocol::config_types::Personality; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::SandboxMode; +use codex_protocol::config_types::ServiceTier; +use codex_protocol::config_types::TrustLevel; +use codex_protocol::config_types::Verbosity; +use codex_protocol::config_types::WebSearchMode; +use codex_protocol::config_types::WebSearchToolConfig; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::ReadOnlyAccess; +use codex_protocol::protocol::SandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; + +const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = [ + OPENAI_PROVIDER_ID, + OLLAMA_OSS_PROVIDER_ID, + LMSTUDIO_OSS_PROVIDER_ID, +]; + +/// Base config deserialized from ~/.codex/config.toml. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ConfigToml { + /// Optional override of model selection. + pub model: Option, + /// Review model override used by the `/review` feature. + pub review_model: Option, + + /// Provider to use from the model_providers map. + pub model_provider: Option, + + /// Size of the context window for the model, in tokens. + pub model_context_window: Option, + + /// Token usage threshold triggering auto-compaction of conversation history. + pub model_auto_compact_token_limit: Option, + + /// Default approval policy for executing commands. + pub approval_policy: Option, + + /// Configures who approval requests are routed to for review once they have + /// been escalated. This does not disable separate safety checks such as + /// ARC. + pub approvals_reviewer: Option, + + #[serde(default)] + pub shell_environment_policy: ShellEnvironmentPolicyToml, + + /// Whether the model may request a login shell for shell-based tools. + /// Default to `true` + /// + /// If `true`, the model may request a login shell (`login = true`), and + /// omitting `login` defaults to using a login shell. + /// If `false`, the model can never use a login shell: `login = true` + /// requests are rejected, and omitting `login` defaults to a non-login + /// shell. + pub allow_login_shell: Option, + + /// Sandbox mode to use. + pub sandbox_mode: Option, + + /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. + pub sandbox_workspace_write: Option, + + /// Default named permissions profile to apply from the `[permissions]` + /// table. + pub default_permissions: Option, + + /// Named permissions profiles. + #[serde(default)] + pub permissions: Option, + + /// Optional external command to spawn for end-user notifications. + #[serde(default)] + pub notify: Option>, + + /// System instructions. + pub instructions: Option, + + /// Developer instructions inserted as a `developer` role message. + #[serde(default)] + pub developer_instructions: Option, + + /// Whether to inject the `` developer block. + pub include_permissions_instructions: Option, + + /// Whether to inject the `` developer block. + pub include_apps_instructions: Option, + + /// Whether to inject the `` user block. + pub include_environment_context: Option, + + /// Optional path to a file containing model instructions that will override + /// the built-in instructions for the selected model. Users are STRONGLY + /// DISCOURAGED from using this field, as deviating from the instructions + /// sanctioned by Codex will likely degrade model performance. + pub model_instructions_file: Option, + + /// Compact prompt used for history compaction. + pub compact_prompt: Option, + + /// Optional commit attribution text for commit message co-author trailers. + /// + /// Set to an empty string to disable automatic commit attribution. + pub commit_attribution: Option, + + /// When set, restricts ChatGPT login to a specific workspace identifier. + #[serde(default)] + pub forced_chatgpt_workspace_id: Option, + + /// When set, restricts the login mechanism users may use. + #[serde(default)] + pub forced_login_method: Option, + + /// 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. + #[serde(default)] + pub cli_auth_credentials_store: Option, + + /// Definition for MCP servers that Codex can reach out to for tool calls. + #[serde(default)] + // Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`. + #[schemars(schema_with = "crate::schema::mcp_servers_schema")] + pub mcp_servers: HashMap, + + /// Preferred backend for storing MCP OAuth credentials. + /// keyring: Use an OS-specific keyring service. + /// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2 + /// file: Use a file in the Codex home directory. + /// auto (default): Use the OS-specific keyring service if available, otherwise use a file. + #[serde(default)] + pub mcp_oauth_credentials_store: Option, + + /// Optional fixed port for the local HTTP callback server used during MCP OAuth login. + /// When unset, Codex will bind to an ephemeral port chosen by the OS. + pub mcp_oauth_callback_port: Option, + + /// Optional redirect URI to use during MCP OAuth login. + /// When set, this URI is used in the OAuth authorization request instead + /// of the local listener address. The local callback listener still binds + /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided). + pub mcp_oauth_callback_url: Option, + + /// User-defined provider entries that extend the built-in list. Built-in + /// IDs cannot be overridden. + #[serde(default, deserialize_with = "deserialize_model_providers")] + pub model_providers: HashMap, + + /// Maximum number of bytes to include from an AGENTS.md project doc file. + pub project_doc_max_bytes: Option, + + /// Ordered list of fallback filenames to look for when AGENTS.md is missing. + pub project_doc_fallback_filenames: Option>, + + /// Token budget applied when storing tool/function outputs in the context manager. + pub tool_output_token_limit: Option, + + /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds. + /// Default: `300000` (5 minutes). + pub background_terminal_max_timeout: Option, + + /// Optional absolute path to the Node runtime used by `js_repl`. + pub js_repl_node_path: Option, + + /// Ordered list of directories to search for Node modules in `js_repl`. + pub js_repl_node_module_dirs: Option>, + + /// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution. + pub zsh_path: Option, + + /// Profile to use from the `profiles` map. + pub profile: Option, + + /// Named profiles to facilitate switching between different configurations. + #[serde(default)] + pub profiles: HashMap, + + /// Settings that govern if and what will be written to `~/.codex/history.jsonl`. + #[serde(default)] + pub history: Option, + + /// Directory where Codex stores the SQLite state DB. + /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`. + pub sqlite_home: Option, + + /// Directory where Codex writes log files, for example `codex-tui.log`. + /// Defaults to `$CODEX_HOME/log`. + pub log_dir: Option, + + /// Optional URI-based file opener. If set, citations to files in the model + /// output will be hyperlinked using the specified URI scheme. + pub file_opener: Option, + + /// Collection of settings that are specific to the TUI. + pub tui: Option, + + /// When set to `true`, `AgentReasoning` events will be hidden from the + /// UI/output. Defaults to `false`. + pub hide_agent_reasoning: Option, + + /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. + /// Defaults to `false`. + pub show_raw_agent_reasoning: Option, + + pub model_reasoning_effort: Option, + pub plan_mode_reasoning_effort: Option, + pub model_reasoning_summary: Option, + /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). + pub model_verbosity: Option, + + /// Override to force-enable reasoning summaries for the configured model. + pub model_supports_reasoning_summaries: Option, + + /// Optional path to a JSON model catalog (applied on startup only). + /// Per-thread `config` overrides are accepted but do not reapply this (no-ops). + pub model_catalog_json: Option, + + /// Optionally specify a personality for the model + pub personality: Option, + + /// Optional explicit service tier preference for new turns (`fast` or `flex`). + pub service_tier: Option, + + /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). + pub chatgpt_base_url: Option, + + /// Base URL override for the built-in `openai` model provider. + pub openai_base_url: Option, + + /// Machine-local realtime audio device preferences used by realtime voice. + #[serde(default)] + pub audio: Option, + + /// Experimental / do not use. Overrides only the realtime conversation + /// websocket transport base URL (the `Op::RealtimeConversation` + /// `/v1/realtime` + /// connection) without changing normal provider HTTP requests. + pub experimental_realtime_ws_base_url: Option, + /// Experimental / do not use. Selects the realtime websocket model/snapshot + /// used for the `Op::RealtimeConversation` connection. + pub experimental_realtime_ws_model: Option, + /// Experimental / do not use. Realtime websocket session selection. + /// `version` controls v1/v2 and `type` controls conversational/transcription. + #[serde(default)] + pub realtime: Option, + /// Experimental / do not use. Overrides only the realtime conversation + /// websocket transport instructions (the `Op::RealtimeConversation` + /// `/ws` session.update instructions) without changing normal prompts. + pub experimental_realtime_ws_backend_prompt: Option, + /// Experimental / do not use. Replaces the synthesized realtime startup + /// context appended to websocket session instructions. An empty string + /// disables startup context injection entirely. + pub experimental_realtime_ws_startup_context: Option, + /// Experimental / do not use. Replaces the built-in realtime start + /// instructions inserted into developer messages when realtime becomes + /// active. + pub experimental_realtime_start_instructions: Option, + pub projects: Option>, + + /// Controls the web search tool mode: disabled, cached, or live. + pub web_search: Option, + + /// Nested tools section for feature toggles + pub tools: Option, + + /// Additional discoverable tools that can be suggested for installation. + pub tool_suggest: Option, + + /// Agent-related settings (thread limits, etc.). + pub agents: Option, + + /// Memories subsystem settings. + pub memories: Option, + + /// User-level skill config entries keyed by SKILL.md path. + pub skills: Option, + + /// User-level plugin config entries keyed by plugin name. + #[serde(default)] + pub plugins: HashMap, + + /// Centralized feature flags (new). Prefer this over individual toggles. + #[serde(default)] + // Injects known feature keys into the schema and forbids unknown keys. + #[schemars(schema_with = "crate::schema::features_schema")] + pub features: Option, + + /// Suppress warnings about unstable (under development) features. + pub suppress_unstable_features_warning: Option, + + /// Settings for ghost snapshots (used for undo). + #[serde(default)] + pub ghost_snapshot: Option, + + /// Markers used to detect the project root when searching parent + /// directories for `.codex` folders. Defaults to [".git"] when unset. + #[serde(default)] + pub project_root_markers: Option>, + + /// When `true`, checks for Codex updates on startup and surfaces update prompts. + /// Set to `false` only if your Codex updates are centrally managed. + /// Defaults to `true`. + pub check_for_update_on_startup: Option, + + /// When true, disables burst-paste detection for typed input entirely. + /// All characters are inserted as they are received, and no buffering + /// or placeholder replacement will occur for fast keypress bursts. + pub disable_paste_burst: Option, + + /// When `false`, disables analytics across Codex product surfaces in this machine. + /// Defaults to `true`. + pub analytics: Option, + + /// When `false`, disables feedback collection across Codex product surfaces. + /// Defaults to `true`. + pub feedback: Option, + + /// Settings for app-specific controls. + #[serde(default)] + pub apps: Option, + + /// OTEL configuration. + pub otel: Option, + + /// Windows-specific configuration. + #[serde(default)] + pub windows: Option, + + /// Tracks whether the Windows onboarding screen has been acknowledged. + pub windows_wsl_setup_acknowledged: Option, + + /// Collection of in-product notices (different from notifications) + /// See [`crate::types::Notice`] for more details + pub notice: Option, + + /// Legacy, now use features + /// Deprecated: ignored. Use `model_instructions_file`. + #[schemars(skip)] + pub experimental_instructions_file: Option, + pub experimental_compact_prompt_file: Option, + pub experimental_use_unified_exec_tool: Option, + pub experimental_use_freeform_apply_patch: Option, + /// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama". + pub oss_provider: Option, +} + +impl From for UserSavedConfig { + fn from(config_toml: ConfigToml) -> Self { + let profiles = config_toml + .profiles + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); + + Self { + approval_policy: config_toml.approval_policy, + sandbox_mode: config_toml.sandbox_mode, + sandbox_settings: config_toml.sandbox_workspace_write.map(From::from), + forced_chatgpt_workspace_id: config_toml.forced_chatgpt_workspace_id, + forced_login_method: config_toml.forced_login_method, + model: config_toml.model, + model_reasoning_effort: config_toml.model_reasoning_effort, + model_reasoning_summary: config_toml.model_reasoning_summary, + model_verbosity: config_toml.model_verbosity, + tools: config_toml.tools.map(From::from), + profile: config_toml.profile, + profiles, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ProjectConfig { + pub trust_level: Option, +} + +impl ProjectConfig { + pub fn is_trusted(&self) -> bool { + matches!(self.trust_level, Some(TrustLevel::Trusted)) + } + + pub fn is_untrusted(&self) -> bool { + matches!(self.trust_level, Some(TrustLevel::Untrusted)) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RealtimeAudioConfig { + pub microphone: Option, + pub speaker: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RealtimeWsMode { + #[default] + Conversational, + Transcription, +} + +pub use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion; + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct RealtimeConfig { + pub version: RealtimeWsVersion, + #[serde(rename = "type")] + pub session_type: RealtimeWsMode, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct RealtimeToml { + pub version: Option, + #[serde(rename = "type")] + pub session_type: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct RealtimeAudioToml { + pub microphone: Option, + pub speaker: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ToolsToml { + #[serde( + default, + deserialize_with = "deserialize_optional_web_search_tool_config" + )] + pub web_search: Option, + + /// Enable the `view_image` tool that lets the agent attach local images. + #[serde(default)] + pub view_image: Option, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum WebSearchToolConfigInput { + Enabled(bool), + Config(WebSearchToolConfig), +} + +fn deserialize_optional_web_search_tool_config<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + + Ok(match value { + None => None, + Some(WebSearchToolConfigInput::Enabled(enabled)) => { + let _ = enabled; + None + } + Some(WebSearchToolConfigInput::Config(config)) => Some(config), + }) +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AgentsToml { + /// Maximum number of agent threads that can be open concurrently. + /// When unset, no limit is enforced. + #[schemars(range(min = 1))] + pub max_threads: Option, + /// Maximum nesting depth allowed for spawned agent threads. + /// Root sessions start at depth 0. + #[schemars(range(min = 1))] + pub max_depth: Option, + /// Default maximum runtime in seconds for agent job workers. + #[schemars(range(min = 1))] + pub job_max_runtime_seconds: Option, + + /// User-defined role declarations keyed by role name. + /// + /// Example: + /// ```toml + /// [agents.researcher] + /// description = "Research-focused role." + /// config_file = "./agents/researcher.toml" + /// nickname_candidates = ["Herodotus", "Ibn Battuta"] + /// ``` + #[serde(default, flatten)] + pub roles: BTreeMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct AgentRoleToml { + /// Human-facing role documentation used in spawn tool guidance. + /// Required unless supplied by the referenced agent role file. + pub description: Option, + + /// Path to a role-specific config layer. + /// Relative paths are resolved relative to the `config.toml` that defines them. + pub config_file: Option, + + /// Candidate nicknames for agents spawned with this role. + pub nickname_candidates: Option>, +} + +impl From for Tools { + fn from(tools_toml: ToolsToml) -> Self { + Self { + web_search: tools_toml.web_search.is_some().then_some(true), + view_image: tools_toml.view_image, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct GhostSnapshotToml { + /// Exclude untracked files larger than this many bytes from ghost snapshots. + #[serde(alias = "ignore_untracked_files_over_bytes")] + pub ignore_large_untracked_files: Option, + /// Ignore untracked directories that contain this many files or more. + /// (Still emits a warning unless warnings are disabled.) + #[serde(alias = "large_untracked_dir_warning_threshold")] + pub ignore_large_untracked_dirs: Option, + /// Disable all ghost snapshot warning events. + pub disable_warnings: Option, +} + +impl ConfigToml { + /// Derive the effective sandbox policy from the configuration. + pub fn derive_sandbox_policy( + &self, + sandbox_mode_override: Option, + profile_sandbox_mode: Option, + windows_sandbox_level: WindowsSandboxLevel, + resolved_cwd: &Path, + sandbox_policy_constraint: Option<&crate::Constrained>, + ) -> SandboxPolicy { + let sandbox_mode_was_explicit = sandbox_mode_override.is_some() + || profile_sandbox_mode.is_some() + || self.sandbox_mode.is_some(); + let resolved_sandbox_mode = sandbox_mode_override + .or(profile_sandbox_mode) + .or(self.sandbox_mode) + .or_else(|| { + // If no sandbox_mode is set but this directory has a trust decision, + // default to workspace-write except on unsandboxed Windows where we + // default to read-only. + self.get_active_project(resolved_cwd).and_then(|p| { + if p.is_trusted() || p.is_untrusted() { + if cfg!(target_os = "windows") + && windows_sandbox_level == WindowsSandboxLevel::Disabled + { + Some(SandboxMode::ReadOnly) + } else { + Some(SandboxMode::WorkspaceWrite) + } + } else { + None + } + }) + }) + .unwrap_or_default(); + let mut sandbox_policy = match resolved_sandbox_mode { + SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), + SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { + Some(SandboxWorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + }) => SandboxPolicy::WorkspaceWrite { + writable_roots: writable_roots.clone(), + read_only_access: ReadOnlyAccess::FullAccess, + network_access: *network_access, + exclude_tmpdir_env_var: *exclude_tmpdir_env_var, + exclude_slash_tmp: *exclude_slash_tmp, + }, + None => SandboxPolicy::new_workspace_write_policy(), + }, + SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess, + }; + let downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| { + if cfg!(target_os = "windows") + // If the experimental Windows sandbox is enabled, do not force a downgrade. + && windows_sandbox_level == WindowsSandboxLevel::Disabled + && matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. }) + { + *policy = SandboxPolicy::new_read_only_policy(); + } + }; + if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) { + downgrade_workspace_write_if_unsupported(&mut sandbox_policy); + } + if !sandbox_mode_was_explicit + && let Some(constraint) = sandbox_policy_constraint + && let Err(err) = constraint.can_set(&sandbox_policy) + { + tracing::warn!( + error = %err, + "default sandbox policy is disallowed by requirements; falling back to required default" + ); + sandbox_policy = constraint.get().clone(); + downgrade_workspace_write_if_unsupported(&mut sandbox_policy); + } + sandbox_policy + } + + /// Resolves the cwd to an existing project, or returns None if ConfigToml + /// does not contain a project corresponding to cwd or a git repo for cwd + pub fn get_active_project(&self, resolved_cwd: &Path) -> Option { + let projects = self.projects.clone().unwrap_or_default(); + + let resolved_cwd_key = project_trust_key(resolved_cwd); + let resolved_cwd_raw_key = resolved_cwd.to_string_lossy().to_string(); + if let Some(project_config) = projects + .get(&resolved_cwd_key) + .or_else(|| projects.get(&resolved_cwd_raw_key)) + { + return Some(project_config.clone()); + } + + // If cwd lives inside a git repo/worktree, check whether the root git project + // (the primary repository working directory) is trusted. This lets + // worktrees inherit trust from the main project. + if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd) { + let repo_root_key = project_trust_key(repo_root.as_path()); + let repo_root_raw_key = repo_root.to_string_lossy().to_string(); + if let Some(project_config_for_root) = projects + .get(&repo_root_key) + .or_else(|| projects.get(&repo_root_raw_key)) + { + return Some(project_config_for_root.clone()); + } + } + + None + } + + pub fn get_config_profile( + &self, + override_profile: Option, + ) -> Result { + let profile = override_profile.or_else(|| self.profile.clone()); + + match profile { + Some(key) => { + if let Some(profile) = self.profiles.get(key.as_str()) { + return Ok(profile.clone()); + } + + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("config profile `{key}` not found"), + )) + } + None => Ok(ConfigProfile::default()), + } + } +} + +/// Canonicalize the path and convert it to a string to be used as a key in the +/// projects trust map. On Windows, strips UNC, when possible, to try to ensure +/// that different paths that point to the same location have the same key. +fn project_trust_key(project_path: &Path) -> String { + dunce::canonicalize(project_path) + .unwrap_or_else(|_| project_path.to_path_buf()) + .to_string_lossy() + .to_string() +} + +pub fn validate_reserved_model_provider_ids( + model_providers: &HashMap, +) -> Result<(), String> { + let mut conflicts = model_providers + .keys() + .filter(|key| RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())) + .map(|key| format!("`{key}`")) + .collect::>(); + conflicts.sort_unstable(); + if conflicts.is_empty() { + Ok(()) + } else { + Err(format!( + "model_providers contains reserved built-in provider IDs: {}. \ +Built-in providers cannot be overridden. Rename your custom provider (for example, `openai-custom`).", + conflicts.join(", ") + )) + } +} + +pub fn validate_model_providers( + model_providers: &HashMap, +) -> Result<(), String> { + validate_reserved_model_provider_ids(model_providers)?; + for (key, provider) in model_providers { + provider + .validate() + .map_err(|message| format!("model_providers.{key}: {message}"))?; + } + Ok(()) +} + +fn deserialize_model_providers<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let model_providers = HashMap::::deserialize(deserializer)?; + validate_model_providers(&model_providers).map_err(serde::de::Error::custom)?; + Ok(model_providers) +} + +pub fn validate_oss_provider(provider: &str) -> std::io::Result<()> { + match provider { + LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => Ok(()), + LEGACY_OLLAMA_CHAT_PROVIDER_ID => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + OLLAMA_CHAT_PROVIDER_REMOVED_ERROR, + )), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}" + ), + )), + } +} diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 219601132..5b8e5e687 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -1,5 +1,6 @@ mod cloud_requirements; mod config_requirements; +pub mod config_toml; mod constraint; mod diagnostics; mod fingerprint; @@ -7,8 +8,11 @@ mod mcp_edit; mod mcp_types; mod merge; mod overrides; +pub mod permissions_toml; +pub mod profile_toml; mod project_root_markers; mod requirements_exec_policy; +pub mod schema; mod skills_config; mod state; pub mod types; diff --git a/codex-rs/config/src/permissions_toml.rs b/codex-rs/config/src/permissions_toml.rs new file mode 100644 index 000000000..fcc3e006b --- /dev/null +++ b/codex-rs/config/src/permissions_toml.rs @@ -0,0 +1,240 @@ +use std::collections::BTreeMap; + +use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission; +use codex_network_proxy::NetworkMode; +use codex_network_proxy::NetworkProxyConfig; +use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; +use codex_network_proxy::normalize_host; +use codex_protocol::permissions::FileSystemAccessMode; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +pub struct PermissionsToml { + #[serde(flatten)] + pub entries: BTreeMap, +} + +impl PermissionsToml { + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct PermissionProfileToml { + pub filesystem: Option, + pub network: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +pub struct FilesystemPermissionsToml { + #[serde(flatten)] + pub entries: BTreeMap, +} + +impl FilesystemPermissionsToml { + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[serde(untagged)] +pub enum FilesystemPermissionToml { + Access(FileSystemAccessMode), + Scoped(BTreeMap), +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +pub struct NetworkDomainPermissionsToml { + #[serde(flatten)] + pub entries: BTreeMap, +} + +impl NetworkDomainPermissionsToml { + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn allowed_domains(&self) -> Option> { + let allowed_domains: Vec = self + .entries + .iter() + .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Allow)) + .map(|(pattern, _)| pattern.clone()) + .collect(); + (!allowed_domains.is_empty()).then_some(allowed_domains) + } + + pub fn denied_domains(&self) -> Option> { + let denied_domains: Vec = self + .entries + .iter() + .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Deny)) + .map(|(pattern, _)| pattern.clone()) + .collect(); + (!denied_domains.is_empty()).then_some(denied_domains) + } +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum NetworkDomainPermissionToml { + Allow, + Deny, +} + +impl std::fmt::Display for NetworkDomainPermissionToml { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let permission = match self { + Self::Allow => "allow", + Self::Deny => "deny", + }; + f.write_str(permission) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +pub struct NetworkUnixSocketPermissionsToml { + #[serde(flatten)] + pub entries: BTreeMap, +} + +impl NetworkUnixSocketPermissionsToml { + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn allow_unix_sockets(&self) -> Vec { + self.entries + .iter() + .filter(|(_, permission)| matches!(permission, NetworkUnixSocketPermissionToml::Allow)) + .map(|(path, _)| path.clone()) + .collect() + } +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema, +)] +#[serde(rename_all = "lowercase")] +pub enum NetworkUnixSocketPermissionToml { + Allow, + None, +} + +impl std::fmt::Display for NetworkUnixSocketPermissionToml { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let permission = match self { + Self::Allow => "allow", + Self::None => "none", + }; + f.write_str(permission) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct NetworkToml { + pub enabled: Option, + pub proxy_url: Option, + pub enable_socks5: Option, + pub socks_url: Option, + pub enable_socks5_udp: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_all_unix_sockets: Option, + #[schemars(with = "Option")] + pub mode: Option, + pub domains: Option, + pub unix_sockets: Option, + pub allow_local_binding: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "lowercase")] +enum NetworkModeSchema { + Limited, + Full, +} + +impl NetworkToml { + pub fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) { + if let Some(enabled) = self.enabled { + config.network.enabled = enabled; + } + if let Some(proxy_url) = self.proxy_url.as_ref() { + config.network.proxy_url = proxy_url.clone(); + } + if let Some(enable_socks5) = self.enable_socks5 { + config.network.enable_socks5 = enable_socks5; + } + if let Some(socks_url) = self.socks_url.as_ref() { + config.network.socks_url = socks_url.clone(); + } + if let Some(enable_socks5_udp) = self.enable_socks5_udp { + config.network.enable_socks5_udp = enable_socks5_udp; + } + if let Some(allow_upstream_proxy) = self.allow_upstream_proxy { + config.network.allow_upstream_proxy = allow_upstream_proxy; + } + if let Some(dangerously_allow_non_loopback_proxy) = + self.dangerously_allow_non_loopback_proxy + { + config.network.dangerously_allow_non_loopback_proxy = + dangerously_allow_non_loopback_proxy; + } + if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets { + config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets; + } + if let Some(mode) = self.mode { + config.network.mode = mode; + } + if let Some(domains) = self.domains.as_ref() { + overlay_network_domain_permissions(config, domains); + } + if let Some(unix_sockets) = self.unix_sockets.as_ref() { + let mut proxy_unix_sockets = config.network.unix_sockets.take().unwrap_or_default(); + for (path, permission) in &unix_sockets.entries { + let permission = match permission { + NetworkUnixSocketPermissionToml::Allow => { + ProxyNetworkUnixSocketPermission::Allow + } + NetworkUnixSocketPermissionToml::None => ProxyNetworkUnixSocketPermission::None, + }; + proxy_unix_sockets.entries.insert(path.clone(), permission); + } + config.network.unix_sockets = + (!proxy_unix_sockets.entries.is_empty()).then_some(proxy_unix_sockets); + } + if let Some(allow_local_binding) = self.allow_local_binding { + config.network.allow_local_binding = allow_local_binding; + } + } + + pub fn to_network_proxy_config(&self) -> NetworkProxyConfig { + let mut config = NetworkProxyConfig::default(); + self.apply_to_network_proxy_config(&mut config); + config + } +} + +pub fn overlay_network_domain_permissions( + config: &mut NetworkProxyConfig, + domains: &NetworkDomainPermissionsToml, +) { + for (pattern, permission) in &domains.entries { + let permission = match permission { + NetworkDomainPermissionToml::Allow => ProxyNetworkDomainPermission::Allow, + NetworkDomainPermissionToml::Deny => ProxyNetworkDomainPermission::Deny, + }; + config + .network + .upsert_domain_permission(pattern.clone(), permission, normalize_host); + } +} diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/config/src/profile_toml.rs similarity index 92% rename from codex-rs/core/src/config/profile.rs rename to codex-rs/config/src/profile_toml.rs index 7a83428b8..69215c044 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/config/src/profile_toml.rs @@ -3,10 +3,11 @@ use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use crate::config::ToolsToml; -use codex_config::types::ApprovalsReviewer; -use codex_config::types::Personality; -use codex_config::types::WindowsToml; +use crate::config_toml::ToolsToml; +use crate::types::AnalyticsConfigToml; +use crate::types::ApprovalsReviewer; +use crate::types::Personality; +use crate::types::WindowsToml; use codex_features::FeaturesToml; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; @@ -57,13 +58,13 @@ pub struct ConfigProfile { pub tools_view_image: Option, pub tools: Option, pub web_search: Option, - pub analytics: Option, + pub analytics: Option, #[serde(default)] pub windows: Option, /// Optional feature toggles scoped to this profile. #[serde(default)] // Injects known feature keys into the schema and forbids unknown keys. - #[schemars(schema_with = "crate::config::schema::features_schema")] + #[schemars(schema_with = "crate::schema::features_schema")] pub features: Option, pub oss_provider: Option, } diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs new file mode 100644 index 000000000..72252d7a2 --- /dev/null +++ b/codex-rs/config/src/schema.rs @@ -0,0 +1,100 @@ +use crate::config_toml::ConfigToml; +use crate::types::RawMcpServerConfig; +use codex_features::FEATURES; +use codex_features::legacy_feature_keys; +use schemars::r#gen::SchemaGenerator; +use schemars::r#gen::SchemaSettings; +use schemars::schema::InstanceType; +use schemars::schema::ObjectValidation; +use schemars::schema::RootSchema; +use schemars::schema::Schema; +use schemars::schema::SchemaObject; +use serde_json::Map; +use serde_json::Value; +use std::path::Path; + +/// Schema for the `[features]` map with known + legacy keys only. +pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { + let mut object = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + ..Default::default() + }; + + let mut validation = ObjectValidation::default(); + for feature in FEATURES { + if feature.id == codex_features::Feature::Artifact { + continue; + } + validation + .properties + .insert(feature.key.to_string(), schema_gen.subschema_for::()); + } + for legacy_key in legacy_feature_keys() { + validation + .properties + .insert(legacy_key.to_string(), schema_gen.subschema_for::()); + } + validation.additional_properties = Some(Box::new(Schema::Bool(false))); + object.object = Some(Box::new(validation)); + + Schema::Object(object) +} + +/// Schema for the `[mcp_servers]` map using the raw input shape. +pub fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema { + let mut object = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + ..Default::default() + }; + + let validation = ObjectValidation { + additional_properties: Some(Box::new(schema_gen.subschema_for::())), + ..Default::default() + }; + object.object = Some(Box::new(validation)); + + Schema::Object(object) +} + +/// Build the config schema for `config.toml`. +pub fn config_schema() -> RootSchema { + SchemaSettings::draft07() + .with(|settings| { + settings.option_add_null_type = false; + }) + .into_generator() + .into_root_schema_for::() +} + +/// Canonicalize a JSON value by sorting its keys. +pub fn canonicalize(value: &Value) -> Value { + match value { + Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()), + Value::Object(map) => { + let mut entries: Vec<_> = map.iter().collect(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + let mut sorted = Map::with_capacity(map.len()); + for (key, child) in entries { + sorted.insert(key.clone(), canonicalize(child)); + } + Value::Object(sorted) + } + _ => value.clone(), + } +} + +/// Render the config schema as pretty-printed JSON. +pub fn config_schema_json() -> anyhow::Result> { + let schema = config_schema(); + let value = serde_json::to_value(schema)?; + let value = canonicalize(&value); + let json = serde_json::to_vec_pretty(&value)?; + Ok(json) +} + +/// Write the config schema fixture to disk. +pub fn write_config_schema(out_path: &Path) -> anyhow::Result<()> { + let json = config_schema_json()?; + std::fs::write(out_path, json)?; + Ok(()) +} diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index c52383352..f26d33601 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -36,6 +36,36 @@ const fn default_enabled() -> bool { true } +/// Determine where Codex should store CLI auth credentials. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum AuthCredentialsStoreMode { + #[default] + /// 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, +} + +/// Determine where Codex should store and read MCP credentials. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum OAuthCredentialsStoreMode { + /// `Keyring` when available; otherwise, `File`. + /// Credentials stored in the keyring will only be readable by Codex unless the user explicitly grants access via OS-level keyring access. + #[default] + Auto, + /// CODEX_HOME/.credentials.json + /// This file will be readable to Codex and other applications running as the same user. + File, + /// Keyring when available, otherwise fail. + Keyring, +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum WindowsSandboxModeToml { diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 0a9dfb521..d2cbad83e 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -93,7 +93,6 @@ rmcp = { workspace = true, default-features = false, features = [ "schemars", "server", ] } -schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha1 = { workspace = true } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 5b8e5dfc1..8da02dc80 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2423,7 +2423,7 @@ "$ref": "#/definitions/Notice" } ], - "description": "Collection of in-product notices (different from notifications) See [`codex_config::types::Notice`] for more details" + "description": "Collection of in-product notices (different from notifications) See [`crate::types::Notice`] for more details" }, "notify": { "default": null, diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index b7d7b55ab..6570baa45 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -17,6 +17,7 @@ use crate::config_loader::ConfigLayerStackOrdering; use crate::config_loader::resolve_relative_paths_in_config_toml; use anyhow::anyhow; use codex_app_server_protocol::ConfigLayerSource; +use codex_config::config_toml::ConfigToml; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::path::Path; @@ -221,7 +222,7 @@ mod reload { fn deserialize_effective_config( config: &Config, config_layer_stack: &ConfigLayerStack, - ) -> anyhow::Result { + ) -> anyhow::Result { Ok(deserialize_config_toml_with_base( config_layer_stack.effective_config(), &config.codex_home, diff --git a/codex-rs/core/src/bin/config_schema.rs b/codex-rs/core/src/bin/config_schema.rs index 8d33df42e..f92ce6230 100644 --- a/codex-rs/core/src/bin/config_schema.rs +++ b/codex-rs/core/src/bin/config_schema.rs @@ -15,6 +15,6 @@ fn main() -> Result<()> { let out_path = args .out .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.schema.json")); - codex_core::config::schema::write_config_schema(&out_path)?; + codex_config::schema::write_config_schema(&out_path)?; Ok(()) } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c7923beaf..4bf309c90 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -54,6 +54,7 @@ use codex_analytics::SubAgentThreadStartedInput; use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; +use codex_config::types::OAuthCredentialsStoreMode; use codex_exec_server::Environment; use codex_exec_server::EnvironmentManager; use codex_features::FEATURES; @@ -128,7 +129,6 @@ use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_rmcp_client::ElicitationResponse; -use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rollout::state_db; use codex_shell_command::parse_command::parse_command; use codex_terminal_detection::user_agent; @@ -905,8 +905,13 @@ impl TurnContext { } pub(crate) fn apps_enabled(&self) -> bool { - self.features - .apps_enabled_cached(self.auth_manager.as_deref()) + let is_chatgpt_auth = self + .auth_manager + .as_deref() + .and_then(AuthManager::auth_cached) + .as_ref() + .is_some_and(CodexAuth::is_chatgpt_auth); + self.features.apps_enabled_for_auth(is_chatgpt_auth) } pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self { diff --git a/codex-rs/core/src/config/agent_roles.rs b/codex-rs/core/src/config/agent_roles.rs index c527435e9..24d26ebf4 100644 --- a/codex-rs/core/src/config/agent_roles.rs +++ b/codex-rs/core/src/config/agent_roles.rs @@ -1,9 +1,9 @@ use super::AgentRoleConfig; -use super::AgentRoleToml; -use super::AgentsToml; -use super::ConfigToml; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigLayerStackOrdering; +use codex_config::config_toml::AgentRoleToml; +use codex_config::config_toml::AgentsToml; +use codex_config::config_toml::ConfigToml; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use serde::Deserialize; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 13e60ecb7..deba8a47b 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5,6 +5,24 @@ use crate::config_loader::RequirementSource; use crate::plugins::PluginsManager; use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; +use codex_config::config_toml::AgentRoleToml; +use codex_config::config_toml::AgentsToml; +use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ProjectConfig; +use codex_config::config_toml::RealtimeAudioConfig; +use codex_config::config_toml::RealtimeConfig; +use codex_config::config_toml::RealtimeToml; +use codex_config::config_toml::RealtimeWsMode; +use codex_config::config_toml::RealtimeWsVersion; +use codex_config::config_toml::ToolsToml; +use codex_config::permissions_toml::FilesystemPermissionToml; +use codex_config::permissions_toml::FilesystemPermissionsToml; +use codex_config::permissions_toml::NetworkDomainPermissionToml; +use codex_config::permissions_toml::NetworkDomainPermissionsToml; +use codex_config::permissions_toml::NetworkToml; +use codex_config::permissions_toml::PermissionProfileToml; +use codex_config::permissions_toml::PermissionsToml; +use codex_config::profile_toml::ConfigProfile; use codex_config::types::AppToolApproval; use codex_config::types::ApprovalsReviewer; use codex_config::types::BundledSkillsConfig; @@ -17,9 +35,14 @@ use codex_config::types::MemoriesToml; use codex_config::types::ModelAvailabilityNuxConfig; use codex_config::types::NotificationMethod; use codex_config::types::Notifications; +use codex_config::types::SandboxWorkspaceWrite; +use codex_config::types::SkillsConfig; use codex_config::types::ToolSuggestDiscoverableType; +use codex_config::types::Tui; use codex_features::Feature; use codex_features::FeaturesToml; +use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; +use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; use codex_protocol::permissions::FileSystemAccessMode; @@ -28,6 +51,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::ReadOnlyAccess; use serde::Deserialize; use tempfile::tempdir; diff --git a/codex-rs/core/src/config/managed_features.rs b/codex-rs/core/src/config/managed_features.rs index 44daa241c..cbad012c6 100644 --- a/codex-rs/core/src/config/managed_features.rs +++ b/codex-rs/core/src/config/managed_features.rs @@ -8,8 +8,8 @@ use codex_config::FeatureRequirementsToml; use codex_config::RequirementSource; use codex_config::Sourced; -use crate::config::ConfigToml; -use crate::config::profile::ConfigProfile; +use codex_config::config_toml::ConfigToml; +use codex_config::profile_toml::ConfigProfile; use codex_features::Feature; use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index cb497815f..3c2d80464 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -22,50 +22,42 @@ use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS; use crate::windows_sandbox::WindowsSandboxLevelExt; use crate::windows_sandbox::resolve_windows_sandbox_mode; use crate::windows_sandbox::resolve_windows_sandbox_private_desktop; -use codex_app_server_protocol::Tools; -use codex_app_server_protocol::UserSavedConfig; +use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ProjectConfig; +use codex_config::config_toml::RealtimeAudioConfig; +use codex_config::config_toml::RealtimeConfig; +use codex_config::config_toml::validate_model_providers; +use codex_config::profile_toml::ConfigProfile; use codex_config::types::ApprovalsReviewer; -use codex_config::types::AppsConfigToml; +use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::DEFAULT_OTEL_ENVIRONMENT; use codex_config::types::History; use codex_config::types::McpServerConfig; use codex_config::types::McpServerDisabledReason; use codex_config::types::McpServerTransportConfig; use codex_config::types::MemoriesConfig; -use codex_config::types::MemoriesToml; use codex_config::types::ModelAvailabilityNuxConfig; use codex_config::types::Notice; use codex_config::types::NotificationMethod; use codex_config::types::Notifications; +use codex_config::types::OAuthCredentialsStoreMode; use codex_config::types::OtelConfig; use codex_config::types::OtelConfigToml; use codex_config::types::OtelExporterKind; -use codex_config::types::PluginConfig; -use codex_config::types::SandboxWorkspaceWrite; use codex_config::types::ShellEnvironmentPolicy; -use codex_config::types::ShellEnvironmentPolicyToml; -use codex_config::types::SkillsConfig; use codex_config::types::ToolSuggestConfig; use codex_config::types::ToolSuggestDiscoverable; -use codex_config::types::Tui; use codex_config::types::UriBasedFileOpener; use codex_config::types::WindowsSandboxModeToml; -use codex_config::types::WindowsToml; use codex_features::Feature; use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; use codex_features::Features; -use codex_features::FeaturesToml; -use codex_git_utils::resolve_root_git_project_for_trust; -use codex_login::AuthCredentialsStoreMode; use codex_login::AuthManagerConfig; use codex_mcp::mcp::McpConfig; use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; -use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; -use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; -use codex_model_provider_info::OPENAI_PROVIDER_ID; use codex_model_provider_info::built_in_model_providers; use codex_models_manager::ModelsManagerConfig; use codex_protocol::config_types::AltScreenMode; @@ -78,22 +70,16 @@ use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; -use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; -use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; -use schemars::JsonSchema; use serde::Deserialize; -use serde::Deserializer; -use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; use std::io::ErrorKind; @@ -103,7 +89,6 @@ use std::path::PathBuf; use crate::config::permissions::compile_permission_profile; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; use crate::config::permissions::network_proxy_config_from_profile_network; -use crate::config::profile::ConfigProfile; use codex_network_proxy::NetworkProxyConfig; use toml::Value as TomlValue; use toml_edit::DocumentMut; @@ -113,8 +98,8 @@ pub mod edit; mod managed_features; mod network_proxy_spec; mod permissions; -pub mod profile; -pub mod schema; +#[cfg(test)] +mod schema; pub mod service; pub use codex_config::Constrained; pub use codex_config::ConstraintError; @@ -124,16 +109,6 @@ pub use codex_sandboxing::system_bwrap_warning; pub use managed_features::ManagedFeatures; pub use network_proxy_spec::NetworkProxySpec; pub use network_proxy_spec::StartedNetworkProxy; -pub use permissions::FilesystemPermissionToml; -pub use permissions::FilesystemPermissionsToml; -pub use permissions::NetworkDomainPermissionToml; -pub use permissions::NetworkDomainPermissionsToml; -pub use permissions::NetworkToml; -pub use permissions::NetworkUnixSocketPermissionToml; -pub use permissions::NetworkUnixSocketPermissionsToml; -pub use permissions::PermissionProfileToml; -pub use permissions::PermissionsToml; -pub(crate) use permissions::overlay_network_domain_permissions; pub(crate) use permissions::resolve_permission_profile; pub use service::ConfigService; pub use service::ConfigServiceError; @@ -149,11 +124,6 @@ pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1; pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option = None; pub const CONFIG_TOML_FILE: &str = "config.toml"; -const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = [ - OPENAI_PROVIDER_ID, - OLLAMA_OSS_PROVIDER_ID, - LMSTUDIO_OSS_PROVIDER_ID, -]; fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option { let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?; @@ -342,7 +312,7 @@ pub struct Config { /// Controls whether the TUI uses the terminal's alternate screen buffer. /// - /// This is the same `tui.alternate_screen` value from `config.toml` (see [`Tui`]). + /// This is the same `tui.alternate_screen` value from `config.toml`. /// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere. /// - `always`: Always use alternate screen (original behavior). /// - `never`: Never use alternate screen (inline mode, preserves scrollback). @@ -1115,26 +1085,7 @@ pub fn set_project_trust_level( /// Save the default OSS provider preference to config.toml pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> { - // Validate that the provider is one of the known OSS providers - match provider { - LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => { - // Valid provider, continue - } - LEGACY_OLLAMA_CHAT_PROVIDER_ID => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - OLLAMA_CHAT_PROVIDER_REMOVED_ERROR, - )); - } - _ => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!( - "Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}" - ), - )); - } - } + codex_config::config_toml::validate_oss_provider(provider)?; use toml_edit::value; let edits = [ConfigEdit::SetPath { @@ -1148,452 +1099,15 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R .map_err(|err| std::io::Error::other(format!("failed to persist config.toml: {err}"))) } -/// Base config deserialized from ~/.codex/config.toml. -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct ConfigToml { - /// Optional override of model selection. - pub model: Option, - /// Review model override used by the `/review` feature. - pub review_model: Option, - - /// Provider to use from the model_providers map. - pub model_provider: Option, - - /// Size of the context window for the model, in tokens. - pub model_context_window: Option, - - /// Token usage threshold triggering auto-compaction of conversation history. - pub model_auto_compact_token_limit: Option, - - /// Default approval policy for executing commands. - pub approval_policy: Option, - - /// Configures who approval requests are routed to for review once they have - /// been escalated. This does not disable separate safety checks such as - /// ARC. - pub approvals_reviewer: Option, - - #[serde(default)] - pub shell_environment_policy: ShellEnvironmentPolicyToml, - - /// Whether the model may request a login shell for shell-based tools. - /// Default to `true` - /// - /// If `true`, the model may request a login shell (`login = true`), and - /// omitting `login` defaults to using a login shell. - /// If `false`, the model can never use a login shell: `login = true` - /// requests are rejected, and omitting `login` defaults to a non-login - /// shell. - pub allow_login_shell: Option, - - /// Sandbox mode to use. - pub sandbox_mode: Option, - - /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. - pub sandbox_workspace_write: Option, - - /// Default named permissions profile to apply from the `[permissions]` - /// table. - pub default_permissions: Option, - - /// Named permissions profiles. - #[serde(default)] - pub permissions: Option, - - /// Optional external command to spawn for end-user notifications. - #[serde(default)] - pub notify: Option>, - - /// System instructions. - pub instructions: Option, - - /// Developer instructions inserted as a `developer` role message. - #[serde(default)] - pub developer_instructions: Option, - - /// Whether to inject the `` developer block. - pub include_permissions_instructions: Option, - - /// Whether to inject the `` developer block. - pub include_apps_instructions: Option, - - /// Whether to inject the `` user block. - pub include_environment_context: Option, - - /// Optional path to a file containing model instructions that will override - /// the built-in instructions for the selected model. Users are STRONGLY - /// DISCOURAGED from using this field, as deviating from the instructions - /// sanctioned by Codex will likely degrade model performance. - pub model_instructions_file: Option, - - /// Compact prompt used for history compaction. - pub compact_prompt: Option, - - /// Optional commit attribution text for commit message co-author trailers. - /// - /// Set to an empty string to disable automatic commit attribution. - pub commit_attribution: Option, - - /// When set, restricts ChatGPT login to a specific workspace identifier. - #[serde(default)] - pub forced_chatgpt_workspace_id: Option, - - /// When set, restricts the login mechanism users may use. - #[serde(default)] - pub forced_login_method: Option, - - /// 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. - #[serde(default)] - pub cli_auth_credentials_store: Option, - - /// Definition for MCP servers that Codex can reach out to for tool calls. - #[serde(default)] - // Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`. - #[schemars(schema_with = "crate::config::schema::mcp_servers_schema")] - pub mcp_servers: HashMap, - - /// Preferred backend for storing MCP OAuth credentials. - /// keyring: Use an OS-specific keyring service. - /// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2 - /// file: Use a file in the Codex home directory. - /// auto (default): Use the OS-specific keyring service if available, otherwise use a file. - #[serde(default)] - pub mcp_oauth_credentials_store: Option, - - /// Optional fixed port for the local HTTP callback server used during MCP OAuth login. - /// When unset, Codex will bind to an ephemeral port chosen by the OS. - pub mcp_oauth_callback_port: Option, - - /// Optional redirect URI to use during MCP OAuth login. - /// When set, this URI is used in the OAuth authorization request instead - /// of the local listener address. The local callback listener still binds - /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided). - pub mcp_oauth_callback_url: Option, - - /// User-defined provider entries that extend the built-in list. Built-in - /// IDs cannot be overridden. - #[serde(default, deserialize_with = "deserialize_model_providers")] - pub model_providers: HashMap, - - /// Maximum number of bytes to include from an AGENTS.md project doc file. - pub project_doc_max_bytes: Option, - - /// Ordered list of fallback filenames to look for when AGENTS.md is missing. - pub project_doc_fallback_filenames: Option>, - - /// Token budget applied when storing tool/function outputs in the context manager. - pub tool_output_token_limit: Option, - - /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds. - /// Default: `300000` (5 minutes). - pub background_terminal_max_timeout: Option, - - /// Optional absolute path to the Node runtime used by `js_repl`. - pub js_repl_node_path: Option, - - /// Ordered list of directories to search for Node modules in `js_repl`. - pub js_repl_node_module_dirs: Option>, - - /// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution. - pub zsh_path: Option, - - /// Profile to use from the `profiles` map. - pub profile: Option, - - /// Named profiles to facilitate switching between different configurations. - #[serde(default)] - pub profiles: HashMap, - - /// Settings that govern if and what will be written to `~/.codex/history.jsonl`. - #[serde(default)] - pub history: Option, - - /// Directory where Codex stores the SQLite state DB. - /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`. - pub sqlite_home: Option, - - /// Directory where Codex writes log files, for example `codex-tui.log`. - /// Defaults to `$CODEX_HOME/log`. - pub log_dir: Option, - - /// Optional URI-based file opener. If set, citations to files in the model - /// output will be hyperlinked using the specified URI scheme. - pub file_opener: Option, - - /// Collection of settings that are specific to the TUI. - pub tui: Option, - - /// When set to `true`, `AgentReasoning` events will be hidden from the - /// UI/output. Defaults to `false`. - pub hide_agent_reasoning: Option, - - /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. - /// Defaults to `false`. - pub show_raw_agent_reasoning: Option, - - pub model_reasoning_effort: Option, - pub plan_mode_reasoning_effort: Option, - pub model_reasoning_summary: Option, - /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). - pub model_verbosity: Option, - - /// Override to force-enable reasoning summaries for the configured model. - pub model_supports_reasoning_summaries: Option, - - /// Optional path to a JSON model catalog (applied on startup only). - /// Per-thread `config` overrides are accepted but do not reapply this (no-ops). - pub model_catalog_json: Option, - - /// Optionally specify a personality for the model - pub personality: Option, - - /// Optional explicit service tier preference for new turns (`fast` or `flex`). - pub service_tier: Option, - - /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). - pub chatgpt_base_url: Option, - - /// Base URL override for the built-in `openai` model provider. - pub openai_base_url: Option, - - /// Machine-local realtime audio device preferences used by realtime voice. - #[serde(default)] - pub audio: Option, - - /// Experimental / do not use. Overrides only the realtime conversation - /// websocket transport base URL (the `Op::RealtimeConversation` - /// `/v1/realtime` - /// connection) without changing normal provider HTTP requests. - pub experimental_realtime_ws_base_url: Option, - /// Experimental / do not use. Selects the realtime websocket model/snapshot - /// used for the `Op::RealtimeConversation` connection. - pub experimental_realtime_ws_model: Option, - /// Experimental / do not use. Realtime websocket session selection. - /// `version` controls v1/v2 and `type` controls conversational/transcription. - #[serde(default)] - pub realtime: Option, - /// Experimental / do not use. Overrides only the realtime conversation - /// websocket transport instructions (the `Op::RealtimeConversation` - /// `/ws` session.update instructions) without changing normal prompts. - pub experimental_realtime_ws_backend_prompt: Option, - /// Experimental / do not use. Replaces the synthesized realtime startup - /// context appended to websocket session instructions. An empty string - /// disables startup context injection entirely. - pub experimental_realtime_ws_startup_context: Option, - /// Experimental / do not use. Replaces the built-in realtime start - /// instructions inserted into developer messages when realtime becomes - /// active. - pub experimental_realtime_start_instructions: Option, - pub projects: Option>, - - /// Controls the web search tool mode: disabled, cached, or live. - pub web_search: Option, - - /// Nested tools section for feature toggles - pub tools: Option, - - /// Additional discoverable tools that can be suggested for installation. - pub tool_suggest: Option, - - /// Agent-related settings (thread limits, etc.). - pub agents: Option, - - /// Memories subsystem settings. - pub memories: Option, - - /// User-level skill config entries keyed by SKILL.md path. - pub skills: Option, - - /// User-level plugin config entries keyed by plugin name. - #[serde(default)] - pub plugins: HashMap, - - /// Centralized feature flags (new). Prefer this over individual toggles. - #[serde(default)] - // Injects known feature keys into the schema and forbids unknown keys. - #[schemars(schema_with = "crate::config::schema::features_schema")] - pub features: Option, - - /// Suppress warnings about unstable (under development) features. - pub suppress_unstable_features_warning: Option, - - /// Settings for ghost snapshots (used for undo). - #[serde(default)] - pub ghost_snapshot: Option, - - /// Markers used to detect the project root when searching parent - /// directories for `.codex` folders. Defaults to [".git"] when unset. - #[serde(default)] - pub project_root_markers: Option>, - - /// When `true`, checks for Codex updates on startup and surfaces update prompts. - /// Set to `false` only if your Codex updates are centrally managed. - /// Defaults to `true`. - pub check_for_update_on_startup: Option, - - /// When true, disables burst-paste detection for typed input entirely. - /// All characters are inserted as they are received, and no buffering - /// or placeholder replacement will occur for fast keypress bursts. - pub disable_paste_burst: Option, - - /// When `false`, disables analytics across Codex product surfaces in this machine. - /// Defaults to `true`. - pub analytics: Option, - - /// When `false`, disables feedback collection across Codex product surfaces. - /// Defaults to `true`. - pub feedback: Option, - - /// Settings for app-specific controls. - #[serde(default)] - pub apps: Option, - - /// OTEL configuration. - pub otel: Option, - - /// Windows-specific configuration. - #[serde(default)] - pub windows: Option, - - /// Tracks whether the Windows onboarding screen has been acknowledged. - pub windows_wsl_setup_acknowledged: Option, - - /// Collection of in-product notices (different from notifications) - /// See [`codex_config::types::Notice`] for more details - pub notice: Option, - - /// Legacy, now use features - /// Deprecated: ignored. Use `model_instructions_file`. - #[schemars(skip)] - pub experimental_instructions_file: Option, - pub experimental_compact_prompt_file: Option, - pub experimental_use_unified_exec_tool: Option, - pub experimental_use_freeform_apply_patch: Option, - /// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama". - pub oss_provider: Option, -} - -impl From for UserSavedConfig { - fn from(config_toml: ConfigToml) -> Self { - let profiles = config_toml - .profiles - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect(); - - Self { - approval_policy: config_toml.approval_policy, - sandbox_mode: config_toml.sandbox_mode, - sandbox_settings: config_toml.sandbox_workspace_write.map(From::from), - forced_chatgpt_workspace_id: config_toml.forced_chatgpt_workspace_id, - forced_login_method: config_toml.forced_login_method, - model: config_toml.model, - model_reasoning_effort: config_toml.model_reasoning_effort, - model_reasoning_summary: config_toml.model_reasoning_summary, - model_verbosity: config_toml.model_verbosity, - tools: config_toml.tools.map(From::from), - profile: config_toml.profile, - profiles, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct ProjectConfig { - pub trust_level: Option, -} - -impl ProjectConfig { - pub fn is_trusted(&self) -> bool { - matches!(self.trust_level, Some(TrustLevel::Trusted)) - } - - pub fn is_untrusted(&self) -> bool { - matches!(self.trust_level, Some(TrustLevel::Untrusted)) - } -} - #[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct RealtimeAudioConfig { - pub microphone: Option, - pub speaker: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum RealtimeWsMode { - #[default] - Conversational, - Transcription, -} - -pub use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion; - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct RealtimeConfig { - pub version: RealtimeWsVersion, - #[serde(rename = "type")] - pub session_type: RealtimeWsMode, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct RealtimeToml { - pub version: Option, - #[serde(rename = "type")] - pub session_type: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct RealtimeAudioToml { - pub microphone: Option, - pub speaker: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct ToolsToml { - #[serde( - default, - deserialize_with = "deserialize_optional_web_search_tool_config" - )] - pub web_search: Option, - - /// Enable the `view_image` tool that lets the agent attach local images. - #[serde(default)] - pub view_image: Option, -} - -#[derive(Deserialize)] -#[serde(untagged)] -enum WebSearchToolConfigInput { - Enabled(bool), - Config(WebSearchToolConfig), -} - -fn deserialize_optional_web_search_tool_config<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = Option::::deserialize(deserializer)?; - - Ok(match value { - None => None, - Some(WebSearchToolConfigInput::Enabled(enabled)) => { - let _ = enabled; - None - } - Some(WebSearchToolConfigInput::Config(config)) => Some(config), - }) +pub struct AgentRoleConfig { + /// Human-facing role documentation used in spawn tool guidance. + /// Required for loaded user-defined roles after deprecated/new metadata precedence resolves. + pub description: Option, + /// Path to a role-specific config layer. + pub config_file: Option, + /// Candidate nicknames for agents spawned with this role. + pub nickname_candidates: Option>, } fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig { @@ -1618,218 +1132,6 @@ fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig { ToolSuggestConfig { discoverables } } -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct AgentsToml { - /// Maximum number of agent threads that can be open concurrently. - /// When unset, no limit is enforced. - #[schemars(range(min = 1))] - pub max_threads: Option, - /// Maximum nesting depth allowed for spawned agent threads. - /// Root sessions start at depth 0. - #[schemars(range(min = 1))] - pub max_depth: Option, - /// Default maximum runtime in seconds for agent job workers. - #[schemars(range(min = 1))] - pub job_max_runtime_seconds: Option, - - /// User-defined role declarations keyed by role name. - /// - /// Example: - /// ```toml - /// [agents.researcher] - /// description = "Research-focused role." - /// config_file = "./agents/researcher.toml" - /// nickname_candidates = ["Herodotus", "Ibn Battuta"] - /// ``` - #[serde(default, flatten)] - pub roles: BTreeMap, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct AgentRoleConfig { - /// Human-facing role documentation used in spawn tool guidance. - /// Required for loaded user-defined roles after deprecated/new metadata precedence resolves. - pub description: Option, - /// Path to a role-specific config layer. - pub config_file: Option, - /// Candidate nicknames for agents spawned with this role. - pub nickname_candidates: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct AgentRoleToml { - /// Human-facing role documentation used in spawn tool guidance. - /// Required unless supplied by the referenced agent role file. - pub description: Option, - - /// Path to a role-specific config layer. - /// Relative paths are resolved relative to the `config.toml` that defines them. - pub config_file: Option, - - /// Candidate nicknames for agents spawned with this role. - pub nickname_candidates: Option>, -} - -impl From for Tools { - fn from(tools_toml: ToolsToml) -> Self { - Self { - web_search: tools_toml.web_search.is_some().then_some(true), - view_image: tools_toml.view_image, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct GhostSnapshotToml { - /// Exclude untracked files larger than this many bytes from ghost snapshots. - #[serde(alias = "ignore_untracked_files_over_bytes")] - pub ignore_large_untracked_files: Option, - /// Ignore untracked directories that contain this many files or more. - /// (Still emits a warning unless warnings are disabled.) - #[serde(alias = "large_untracked_dir_warning_threshold")] - pub ignore_large_untracked_dirs: Option, - /// Disable all ghost snapshot warning events. - pub disable_warnings: Option, -} - -impl ConfigToml { - /// Derive the effective sandbox policy from the configuration. - fn derive_sandbox_policy( - &self, - sandbox_mode_override: Option, - profile_sandbox_mode: Option, - windows_sandbox_level: WindowsSandboxLevel, - resolved_cwd: &Path, - sandbox_policy_constraint: Option<&Constrained>, - ) -> SandboxPolicy { - let sandbox_mode_was_explicit = sandbox_mode_override.is_some() - || profile_sandbox_mode.is_some() - || self.sandbox_mode.is_some(); - let resolved_sandbox_mode = sandbox_mode_override - .or(profile_sandbox_mode) - .or(self.sandbox_mode) - .or_else(|| { - // If no sandbox_mode is set but this directory has a trust decision, - // default to workspace-write except on unsandboxed Windows where we - // default to read-only. - self.get_active_project(resolved_cwd).and_then(|p| { - if p.is_trusted() || p.is_untrusted() { - if cfg!(target_os = "windows") - && windows_sandbox_level - == codex_protocol::config_types::WindowsSandboxLevel::Disabled - { - Some(SandboxMode::ReadOnly) - } else { - Some(SandboxMode::WorkspaceWrite) - } - } else { - None - } - }) - }) - .unwrap_or_default(); - let mut sandbox_policy = match resolved_sandbox_mode { - SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), - SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { - Some(SandboxWorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - }) => SandboxPolicy::WorkspaceWrite { - writable_roots: writable_roots.clone(), - read_only_access: ReadOnlyAccess::FullAccess, - network_access: *network_access, - exclude_tmpdir_env_var: *exclude_tmpdir_env_var, - exclude_slash_tmp: *exclude_slash_tmp, - }, - None => SandboxPolicy::new_workspace_write_policy(), - }, - SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess, - }; - let downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| { - if cfg!(target_os = "windows") - // If the experimental Windows sandbox is enabled, do not force a downgrade. - && windows_sandbox_level - == codex_protocol::config_types::WindowsSandboxLevel::Disabled - && matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. }) - { - *policy = SandboxPolicy::new_read_only_policy(); - } - }; - if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) { - downgrade_workspace_write_if_unsupported(&mut sandbox_policy); - } - if !sandbox_mode_was_explicit - && let Some(constraint) = sandbox_policy_constraint - && let Err(err) = constraint.can_set(&sandbox_policy) - { - tracing::warn!( - error = %err, - "default sandbox policy is disallowed by requirements; falling back to required default" - ); - sandbox_policy = constraint.get().clone(); - downgrade_workspace_write_if_unsupported(&mut sandbox_policy); - } - sandbox_policy - } - - /// Resolves the cwd to an existing project, or returns None if ConfigToml - /// does not contain a project corresponding to cwd or a git repo for cwd - pub fn get_active_project(&self, resolved_cwd: &Path) -> Option { - let projects = self.projects.clone().unwrap_or_default(); - - let resolved_cwd_key = project_trust_key(resolved_cwd); - let resolved_cwd_raw_key = resolved_cwd.to_string_lossy().to_string(); - if let Some(project_config) = projects - .get(&resolved_cwd_key) - .or_else(|| projects.get(&resolved_cwd_raw_key)) - { - return Some(project_config.clone()); - } - - // If cwd lives inside a git repo/worktree, check whether the root git project - // (the primary repository working directory) is trusted. This lets - // worktrees inherit trust from the main project. - if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd) { - let repo_root_key = project_trust_key(repo_root.as_path()); - let repo_root_raw_key = repo_root.to_string_lossy().to_string(); - if let Some(project_config_for_root) = projects - .get(&repo_root_key) - .or_else(|| projects.get(&repo_root_raw_key)) - { - return Some(project_config_for_root.clone()); - } - } - - None - } - - pub fn get_config_profile( - &self, - override_profile: Option, - ) -> Result { - let profile = override_profile.or_else(|| self.profile.clone()); - - match profile { - Some(key) => { - if let Some(profile) = self.profiles.get(key.as_str()) { - return Ok(profile.clone()); - } - - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("config profile `{key}` not found"), - )) - } - None => Ok(ConfigProfile::default()), - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PermissionConfigSyntax { Legacy, @@ -1910,49 +1212,6 @@ pub struct ConfigOverrides { pub additional_writable_roots: Vec, } -fn validate_reserved_model_provider_ids( - model_providers: &HashMap, -) -> Result<(), String> { - let mut conflicts = model_providers - .keys() - .filter(|key| RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())) - .map(|key| format!("`{key}`")) - .collect::>(); - conflicts.sort_unstable(); - if conflicts.is_empty() { - Ok(()) - } else { - Err(format!( - "model_providers contains reserved built-in provider IDs: {}. \ -Built-in providers cannot be overridden. Rename your custom provider (for example, `openai-custom`).", - conflicts.join(", ") - )) - } -} - -fn validate_model_providers( - model_providers: &HashMap, -) -> Result<(), String> { - validate_reserved_model_provider_ids(model_providers)?; - for (key, provider) in model_providers { - provider - .validate() - .map_err(|message| format!("model_providers.{key}: {message}"))?; - } - Ok(()) -} - -fn deserialize_model_providers<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let model_providers = HashMap::::deserialize(deserializer)?; - validate_model_providers(&model_providers).map_err(serde::de::Error::custom)?; - Ok(model_providers) -} - /// Resolves the OSS provider from CLI override, profile config, or global config. /// Returns `None` if no provider is configured at any level. pub fn resolve_oss_provider( diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 73dad1c73..f8284d4ae 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -1,256 +1,22 @@ use std::borrow::Cow; -use std::collections::BTreeMap; use std::io; use std::path::Component; use std::path::Path; use std::path::PathBuf; -use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission; -use codex_network_proxy::NetworkMode; +use codex_config::permissions_toml::FilesystemPermissionToml; +use codex_config::permissions_toml::NetworkToml; +use codex_config::permissions_toml::PermissionProfileToml; +use codex_config::permissions_toml::PermissionsToml; use codex_network_proxy::NetworkProxyConfig; +#[cfg(test)] use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; -use codex_network_proxy::normalize_host; -use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -pub struct PermissionsToml { - #[serde(flatten)] - pub entries: BTreeMap, -} - -impl PermissionsToml { - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct PermissionProfileToml { - pub filesystem: Option, - pub network: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -pub struct FilesystemPermissionsToml { - #[serde(flatten)] - pub entries: BTreeMap, -} - -impl FilesystemPermissionsToml { - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] -#[serde(untagged)] -pub enum FilesystemPermissionToml { - Access(FileSystemAccessMode), - Scoped(BTreeMap), -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -pub struct NetworkDomainPermissionsToml { - #[serde(flatten)] - pub entries: BTreeMap, -} - -impl NetworkDomainPermissionsToml { - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - #[cfg(test)] - pub(crate) fn allowed_domains(&self) -> Option> { - let allowed_domains: Vec = self - .entries - .iter() - .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Allow)) - .map(|(pattern, _)| pattern.clone()) - .collect(); - (!allowed_domains.is_empty()).then_some(allowed_domains) - } - - #[cfg(test)] - pub(crate) fn denied_domains(&self) -> Option> { - let denied_domains: Vec = self - .entries - .iter() - .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Deny)) - .map(|(pattern, _)| pattern.clone()) - .collect(); - (!denied_domains.is_empty()).then_some(denied_domains) - } -} - -#[derive( - Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema, -)] -#[serde(rename_all = "lowercase")] -pub enum NetworkDomainPermissionToml { - Allow, - Deny, -} - -impl std::fmt::Display for NetworkDomainPermissionToml { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let permission = match self { - Self::Allow => "allow", - Self::Deny => "deny", - }; - f.write_str(permission) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -pub struct NetworkUnixSocketPermissionsToml { - #[serde(flatten)] - pub entries: BTreeMap, -} - -impl NetworkUnixSocketPermissionsToml { - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub(crate) fn allow_unix_sockets(&self) -> Vec { - self.entries - .iter() - .filter(|(_, permission)| matches!(permission, NetworkUnixSocketPermissionToml::Allow)) - .map(|(path, _)| path.clone()) - .collect() - } -} - -#[derive( - Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, JsonSchema, -)] -#[serde(rename_all = "lowercase")] -pub enum NetworkUnixSocketPermissionToml { - Allow, - None, -} - -impl std::fmt::Display for NetworkUnixSocketPermissionToml { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let permission = match self { - Self::Allow => "allow", - Self::None => "none", - }; - f.write_str(permission) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct NetworkToml { - pub enabled: Option, - pub proxy_url: Option, - pub enable_socks5: Option, - pub socks_url: Option, - pub enable_socks5_udp: Option, - pub allow_upstream_proxy: Option, - pub dangerously_allow_non_loopback_proxy: Option, - pub dangerously_allow_all_unix_sockets: Option, - #[schemars(with = "Option")] - pub mode: Option, - pub domains: Option, - pub unix_sockets: Option, - pub allow_local_binding: Option, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "lowercase")] -enum NetworkModeSchema { - Limited, - Full, -} - -impl NetworkToml { - pub(crate) fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) { - if let Some(enabled) = self.enabled { - config.network.enabled = enabled; - } - if let Some(proxy_url) = self.proxy_url.as_ref() { - config.network.proxy_url = proxy_url.clone(); - } - if let Some(enable_socks5) = self.enable_socks5 { - config.network.enable_socks5 = enable_socks5; - } - if let Some(socks_url) = self.socks_url.as_ref() { - config.network.socks_url = socks_url.clone(); - } - if let Some(enable_socks5_udp) = self.enable_socks5_udp { - config.network.enable_socks5_udp = enable_socks5_udp; - } - if let Some(allow_upstream_proxy) = self.allow_upstream_proxy { - config.network.allow_upstream_proxy = allow_upstream_proxy; - } - if let Some(dangerously_allow_non_loopback_proxy) = - self.dangerously_allow_non_loopback_proxy - { - config.network.dangerously_allow_non_loopback_proxy = - dangerously_allow_non_loopback_proxy; - } - if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets { - config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets; - } - if let Some(mode) = self.mode { - config.network.mode = mode; - } - if let Some(domains) = self.domains.as_ref() { - overlay_network_domain_permissions(config, domains); - } - if let Some(unix_sockets) = self.unix_sockets.as_ref() { - let mut proxy_unix_sockets = config.network.unix_sockets.take().unwrap_or_default(); - for (path, permission) in &unix_sockets.entries { - let permission = match permission { - NetworkUnixSocketPermissionToml::Allow => { - ProxyNetworkUnixSocketPermission::Allow - } - NetworkUnixSocketPermissionToml::None => ProxyNetworkUnixSocketPermission::None, - }; - proxy_unix_sockets.entries.insert(path.clone(), permission); - } - config.network.unix_sockets = - (!proxy_unix_sockets.entries.is_empty()).then_some(proxy_unix_sockets); - } - if let Some(allow_local_binding) = self.allow_local_binding { - config.network.allow_local_binding = allow_local_binding; - } - } - - pub(crate) fn to_network_proxy_config(&self) -> NetworkProxyConfig { - let mut config = NetworkProxyConfig::default(); - self.apply_to_network_proxy_config(&mut config); - config - } -} - -pub(crate) fn overlay_network_domain_permissions( - config: &mut NetworkProxyConfig, - domains: &NetworkDomainPermissionsToml, -) { - for (pattern, permission) in &domains.entries { - let permission = match permission { - NetworkDomainPermissionToml::Allow => ProxyNetworkDomainPermission::Allow, - NetworkDomainPermissionToml::Deny => ProxyNetworkDomainPermission::Deny, - }; - config - .network - .upsert_domain_permission(pattern.clone(), permission, normalize_host); - } -} pub(crate) fn network_proxy_config_from_profile_network( network: Option<&NetworkToml>, diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index e3ea67d7b..e9191dbfa 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -1,7 +1,15 @@ use super::*; use crate::config::Config; use crate::config::ConfigOverrides; -use crate::config::ConfigToml; +use codex_config::config_toml::ConfigToml; +use codex_config::permissions_toml::FilesystemPermissionsToml; +use codex_config::permissions_toml::NetworkDomainPermissionToml; +use codex_config::permissions_toml::NetworkDomainPermissionsToml; +use codex_config::permissions_toml::NetworkToml; +use codex_config::permissions_toml::NetworkUnixSocketPermissionToml; +use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml; +use codex_config::permissions_toml::PermissionProfileToml; +use codex_config::permissions_toml::PermissionsToml; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::BTreeMap; diff --git a/codex-rs/core/src/config/schema.rs b/codex-rs/core/src/config/schema.rs index bde38f7eb..9507aff58 100644 --- a/codex-rs/core/src/config/schema.rs +++ b/codex-rs/core/src/config/schema.rs @@ -1,103 +1,6 @@ -use crate::config::ConfigToml; -use codex_config::types::RawMcpServerConfig; -use codex_features::FEATURES; -use codex_features::legacy_feature_keys; -use schemars::r#gen::SchemaGenerator; -use schemars::r#gen::SchemaSettings; -use schemars::schema::InstanceType; -use schemars::schema::ObjectValidation; -use schemars::schema::RootSchema; -use schemars::schema::Schema; -use schemars::schema::SchemaObject; -use serde_json::Map; -use serde_json::Value; -use std::path::Path; - -/// Schema for the `[features]` map with known + legacy keys only. -pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { - let mut object = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - - let mut validation = ObjectValidation::default(); - for feature in FEATURES { - if feature.id == codex_features::Feature::Artifact { - continue; - } - validation - .properties - .insert(feature.key.to_string(), schema_gen.subschema_for::()); - } - for legacy_key in legacy_feature_keys() { - validation - .properties - .insert(legacy_key.to_string(), schema_gen.subschema_for::()); - } - validation.additional_properties = Some(Box::new(Schema::Bool(false))); - object.object = Some(Box::new(validation)); - - Schema::Object(object) -} - -/// Schema for the `[mcp_servers]` map using the raw input shape. -pub(crate) fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema { - let mut object = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - - let validation = ObjectValidation { - additional_properties: Some(Box::new(schema_gen.subschema_for::())), - ..Default::default() - }; - object.object = Some(Box::new(validation)); - - Schema::Object(object) -} - -/// Build the config schema for `config.toml`. -pub fn config_schema() -> RootSchema { - SchemaSettings::draft07() - .with(|settings| { - settings.option_add_null_type = false; - }) - .into_generator() - .into_root_schema_for::() -} - -/// Canonicalize a JSON value by sorting its keys. -fn canonicalize(value: &Value) -> Value { - match value { - Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()), - Value::Object(map) => { - let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); - let mut sorted = Map::with_capacity(map.len()); - for (key, child) in entries { - sorted.insert(key.clone(), canonicalize(child)); - } - Value::Object(sorted) - } - _ => value.clone(), - } -} - -/// Render the config schema as pretty-printed JSON. -pub fn config_schema_json() -> anyhow::Result> { - let schema = config_schema(); - let value = serde_json::to_value(schema)?; - let value = canonicalize(&value); - let json = serde_json::to_vec_pretty(&value)?; - Ok(json) -} - -/// Write the config schema fixture to disk. -pub fn write_config_schema(out_path: &Path) -> anyhow::Result<()> { - let json = config_schema_json()?; - std::fs::write(out_path, json)?; - Ok(()) -} +use codex_config::schema::canonicalize; +use codex_config::schema::config_schema_json; +use codex_config::schema::write_config_schema; #[cfg(test)] #[path = "schema_tests.rs"] diff --git a/codex-rs/core/src/config/service.rs b/codex-rs/core/src/config/service.rs index e878270b8..d2426b138 100644 --- a/codex-rs/core/src/config/service.rs +++ b/codex-rs/core/src/config/service.rs @@ -1,4 +1,3 @@ -use super::ConfigToml; use super::deserialize_config_toml_with_base; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; @@ -29,6 +28,7 @@ use codex_app_server_protocol::MergeStrategy; use codex_app_server_protocol::OverriddenMetadata; use codex_app_server_protocol::WriteStatus; use codex_config::CONFIG_TOML_FILE; +use codex_config::config_toml::ConfigToml; use codex_utils_absolute_path::AbsolutePathBuf; use serde_json::Value as JsonValue; use std::borrow::Cow; diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 48555f0b9..8961c00aa 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -5,11 +5,12 @@ mod macos; #[cfg(test)] mod tests; -use crate::config::ConfigToml; use crate::config_loader::layer_io::LoadedConfigLayers; use codex_app_server_protocol::ConfigLayerSource; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigRequirementsWithSources; +use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ProjectConfig; use codex_git_utils::resolve_root_git_project_for_trust; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; @@ -544,7 +545,7 @@ struct ProjectTrustContext { #[derive(Deserialize)] struct ProjectTrustConfigToml { - projects: Option>, + projects: Option>, } struct ProjectTrustDecision { diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index ea782922b..7978f37ec 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -2,9 +2,7 @@ use super::LoaderOverrides; use super::load_config_layers_state; use crate::config::ConfigBuilder; use crate::config::ConfigOverrides; -use crate::config::ConfigToml; use crate::config::ConstraintError; -use crate::config::ProjectConfig; use crate::config_loader::CloudRequirementsLoadError; use crate::config_loader::CloudRequirementsLoader; use crate::config_loader::ConfigLayerEntry; @@ -16,6 +14,8 @@ use crate::config_loader::RequirementSource; use crate::config_loader::load_requirements_toml; use crate::config_loader::version_for_toml; use codex_config::CONFIG_TOML_FILE; +use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ProjectConfig; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::WebSearchMode; use codex_protocol::protocol::AskForApproval; diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 66ba6a406..334e160f1 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -146,7 +146,10 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools( let auth_manager = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); let auth = auth_manager.auth().await; - if !config.features.apps_enabled_for_auth(auth.as_ref()) { + if !config + .features + .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth)) + { return Some(Vec::new()); } let cache_key = accessible_connectors_cache_key(config, auth.as_ref()); @@ -186,7 +189,10 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( let auth_manager = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); let auth = auth_manager.auth().await; - if !config.features.apps_enabled_for_auth(auth.as_ref()) { + if !config + .features + .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth)) + { return Ok(AccessibleConnectorsStatus { connectors: Vec::new(), codex_apps_ready: true, diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 17813ee7d..1cd732f03 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -3,7 +3,6 @@ use crate::codex::Session; use crate::codex::TurnContext; use crate::config::Config; use crate::config::ConfigOverrides; -use crate::config::ConfigToml; use crate::config::Constrained; use crate::config::ManagedFeatures; use crate::config::NetworkProxySpec; @@ -16,6 +15,7 @@ use crate::config_loader::NetworkDomainPermissionsToml; use crate::config_loader::RequirementSource; use crate::config_loader::Sourced; use crate::test_support; +use codex_config::config_toml::ConfigToml; use codex_network_proxy::NetworkProxyConfig; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::config_types::ApprovalsReviewer; diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 5bb919485..b313a43e7 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -2,9 +2,9 @@ use super::*; use crate::codex::make_session_and_context; use crate::codex::make_session_and_context_with_rx; use crate::config::ConfigBuilder; -use crate::config::ConfigToml; use crate::state::ActiveTurn; use codex_config::CONFIG_TOML_FILE; +use codex_config::config_toml::ConfigToml; use codex_config::types::AppConfig; use codex_config::types::AppToolConfig; use codex_config::types::AppToolsConfig; diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs index a6740b05f..5218256e2 100644 --- a/codex-rs/core/src/network_proxy_loader.rs +++ b/codex-rs/core/src/network_proxy_loader.rs @@ -1,7 +1,4 @@ -use crate::config::NetworkToml; -use crate::config::PermissionsToml; use crate::config::find_codex_home; -use crate::config::overlay_network_domain_permissions; use crate::config::resolve_permission_profile; use crate::config_loader::CloudRequirementsLoader; use crate::config_loader::ConfigLayerStack; @@ -16,6 +13,9 @@ use anyhow::Result; use async_trait::async_trait; use codex_app_server_protocol::ConfigLayerSource; use codex_config::CONFIG_TOML_FILE; +use codex_config::permissions_toml::NetworkToml; +use codex_config::permissions_toml::PermissionsToml; +use codex_config::permissions_toml::overlay_network_domain_permissions; use codex_network_proxy::ConfigReloader; use codex_network_proxy::ConfigState; use codex_network_proxy::NetworkProxyConfig; diff --git a/codex-rs/core/src/personality_migration.rs b/codex-rs/core/src/personality_migration.rs index 8a3786e79..52cabf55d 100644 --- a/codex-rs/core/src/personality_migration.rs +++ b/codex-rs/core/src/personality_migration.rs @@ -1,4 +1,3 @@ -use crate::config::ConfigToml; use crate::config::edit::ConfigEditsBuilder; use crate::rollout::ARCHIVED_SESSIONS_SUBDIR; use crate::rollout::SESSIONS_SUBDIR; @@ -6,6 +5,7 @@ use crate::rollout::list::ThreadListConfig; use crate::rollout::list::ThreadListLayout; use crate::rollout::list::ThreadSortKey; use crate::rollout::list::get_threads_in_root; +use codex_config::config_toml::ConfigToml; use codex_protocol::config_types::Personality; use codex_protocol::protocol::SessionSource; use codex_rollout::state_db; diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index dae8f5c5c..4eb427804 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -1,6 +1,4 @@ use crate::codex::Session; -use crate::config::RealtimeWsMode; -use crate::config::RealtimeWsVersion; use crate::realtime_context::build_realtime_startup_context; use async_channel::Receiver; use async_channel::Sender; @@ -18,6 +16,8 @@ use codex_api::api_bridge::map_api_error; use codex_api::endpoint::realtime_websocket::RealtimeWebsocketEvents; use codex_api::endpoint::realtime_websocket::RealtimeWebsocketWriter; use codex_app_server_protocol::AuthMode; +use codex_config::config_toml::RealtimeWsMode; +use codex_config::config_toml::RealtimeWsVersion; use codex_login::CodexAuth; use codex_login::default_client::default_headers; use codex_login::read_openai_api_key_from_env; diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 0def23d00..789b1ce34 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -1,7 +1,7 @@ use crate::config::Config; -use crate::config::ConfigToml; use crate::config::edit::ConfigEditsBuilder; -use crate::config::profile::ConfigProfile; +use codex_config::config_toml::ConfigToml; +use codex_config::profile_toml::ConfigProfile; use codex_config::types::WindowsSandboxModeToml; use codex_features::Feature; use codex_features::Features; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 0148d62bf..8e719460e 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,10 +1,10 @@ +use codex_config::types::AuthCredentialsStoreMode; use codex_core::ModelClient; use codex_core::NewThread; use codex_core::Prompt; use codex_core::ResponseEvent; use codex_core::ThreadManager; use codex_features::Feature; -use codex_login::AuthCredentialsStoreMode; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::default_client::originator; diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 663cf4748..6ab001383 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use std::time::Duration; use anyhow::Result; -use codex_core::config::ProjectConfig; +use codex_config::config_toml::ProjectConfig; use codex_protocol::config_types::TrustLevel; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; diff --git a/codex-rs/core/tests/suite/personality_migration.rs b/codex-rs/core/tests/suite/personality_migration.rs index 0a8dd61d9..3f19d55ef 100644 --- a/codex-rs/core/tests/suite/personality_migration.rs +++ b/codex-rs/core/tests/suite/personality_migration.rs @@ -1,6 +1,6 @@ +use codex_config::config_toml::ConfigToml; use codex_core::ARCHIVED_SESSIONS_SUBDIR; use codex_core::SESSIONS_SUBDIR; -use codex_core::config::ConfigToml; use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME; use codex_core::personality_migration::PersonalityMigrationStatus; use codex_core::personality_migration::maybe_migrate_personality; diff --git a/codex-rs/features/Cargo.toml b/codex-rs/features/Cargo.toml index add5296d8..95be6c400 100644 --- a/codex-rs/features/Cargo.toml +++ b/codex-rs/features/Cargo.toml @@ -13,7 +13,6 @@ path = "src/lib.rs" workspace = true [dependencies] -codex-login = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } schemars = { workspace = true } diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index b022243fd..f49e0edc0 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -3,8 +3,6 @@ //! This crate defines the feature registry plus the logic used to resolve an //! effective feature set from config-like inputs. -use codex_login::AuthManager; -use codex_login::CodexAuth; use codex_otel::SessionTelemetry; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -275,25 +273,8 @@ impl Features { self.enabled.contains(&f) } - pub async fn apps_enabled(&self, auth_manager: Option<&AuthManager>) -> bool { - if !self.enabled(Feature::Apps) { - return false; - } - - let auth = match auth_manager { - Some(auth_manager) => auth_manager.auth().await, - None => None, - }; - self.apps_enabled_for_auth(auth.as_ref()) - } - - pub fn apps_enabled_cached(&self, auth_manager: Option<&AuthManager>) -> bool { - let auth = auth_manager.and_then(AuthManager::auth_cached); - self.apps_enabled_for_auth(auth.as_ref()) - } - - pub fn apps_enabled_for_auth(&self, auth: Option<&CodexAuth>) -> bool { - self.enabled(Feature::Apps) && auth.is_some_and(CodexAuth::is_chatgpt_auth) + pub fn apps_enabled_for_auth(&self, has_chatgpt_auth: bool) -> bool { + self.enabled(Feature::Apps) && has_chatgpt_auth } pub fn use_legacy_landlock(&self) -> bool { diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 9df9d78ba..23653fadc 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -198,16 +198,11 @@ fn enable_fanout_normalization_enables_multi_agent_one_way() { #[test] fn apps_require_feature_flag_and_chatgpt_auth() { let mut features = Features::with_defaults(); - assert!(!features.apps_enabled_for_auth(/*auth*/ None)); + assert!(!features.apps_enabled_for_auth(/*has_chatgpt_auth*/ false)); features.enable(Feature::Apps); - assert!(!features.apps_enabled_for_auth(/*auth*/ None)); - - let api_key_auth = codex_login::CodexAuth::from_api_key("test-api-key"); - assert!(!features.apps_enabled_for_auth(Some(&api_key_auth))); - - let chatgpt_auth = codex_login::CodexAuth::create_dummy_chatgpt_auth_for_testing(); - assert!(features.apps_enabled_for_auth(Some(&chatgpt_auth))); + assert!(!features.apps_enabled_for_auth(/*has_chatgpt_auth*/ false)); + assert!(features.apps_enabled_for_auth(/*has_chatgpt_auth*/ true)); } #[test] diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index 524c574fb..751d6d5d1 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -17,8 +17,6 @@ workspace = true [target.'cfg(target_os = "linux")'.dependencies] clap = { workspace = true, features = ["derive"] } -codex-config = { workspace = true } -codex-core = { workspace = true } codex-protocol = { workspace = true } codex-sandboxing = { workspace = true } codex-utils-absolute-path = { workspace = true } @@ -30,6 +28,8 @@ serde_json = { workspace = true } url = { workspace = true } [target.'cfg(target_os = "linux")'.dev-dependencies] +codex-config = { workspace = true } +codex-core = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = [ diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 459d76c38..9c2d021c7 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -25,7 +25,6 @@ once_cell = { workspace = true } os_info = { workspace = true } rand = { workspace = true } reqwest = { workspace = true, features = ["json", "blocking"] } -schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index f0b31f0f4..71857c970 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -20,7 +20,6 @@ use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ModelProviderAuthInfo; use super::external_bearer::BearerTokenRefresher; -pub use crate::auth::storage::AuthCredentialsStoreMode; pub use crate::auth::storage::AuthDotJson; use crate::auth::storage::AuthStorageBackend; use crate::auth::storage::create_auth_storage; @@ -30,6 +29,7 @@ use crate::token_data::TokenData; use crate::token_data::parse_chatgpt_jwt_claims; use crate::token_data::parse_jwt_expiration; use codex_client::CodexHttpClient; +use codex_config::types::AuthCredentialsStoreMode; use codex_protocol::account::PlanType as AccountPlanType; use codex_protocol::auth::KnownPlan as InternalKnownPlan; use codex_protocol::auth::PlanType as InternalPlanType; diff --git a/codex-rs/login/src/auth/storage.rs b/codex-rs/login/src/auth/storage.rs index b1e04b868..97e801415 100644 --- a/codex-rs/login/src/auth/storage.rs +++ b/codex-rs/login/src/auth/storage.rs @@ -1,6 +1,5 @@ use chrono::DateTime; use chrono::Utc; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use sha2::Digest; @@ -21,25 +20,11 @@ use tracing::warn; use crate::token_data::TokenData; use codex_app_server_protocol::AuthMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_keyring_store::DefaultKeyringStore; use codex_keyring_store::KeyringStore; use once_cell::sync::Lazy; -/// Determine where Codex should store CLI auth credentials. -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum AuthCredentialsStoreMode { - #[default] - /// 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, -} - /// Expected structure for $CODEX_HOME/auth.json. #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] pub struct AuthDotJson { diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 247e5a876..f786884b3 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -20,7 +20,6 @@ pub use server::run_login_server; pub use api_bridge::auth_provider_from_auth; pub use auth::AuthConfig; -pub use auth::AuthCredentialsStoreMode; pub use auth::AuthDotJson; pub use auth::AuthManager; pub use auth::AuthManagerConfig; diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index c811fa36d..169a8a309 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -24,7 +24,6 @@ use std::sync::LazyLock; use std::thread; use std::time::Duration; -use crate::auth::AuthCredentialsStoreMode; use crate::auth::AuthDotJson; use crate::auth::save_auth; use crate::default_client::originator; @@ -36,6 +35,7 @@ use base64::Engine; use chrono::Utc; use codex_app_server_protocol::AuthMode; use codex_client::build_reqwest_client_with_custom_ca; +use codex_config::types::AuthCredentialsStoreMode; use codex_utils_template::Template; use rand::RngCore; use serde_json::Value as JsonValue; diff --git a/codex-rs/login/tests/suite/auth_refresh.rs b/codex-rs/login/tests/suite/auth_refresh.rs index 94ba8220e..bf9e03bc2 100644 --- a/codex-rs/login/tests/suite/auth_refresh.rs +++ b/codex-rs/login/tests/suite/auth_refresh.rs @@ -4,7 +4,7 @@ use base64::Engine; use chrono::Duration; use chrono::Utc; use codex_app_server_protocol::AuthMode; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthDotJson; use codex_login::AuthManager; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; diff --git a/codex-rs/login/tests/suite/device_code_login.rs b/codex-rs/login/tests/suite/device_code_login.rs index 80c4fc0e5..bed94c700 100644 --- a/codex-rs/login/tests/suite/device_code_login.rs +++ b/codex-rs/login/tests/suite/device_code_login.rs @@ -3,8 +3,8 @@ use anyhow::Context; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::ServerOptions; -use codex_login::auth::AuthCredentialsStoreMode; use codex_login::auth::load_auth_dot_json; use codex_login::run_device_code_login; use serde_json::json; diff --git a/codex-rs/login/tests/suite/login_server_e2e.rs b/codex-rs/login/tests/suite/login_server_e2e.rs index 5b0ddd9b7..9522f5b0b 100644 --- a/codex-rs/login/tests/suite/login_server_e2e.rs +++ b/codex-rs/login/tests/suite/login_server_e2e.rs @@ -7,8 +7,8 @@ use std::time::Duration; use anyhow::Result; use base64::Engine; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::ServerOptions; -use codex_login::auth::AuthCredentialsStoreMode; use codex_login::run_login_server; use core_test_support::skip_if_no_network; use tempfile::tempdir; diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 29de014d4..9deadea81 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -25,7 +25,6 @@ codex-features = { workspace = true } codex-login = { workspace = true } codex-models-manager = { workspace = true } codex-protocol = { workspace = true } -codex-shell-command = { workspace = true } codex-utils-cli = { workspace = true } codex-utils-json-to-toml = { workspace = true } rmcp = { workspace = true } @@ -44,6 +43,7 @@ tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } [dev-dependencies] +codex-shell-command = { workspace = true } core_test_support = { workspace = true } mcp_test_support = { workspace = true } os_info = { workspace = true } diff --git a/codex-rs/models-manager/Cargo.toml b/codex-rs/models-manager/Cargo.toml index fbd5d5f3e..58eff2437 100644 --- a/codex-rs/models-manager/Cargo.toml +++ b/codex-rs/models-manager/Cargo.toml @@ -17,6 +17,7 @@ chrono = { workspace = true, features = ["serde"] } codex-api = { workspace = true } codex-app-server-protocol = { workspace = true } codex-collaboration-mode-templates = { workspace = true } +codex-config = { workspace = true } codex-feedback = { workspace = true } codex-login = { workspace = true } codex-model-provider-info = { workspace = true } diff --git a/codex-rs/models-manager/src/lib.rs b/codex-rs/models-manager/src/lib.rs index 5f26b37f8..a9c0d489d 100644 --- a/codex-rs/models-manager/src/lib.rs +++ b/codex-rs/models-manager/src/lib.rs @@ -6,7 +6,6 @@ pub mod model_info; pub mod model_presets; pub use codex_app_server_protocol::AuthMode; -pub use codex_login::AuthCredentialsStoreMode; pub use codex_login::AuthManager; pub use codex_login::CodexAuth; pub use codex_model_provider_info::ModelProviderInfo; diff --git a/codex-rs/models-manager/src/manager_tests.rs b/codex-rs/models-manager/src/manager_tests.rs index b955786cc..23de81781 100644 --- a/codex-rs/models-manager/src/manager_tests.rs +++ b/codex-rs/models-manager/src/manager_tests.rs @@ -3,7 +3,7 @@ use crate::ModelsManagerConfig; use base64::Engine as _; use chrono::Utc; use codex_api::TransportError; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_model_provider_info::WireApi; diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index 4b20e9d6e..aa5ab5eee 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -14,6 +14,7 @@ axum = { workspace = true, default-features = false, features = [ "tokio", ] } codex-client = { workspace = true } +codex-config = { workspace = true } codex-keyring-store = { workspace = true } codex-protocol = { workspace = true } codex-utils-pty = { workspace = true } @@ -37,7 +38,6 @@ rmcp = { workspace = true, default-features = false, features = [ "transport-streamable-http-client-reqwest", "transport-streamable-http-server", ] } -schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/codex-rs/rmcp-client/src/auth_status.rs b/codex-rs/rmcp-client/src/auth_status.rs index f97898548..0b3b3bf6a 100644 --- a/codex-rs/rmcp-client/src/auth_status.rs +++ b/codex-rs/rmcp-client/src/auth_status.rs @@ -12,10 +12,10 @@ use reqwest::header::HeaderMap; use serde::Deserialize; use tracing::debug; -use crate::OAuthCredentialsStoreMode; use crate::oauth::has_oauth_tokens; use crate::utils::apply_default_headers; use crate::utils::build_default_headers; +use codex_config::types::OAuthCredentialsStoreMode; const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5); const OAUTH_DISCOVERY_HEADER: &str = "MCP-Protocol-Version"; diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 627b9f2e7..65b9cf8e2 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -11,7 +11,6 @@ pub use auth_status::determine_streamable_http_auth_status; pub use auth_status::discover_streamable_http_oauth; pub use auth_status::supports_oauth_login; pub use codex_protocol::protocol::McpAuthStatus; -pub use oauth::OAuthCredentialsStoreMode; pub use oauth::StoredOAuthTokens; pub use oauth::WrappedOAuthTokenResponse; pub use oauth::delete_oauth_tokens; diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs index cdb64ff15..ddabffe29 100644 --- a/codex-rs/rmcp-client/src/oauth.rs +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -19,6 +19,7 @@ use anyhow::Context; use anyhow::Error; use anyhow::Result; +use codex_config::types::OAuthCredentialsStoreMode; use oauth2::AccessToken; use oauth2::EmptyExtraTokenFields; use oauth2::RefreshToken; @@ -26,7 +27,6 @@ use oauth2::Scope; use oauth2::TokenResponse; use oauth2::basic::BasicTokenType; use rmcp::transport::auth::OAuthTokenResponse; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value; @@ -63,21 +63,6 @@ pub struct StoredOAuthTokens { pub expires_at: Option, } -/// Determine where Codex should store and read MCP credentials. -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum OAuthCredentialsStoreMode { - /// `Keyring` when available; otherwise, `File`. - /// Credentials stored in the keyring will only be readable by Codex unless the user explicitly grants access via OS-level keyring access. - #[default] - Auto, - /// CODEX_HOME/.credentials.json - /// This file will be readable to Codex and other applications running as the same user. - File, - /// Keyring when available, otherwise fail. - Keyring, -} - /// Wrap OAuthTokenResponse to allow for partial equality comparison. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WrappedOAuthTokenResponse(pub OAuthTokenResponse); diff --git a/codex-rs/rmcp-client/src/perform_oauth_login.rs b/codex-rs/rmcp-client/src/perform_oauth_login.rs index 821fbf275..5bdb31538 100644 --- a/codex-rs/rmcp-client/src/perform_oauth_login.rs +++ b/codex-rs/rmcp-client/src/perform_oauth_login.rs @@ -16,13 +16,13 @@ use tokio::sync::oneshot; use tokio::time::timeout; use urlencoding::decode; -use crate::OAuthCredentialsStoreMode; use crate::StoredOAuthTokens; use crate::WrappedOAuthTokenResponse; use crate::oauth::compute_expires_at_millis; use crate::save_oauth_tokens; use crate::utils::apply_default_headers; use crate::utils::build_default_headers; +use codex_config::types::OAuthCredentialsStoreMode; struct OauthHeaders { http_headers: Option>, diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index aa460c21b..d3316a049 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -69,13 +69,13 @@ use tracing::warn; use crate::load_oauth_tokens; use crate::logging_client_handler::LoggingClientHandler; -use crate::oauth::OAuthCredentialsStoreMode; use crate::oauth::OAuthPersistor; use crate::oauth::StoredOAuthTokens; use crate::program_resolver; use crate::utils::apply_default_headers; use crate::utils::build_default_headers; use crate::utils::create_env_for_mcp_server; +use codex_config::types::OAuthCredentialsStoreMode; const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream"; const JSON_MIME_TYPE: &str = "application/json"; diff --git a/codex-rs/rmcp-client/tests/streamable_http_recovery.rs b/codex-rs/rmcp-client/tests/streamable_http_recovery.rs index 6a75582c9..c0525aafa 100644 --- a/codex-rs/rmcp-client/tests/streamable_http_recovery.rs +++ b/codex-rs/rmcp-client/tests/streamable_http_recovery.rs @@ -3,9 +3,9 @@ use std::path::PathBuf; use std::time::Duration; use std::time::Instant; +use codex_config::types::OAuthCredentialsStoreMode; use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; -use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rmcp_client::RmcpClient; use codex_utils_cargo_bin::CargoBinError; use futures::FutureExt as _; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 991c91c55..4e90c909e 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1697,9 +1697,9 @@ mod tests { use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; + use codex_config::config_toml::ProjectConfig; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; - use codex_core::config::ProjectConfig; use codex_features::Feature; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::RolloutItem; diff --git a/codex-rs/tui/src/local_chatgpt_auth.rs b/codex-rs/tui/src/local_chatgpt_auth.rs index e1728add6..e888c0387 100644 --- a/codex-rs/tui/src/local_chatgpt_auth.rs +++ b/codex-rs/tui/src/local_chatgpt_auth.rs @@ -3,7 +3,7 @@ use std::path::Path; use codex_app_server_protocol::AuthMode; -use codex_login::AuthCredentialsStoreMode; +use codex_config::types::AuthCredentialsStoreMode; use codex_login::load_auth_dot_json; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 592fbd975..f991d028c 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -960,8 +960,8 @@ mod tests { use codex_app_server_client::InProcessClientStartArgs; use codex_arg0::Arg0DispatchPaths; use codex_cloud_requirements::cloud_requirements_loader_for_storage; + use codex_config::types::AuthCredentialsStoreMode; use codex_core::config::ConfigBuilder; - use codex_login::AuthCredentialsStoreMode; use codex_protocol::protocol::SessionSource; use pretty_assertions::assert_eq;