Refactor config types into a separate crate (#16962)

Move config types into a separate crate because their macros expand into
a lot of new code.
This commit is contained in:
pakrym-oai
2026-04-06 17:32:41 -07:00
committed by GitHub
Unverified
parent d2df7c54b2
commit 1f2411629f
90 changed files with 1356 additions and 1261 deletions
+10 -4
View File
@@ -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",
@@ -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!(
+5 -1
View File
@@ -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;
}
@@ -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;
@@ -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;
@@ -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 }
@@ -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;
+1 -1
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+4 -3
View File
@@ -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 }
+1 -1
View File
@@ -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;
+5 -1
View File
@@ -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<Vec<AppInfo>> {
if !apps_enabled(config).await {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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<HashMap<String, String>>,
env_http_headers: Option<HashMap<String, String>>,
resolved_scopes: &ResolvedMcpOAuthScopes,
+1
View File
@@ -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 }
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
@@ -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;
+6 -1
View File
@@ -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"] }
+778
View File
@@ -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<String>,
/// Review model override used by the `/review` feature.
pub review_model: Option<String>,
/// Provider to use from the model_providers map.
pub model_provider: Option<String>,
/// Size of the context window for the model, in tokens.
pub model_context_window: Option<i64>,
/// Token usage threshold triggering auto-compaction of conversation history.
pub model_auto_compact_token_limit: Option<i64>,
/// Default approval policy for executing commands.
pub approval_policy: Option<AskForApproval>,
/// 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<ApprovalsReviewer>,
#[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<bool>,
/// Sandbox mode to use.
pub sandbox_mode: Option<SandboxMode>,
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
/// Default named permissions profile to apply from the `[permissions]`
/// table.
pub default_permissions: Option<String>,
/// Named permissions profiles.
#[serde(default)]
pub permissions: Option<PermissionsToml>,
/// Optional external command to spawn for end-user notifications.
#[serde(default)]
pub notify: Option<Vec<String>>,
/// System instructions.
pub instructions: Option<String>,
/// Developer instructions inserted as a `developer` role message.
#[serde(default)]
pub developer_instructions: Option<String>,
/// Whether to inject the `<permissions instructions>` developer block.
pub include_permissions_instructions: Option<bool>,
/// Whether to inject the `<apps_instructions>` developer block.
pub include_apps_instructions: Option<bool>,
/// Whether to inject the `<environment_context>` user block.
pub include_environment_context: Option<bool>,
/// 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<AbsolutePathBuf>,
/// Compact prompt used for history compaction.
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
///
/// Set to an empty string to disable automatic commit attribution.
pub commit_attribution: Option<String>,
/// When set, restricts ChatGPT login to a specific workspace identifier.
#[serde(default)]
pub forced_chatgpt_workspace_id: Option<String>,
/// When set, restricts the login mechanism users may use.
#[serde(default)]
pub forced_login_method: Option<ForcedLoginMethod>,
/// 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<AuthCredentialsStoreMode>,
/// 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<String, McpServerConfig>,
/// 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<OAuthCredentialsStoreMode>,
/// 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<u16>,
/// 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<String>,
/// 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<String, ModelProviderInfo>,
/// Maximum number of bytes to include from an AGENTS.md project doc file.
pub project_doc_max_bytes: Option<usize>,
/// Ordered list of fallback filenames to look for when AGENTS.md is missing.
pub project_doc_fallback_filenames: Option<Vec<String>>,
/// Token budget applied when storing tool/function outputs in the context manager.
pub tool_output_token_limit: Option<usize>,
/// Maximum poll window for background terminal output (`write_stdin`), in milliseconds.
/// Default: `300000` (5 minutes).
pub background_terminal_max_timeout: Option<u64>,
/// Optional absolute path to the Node runtime used by `js_repl`.
pub js_repl_node_path: Option<AbsolutePathBuf>,
/// Ordered list of directories to search for Node modules in `js_repl`.
pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
pub zsh_path: Option<AbsolutePathBuf>,
/// Profile to use from the `profiles` map.
pub profile: Option<String>,
/// Named profiles to facilitate switching between different configurations.
#[serde(default)]
pub profiles: HashMap<String, ConfigProfile>,
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
#[serde(default)]
pub history: Option<History>,
/// Directory where Codex stores the SQLite state DB.
/// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`.
pub sqlite_home: Option<AbsolutePathBuf>,
/// Directory where Codex writes log files, for example `codex-tui.log`.
/// Defaults to `$CODEX_HOME/log`.
pub log_dir: Option<AbsolutePathBuf>,
/// 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<UriBasedFileOpener>,
/// Collection of settings that are specific to the TUI.
pub tui: Option<Tui>,
/// When set to `true`, `AgentReasoning` events will be hidden from the
/// UI/output. Defaults to `false`.
pub hide_agent_reasoning: Option<bool>,
/// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output.
/// Defaults to `false`.
pub show_raw_agent_reasoning: Option<bool>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
pub model_verbosity: Option<Verbosity>,
/// Override to force-enable reasoning summaries for the configured model.
pub model_supports_reasoning_summaries: Option<bool>,
/// 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<AbsolutePathBuf>,
/// Optionally specify a personality for the model
pub personality: Option<Personality>,
/// Optional explicit service tier preference for new turns (`fast` or `flex`).
pub service_tier: Option<ServiceTier>,
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: Option<String>,
/// Base URL override for the built-in `openai` model provider.
pub openai_base_url: Option<String>,
/// Machine-local realtime audio device preferences used by realtime voice.
#[serde(default)]
pub audio: Option<RealtimeAudioToml>,
/// 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<String>,
/// Experimental / do not use. Selects the realtime websocket model/snapshot
/// used for the `Op::RealtimeConversation` connection.
pub experimental_realtime_ws_model: Option<String>,
/// Experimental / do not use. Realtime websocket session selection.
/// `version` controls v1/v2 and `type` controls conversational/transcription.
#[serde(default)]
pub realtime: Option<RealtimeToml>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
pub projects: Option<HashMap<String, ProjectConfig>>,
/// Controls the web search tool mode: disabled, cached, or live.
pub web_search: Option<WebSearchMode>,
/// Nested tools section for feature toggles
pub tools: Option<ToolsToml>,
/// Additional discoverable tools that can be suggested for installation.
pub tool_suggest: Option<ToolSuggestConfig>,
/// Agent-related settings (thread limits, etc.).
pub agents: Option<AgentsToml>,
/// Memories subsystem settings.
pub memories: Option<MemoriesToml>,
/// User-level skill config entries keyed by SKILL.md path.
pub skills: Option<SkillsConfig>,
/// User-level plugin config entries keyed by plugin name.
#[serde(default)]
pub plugins: HashMap<String, PluginConfig>,
/// 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<FeaturesToml>,
/// Suppress warnings about unstable (under development) features.
pub suppress_unstable_features_warning: Option<bool>,
/// Settings for ghost snapshots (used for undo).
#[serde(default)]
pub ghost_snapshot: Option<GhostSnapshotToml>,
/// 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<Vec<String>>,
/// 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<bool>,
/// 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<bool>,
/// When `false`, disables analytics across Codex product surfaces in this machine.
/// Defaults to `true`.
pub analytics: Option<AnalyticsConfigToml>,
/// When `false`, disables feedback collection across Codex product surfaces.
/// Defaults to `true`.
pub feedback: Option<FeedbackConfigToml>,
/// Settings for app-specific controls.
#[serde(default)]
pub apps: Option<AppsConfigToml>,
/// OTEL configuration.
pub otel: Option<OtelConfigToml>,
/// Windows-specific configuration.
#[serde(default)]
pub windows: Option<WindowsToml>,
/// Tracks whether the Windows onboarding screen has been acknowledged.
pub windows_wsl_setup_acknowledged: Option<bool>,
/// Collection of in-product notices (different from notifications)
/// See [`crate::types::Notice`] for more details
pub notice: Option<Notice>,
/// Legacy, now use features
/// Deprecated: ignored. Use `model_instructions_file`.
#[schemars(skip)]
pub experimental_instructions_file: Option<AbsolutePathBuf>,
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
/// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
pub oss_provider: Option<String>,
}
impl From<ConfigToml> 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<TrustLevel>,
}
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<String>,
pub speaker: Option<String>,
}
#[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<RealtimeWsVersion>,
#[serde(rename = "type")]
pub session_type: Option<RealtimeWsMode>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct RealtimeAudioToml {
pub microphone: Option<String>,
pub speaker: Option<String>,
}
#[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<WebSearchToolConfig>,
/// Enable the `view_image` tool that lets the agent attach local images.
#[serde(default)]
pub view_image: Option<bool>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum WebSearchToolConfigInput {
Enabled(bool),
Config(WebSearchToolConfig),
}
fn deserialize_optional_web_search_tool_config<'de, D>(
deserializer: D,
) -> Result<Option<WebSearchToolConfig>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<WebSearchToolConfigInput>::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<usize>,
/// Maximum nesting depth allowed for spawned agent threads.
/// Root sessions start at depth 0.
#[schemars(range(min = 1))]
pub max_depth: Option<i32>,
/// Default maximum runtime in seconds for agent job workers.
#[schemars(range(min = 1))]
pub job_max_runtime_seconds: Option<u64>,
/// 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<String, AgentRoleToml>,
}
#[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<String>,
/// Path to a role-specific config layer.
/// Relative paths are resolved relative to the `config.toml` that defines them.
pub config_file: Option<AbsolutePathBuf>,
/// Candidate nicknames for agents spawned with this role.
pub nickname_candidates: Option<Vec<String>>,
}
impl From<ToolsToml> 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<i64>,
/// 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<i64>,
/// Disable all ghost snapshot warning events.
pub disable_warnings: Option<bool>,
}
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
pub fn derive_sandbox_policy(
&self,
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
windows_sandbox_level: WindowsSandboxLevel,
resolved_cwd: &Path,
sandbox_policy_constraint: Option<&crate::Constrained<SandboxPolicy>>,
) -> 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<ProjectConfig> {
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<String>,
) -> Result<ConfigProfile, std::io::Error> {
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<String, ModelProviderInfo>,
) -> Result<(), String> {
let mut conflicts = model_providers
.keys()
.filter(|key| RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str()))
.map(|key| format!("`{key}`"))
.collect::<Vec<_>>();
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<String, ModelProviderInfo>,
) -> 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<HashMap<String, ModelProviderInfo>, D::Error>
where
D: serde::Deserializer<'de>,
{
let model_providers = HashMap::<String, ModelProviderInfo>::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}"
),
)),
}
}
+4
View File
@@ -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;
+240
View File
@@ -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<String, PermissionProfileToml>,
}
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<FilesystemPermissionsToml>,
pub network: Option<NetworkToml>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct FilesystemPermissionsToml {
#[serde(flatten)]
pub entries: BTreeMap<String, FilesystemPermissionToml>,
}
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<String, FileSystemAccessMode>),
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct NetworkDomainPermissionsToml {
#[serde(flatten)]
pub entries: BTreeMap<String, NetworkDomainPermissionToml>,
}
impl NetworkDomainPermissionsToml {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn allowed_domains(&self) -> Option<Vec<String>> {
let allowed_domains: Vec<String> = 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<Vec<String>> {
let denied_domains: Vec<String> = 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<String, NetworkUnixSocketPermissionToml>,
}
impl NetworkUnixSocketPermissionsToml {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn allow_unix_sockets(&self) -> Vec<String> {
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<bool>,
pub proxy_url: Option<String>,
pub enable_socks5: Option<bool>,
pub socks_url: Option<String>,
pub enable_socks5_udp: Option<bool>,
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
#[schemars(with = "Option<NetworkModeSchema>")]
pub mode: Option<NetworkMode>,
pub domains: Option<NetworkDomainPermissionsToml>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
pub allow_local_binding: Option<bool>,
}
#[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);
}
}
@@ -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<bool>,
pub tools: Option<ToolsToml>,
pub web_search: Option<WebSearchMode>,
pub analytics: Option<codex_config::types::AnalyticsConfigToml>,
pub analytics: Option<AnalyticsConfigToml>,
#[serde(default)]
pub windows: Option<WindowsToml>,
/// 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<FeaturesToml>,
pub oss_provider: Option<String>,
}
+100
View File
@@ -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::<bool>());
}
for legacy_key in legacy_feature_keys() {
validation
.properties
.insert(legacy_key.to_string(), schema_gen.subschema_for::<bool>());
}
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::<RawMcpServerConfig>())),
..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::<ConfigToml>()
}
/// 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<Vec<u8>> {
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(())
}
+30
View File
@@ -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 {
-1
View File
@@ -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 }
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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<crate::config::ConfigToml> {
) -> anyhow::Result<ConfigToml> {
Ok(deserialize_config_toml_with_base(
config_layer_stack.effective_config(),
&config.codex_home,
+1 -1
View File
@@ -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(())
}
+8 -3
View File
@@ -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 {
+3 -3
View File
@@ -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;
+24
View File
@@ -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;
+2 -2
View File
@@ -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;
+20 -761
View File
@@ -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<u64> = 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<PathBuf> {
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<String>,
/// Review model override used by the `/review` feature.
pub review_model: Option<String>,
/// Provider to use from the model_providers map.
pub model_provider: Option<String>,
/// Size of the context window for the model, in tokens.
pub model_context_window: Option<i64>,
/// Token usage threshold triggering auto-compaction of conversation history.
pub model_auto_compact_token_limit: Option<i64>,
/// Default approval policy for executing commands.
pub approval_policy: Option<AskForApproval>,
/// 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<ApprovalsReviewer>,
#[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<bool>,
/// Sandbox mode to use.
pub sandbox_mode: Option<SandboxMode>,
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
/// Default named permissions profile to apply from the `[permissions]`
/// table.
pub default_permissions: Option<String>,
/// Named permissions profiles.
#[serde(default)]
pub permissions: Option<PermissionsToml>,
/// Optional external command to spawn for end-user notifications.
#[serde(default)]
pub notify: Option<Vec<String>>,
/// System instructions.
pub instructions: Option<String>,
/// Developer instructions inserted as a `developer` role message.
#[serde(default)]
pub developer_instructions: Option<String>,
/// Whether to inject the `<permissions instructions>` developer block.
pub include_permissions_instructions: Option<bool>,
/// Whether to inject the `<apps_instructions>` developer block.
pub include_apps_instructions: Option<bool>,
/// Whether to inject the `<environment_context>` user block.
pub include_environment_context: Option<bool>,
/// 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<AbsolutePathBuf>,
/// Compact prompt used for history compaction.
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
///
/// Set to an empty string to disable automatic commit attribution.
pub commit_attribution: Option<String>,
/// When set, restricts ChatGPT login to a specific workspace identifier.
#[serde(default)]
pub forced_chatgpt_workspace_id: Option<String>,
/// When set, restricts the login mechanism users may use.
#[serde(default)]
pub forced_login_method: Option<ForcedLoginMethod>,
/// 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<AuthCredentialsStoreMode>,
/// 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<String, McpServerConfig>,
/// 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<OAuthCredentialsStoreMode>,
/// 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<u16>,
/// 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<String>,
/// 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<String, ModelProviderInfo>,
/// Maximum number of bytes to include from an AGENTS.md project doc file.
pub project_doc_max_bytes: Option<usize>,
/// Ordered list of fallback filenames to look for when AGENTS.md is missing.
pub project_doc_fallback_filenames: Option<Vec<String>>,
/// Token budget applied when storing tool/function outputs in the context manager.
pub tool_output_token_limit: Option<usize>,
/// Maximum poll window for background terminal output (`write_stdin`), in milliseconds.
/// Default: `300000` (5 minutes).
pub background_terminal_max_timeout: Option<u64>,
/// Optional absolute path to the Node runtime used by `js_repl`.
pub js_repl_node_path: Option<AbsolutePathBuf>,
/// Ordered list of directories to search for Node modules in `js_repl`.
pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
pub zsh_path: Option<AbsolutePathBuf>,
/// Profile to use from the `profiles` map.
pub profile: Option<String>,
/// Named profiles to facilitate switching between different configurations.
#[serde(default)]
pub profiles: HashMap<String, ConfigProfile>,
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
#[serde(default)]
pub history: Option<History>,
/// Directory where Codex stores the SQLite state DB.
/// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`.
pub sqlite_home: Option<AbsolutePathBuf>,
/// Directory where Codex writes log files, for example `codex-tui.log`.
/// Defaults to `$CODEX_HOME/log`.
pub log_dir: Option<AbsolutePathBuf>,
/// 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<UriBasedFileOpener>,
/// Collection of settings that are specific to the TUI.
pub tui: Option<Tui>,
/// When set to `true`, `AgentReasoning` events will be hidden from the
/// UI/output. Defaults to `false`.
pub hide_agent_reasoning: Option<bool>,
/// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output.
/// Defaults to `false`.
pub show_raw_agent_reasoning: Option<bool>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
pub model_verbosity: Option<Verbosity>,
/// Override to force-enable reasoning summaries for the configured model.
pub model_supports_reasoning_summaries: Option<bool>,
/// 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<AbsolutePathBuf>,
/// Optionally specify a personality for the model
pub personality: Option<Personality>,
/// Optional explicit service tier preference for new turns (`fast` or `flex`).
pub service_tier: Option<ServiceTier>,
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: Option<String>,
/// Base URL override for the built-in `openai` model provider.
pub openai_base_url: Option<String>,
/// Machine-local realtime audio device preferences used by realtime voice.
#[serde(default)]
pub audio: Option<RealtimeAudioToml>,
/// 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<String>,
/// Experimental / do not use. Selects the realtime websocket model/snapshot
/// used for the `Op::RealtimeConversation` connection.
pub experimental_realtime_ws_model: Option<String>,
/// Experimental / do not use. Realtime websocket session selection.
/// `version` controls v1/v2 and `type` controls conversational/transcription.
#[serde(default)]
pub realtime: Option<RealtimeToml>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
pub projects: Option<HashMap<String, ProjectConfig>>,
/// Controls the web search tool mode: disabled, cached, or live.
pub web_search: Option<WebSearchMode>,
/// Nested tools section for feature toggles
pub tools: Option<ToolsToml>,
/// Additional discoverable tools that can be suggested for installation.
pub tool_suggest: Option<ToolSuggestConfig>,
/// Agent-related settings (thread limits, etc.).
pub agents: Option<AgentsToml>,
/// Memories subsystem settings.
pub memories: Option<MemoriesToml>,
/// User-level skill config entries keyed by SKILL.md path.
pub skills: Option<SkillsConfig>,
/// User-level plugin config entries keyed by plugin name.
#[serde(default)]
pub plugins: HashMap<String, PluginConfig>,
/// 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<FeaturesToml>,
/// Suppress warnings about unstable (under development) features.
pub suppress_unstable_features_warning: Option<bool>,
/// Settings for ghost snapshots (used for undo).
#[serde(default)]
pub ghost_snapshot: Option<GhostSnapshotToml>,
/// 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<Vec<String>>,
/// 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<bool>,
/// 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<bool>,
/// When `false`, disables analytics across Codex product surfaces in this machine.
/// Defaults to `true`.
pub analytics: Option<codex_config::types::AnalyticsConfigToml>,
/// When `false`, disables feedback collection across Codex product surfaces.
/// Defaults to `true`.
pub feedback: Option<codex_config::types::FeedbackConfigToml>,
/// Settings for app-specific controls.
#[serde(default)]
pub apps: Option<AppsConfigToml>,
/// OTEL configuration.
pub otel: Option<codex_config::types::OtelConfigToml>,
/// Windows-specific configuration.
#[serde(default)]
pub windows: Option<WindowsToml>,
/// Tracks whether the Windows onboarding screen has been acknowledged.
pub windows_wsl_setup_acknowledged: Option<bool>,
/// Collection of in-product notices (different from notifications)
/// See [`codex_config::types::Notice`] for more details
pub notice: Option<Notice>,
/// Legacy, now use features
/// Deprecated: ignored. Use `model_instructions_file`.
#[schemars(skip)]
pub experimental_instructions_file: Option<AbsolutePathBuf>,
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
/// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
pub oss_provider: Option<String>,
}
impl From<ConfigToml> 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<TrustLevel>,
}
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<String>,
pub speaker: Option<String>,
}
#[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<RealtimeWsVersion>,
#[serde(rename = "type")]
pub session_type: Option<RealtimeWsMode>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct RealtimeAudioToml {
pub microphone: Option<String>,
pub speaker: Option<String>,
}
#[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<WebSearchToolConfig>,
/// Enable the `view_image` tool that lets the agent attach local images.
#[serde(default)]
pub view_image: Option<bool>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum WebSearchToolConfigInput {
Enabled(bool),
Config(WebSearchToolConfig),
}
fn deserialize_optional_web_search_tool_config<'de, D>(
deserializer: D,
) -> Result<Option<WebSearchToolConfig>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<WebSearchToolConfigInput>::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<String>,
/// Path to a role-specific config layer.
pub config_file: Option<PathBuf>,
/// Candidate nicknames for agents spawned with this role.
pub nickname_candidates: Option<Vec<String>>,
}
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<usize>,
/// Maximum nesting depth allowed for spawned agent threads.
/// Root sessions start at depth 0.
#[schemars(range(min = 1))]
pub max_depth: Option<i32>,
/// Default maximum runtime in seconds for agent job workers.
#[schemars(range(min = 1))]
pub job_max_runtime_seconds: Option<u64>,
/// 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<String, AgentRoleToml>,
}
#[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<String>,
/// Path to a role-specific config layer.
pub config_file: Option<PathBuf>,
/// Candidate nicknames for agents spawned with this role.
pub nickname_candidates: Option<Vec<String>>,
}
#[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<String>,
/// Path to a role-specific config layer.
/// Relative paths are resolved relative to the `config.toml` that defines them.
pub config_file: Option<AbsolutePathBuf>,
/// Candidate nicknames for agents spawned with this role.
pub nickname_candidates: Option<Vec<String>>,
}
impl From<ToolsToml> 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<i64>,
/// 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<i64>,
/// Disable all ghost snapshot warning events.
pub disable_warnings: Option<bool>,
}
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
fn derive_sandbox_policy(
&self,
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
windows_sandbox_level: WindowsSandboxLevel,
resolved_cwd: &Path,
sandbox_policy_constraint: Option<&Constrained<SandboxPolicy>>,
) -> 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<ProjectConfig> {
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<String>,
) -> Result<ConfigProfile, std::io::Error> {
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<PathBuf>,
}
fn validate_reserved_model_provider_ids(
model_providers: &HashMap<String, ModelProviderInfo>,
) -> Result<(), String> {
let mut conflicts = model_providers
.keys()
.filter(|key| RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str()))
.map(|key| format!("`{key}`"))
.collect::<Vec<_>>();
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<String, ModelProviderInfo>,
) -> 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<HashMap<String, ModelProviderInfo>, D::Error>
where
D: serde::Deserializer<'de>,
{
let model_providers = HashMap::<String, ModelProviderInfo>::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(
+5 -239
View File
@@ -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<String, PermissionProfileToml>,
}
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<FilesystemPermissionsToml>,
pub network: Option<NetworkToml>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct FilesystemPermissionsToml {
#[serde(flatten)]
pub entries: BTreeMap<String, FilesystemPermissionToml>,
}
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<String, FileSystemAccessMode>),
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct NetworkDomainPermissionsToml {
#[serde(flatten)]
pub entries: BTreeMap<String, NetworkDomainPermissionToml>,
}
impl NetworkDomainPermissionsToml {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[cfg(test)]
pub(crate) fn allowed_domains(&self) -> Option<Vec<String>> {
let allowed_domains: Vec<String> = 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<Vec<String>> {
let denied_domains: Vec<String> = 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<String, NetworkUnixSocketPermissionToml>,
}
impl NetworkUnixSocketPermissionsToml {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn allow_unix_sockets(&self) -> Vec<String> {
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<bool>,
pub proxy_url: Option<String>,
pub enable_socks5: Option<bool>,
pub socks_url: Option<String>,
pub enable_socks5_udp: Option<bool>,
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
#[schemars(with = "Option<NetworkModeSchema>")]
pub mode: Option<NetworkMode>,
pub domains: Option<NetworkDomainPermissionsToml>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
pub allow_local_binding: Option<bool>,
}
#[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>,
@@ -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;
+3 -100
View File
@@ -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::<bool>());
}
for legacy_key in legacy_feature_keys() {
validation
.properties
.insert(legacy_key.to_string(), schema_gen.subschema_for::<bool>());
}
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::<RawMcpServerConfig>())),
..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::<ConfigToml>()
}
/// 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<Vec<u8>> {
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"]
+1 -1
View File
@@ -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;
+3 -2
View File
@@ -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<std::collections::HashMap<String, crate::config::ProjectConfig>>,
projects: Option<std::collections::HashMap<String, ProjectConfig>>,
}
struct ProjectTrustDecision {
+2 -2
View File
@@ -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;
+8 -2
View File
@@ -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,
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
@@ -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;
-1
View File
@@ -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 }
+2 -21
View File
@@ -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 {
+3 -8
View File
@@ -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]
+2 -2
View File
@@ -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 = [
-1
View File
@@ -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 }
+1 -1
View File
@@ -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;
+1 -16
View File
@@ -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 {
-1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
@@ -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;
@@ -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;
+1 -1
View File
@@ -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 }
+1
View File
@@ -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 }
-1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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";
-1
View File
@@ -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;
+1 -16
View File
@@ -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<u64>,
}
/// 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);
@@ -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<HashMap<String, String>>,
+1 -1
View File
@@ -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";
@@ -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 _;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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)]
+1 -1
View File
@@ -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;