Print warning when config does not meet requirements (#10792)

<img width="1019" height="284" alt="Screenshot 2026-02-05 at 23 34 08"
src="https://github.com/user-attachments/assets/19ec3ce1-3c3b-40f5-b251-a31d964bf3bb"
/>

Currently, if a config value is set that fails the requirements, we exit
Codex.

Now, instead of this, we print a warning and default to a
requirements-permitting value.
This commit is contained in:
gt-oai 2026-02-06 01:12:44 +00:00 committed by GitHub
parent 0d8b2b74c4
commit d74fa8edd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 112 additions and 24 deletions

View file

@ -961,6 +961,14 @@ impl Session {
}),
});
}
for message in &config.startup_warnings {
post_session_configured_events.push(Event {
id: "".to_owned(),
msg: EventMsg::Warning(WarningEvent {
message: message.clone(),
}),
});
}
maybe_push_unstable_features_warning(&config, &mut post_session_configured_events);
let auth = auth.as_ref();

View file

@ -21,6 +21,7 @@ use crate::config::types::UriBasedFileOpener;
use crate::config_loader::CloudRequirementsLoader;
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConstrainedWithSource;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::McpServerIdentity;
use crate::config_loader::McpServerRequirement;
@ -116,6 +117,9 @@ pub struct Config {
/// requirements).
pub config_layer_stack: ConfigLayerStack,
/// Warnings collected during config load that should be shown on startup.
pub startup_warnings: Vec<String>,
/// Optional override of model selection.
pub model: Option<String>,
@ -601,6 +605,41 @@ fn constrain_mcp_servers(
})
}
fn apply_requirement_constrained_value<T>(
field_name: &'static str,
configured_value: T,
constrained_value: &mut ConstrainedWithSource<T>,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<()>
where
T: Clone + std::fmt::Debug + Send + Sync,
{
if let Err(err) = constrained_value.set(configured_value) {
let fallback_value = constrained_value.get().clone();
tracing::warn!(
error = %err,
?fallback_value,
requirement_source = ?constrained_value.source,
"configured value is disallowed by requirements; falling back to required value for {field_name}"
);
let message = format!(
"Configured value for `{field_name}` is disallowed by requirements; falling back to required value {fallback_value:?}. Details: {err}"
);
startup_warnings.push(message);
constrained_value.set(fallback_value).map_err(|fallback_err| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"configured value for `{field_name}` is disallowed by requirements ({err}); fallback to a requirement-compliant value also failed ({fallback_err})"
),
)
})?;
}
Ok(())
}
fn mcp_server_matches_requirement(
requirement: &McpServerRequirement,
server: &McpServerConfig,
@ -1320,6 +1359,7 @@ impl Config {
) -> std::io::Result<Self> {
let requirements = config_layer_stack.requirements().clone();
let user_instructions = Self::load_instructions(Some(&codex_home));
let mut startup_warnings = Vec::new();
// Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides {
@ -1586,12 +1626,18 @@ impl Config {
enforce_residency,
} = requirements;
constrained_approval_policy
.set(approval_policy)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
constrained_sandbox_policy
.set(sandbox_policy)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
apply_requirement_constrained_value(
"approval_policy",
approval_policy,
&mut constrained_approval_policy,
&mut startup_warnings,
)?;
apply_requirement_constrained_value(
"sandbox_mode",
sandbox_policy,
&mut constrained_sandbox_policy,
&mut startup_warnings,
)?;
let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
@ -1604,6 +1650,7 @@ impl Config {
model_provider_id,
model_provider,
cwd: resolved_cwd,
startup_warnings,
approval_policy: constrained_approval_policy.value,
sandbox_policy: constrained_sandbox_policy.value,
enforce_residency: enforce_residency.value,
@ -3853,6 +3900,7 @@ model_verbosity = "high"
codex_home: fixture.codex_home(),
log_dir: fixture.codex_home().join("log"),
config_layer_stack: Default::default(),
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
@ -3940,6 +3988,7 @@ model_verbosity = "high"
codex_home: fixture.codex_home(),
log_dir: fixture.codex_home().join("log"),
config_layer_stack: Default::default(),
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
@ -4042,6 +4091,7 @@ model_verbosity = "high"
codex_home: fixture.codex_home(),
log_dir: fixture.codex_home().join("log"),
config_layer_stack: Default::default(),
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
@ -4130,6 +4180,7 @@ model_verbosity = "high"
codex_home: fixture.codex_home(),
log_dir: fixture.codex_home().join("log"),
config_layer_stack: Default::default(),
startup_warnings: Vec::new(),
history: History::default(),
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
@ -4681,7 +4732,7 @@ mcp_oauth_callback_port = 5678
}
#[tokio::test]
async fn explicit_sandbox_mode_still_errors_when_disallowed_by_requirements()
async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
@ -4700,19 +4751,15 @@ mcp_oauth_callback_port = 5678
enforce_residency: None,
};
let err = ConfigBuilder::default()
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_requirements(CloudRequirementsLoader::new(
async move { Some(requirements) },
))
.build()
.await
.expect_err("explicit disallowed mode should still fail");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
let message = err.to_string();
assert!(message.contains("invalid value for `sandbox_mode`"));
assert!(message.contains("set by cloud requirements"));
.await?;
assert_eq!(*config.sandbox_policy.get(), SandboxPolicy::ReadOnly);
Ok(())
}
@ -4749,7 +4796,7 @@ trust_level = "untrusted"
}
#[tokio::test]
async fn explicit_approval_policy_still_errors_when_disallowed_by_requirements()
async fn explicit_approval_policy_falls_back_when_disallowed_by_requirements()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
@ -4758,7 +4805,7 @@ trust_level = "untrusted"
"#,
)?;
let err = ConfigBuilder::default()
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_requirements(CloudRequirementsLoader::new(async {
@ -4768,12 +4815,8 @@ trust_level = "untrusted"
})
}))
.build()
.await
.expect_err("explicit disallowed approval policy should fail");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
let message = err.to_string();
assert!(message.contains("invalid value for `approval_policy`"));
assert!(message.contains("set by cloud requirements"));
.await?;
assert_eq!(config.approval_policy.value(), AskForApproval::OnRequest);
Ok(())
}
}

