Enforce exact managed config values

This commit is contained in:
Abhinav Vedmala
2026-06-16 06:07:11 +00:00
parent 822b6dc1b5
commit 232618fa9e
8 changed files with 372 additions and 39 deletions
+122 -30
View File
@@ -20,6 +20,7 @@ use crate::ConstraintError;
use crate::ManagedHooksRequirementsToml;
use crate::mcp_types::AppToolApproval;
use crate::permissions_toml::PermissionProfileToml;
use crate::types::FeedbackConfigToml;
use crate::types::WindowsSandboxModeToml;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -142,10 +143,17 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
/// normalization.
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigRequirements {
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
pub check_for_update_on_startup: Option<Sourced<bool>>,
pub allow_login_shell: Option<Sourced<bool>>,
pub feedback: Option<Sourced<FeedbackConfigToml>>,
pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
pub windows_sandbox_mode: ConstrainedWithSource<Option<WindowsSandboxModeToml>>,
pub windows_sandbox_private_desktop: Option<Sourced<bool>>,
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
pub allow_managed_hooks_only: Option<Sourced<bool>>,
pub allow_appshots: Option<Sourced<bool>>,
@@ -168,6 +176,12 @@ pub struct ConfigRequirements {
impl Default for ConfigRequirements {
fn default() -> Self {
Self {
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
allow_login_shell: None,
feedback: None,
approval_policy: ConstrainedWithSource::new(
Constrained::allow_any_from_default(),
/*source*/ None,
@@ -184,6 +198,7 @@ impl Default for ConfigRequirements {
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
windows_sandbox_private_desktop: None,
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Cached),
/*source*/ None,
@@ -716,11 +731,12 @@ impl ComputerUseRequirementsToml {
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct WindowsRequirementsToml {
pub allowed_sandbox_implementations: Option<Vec<WindowsSandboxModeToml>>,
pub sandbox_private_desktop: Option<bool>,
}
impl WindowsRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_sandbox_implementations.is_none()
self.allowed_sandbox_implementations.is_none() && self.sandbox_private_desktop.is_none()
}
}
@@ -821,6 +837,12 @@ pub(crate) fn merge_app_requirements_descending(
/// Base config deserialized from system `requirements.toml` or MDM.
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsToml {
pub sqlite_home: Option<AbsolutePathBuf>,
pub log_dir: Option<AbsolutePathBuf>,
pub model_catalog_json: Option<AbsolutePathBuf>,
pub check_for_update_on_startup: Option<bool>,
pub allow_login_shell: Option<bool>,
pub feedback: Option<FeedbackConfigToml>,
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
@@ -877,6 +899,12 @@ impl<T> std::ops::Deref for Sourced<T> {
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsWithSources {
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
pub check_for_update_on_startup: Option<Sourced<bool>>,
pub allow_login_shell: Option<Sourced<bool>>,
pub feedback: Option<Sourced<FeedbackConfigToml>>,
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
@@ -919,6 +947,12 @@ impl ConfigRequirementsWithSources {
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
// forces this merge logic to be updated.
let ConfigRequirementsToml {
sqlite_home: _,
log_dir: _,
model_catalog_json: _,
check_for_update_on_startup: _,
allow_login_shell: _,
feedback: _,
allowed_approval_policies: _,
allowed_approvals_reviewers: _,
allowed_sandbox_modes: _,
@@ -956,6 +990,12 @@ impl ConfigRequirementsWithSources {
other,
source,
{
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
allow_login_shell,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -990,6 +1030,12 @@ impl ConfigRequirementsWithSources {
pub fn into_toml(self) -> ConfigRequirementsToml {
let ConfigRequirementsWithSources {
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
allow_login_shell,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -1013,6 +1059,12 @@ impl ConfigRequirementsWithSources {
guardian_policy_config,
} = self;
ConfigRequirementsToml {
sqlite_home: sqlite_home.map(|sourced| sourced.value),
log_dir: log_dir.map(|sourced| sourced.value),
model_catalog_json: model_catalog_json.map(|sourced| sourced.value),
check_for_update_on_startup: check_for_update_on_startup.map(|sourced| sourced.value),
allow_login_shell: allow_login_shell.map(|sourced| sourced.value),
feedback: feedback.map(|sourced| sourced.value),
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value),
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
@@ -1103,7 +1155,16 @@ impl ConfigRequirementsToml {
}
pub fn is_empty(&self) -> bool {
self.allowed_approval_policies.is_none()
self.sqlite_home.is_none()
&& self.log_dir.is_none()
&& self.model_catalog_json.is_none()
&& self.check_for_update_on_startup.is_none()
&& self.allow_login_shell.is_none()
&& self
.feedback
.as_ref()
.is_none_or(|feedback| feedback == &FeedbackConfigToml::default())
&& self.allowed_approval_policies.is_none()
&& self.allowed_approvals_reviewers.is_none()
&& self.allowed_sandbox_modes.is_none()
&& self.allowed_permission_profiles.is_none()
@@ -1157,6 +1218,12 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
// config loading and requirements API projection. The normalized
// constraints below only need the compiled PermissionProfile envelope.
let ConfigRequirementsWithSources {
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
allow_login_shell,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -1277,42 +1344,60 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
/*source*/ None,
),
};
let windows_sandbox_mode = match windows {
let (windows_sandbox_mode, windows_sandbox_private_desktop) = match windows {
Some(Sourced {
value:
WindowsRequirementsToml {
allowed_sandbox_implementations: Some(implementations),
allowed_sandbox_implementations,
sandbox_private_desktop,
},
source: requirement_source,
}) => {
if implementations.is_empty() {
return Err(ConstraintError::empty_field(
"windows.allowed_sandbox_implementations",
));
}
// Prefer elevated when both Windows sandbox implementations are allowed.
let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) {
WindowsSandboxModeToml::Elevated
} else {
WindowsSandboxModeToml::Unelevated
};
let sandbox_private_desktop = sandbox_private_desktop
.map(|value| Sourced::new(value, requirement_source.clone()));
let sandbox_mode = match allowed_sandbox_implementations {
Some(implementations) => {
if implementations.is_empty() {
return Err(ConstraintError::empty_field(
"windows.allowed_sandbox_implementations",
));
}
// Prefer elevated when both Windows sandbox implementations are allowed.
let initial_value =
if implementations.contains(&WindowsSandboxModeToml::Elevated) {
WindowsSandboxModeToml::Elevated
} else {
WindowsSandboxModeToml::Unelevated
};
let requirement_source_for_error = requirement_source.clone();
let constrained =
Constrained::new(Some(initial_value), move |candidate| match candidate {
Some(candidate) if implementations.contains(candidate) => Ok(()),
_ => Err(ConstraintError::InvalidValue {
field_name: "windows.sandbox",
candidate: format!("{candidate:?}"),
allowed: format!("{implementations:?}"),
requirement_source: requirement_source_for_error.clone(),
}),
})?;
ConstrainedWithSource::new(constrained, Some(requirement_source))
let requirement_source_for_error = requirement_source.clone();
let constrained = Constrained::new(
Some(initial_value),
move |candidate| match candidate {
Some(candidate) if implementations.contains(candidate) => Ok(()),
_ => Err(ConstraintError::InvalidValue {
field_name: "windows.sandbox",
candidate: format!("{candidate:?}"),
allowed: format!("{implementations:?}"),
requirement_source: requirement_source_for_error.clone(),
}),
},
)?;
ConstrainedWithSource::new(constrained, Some(requirement_source))
}
None => ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
};
(sandbox_mode, sandbox_private_desktop)
}
Some(_) | None => ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
None => (
ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
None,
),
};
let exec_policy = match rules {
@@ -1437,10 +1522,17 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
});
let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source);
Ok(ConfigRequirements {
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
allow_login_shell,
feedback,
approval_policy,
approvals_reviewer,
permission_profile,
windows_sandbox_mode,
windows_sandbox_private_desktop,
web_search_mode,
allow_managed_hooks_only,
allow_appshots,
@@ -173,6 +173,12 @@ fn populate_merged_regular_fields_with_sources(
// Destructure without `..` so every new requirements field must choose
// whether it belongs in the regular TOML merge path or in a special merger.
let ConfigRequirementsToml {
sqlite_home,
log_dir,
model_catalog_json,
check_for_update_on_startup,
allow_login_shell,
feedback,
allowed_approval_policies,
allowed_approvals_reviewers,
allowed_sandbox_modes,
@@ -197,6 +203,15 @@ fn populate_merged_regular_fields_with_sources(
guardian_policy_config,
} = requirements;
set_sourced!(sqlite_home, &["sqlite_home"]);
set_sourced!(log_dir, &["log_dir"]);
set_sourced!(model_catalog_json, &["model_catalog_json"]);
set_sourced!(
check_for_update_on_startup,
&["check_for_update_on_startup"]
);
set_sourced!(allow_login_shell, &["allow_login_shell"]);
set_sourced!(feedback, &["feedback"]);
set_sourced!(allowed_approval_policies, &["allowed_approval_policies"]);
set_sourced!(
allowed_approvals_reviewers,
+80
View File
@@ -8330,6 +8330,12 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
let fixture = create_test_fixture()?;
let requirements_toml = codex_config::ConfigRequirementsToml {
sqlite_home: None,
log_dir: None,
model_catalog_json: None,
check_for_update_on_startup: None,
allow_login_shell: None,
feedback: None,
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
@@ -10848,3 +10854,77 @@ fn test_tui_notification_condition_rejects_unknown_value() {
"unexpected error: {err}"
);
}
async fn load_with_enterprise_requirement(
codex_home: &TempDir,
requirements: impl Into<String>,
) -> std::io::Result<Config> {
ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements),
)
.build()
.await
}
#[tokio::test]
async fn exact_requirements_override_only_managed_leaves() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let catalog_path = codex_home.path().join("required-models.json");
let mut catalog = bundled_models_response()
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
catalog.models = catalog.models.into_iter().take(1).collect();
std::fs::write(
&catalog_path,
serde_json::to_string(&catalog).expect("serialize catalog"),
)?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
check_for_update_on_startup = true
allow_login_shell = true
[feedback]
enabled = true
[windows]
sandbox_private_desktop = true
"#,
)?;
let required_sqlite_home = codex_home.path().join("required-state");
let required_log_dir = codex_home.path().join("required-logs");
let requirements = format!(
r#"
sqlite_home = {:?}
log_dir = {:?}
model_catalog_json = {:?}
check_for_update_on_startup = false
allow_login_shell = false
[feedback]
enabled = false
[windows]
sandbox_private_desktop = false
"#,
required_sqlite_home.display(),
required_log_dir.display(),
catalog_path.display(),
);
let config = load_with_enterprise_requirement(&codex_home, requirements).await?;
assert_eq!(config.sqlite_home, required_sqlite_home);
assert_eq!(config.log_dir, required_log_dir);
assert_eq!(config.model_catalog, Some(catalog));
assert!(!config.check_for_update_on_startup);
assert!(!config.permissions.allow_login_shell);
assert!(!config.feedback_enabled);
assert!(!config.permissions.windows_sandbox_private_desktop);
assert!(config.startup_warnings.iter().any(|warning| {
warning.contains("Configured value for `check_for_update_on_startup` is overridden")
}));
Ok(())
}
+21 -8
View File
@@ -145,6 +145,7 @@ mod managed_features;
mod network_proxy_spec;
mod otel;
mod permissions;
mod requirements;
mod resolved_permission_profile;
#[cfg(test)]
mod schema;
@@ -2642,7 +2643,7 @@ impl Config {
pub(crate) async fn load_config_with_layer_stack(
fs: &dyn ExecutorFileSystem,
cfg: ConfigToml,
mut cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
@@ -2658,13 +2659,30 @@ impl Config {
validate_model_providers(&cfg.model_providers)
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
// Ensure that every field of ConfigRequirements is applied to the final
// Config.
let mut startup_warnings = config_layer_stack
.startup_warnings()
.unwrap_or_default()
.to_vec();
requirements::apply_to_config(
&mut cfg,
config_layer_stack.requirements(),
&mut startup_warnings,
)?;
// Destructure every field to ensure ConfigRequirements additions are
// either applied above or handled while constructing the final Config.
let ConfigRequirements {
sqlite_home: _,
log_dir: _,
model_catalog_json: _,
check_for_update_on_startup: _,
allow_login_shell: _,
feedback: _,
approval_policy: mut constrained_approval_policy,
approvals_reviewer: mut constrained_approvals_reviewer,
permission_profile: mut constrained_permission_profile,
windows_sandbox_mode: mut constrained_windows_sandbox_mode,
windows_sandbox_private_desktop: _,
web_search_mode: mut constrained_web_search_mode,
allow_managed_hooks_only: _,
allow_appshots: _,
@@ -2681,11 +2699,6 @@ impl Config {
guardian_policy_config_source: _,
} = config_layer_stack.requirements().clone();
let mut startup_warnings = config_layer_stack
.startup_warnings()
.unwrap_or_default()
.to_vec();
// Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides {
model,
+125
View File
@@ -0,0 +1,125 @@
use codex_config::ConfigRequirements;
use codex_config::RequirementSource;
use codex_config::Sourced;
use codex_config::config_toml::ConfigToml;
use codex_config::types::FeedbackConfigToml;
/// Applies managed requirements to regular config before final config construction.
///
/// Managed values replace their configured counterparts, and conflicts produce
/// source-aware startup warnings.
pub(super) fn apply_to_config(
config: &mut ConfigToml,
requirements: &ConfigRequirements,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<()> {
macro_rules! apply_exact {
($field:ident) => {
apply_exact_requirement(
stringify!($field),
&mut config.$field,
requirements.$field.as_ref(),
startup_warnings,
);
};
}
apply_exact!(sqlite_home);
apply_exact!(log_dir);
apply_exact!(model_catalog_json);
apply_exact!(check_for_update_on_startup);
apply_exact!(allow_login_shell);
apply_feedback_requirement(
&mut config.feedback,
requirements.feedback.as_ref(),
startup_warnings,
);
if let Some(requirement) = requirements.windows_sandbox_private_desktop.as_ref() {
apply_exact_requirement(
"windows.sandbox_private_desktop",
&mut config
.windows
.get_or_insert_default()
.sandbox_private_desktop,
Some(requirement),
startup_warnings,
);
}
Ok(())
}
fn apply_exact_requirement<T>(
field_name: &'static str,
configured_value: &mut Option<T>,
requirement: Option<&Sourced<T>>,
startup_warnings: &mut Vec<String>,
) where
T: Clone + PartialEq + std::fmt::Debug,
{
let Some(Sourced { value, source }) = requirement else {
return;
};
if configured_value
.as_ref()
.is_some_and(|configured| configured != value)
{
tracing::warn!(
?source,
?value,
"configured value is overridden by an exact requirement for {field_name}"
);
startup_warnings.push(format!(
"Configured value for `{field_name}` is overridden by the required value {value:?} from {source}."
));
}
*configured_value = Some(value.clone());
}
pub(super) fn replace_required_leaf<T: Clone + PartialEq>(
configured: &mut Option<T>,
required: &Option<T>,
) -> bool {
let Some(required) = required else {
return false;
};
let conflict = configured
.as_ref()
.is_some_and(|configured| configured != required);
*configured = Some(required.clone());
conflict
}
fn apply_feedback_requirement(
configured: &mut Option<FeedbackConfigToml>,
requirement: Option<&Sourced<FeedbackConfigToml>>,
startup_warnings: &mut Vec<String>,
) {
let Some(Sourced { value, source }) = requirement else {
return;
};
let FeedbackConfigToml { enabled } = value;
let configured = configured.get_or_insert_default();
let conflict = replace_required_leaf(&mut configured.enabled, enabled);
push_structured_requirement_override_warning("feedback", conflict, source, startup_warnings);
}
/// Emits one source-aware warning when a structured requirement replaces one
/// or more configured values.
pub(super) fn push_structured_requirement_override_warning(
field_name: &str,
conflict: bool,
source: &RequirementSource,
startup_warnings: &mut Vec<String>,
) {
if !conflict {
return;
}
tracing::warn!(
?source,
"configured values are overridden by requirements for {field_name}"
);
startup_warnings.push(format!(
"Configured values under `{field_name}` are overridden by requirements from {source}."
));
}
@@ -54,6 +54,7 @@ fn windows_sandbox_requirements_stack(
let requirements_toml = codex_config::ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(allowed_sandbox_implementations),
sandbox_private_desktop: None,
}),
..Default::default()
};
+2
View File
@@ -790,6 +790,7 @@ mod tests {
enforce_residency: Some(ResidencyRequirement::Us),
network: None,
permissions: None,
..ConfigRequirementsToml::default()
};
let user_file = if cfg!(windows) {
@@ -1147,6 +1148,7 @@ approval_policy = "never"
enforce_residency: None,
network: None,
permissions: None,
..ConfigRequirementsToml::default()
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
+6 -1
View File
@@ -1124,7 +1124,12 @@ pub async fn run_main(
.config_layer_stack
.effective_config()
.as_table()
.is_some_and(|table| table.contains_key("log_dir"));
.is_some_and(|table| table.contains_key("log_dir"))
|| config
.config_layer_stack
.requirements_toml()
.log_dir
.is_some();
#[allow(clippy::print_stderr)]
match check_execpolicy_for_warnings(&config.config_layer_stack).await {