mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
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:
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<()> {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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}."
|
||||
));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in new issue
Block a user