View file

@ -1168,6 +1168,37 @@ mod tests {
#[tokio::test]
async fn build_agent_spawn_config_uses_turn_context_values() {
fn pick_allowed_approval_policy(
constraint: &crate::config::Constrained<AskForApproval>,
base: AskForApproval,
) -> AskForApproval {
let candidates = [
AskForApproval::Never,
AskForApproval::UnlessTrusted,
AskForApproval::OnRequest,
AskForApproval::OnFailure,
];
candidates
.into_iter()
.find(|candidate| *candidate != base && constraint.can_set(candidate).is_ok())
.unwrap_or(base)
}
fn pick_allowed_sandbox_policy(
constraint: &crate::config::Constrained<SandboxPolicy>,
base: SandboxPolicy,
) -> SandboxPolicy {
let candidates = [
SandboxPolicy::new_read_only_policy(),
SandboxPolicy::new_workspace_write_policy(),
SandboxPolicy::DangerFullAccess,
];
candidates
.into_iter()
.find(|candidate| *candidate != base && constraint.can_set(candidate).is_ok())
.unwrap_or(base)
}
let (_session, mut turn) = make_session_and_context().await;
let base_instructions = BaseInstructions {
text: "base".to_string(),
@ -1181,8 +1212,14 @@ mod tests {
let temp_dir = tempfile::tempdir().expect("temp dir");
turn.cwd = temp_dir.path().to_path_buf();
turn.codex_linux_sandbox_exe = Some(PathBuf::from("/bin/echo"));
turn.approval_policy = AskForApproval::Never;
turn.sandbox_policy = SandboxPolicy::DangerFullAccess;
turn.approval_policy = pick_allowed_approval_policy(
&turn.config.approval_policy,
*turn.config.approval_policy.get(),
);
turn.sandbox_policy = pick_allowed_sandbox_policy(
&turn.config.sandbox_policy,
turn.config.sandbox_policy.get().clone(),
);
let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config");
let mut expected = (*turn.config).clone();