mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Keep invalid config enums at load boundary
This commit is contained in:
@@ -5,7 +5,6 @@ use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::HooksToml;
|
||||
use crate::Lenient;
|
||||
use crate::permissions_toml::PermissionsToml;
|
||||
use crate::profile_toml::ConfigProfile;
|
||||
use crate::types::AnalyticsConfigToml;
|
||||
@@ -107,12 +106,12 @@ pub struct ConfigToml {
|
||||
pub model_auto_compact_token_limit: Option<i64>,
|
||||
|
||||
/// Default approval policy for executing commands.
|
||||
pub approval_policy: Option<Lenient<AskForApproval>>,
|
||||
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<Lenient<ApprovalsReviewer>>,
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
|
||||
/// Optional policy instructions for the guardian auto-reviewer.
|
||||
#[serde(default)]
|
||||
@@ -133,7 +132,7 @@ pub struct ConfigToml {
|
||||
pub allow_login_shell: Option<bool>,
|
||||
|
||||
/// Sandbox mode to use.
|
||||
pub sandbox_mode: Option<Lenient<SandboxMode>>,
|
||||
pub sandbox_mode: Option<SandboxMode>,
|
||||
|
||||
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
|
||||
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
|
||||
@@ -187,14 +186,14 @@ pub struct ConfigToml {
|
||||
|
||||
/// When set, restricts the login mechanism users may use.
|
||||
#[serde(default)]
|
||||
pub forced_login_method: Option<Lenient<ForcedLoginMethod>>,
|
||||
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<Lenient<AuthCredentialsStoreMode>>,
|
||||
pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>,
|
||||
|
||||
/// Definition for MCP servers that Codex can reach out to for tool calls.
|
||||
#[serde(default)]
|
||||
@@ -208,7 +207,7 @@ pub struct ConfigToml {
|
||||
/// 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<Lenient<OAuthCredentialsStoreMode>>,
|
||||
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.
|
||||
@@ -275,7 +274,7 @@ pub struct ConfigToml {
|
||||
|
||||
/// 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<Lenient<UriBasedFileOpener>>,
|
||||
pub file_opener: Option<UriBasedFileOpener>,
|
||||
|
||||
/// Collection of settings that are specific to the TUI.
|
||||
pub tui: Option<Tui>,
|
||||
@@ -289,11 +288,11 @@ pub struct ConfigToml {
|
||||
/// Defaults to `false`.
|
||||
pub show_raw_agent_reasoning: Option<bool>,
|
||||
|
||||
pub model_reasoning_effort: Option<Lenient<ReasoningEffort>>,
|
||||
pub plan_mode_reasoning_effort: Option<Lenient<ReasoningEffort>>,
|
||||
pub model_reasoning_summary: Option<Lenient<ReasoningSummary>>,
|
||||
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<Lenient<Verbosity>>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
|
||||
/// Override to force-enable reasoning summaries for the configured model.
|
||||
pub model_supports_reasoning_summaries: Option<bool>,
|
||||
@@ -303,10 +302,10 @@ pub struct ConfigToml {
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
|
||||
/// Optionally specify a personality for the model
|
||||
pub personality: Option<Lenient<Personality>>,
|
||||
pub personality: Option<Personality>,
|
||||
|
||||
/// Optional explicit service tier preference for new turns (`fast` or `flex`).
|
||||
pub service_tier: Option<Lenient<ServiceTier>>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
@@ -352,11 +351,11 @@ pub struct ConfigToml {
|
||||
pub experimental_thread_config_endpoint: Option<String>,
|
||||
|
||||
/// Experimental / do not use. Selects the thread store implementation.
|
||||
pub experimental_thread_store: Option<Lenient<ThreadStoreToml>>,
|
||||
pub experimental_thread_store: Option<ThreadStoreToml>,
|
||||
pub projects: Option<HashMap<String, ProjectConfig>>,
|
||||
|
||||
/// Controls the web search tool mode: disabled, cached, or live.
|
||||
pub web_search: Option<Lenient<WebSearchMode>>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
|
||||
/// Nested tools section for feature toggles
|
||||
pub tools: Option<ToolsToml>,
|
||||
@@ -510,21 +509,15 @@ impl From<ConfigToml> for UserSavedConfig {
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
approval_policy: config_toml.approval_policy.and_then(Lenient::into_valid),
|
||||
sandbox_mode: config_toml.sandbox_mode.and_then(Lenient::into_valid),
|
||||
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
|
||||
.and_then(Lenient::into_valid),
|
||||
forced_login_method: config_toml.forced_login_method,
|
||||
model: config_toml.model,
|
||||
model_reasoning_effort: config_toml
|
||||
.model_reasoning_effort
|
||||
.and_then(Lenient::into_valid),
|
||||
model_reasoning_summary: config_toml
|
||||
.model_reasoning_summary
|
||||
.and_then(Lenient::into_valid),
|
||||
model_verbosity: config_toml.model_verbosity.and_then(Lenient::into_valid),
|
||||
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,
|
||||
@@ -718,17 +711,16 @@ impl ConfigToml {
|
||||
&self,
|
||||
sandbox_mode_override: Option<SandboxMode>,
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
config_sandbox_mode: Option<SandboxMode>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
active_project: Option<&ProjectConfig>,
|
||||
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
|
||||
) -> PermissionProfile {
|
||||
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
|
||||
|| profile_sandbox_mode.is_some()
|
||||
|| config_sandbox_mode.is_some();
|
||||
|| self.sandbox_mode.is_some();
|
||||
let resolved_sandbox_mode = sandbox_mode_override
|
||||
.or(profile_sandbox_mode)
|
||||
.or(config_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
.or(if sandbox_mode_was_explicit {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -1,109 +1,49 @@
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::Schema;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use serde::de::DeserializeOwned;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Lenient<T> {
|
||||
Valid(T),
|
||||
Invalid(TomlValue),
|
||||
}
|
||||
pub(crate) fn deserialize_with_enum_warnings<T>(
|
||||
mut value: TomlValue,
|
||||
) -> Result<(TomlValue, T, Vec<String>), toml::de::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum LenientInput<T> {
|
||||
Valid(T),
|
||||
Invalid(TomlValue),
|
||||
}
|
||||
loop {
|
||||
match serde_path_to_error::deserialize(value.clone()) {
|
||||
Ok(parsed) => return Ok((value, parsed, warnings)),
|
||||
Err(err) => {
|
||||
let path = err.path().to_string();
|
||||
let toml_error = err.into_inner();
|
||||
if !is_unknown_variant_error(&toml_error) {
|
||||
return Err(toml_error);
|
||||
}
|
||||
|
||||
impl<T> Lenient<T> {
|
||||
pub fn as_valid(&self) -> Option<&T> {
|
||||
match self {
|
||||
Self::Valid(value) => Some(value),
|
||||
Self::Invalid(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_valid(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Valid(value) => Some(value),
|
||||
Self::Invalid(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_valid_with_warning(
|
||||
self,
|
||||
field_path: &str,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Option<T> {
|
||||
match self {
|
||||
Self::Valid(value) => Some(value),
|
||||
Self::Invalid(value) => {
|
||||
let Some(invalid_value) = remove_value_at_path(&mut value, &path) else {
|
||||
return Err(toml_error);
|
||||
};
|
||||
warnings.push(format!(
|
||||
"Ignoring invalid config value at {field_path}: {value}"
|
||||
"Ignoring invalid config value at {path}: {invalid_value}"
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_value(&self) -> Option<&TomlValue> {
|
||||
match self {
|
||||
Self::Valid(_) => None,
|
||||
Self::Invalid(value) => Some(value),
|
||||
fn is_unknown_variant_error(err: &toml::de::Error) -> bool {
|
||||
err.message().contains("unknown variant")
|
||||
}
|
||||
|
||||
fn remove_value_at_path(value: &mut TomlValue, path: &str) -> Option<TomlValue> {
|
||||
let mut parts = path.split('.').peekable();
|
||||
let mut current = value;
|
||||
|
||||
while let Some(part) = parts.next() {
|
||||
if parts.peek().is_none() {
|
||||
return current.as_table_mut()?.remove(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Lenient<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::Valid(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for Lenient<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match LenientInput::<T>::deserialize(deserializer)? {
|
||||
LenientInput::Valid(value) => Self::Valid(value),
|
||||
LenientInput::Invalid(value) => Self::Invalid(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Serialize for Lenient<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Valid(value) => value.serialize(serializer),
|
||||
Self::Invalid(value) => value.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> JsonSchema for Lenient<T>
|
||||
where
|
||||
T: JsonSchema,
|
||||
{
|
||||
fn schema_name() -> String {
|
||||
T::schema_name()
|
||||
current = current.as_table_mut()?.get_mut(part)?;
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
T::json_schema(generator)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ pub use hook_config::HooksToml;
|
||||
pub use hook_config::ManagedHooksRequirementsToml;
|
||||
pub use hook_config::MatcherGroup;
|
||||
pub use host_name::host_name;
|
||||
pub use lenient::Lenient;
|
||||
pub use marketplace_edit::MarketplaceConfigUpdate;
|
||||
pub use marketplace_edit::RemoveMarketplaceConfigOutcome;
|
||||
pub use marketplace_edit::record_user_marketplace;
|
||||
|
||||
@@ -3,7 +3,6 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Lenient;
|
||||
use crate::config_toml::ToolsToml;
|
||||
use crate::types::AnalyticsConfigToml;
|
||||
use crate::types::ApprovalsReviewer;
|
||||
@@ -25,20 +24,20 @@ use codex_protocol::protocol::AskForApproval;
|
||||
pub struct ConfigProfile {
|
||||
pub model: Option<String>,
|
||||
/// Optional explicit service tier preference for new turns (`fast` or `flex`).
|
||||
pub service_tier: Option<Lenient<ServiceTier>>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
/// The key in the `model_providers` map identifying the
|
||||
/// [`ModelProviderInfo`] to use.
|
||||
pub model_provider: Option<String>,
|
||||
pub approval_policy: Option<Lenient<AskForApproval>>,
|
||||
pub approvals_reviewer: Option<Lenient<ApprovalsReviewer>>,
|
||||
pub sandbox_mode: Option<Lenient<SandboxMode>>,
|
||||
pub model_reasoning_effort: Option<Lenient<ReasoningEffort>>,
|
||||
pub plan_mode_reasoning_effort: Option<Lenient<ReasoningEffort>>,
|
||||
pub model_reasoning_summary: Option<Lenient<ReasoningSummary>>,
|
||||
pub model_verbosity: Option<Lenient<Verbosity>>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
pub sandbox_mode: Option<SandboxMode>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
/// Optional path to a JSON model catalog (applied on startup only).
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
pub personality: Option<Lenient<Personality>>,
|
||||
pub personality: Option<Personality>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
/// Optional path to a file containing model instructions.
|
||||
pub model_instructions_file: Option<AbsolutePathBuf>,
|
||||
@@ -62,7 +61,7 @@ pub struct ConfigProfile {
|
||||
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||
pub tools_view_image: Option<bool>,
|
||||
pub tools: Option<ToolsToml>,
|
||||
pub web_search: Option<Lenient<WebSearchMode>>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub analytics: Option<AnalyticsConfigToml>,
|
||||
#[serde(default)]
|
||||
pub windows: Option<WindowsToml>,
|
||||
@@ -79,14 +78,10 @@ impl From<ConfigProfile> for codex_app_server_protocol::Profile {
|
||||
Self {
|
||||
model: config_profile.model,
|
||||
model_provider: config_profile.model_provider,
|
||||
approval_policy: config_profile.approval_policy.and_then(Lenient::into_valid),
|
||||
model_reasoning_effort: config_profile
|
||||
.model_reasoning_effort
|
||||
.and_then(Lenient::into_valid),
|
||||
model_reasoning_summary: config_profile
|
||||
.model_reasoning_summary
|
||||
.and_then(Lenient::into_valid),
|
||||
model_verbosity: config_profile.model_verbosity.and_then(Lenient::into_valid),
|
||||
approval_policy: config_profile.approval_policy,
|
||||
model_reasoning_effort: config_profile.model_reasoning_effort,
|
||||
model_reasoning_summary: config_profile.model_reasoning_summary,
|
||||
model_verbosity: config_profile.model_verbosity,
|
||||
chatgpt_base_url: config_profile.chatgpt_base_url,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::config_requirements::ConfigRequirementsToml;
|
||||
use super::fingerprint::record_origins;
|
||||
use super::fingerprint::version_for_toml;
|
||||
use super::key_aliases::normalized_with_key_aliases;
|
||||
use super::lenient::deserialize_with_enum_warnings;
|
||||
use super::merge::merge_toml_values;
|
||||
use codex_app_server_protocol::ConfigLayer;
|
||||
use codex_app_server_protocol::ConfigLayerMetadata;
|
||||
@@ -310,6 +311,15 @@ impl ConfigLayerStack {
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn deserialize_effective_config_with_warnings<T>(
|
||||
&self,
|
||||
) -> Result<(TomlValue, T, Vec<String>), toml::de::Error>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
deserialize_with_enum_warnings(self.effective_config())
|
||||
}
|
||||
|
||||
/// Returns field origins for the merged config-layer view.
|
||||
///
|
||||
/// Requirement sources are tracked separately and are not included here.
|
||||
|
||||
@@ -124,6 +124,9 @@ async fn invalid_enum_values_emit_warnings_without_poisoning_config() -> std::io
|
||||
let contents = r#"
|
||||
model = "gpt-5-codex"
|
||||
sandbox_mode = "make-it-so"
|
||||
|
||||
[tui]
|
||||
notification_method = "loudly"
|
||||
"#;
|
||||
let config_path = tmp.path().join(CONFIG_TOML_FILE);
|
||||
std::fs::write(&config_path, contents).expect("write config");
|
||||
@@ -140,70 +143,28 @@ sandbox_mode = "make-it-so"
|
||||
)
|
||||
.await?;
|
||||
|
||||
let effective_config = layers.effective_config();
|
||||
let config_toml: ConfigToml = effective_config
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(std::io::Error::other)?;
|
||||
let mut enum_warnings = Vec::new();
|
||||
let sandbox_mode = config_toml
|
||||
.sandbox_mode
|
||||
.and_then(|value| value.into_valid_with_warning("sandbox_mode", &mut enum_warnings));
|
||||
let (effective_config, _config_toml, enum_warnings): (TomlValue, ConfigToml, Vec<String>) =
|
||||
layers.deserialize_effective_config_with_warnings()?;
|
||||
let expected_config = toml::from_str::<TomlValue>(
|
||||
r#"
|
||||
model = "gpt-5-codex"
|
||||
sandbox_mode = "make-it-so"
|
||||
|
||||
[tui]
|
||||
"#,
|
||||
)
|
||||
.expect("expected config should parse");
|
||||
let expected_startup_warnings =
|
||||
vec!["Ignoring invalid config value at sandbox_mode: \"make-it-so\"".to_string()];
|
||||
let expected_startup_warnings = vec![
|
||||
"Ignoring invalid config value at sandbox_mode: \"make-it-so\"".to_string(),
|
||||
"Ignoring invalid config value at tui.notification_method: \"loudly\"".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
(effective_config, sandbox_mode, enum_warnings),
|
||||
(expected_config, None, expected_startup_warnings)
|
||||
(effective_config, enum_warnings),
|
||||
(expected_config, expected_startup_warnings)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_enum_values_emit_startup_warnings_when_loading_config() -> std::io::Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
model = "gpt-5-codex"
|
||||
sandbox_mode = "make-it-so"
|
||||
profile = "dev"
|
||||
|
||||
[profiles.dev]
|
||||
model_reasoning_effort = "much"
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
(config.model.as_deref(), config.startup_warnings),
|
||||
(
|
||||
Some("gpt-5-codex"),
|
||||
vec![
|
||||
"Ignoring invalid config value at sandbox_mode: \"make-it-so\"".to_string(),
|
||||
"Ignoring invalid config value at profiles.dev.model_reasoning_effort: \"much\""
|
||||
.to_string(),
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_user_config_keeps_empty_user_layer() -> std::io::Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::edit::apply_blocking;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::Lenient;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::config_toml::AgentRoleToml;
|
||||
use codex_config::config_toml::AgentsToml;
|
||||
@@ -152,10 +151,6 @@ async fn derive_legacy_sandbox_policy_for_test(
|
||||
.derive_permission_profile(
|
||||
sandbox_mode_override,
|
||||
profile_sandbox_mode,
|
||||
cfg.sandbox_mode
|
||||
.as_ref()
|
||||
.and_then(Lenient::as_valid)
|
||||
.copied(),
|
||||
windows_sandbox_level,
|
||||
active_project,
|
||||
permission_profile_constraint,
|
||||
@@ -2991,7 +2986,7 @@ async fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> {
|
||||
async fn config_resolves_explicit_keyring_auth_store_mode() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg = ConfigToml {
|
||||
cli_auth_credentials_store: Some(Lenient::from(AuthCredentialsStoreMode::Keyring)),
|
||||
cli_auth_credentials_store: Some(AuthCredentialsStoreMode::Keyring),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -3109,32 +3104,41 @@ async fn feedback_enabled_defaults_to_true() -> std::io::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_defaults_to_none_if_unset() {
|
||||
let cfg = ConfigToml::default();
|
||||
let profile = ConfigProfile::default();
|
||||
let features = Features::with_defaults();
|
||||
|
||||
assert_eq!(
|
||||
resolve_web_search_mode(/*configured_mode*/ None, &features),
|
||||
None
|
||||
);
|
||||
assert_eq!(resolve_web_search_mode(&cfg, &profile, &features), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_prefers_profile_over_legacy_flags() {
|
||||
let cfg = ConfigToml::default();
|
||||
let profile = ConfigProfile {
|
||||
web_search: Some(WebSearchMode::Live),
|
||||
..Default::default()
|
||||
};
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::WebSearchCached);
|
||||
|
||||
assert_eq!(
|
||||
resolve_web_search_mode(Some(WebSearchMode::Live), &features),
|
||||
resolve_web_search_mode(&cfg, &profile, &features),
|
||||
Some(WebSearchMode::Live)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_disabled_overrides_legacy_request() {
|
||||
let cfg = ConfigToml {
|
||||
web_search: Some(WebSearchMode::Disabled),
|
||||
..Default::default()
|
||||
};
|
||||
let profile = ConfigProfile::default();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::WebSearchRequest);
|
||||
|
||||
assert_eq!(
|
||||
resolve_web_search_mode(Some(WebSearchMode::Disabled), &features),
|
||||
resolve_web_search_mode(&cfg, &profile, &features),
|
||||
Some(WebSearchMode::Disabled)
|
||||
);
|
||||
}
|
||||
@@ -3253,14 +3257,14 @@ async fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> {
|
||||
profiles.insert(
|
||||
"work".to_string(),
|
||||
ConfigProfile {
|
||||
sandbox_mode: Some(Lenient::from(SandboxMode::DangerFullAccess)),
|
||||
sandbox_mode: Some(SandboxMode::DangerFullAccess),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let cfg = ConfigToml {
|
||||
profiles,
|
||||
profile: Some("work".to_string()),
|
||||
sandbox_mode: Some(Lenient::from(SandboxMode::ReadOnly)),
|
||||
sandbox_mode: Some(SandboxMode::ReadOnly),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -3286,7 +3290,7 @@ async fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::R
|
||||
profiles.insert(
|
||||
"work".to_string(),
|
||||
ConfigProfile {
|
||||
sandbox_mode: Some(Lenient::from(SandboxMode::DangerFullAccess)),
|
||||
sandbox_mode: Some(SandboxMode::DangerFullAccess),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -3396,7 +3400,7 @@ async fn responses_websocket_features_do_not_change_wire_api() -> std::io::Resul
|
||||
async fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg = ConfigToml {
|
||||
mcp_oauth_credentials_store: Some(Lenient::from(OAuthCredentialsStoreMode::File)),
|
||||
mcp_oauth_credentials_store: Some(OAuthCredentialsStoreMode::File),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -3444,9 +3448,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
|
||||
e
|
||||
})?;
|
||||
assert_eq!(
|
||||
cfg.mcp_oauth_credentials_store
|
||||
.clone()
|
||||
.and_then(codex_config::Lenient::into_valid),
|
||||
cfg.mcp_oauth_credentials_store,
|
||||
Some(OAuthCredentialsStoreMode::Keyring),
|
||||
);
|
||||
|
||||
@@ -4570,12 +4572,7 @@ async fn set_model_updates_defaults() -> anyhow::Result<()> {
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
|
||||
assert_eq!(parsed.model.as_deref(), Some("gpt-5.4"));
|
||||
assert_eq!(
|
||||
parsed
|
||||
.model_reasoning_effort
|
||||
.and_then(codex_config::Lenient::into_valid),
|
||||
Some(ReasoningEffort::High)
|
||||
);
|
||||
assert_eq!(parsed.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4606,12 +4603,7 @@ model = "gpt-4.1"
|
||||
let parsed: ConfigToml = toml::from_str(&serialized)?;
|
||||
|
||||
assert_eq!(parsed.model.as_deref(), Some("o4-mini"));
|
||||
assert_eq!(
|
||||
parsed
|
||||
.model_reasoning_effort
|
||||
.and_then(codex_config::Lenient::into_valid),
|
||||
Some(ReasoningEffort::High)
|
||||
);
|
||||
assert_eq!(parsed.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
assert_eq!(
|
||||
parsed
|
||||
.profiles
|
||||
@@ -4642,10 +4634,7 @@ async fn set_model_updates_profile() -> anyhow::Result<()> {
|
||||
|
||||
assert_eq!(profile.model.as_deref(), Some("gpt-5.4"));
|
||||
assert_eq!(
|
||||
profile
|
||||
.model_reasoning_effort
|
||||
.clone()
|
||||
.and_then(codex_config::Lenient::into_valid),
|
||||
profile.model_reasoning_effort,
|
||||
Some(ReasoningEffort::Medium)
|
||||
);
|
||||
|
||||
@@ -4685,10 +4674,7 @@ model = "gpt-5.4"
|
||||
.expect("dev profile should survive updates");
|
||||
assert_eq!(dev_profile.model.as_deref(), Some("o4-high"));
|
||||
assert_eq!(
|
||||
dev_profile
|
||||
.model_reasoning_effort
|
||||
.clone()
|
||||
.and_then(codex_config::Lenient::into_valid),
|
||||
dev_profile.model_reasoning_effort,
|
||||
Some(ReasoningEffort::Medium)
|
||||
);
|
||||
|
||||
|
||||
+72
-228
@@ -15,7 +15,6 @@ use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ConstrainedWithSource;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::Lenient;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::McpServerIdentity;
|
||||
use codex_config::McpServerRequirement;
|
||||
@@ -968,9 +967,10 @@ impl ConfigBuilder {
|
||||
// relative paths to absolute paths based on the parent folder of the
|
||||
// respective config file, so we should be safe to deserialize without
|
||||
// AbsolutePathBufGuard here.
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
let config_toml: ConfigToml = match merged_toml.try_into() {
|
||||
Ok(config_toml) => config_toml,
|
||||
let (_merged_toml, config_toml, enum_warnings) = match config_layer_stack
|
||||
.deserialize_effective_config_with_warnings::<ConfigToml>()
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
if let Some(config_error) = codex_config::first_layer_config_error::<ConfigToml>(
|
||||
&config_layer_stack,
|
||||
@@ -987,6 +987,7 @@ impl ConfigBuilder {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err));
|
||||
}
|
||||
};
|
||||
let config_layer_stack = config_layer_stack.with_additional_startup_warnings(enum_warnings);
|
||||
let config_lock_settings = config_toml
|
||||
.debug
|
||||
.as_ref()
|
||||
@@ -1225,11 +1226,14 @@ pub async fn load_config_as_toml_with_cli_and_loader_overrides(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
let cfg = deserialize_config_toml_with_base(merged_toml, codex_home).map_err(|e| {
|
||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||
e
|
||||
})?;
|
||||
let _guard = AbsolutePathBufGuard::new(codex_home);
|
||||
let (_merged_toml, cfg, _enum_warnings) = config_layer_stack
|
||||
.deserialize_effective_config_with_warnings::<ConfigToml>()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to deserialize overridden config: {e}");
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
@@ -1673,17 +1677,6 @@ fn thread_store_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_lenient_ref_silent<T: Clone>(value: &Option<Lenient<T>>) -> Option<T> {
|
||||
value.as_ref().and_then(Lenient::as_valid).cloned()
|
||||
}
|
||||
|
||||
fn active_profile_field_path(active_profile_name: Option<&str>, field: &str) -> String {
|
||||
active_profile_name.map_or_else(
|
||||
|| field.to_string(),
|
||||
|name| format!("profiles.{name}.{field}"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PermissionConfigSyntax {
|
||||
Legacy,
|
||||
@@ -1848,10 +1841,11 @@ pub fn resolve_oss_provider(
|
||||
|
||||
/// Resolve the web search mode from explicit config and feature flags.
|
||||
fn resolve_web_search_mode(
|
||||
configured_mode: Option<WebSearchMode>,
|
||||
config_toml: &ConfigToml,
|
||||
config_profile: &ConfigProfile,
|
||||
features: &Features,
|
||||
) -> Option<WebSearchMode> {
|
||||
if let Some(mode) = configured_mode {
|
||||
if let Some(mode) = config_profile.web_search.or(config_toml.web_search) {
|
||||
return Some(mode);
|
||||
}
|
||||
if features.enabled(Feature::WebSearchCached) {
|
||||
@@ -2177,12 +2171,6 @@ impl Config {
|
||||
.clone(),
|
||||
None => ConfigProfile::default(),
|
||||
};
|
||||
let profile_sandbox_mode_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "sandbox_mode");
|
||||
let profile_approval_policy_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "approval_policy");
|
||||
let profile_approvals_reviewer_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "approvals_reviewer");
|
||||
let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack);
|
||||
let feature_overrides = FeatureOverrides {
|
||||
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||
@@ -2248,7 +2236,7 @@ impl Config {
|
||||
&config_layer_stack,
|
||||
&cfg,
|
||||
sandbox_mode,
|
||||
valid_lenient_ref_silent(&config_profile.sandbox_mode),
|
||||
config_profile.sandbox_mode,
|
||||
);
|
||||
let has_permission_profiles = cfg
|
||||
.permissions
|
||||
@@ -2451,20 +2439,10 @@ impl Config {
|
||||
// Derive the old `sandbox_mode` defaults as a profile first, then
|
||||
// keep a legacy-compatible projection only for the remaining code
|
||||
// paths that still speak `SandboxPolicy`.
|
||||
let config_sandbox_mode = cfg
|
||||
.sandbox_mode
|
||||
.clone()
|
||||
.and_then(|value| value.into_valid_with_warning("sandbox_mode", &mut startup_warnings));
|
||||
let mut permission_profile = cfg
|
||||
.derive_permission_profile(
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode.clone().and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
&profile_sandbox_mode_path,
|
||||
&mut startup_warnings,
|
||||
)
|
||||
}),
|
||||
config_sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
Some(&active_project),
|
||||
Some(&constrained_permission_profile),
|
||||
@@ -2518,27 +2496,20 @@ impl Config {
|
||||
)
|
||||
};
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
|| valid_lenient_ref_silent(&config_profile.approval_policy).is_some()
|
||||
|| valid_lenient_ref_silent(&cfg.approval_policy).is_some();
|
||||
let mut approval_policy = if let Some(approval_policy) = approval_policy_override {
|
||||
approval_policy
|
||||
} else if let Some(approval_policy) =
|
||||
config_profile.approval_policy.clone().and_then(|value| {
|
||||
value.into_valid_with_warning(&profile_approval_policy_path, &mut startup_warnings)
|
||||
})
|
||||
{
|
||||
approval_policy
|
||||
} else if let Some(approval_policy) = cfg.approval_policy.clone().and_then(|value| {
|
||||
value.into_valid_with_warning("approval_policy", &mut startup_warnings)
|
||||
}) {
|
||||
approval_policy
|
||||
} else if active_project.is_trusted() {
|
||||
AskForApproval::OnRequest
|
||||
} else if active_project.is_untrusted() {
|
||||
AskForApproval::UnlessTrusted
|
||||
} else {
|
||||
AskForApproval::default()
|
||||
};
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some();
|
||||
let mut approval_policy = approval_policy_override
|
||||
.or(config_profile.approval_policy)
|
||||
.or(cfg.approval_policy)
|
||||
.unwrap_or_else(|| {
|
||||
if active_project.is_trusted() {
|
||||
AskForApproval::OnRequest
|
||||
} else if active_project.is_untrusted() {
|
||||
AskForApproval::UnlessTrusted
|
||||
} else {
|
||||
AskForApproval::default()
|
||||
}
|
||||
});
|
||||
if !approval_policy_was_explicit
|
||||
&& let Err(err) = constrained_approval_policy.can_set(&approval_policy)
|
||||
{
|
||||
@@ -2549,28 +2520,12 @@ impl Config {
|
||||
approval_policy = constrained_approval_policy.value();
|
||||
}
|
||||
let approvals_reviewer_was_explicit = approvals_reviewer_override.is_some()
|
||||
|| valid_lenient_ref_silent(&config_profile.approvals_reviewer)
|
||||
.is_some()
|
||||
|| valid_lenient_ref_silent(&cfg.approvals_reviewer).is_some();
|
||||
let mut approvals_reviewer = if let Some(approvals_reviewer) = approvals_reviewer_override {
|
||||
approvals_reviewer
|
||||
} else if let Some(approvals_reviewer) =
|
||||
config_profile.approvals_reviewer.clone().and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
&profile_approvals_reviewer_path,
|
||||
&mut startup_warnings,
|
||||
)
|
||||
})
|
||||
{
|
||||
approvals_reviewer
|
||||
} else {
|
||||
cfg.approvals_reviewer
|
||||
.clone()
|
||||
.and_then(|value| {
|
||||
value.into_valid_with_warning("approvals_reviewer", &mut startup_warnings)
|
||||
})
|
||||
.unwrap_or(ApprovalsReviewer::User)
|
||||
};
|
||||
|| config_profile.approvals_reviewer.is_some()
|
||||
|| cfg.approvals_reviewer.is_some();
|
||||
let mut approvals_reviewer = approvals_reviewer_override
|
||||
.or(config_profile.approvals_reviewer)
|
||||
.or(cfg.approvals_reviewer)
|
||||
.unwrap_or(ApprovalsReviewer::User);
|
||||
if !approvals_reviewer_was_explicit
|
||||
&& let Err(err) = constrained_approvals_reviewer.can_set(&approvals_reviewer)
|
||||
{
|
||||
@@ -2580,17 +2535,7 @@ impl Config {
|
||||
);
|
||||
approvals_reviewer = constrained_approvals_reviewer.value();
|
||||
}
|
||||
let profile_web_search_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "web_search");
|
||||
let configured_web_search_mode = match config_profile.web_search.clone().and_then(|value| {
|
||||
value.into_valid_with_warning(&profile_web_search_path, &mut startup_warnings)
|
||||
}) {
|
||||
Some(mode) => Some(mode),
|
||||
None => cfg.web_search.clone().and_then(|value| {
|
||||
value.into_valid_with_warning("web_search", &mut startup_warnings)
|
||||
}),
|
||||
};
|
||||
let web_search_mode = resolve_web_search_mode(configured_web_search_mode, &features)
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
|
||||
.unwrap_or(WebSearchMode::Cached);
|
||||
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
|
||||
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile);
|
||||
@@ -2761,9 +2706,7 @@ impl Config {
|
||||
}
|
||||
});
|
||||
|
||||
let forced_login_method = cfg.forced_login_method.clone().and_then(|value| {
|
||||
value.into_valid_with_warning("forced_login_method", &mut startup_warnings)
|
||||
});
|
||||
let forced_login_method = cfg.forced_login_method;
|
||||
|
||||
let model = model.or(config_profile.model).or(cfg.model);
|
||||
let mut notices = cfg.notice.unwrap_or_default();
|
||||
@@ -2775,21 +2718,7 @@ impl Config {
|
||||
notices.fast_default_opt_out = Some(true);
|
||||
None
|
||||
}
|
||||
None => {
|
||||
let profile_service_tier_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "service_tier");
|
||||
match config_profile.service_tier.clone().and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
&profile_service_tier_path,
|
||||
&mut startup_warnings,
|
||||
)
|
||||
}) {
|
||||
Some(service_tier) => Some(service_tier),
|
||||
None => cfg.service_tier.clone().and_then(|value| {
|
||||
value.into_valid_with_warning("service_tier", &mut startup_warnings)
|
||||
}),
|
||||
}
|
||||
}
|
||||
None => config_profile.service_tier.or(cfg.service_tier),
|
||||
};
|
||||
let service_tier = match service_tier {
|
||||
Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => {
|
||||
@@ -2854,28 +2783,14 @@ impl Config {
|
||||
auto_review.policy.as_deref(),
|
||||
))
|
||||
});
|
||||
let personality = if personality.is_some() {
|
||||
personality
|
||||
} else {
|
||||
let profile_personality_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "personality");
|
||||
match config_profile.personality.clone().and_then(|value| {
|
||||
value.into_valid_with_warning(&profile_personality_path, &mut startup_warnings)
|
||||
}) {
|
||||
Some(personality) => Some(personality),
|
||||
None => cfg
|
||||
.personality
|
||||
.clone()
|
||||
.and_then(|value| {
|
||||
value.into_valid_with_warning("personality", &mut startup_warnings)
|
||||
})
|
||||
.or_else(|| {
|
||||
features
|
||||
.enabled(Feature::Personality)
|
||||
.then_some(Personality::Pragmatic)
|
||||
}),
|
||||
}
|
||||
};
|
||||
let personality = personality
|
||||
.or(config_profile.personality)
|
||||
.or(cfg.personality)
|
||||
.or_else(|| {
|
||||
features
|
||||
.enabled(Feature::Personality)
|
||||
.then_some(Personality::Pragmatic)
|
||||
});
|
||||
|
||||
let experimental_compact_prompt_path = config_profile
|
||||
.experimental_compact_prompt_file
|
||||
@@ -3039,92 +2954,6 @@ impl Config {
|
||||
.value
|
||||
.set(effective_permission_profile)
|
||||
.map_err(std::io::Error::from)?;
|
||||
let cli_auth_credentials_store_mode = resolve_cli_auth_credentials_store_mode(
|
||||
cfg.cli_auth_credentials_store
|
||||
.clone()
|
||||
.and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
"cli_auth_credentials_store",
|
||||
&mut startup_warnings,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
let mcp_oauth_credentials_store_mode = resolve_mcp_oauth_credentials_store_mode(
|
||||
cfg.mcp_oauth_credentials_store
|
||||
.clone()
|
||||
.and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
"mcp_oauth_credentials_store",
|
||||
&mut startup_warnings,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
let file_opener = cfg
|
||||
.file_opener
|
||||
.and_then(|value| {
|
||||
value.into_valid_with_warning("file_opener", &mut startup_warnings)
|
||||
})
|
||||
.unwrap_or(UriBasedFileOpener::VsCode);
|
||||
let model_reasoning_effort_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "model_reasoning_effort");
|
||||
let model_reasoning_effort = match config_profile.model_reasoning_effort.and_then(|value| {
|
||||
value.into_valid_with_warning(&model_reasoning_effort_path, &mut startup_warnings)
|
||||
}) {
|
||||
Some(effort) => Some(effort),
|
||||
None => cfg.model_reasoning_effort.and_then(|value| {
|
||||
value.into_valid_with_warning("model_reasoning_effort", &mut startup_warnings)
|
||||
}),
|
||||
};
|
||||
let plan_mode_reasoning_effort_path = active_profile_field_path(
|
||||
active_profile_name.as_deref(),
|
||||
"plan_mode_reasoning_effort",
|
||||
);
|
||||
let plan_mode_reasoning_effort =
|
||||
match config_profile.plan_mode_reasoning_effort.and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
&plan_mode_reasoning_effort_path,
|
||||
&mut startup_warnings,
|
||||
)
|
||||
}) {
|
||||
Some(effort) => Some(effort),
|
||||
None => cfg.plan_mode_reasoning_effort.and_then(|value| {
|
||||
value.into_valid_with_warning(
|
||||
"plan_mode_reasoning_effort",
|
||||
&mut startup_warnings,
|
||||
)
|
||||
}),
|
||||
};
|
||||
let model_reasoning_summary_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "model_reasoning_summary");
|
||||
let model_reasoning_summary =
|
||||
match config_profile.model_reasoning_summary.and_then(|value| {
|
||||
value.into_valid_with_warning(&model_reasoning_summary_path, &mut startup_warnings)
|
||||
}) {
|
||||
Some(summary) => Some(summary),
|
||||
None => cfg.model_reasoning_summary.and_then(|value| {
|
||||
value.into_valid_with_warning("model_reasoning_summary", &mut startup_warnings)
|
||||
}),
|
||||
};
|
||||
let model_verbosity_path =
|
||||
active_profile_field_path(active_profile_name.as_deref(), "model_verbosity");
|
||||
let model_verbosity = match config_profile.model_verbosity.and_then(|value| {
|
||||
value.into_valid_with_warning(&model_verbosity_path, &mut startup_warnings)
|
||||
}) {
|
||||
Some(verbosity) => Some(verbosity),
|
||||
None => cfg.model_verbosity.and_then(|value| {
|
||||
value.into_valid_with_warning("model_verbosity", &mut startup_warnings)
|
||||
}),
|
||||
};
|
||||
let experimental_thread_store = thread_store_config(
|
||||
cfg.experimental_thread_store.and_then(|value| {
|
||||
value.into_valid_with_warning("experimental_thread_store", &mut startup_warnings)
|
||||
}),
|
||||
cfg.experimental_thread_store_endpoint,
|
||||
);
|
||||
let config = Self {
|
||||
model,
|
||||
service_tier,
|
||||
@@ -3160,11 +2989,17 @@ impl Config {
|
||||
include_environment_context,
|
||||
// The config.toml omits "_mode" because it's a config file. However, "_mode"
|
||||
// is important in code to differentiate the mode from the store implementation.
|
||||
cli_auth_credentials_store_mode,
|
||||
cli_auth_credentials_store_mode: resolve_cli_auth_credentials_store_mode(
|
||||
cfg.cli_auth_credentials_store.unwrap_or_default(),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
mcp_servers,
|
||||
// The config.toml omits "_mode" because it's a config file. However, "_mode"
|
||||
// is important in code to differentiate the mode from the store implementation.
|
||||
mcp_oauth_credentials_store_mode,
|
||||
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
|
||||
cfg.mcp_oauth_credentials_store.unwrap_or_default(),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
mcp_oauth_callback_port: cfg.mcp_oauth_callback_port,
|
||||
mcp_oauth_callback_url: cfg.mcp_oauth_callback_url.clone(),
|
||||
model_providers,
|
||||
@@ -3213,7 +3048,7 @@ impl Config {
|
||||
config_layer_stack,
|
||||
history,
|
||||
ephemeral: ephemeral.unwrap_or_default(),
|
||||
file_opener,
|
||||
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
||||
codex_self_exe,
|
||||
codex_linux_sandbox_exe,
|
||||
main_execve_wrapper_exe,
|
||||
@@ -3225,12 +3060,18 @@ impl Config {
|
||||
.or(show_raw_agent_reasoning)
|
||||
.unwrap_or(false),
|
||||
guardian_policy_config,
|
||||
model_reasoning_effort,
|
||||
plan_mode_reasoning_effort,
|
||||
model_reasoning_summary,
|
||||
model_reasoning_effort: config_profile
|
||||
.model_reasoning_effort
|
||||
.or(cfg.model_reasoning_effort),
|
||||
plan_mode_reasoning_effort: config_profile
|
||||
.plan_mode_reasoning_effort
|
||||
.or(cfg.plan_mode_reasoning_effort),
|
||||
model_reasoning_summary: config_profile
|
||||
.model_reasoning_summary
|
||||
.or(cfg.model_reasoning_summary),
|
||||
model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries,
|
||||
model_catalog,
|
||||
model_verbosity,
|
||||
model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity),
|
||||
chatgpt_base_url: config_profile
|
||||
.chatgpt_base_url
|
||||
.or(cfg.chatgpt_base_url)
|
||||
@@ -3259,7 +3100,10 @@ impl Config {
|
||||
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
|
||||
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
|
||||
experimental_thread_config_endpoint: cfg.experimental_thread_config_endpoint,
|
||||
experimental_thread_store,
|
||||
experimental_thread_store: thread_store_config(
|
||||
cfg.experimental_thread_store,
|
||||
cfg.experimental_thread_store_endpoint,
|
||||
),
|
||||
forced_chatgpt_workspace_id,
|
||||
forced_login_method,
|
||||
include_apply_patch_tool: include_apply_patch_tool_flag,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::bail;
|
||||
use codex_config::Lenient;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::MemoriesToml;
|
||||
use codex_features::AppsMcpPathOverrideConfigToml;
|
||||
@@ -66,10 +65,10 @@ pub(crate) fn apply_resolved_config_fields(
|
||||
config: &Config,
|
||||
config_toml: &mut ConfigToml,
|
||||
) -> anyhow::Result<()> {
|
||||
config_toml.web_search = Some(Lenient::from(config.web_search_mode.value()));
|
||||
config_toml.web_search = Some(config.web_search_mode.value());
|
||||
config_toml.model_provider = Some(config.model_provider_id.clone());
|
||||
config_toml.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort.map(Lenient::from);
|
||||
config_toml.model_verbosity = config.model_verbosity.map(Lenient::from);
|
||||
config_toml.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort;
|
||||
config_toml.model_verbosity = config.model_verbosity;
|
||||
config_toml.include_permissions_instructions = Some(config.include_permissions_instructions);
|
||||
config_toml.include_apps_instructions = Some(config.include_apps_instructions);
|
||||
config_toml.include_environment_context = Some(config.include_environment_context);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use codex_config::Lenient;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
@@ -16,10 +15,6 @@ use tokio::io::AsyncWriteExt;
|
||||
|
||||
const TEST_TIMESTAMP: &str = "2025-01-01T00-00-00";
|
||||
|
||||
fn valid_lenient<T>(value: Option<Lenient<T>>) -> Option<T> {
|
||||
value.and_then(Lenient::into_valid)
|
||||
}
|
||||
|
||||
async fn read_config_toml(codex_home: &Path) -> io::Result<ConfigToml> {
|
||||
let contents = tokio::fs::read_to_string(codex_home.join("config.toml")).await?;
|
||||
toml::from_str(&contents).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
|
||||
@@ -98,10 +93,7 @@ async fn applies_when_sessions_exist_and_no_personality() -> io::Result<()> {
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -117,10 +109,7 @@ async fn applies_when_only_archived_sessions_exist_and_no_personality() -> io::R
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -156,10 +145,7 @@ async fn skips_when_personality_explicit() -> io::Result<()> {
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Friendly)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Friendly));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use codex_config::Lenient;
|
||||
use codex_config::config_toml::ConfigLockfileToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -93,16 +92,15 @@ fn session_configuration_to_lock_config_toml(
|
||||
/// from the live session when the lockfile should be fully self-contained.
|
||||
fn save_session_resolved_fields(sc: &SessionConfiguration, lock_config: &mut ConfigToml) {
|
||||
lock_config.model = Some(sc.collaboration_mode.model().to_string());
|
||||
lock_config.model_reasoning_effort =
|
||||
sc.collaboration_mode.reasoning_effort().map(Lenient::from);
|
||||
lock_config.model_reasoning_summary = sc.model_reasoning_summary.map(Lenient::from);
|
||||
lock_config.service_tier = sc.service_tier.map(Lenient::from);
|
||||
lock_config.model_reasoning_effort = sc.collaboration_mode.reasoning_effort();
|
||||
lock_config.model_reasoning_summary = sc.model_reasoning_summary;
|
||||
lock_config.service_tier = sc.service_tier;
|
||||
lock_config.instructions = Some(sc.base_instructions.clone());
|
||||
lock_config.developer_instructions = sc.developer_instructions.clone();
|
||||
lock_config.compact_prompt = sc.compact_prompt.clone();
|
||||
lock_config.personality = sc.personality.map(Lenient::from);
|
||||
lock_config.approval_policy = Some(Lenient::from(sc.approval_policy.value()));
|
||||
lock_config.approvals_reviewer = Some(Lenient::from(sc.approvals_reviewer));
|
||||
lock_config.personality = sc.personality;
|
||||
lock_config.approval_policy = Some(sc.approval_policy.value());
|
||||
lock_config.approvals_reviewer = Some(sc.approvals_reviewer);
|
||||
}
|
||||
|
||||
fn drop_lockfile_inputs(lock_config: &mut ConfigToml) {
|
||||
@@ -147,9 +145,7 @@ mod tests {
|
||||
assert_eq!(lock.compact_prompt, sc.compact_prompt);
|
||||
assert_eq!(lock.model, Some(sc.collaboration_mode.model().to_string()));
|
||||
assert_eq!(
|
||||
lock.model_reasoning_effort
|
||||
.clone()
|
||||
.and_then(codex_config::Lenient::into_valid),
|
||||
lock.model_reasoning_effort,
|
||||
sc.collaboration_mode.reasoning_effort()
|
||||
);
|
||||
assert_eq!(lock.profile, None);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use codex_config::Lenient;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_core::ARCHIVED_SESSIONS_SUBDIR;
|
||||
use codex_core::SESSIONS_SUBDIR;
|
||||
@@ -22,10 +21,6 @@ use tokio::io::AsyncWriteExt;
|
||||
|
||||
const TEST_TIMESTAMP: &str = "2025-01-01T00-00-00";
|
||||
|
||||
fn valid_lenient<T>(value: Option<Lenient<T>>) -> Option<T> {
|
||||
value.and_then(Lenient::into_valid)
|
||||
}
|
||||
|
||||
async fn read_config_toml(codex_home: &Path) -> io::Result<ConfigToml> {
|
||||
let contents = tokio::fs::read_to_string(codex_home.join("config.toml")).await?;
|
||||
toml::from_str(&contents).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
|
||||
@@ -191,10 +186,7 @@ async fn no_marker_sessions_sets_personality() -> io::Result<()> {
|
||||
);
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -210,10 +202,7 @@ async fn no_marker_sessions_preserves_existing_config_fields() -> io::Result<()>
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.model, Some("gpt-5.4".to_string()));
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -332,10 +321,7 @@ async fn applied_migration_is_idempotent_on_second_run() -> io::Result<()> {
|
||||
assert_eq!(first_status, PersonalityMigrationStatus::Applied);
|
||||
assert_eq!(second_status, PersonalityMigrationStatus::SkippedMarker);
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -354,9 +340,6 @@ async fn no_marker_archived_sessions_sets_personality() -> io::Result<()> {
|
||||
);
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(
|
||||
valid_lenient(persisted.personality),
|
||||
Some(Personality::Pragmatic)
|
||||
);
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user