Enforce exact values from managed config requirements (#34597)

## What changed

- Apply managed values for `sqlite_home`, `log_dir`, `model_catalog_json`,
  `check_for_update_on_startup`, `allow_login_shell`, the report-submission
  opt-in, and `windows.sandbox_private_desktop` to the runtime configuration,
  with source-aware warnings when they override configured values.
- Expose these values through `configRequirements/read`, return them from
  `config/read`, include them in session config locks, and show their sources in
  the TUI config debug view.
- Reject app-server writes that overlap an exact managed requirement with
  `configRequirementReadonly` while leaving unrelated sibling keys writable.

## Testing

- Cover runtime overrides, requirement API mapping, config reads and writes,
  session lock serialization, cross-platform path URIs, and debug output.

GitOrigin-RevId: d4e23b6fd7775c1a6dfe2eab313cdcee50b07081
This commit is contained in:
Abhinav authored and copyberry committed 2026-07-21 19:08:58 +00:00
1 parent 7442f5f932
commit ee71c4a90f
34 files changed
+1566 -71

No files matched your search

@@ -1164,6 +1164,56 @@ allowed_sandbox_modes = ["read-only"]
Ok(())
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn managed_preferences_requirements_resolve_paths_against_codex_home() -> anyhow::Result<()> {
use base64::Engine;
let tmp = tempdir()?;
let codex_home = tmp.path().join("codex-home");
std::fs::create_dir_all(&codex_home)?;
let mut loader_overrides =
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
loader_overrides.macos_managed_config_requirements_base64 = Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
sqlite_home = "state"
log_dir = "~/.codex/logs"
model_catalog_json = "models.json"
"#
.as_bytes(),
),
);
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
loader_overrides,
&codex_config::NoopThreadConfigLoader,
)
.await?;
let expected_log_dir = AbsolutePathBuf::resolve_path_against_base("~/.codex/logs", &codex_home);
let requirements = layers.requirements_toml();
assert_eq!(
requirements.sqlite_home.as_deref(),
Some(codex_home.join("state").as_path())
);
assert_eq!(
requirements.log_dir.as_deref(),
Some(expected_log_dir.as_path())
);
assert_eq!(
requirements.model_catalog_json.as_deref(),
Some(codex_home.join("models.json").as_path())
);
Ok(())
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn managed_preferences_requirements_take_precedence() -> anyhow::Result<()> {
+137
View File
@@ -9093,6 +9093,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,
@@ -11710,3 +11716,134 @@ 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_apply_to_runtime_config() -> 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(())
}
#[tokio::test]
async fn absent_allow_login_shell_does_not_report_an_override() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = load_with_enterprise_requirement(&codex_home, "allow_login_shell = false").await?;
assert!(!config.permissions.allow_login_shell);
assert!(
config
.startup_warnings
.iter()
.all(|warning| !warning.contains("allow_login_shell"))
);
Ok(())
}
#[test]
fn sqlite_home_env_conflict_reports_an_override() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let required = AbsolutePathBuf::try_from(codex_home.path().join("required-state"))?;
let environment = codex_home.path().join("environment-state");
let requirement = Sourced::new(required.clone(), RequirementSource::Unknown);
let mut warnings = Vec::new();
super::requirements::push_sqlite_home_env_override_warning(
/*configured_sqlite_home*/ None,
Some(environment.as_path()),
Some(&requirement),
&mut warnings,
);
assert_eq!(
warnings,
vec![format!(
"Environment value for `$CODEX_SQLITE_HOME` is overridden by the required `sqlite_home` value {required:?} from {}.",
RequirementSource::Unknown
)]
);
warnings.clear();
super::requirements::push_sqlite_home_env_override_warning(
/*configured_sqlite_home*/ None,
Some(required.as_path()),
Some(&requirement),
&mut warnings,
);
assert!(warnings.is_empty());
super::requirements::push_sqlite_home_env_override_warning(
Some(&required),
Some(environment.as_path()),
Some(&requirement),
&mut warnings,
);
assert!(warnings.is_empty());
Ok(())
}
+30 -9
View File
@@ -153,6 +153,7 @@ mod network_proxy_spec;
mod otel;
mod permission_profile_catalog;
mod permissions;
mod requirements;
mod resolved_permission_profile;
#[cfg(test)]
mod schema;
@@ -3037,7 +3038,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,
@@ -3058,13 +3059,31 @@ impl Config {
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.skills.as_ref()));
let orchestrator_mcp_enabled =
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.mcp.as_ref()));
// 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();
let configured_sqlite_home = cfg.sqlite_home.clone();
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: _,
@@ -3082,11 +3101,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,
@@ -3754,11 +3768,18 @@ impl Config {
.as_ref()
.map(AbsolutePathBuf::to_path_buf)
.unwrap_or_else(|| codex_home.join("log").to_path_buf());
let sqlite_home_env = resolve_sqlite_home_env(&resolved_cwd);
requirements::push_sqlite_home_env_override_warning(
configured_sqlite_home.as_ref(),
sqlite_home_env.as_deref(),
config_layer_stack.requirements().sqlite_home.as_ref(),
&mut startup_warnings,
);
let sqlite_home = cfg
.sqlite_home
.as_ref()
.map(AbsolutePathBuf::to_path_buf)
.or_else(|| resolve_sqlite_home_env(&resolved_cwd))
.or(sqlite_home_env)
.unwrap_or_else(|| codex_home.to_path_buf());
let original_permission_profile = permission_profile.clone();
apply_requirement_constrained_value(
+154
View File
@@ -0,0 +1,154 @@
use codex_config::ConfigRequirements;
use codex_config::RequirementSource;
use codex_config::Sourced;
use codex_config::config_toml::ConfigToml;
use codex_config::types::FeedbackConfigToml;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
/// 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>,
) {
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,
);
}
}
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());
}
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);
}
pub(super) fn push_sqlite_home_env_override_warning(
configured_sqlite_home: Option<&AbsolutePathBuf>,
sqlite_home_env: Option<&Path>,
requirement: Option<&Sourced<AbsolutePathBuf>>,
startup_warnings: &mut Vec<String>,
) {
if configured_sqlite_home.is_some() {
return;
}
let Some(sqlite_home_env) = sqlite_home_env else {
return;
};
let Some(Sourced { value, source }) = requirement else {
return;
};
if sqlite_home_env == value.as_path() {
return;
}
tracing::warn!(
?source,
?value,
"`CODEX_SQLITE_HOME` is overridden by an exact requirement for sqlite_home"
);
startup_warnings.push(format!(
"Environment value for `$CODEX_SQLITE_HOME` is overridden by the required `sqlite_home` value {value:?} from {source}."
));
}
/// Emits one source-aware warning when a structured requirement replaces one
/// or more configured values.
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}."
));
}
+103 -1
View File
@@ -93,13 +93,17 @@ fn session_configuration_to_lock_config_toml(
.effective_config()
.try_into()
.context("failed to deserialize effective config for config lock")?;
if config.config_lock_save_fields_resolved_from_model_catalog {
save_session_resolved_fields(sc, &mut lock_config);
}
save_config_resolved_fields(config, &mut lock_config)?;
drop_lockfile_inputs(&mut lock_config);
// Apply exact managed values last so cleanup cannot discard their runtime-effective values.
config
.config_layer_stack
.requirements_toml()
.apply_exact_to_config(&mut lock_config);
Ok(lock_config)
}
@@ -235,9 +239,23 @@ where
#[cfg(test)]
mod tests {
use super::*;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_models_manager::bundled_models_response;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::sync::Arc;
fn write_model_catalog(path: &Path) {
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(
path,
serde_json::to_string(&catalog).expect("serialize model catalog"),
)
.expect("write model catalog");
}
#[tokio::test]
async fn lock_contains_prompts_and_materializes_features() {
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
@@ -395,6 +413,90 @@ mod tests {
assert_eq!(lock.approvals_reviewer, None);
}
#[tokio::test]
async fn lock_contains_exact_managed_requirements() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let sqlite_home = codex_home.path().join("managed-state");
let log_dir = codex_home.path().join("managed-logs");
let catalog_path = codex_home.path().join("managed-models.json");
write_model_catalog(&catalog_path);
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
"#,
sqlite_home.display(),
log_dir.display(),
catalog_path.display(),
);
let mut config = crate::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
.expect("config should load");
config.config_lock_save_fields_resolved_from_model_catalog = false;
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
sc.original_config_do_not_use = Arc::new(config);
let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize");
let lock = &lockfile.config;
assert_eq!(lock.sqlite_home.as_deref(), Some(sqlite_home.as_path()));
assert_eq!(lock.log_dir.as_deref(), Some(log_dir.as_path()));
assert_eq!(
lock.model_catalog_json.as_deref(),
Some(catalog_path.as_path())
);
assert_eq!(lock.check_for_update_on_startup, Some(false));
assert_eq!(lock.allow_login_shell, Some(false));
assert_eq!(
lock.feedback.as_ref().and_then(|feedback| feedback.enabled),
Some(false)
);
assert_eq!(
lock.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop),
Some(false)
);
}
#[tokio::test]
async fn lock_drops_unmanaged_model_catalog_input() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let catalog_path = codex_home.path().join("user-models.json");
write_model_catalog(&catalog_path);
let config = crate::config::ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cli_overrides(vec![(
"model_catalog_json".to_string(),
toml::Value::String(catalog_path.display().to_string()),
)])
.build()
.await
.expect("config should load");
let mut sc = crate::session::tests::make_session_configuration_for_tests().await;
sc.original_config_do_not_use = Arc::new(config);
let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize");
assert_eq!(lockfile.config.model_catalog_json, None);
}
#[tokio::test]
async fn lock_validation_reports_config_diff() {
let sc = crate::session::tests::make_session_configuration_for_tests().await;
+33
View File
@@ -1244,6 +1244,39 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R
Ok(())
}
#[tokio::test]
async fn user_shell_commands_remain_login_shells_when_model_login_shells_are_disabled()
-> anyhow::Result<()> {
let (session, rx) = make_session_with_config_and_rx(|config| {
config.permissions.allow_login_shell = false;
})
.await?;
let turn_context = session.new_default_turn().await;
let command = "echo managed-login-shell".to_string();
let expected_command = session
.user_shell()
.derive_exec_args(&command, /*use_login_shell*/ true);
execute_user_shell_command(
Arc::clone(&session),
turn_context,
command,
CancellationToken::new(),
UserShellCommandMode::StandaloneTurn,
)
.await;
loop {
let event = rx.recv().await.expect("channel open");
if let EventMsg::ExecCommandBegin(event) = event.msg {
assert_eq!(event.command, expected_command);
break;
}
}
Ok(())
}
#[tokio::test]
async fn get_base_instructions_no_user_content() {
let prompt_with_apply_patch_instructions =