Skip to content

Commit c0dbe54

Browse files
committed
-
1 parent 14bb25c commit c0dbe54

File tree

4 files changed

+128
-42
lines changed

4 files changed

+128
-42
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ wildmatch = "2.5.0"
6969
landlock = "0.4.1"
7070
seccompiler = "0.5.0"
7171

72+
[target.'cfg(target_os = "macos")'.dependencies]
73+
core-foundation = "0.9"
74+
7275
# Build OpenSSL from source for musl builds.
7376
[target.x86_64-unknown-linux-musl.dependencies]
7477
openssl-sys = { version = "*", features = ["vendored"] }

codex-rs/core/src/config.rs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -212,22 +212,18 @@ impl Config {
212212
// `Config` instance.
213213
let codex_home = find_codex_home()?;
214214

215-
// Step 1: parse `config.toml` into a generic JSON value.
216-
let mut root_value = crate::config_loader::load_config_as_toml(&codex_home)?;
215+
// Step 1: load the base config layered with CLI overrides while
216+
// ensuring admin overrides always win.
217+
let root_value = load_layered_config_with_cli_overrides(&codex_home, cli_overrides)?;
217218

218-
// Step 2: apply the `-c` overrides.
219-
for (path, value) in cli_overrides.into_iter() {
220-
apply_toml_override(&mut root_value, &path, value);
221-
}
222-
223-
// Step 3: deserialize into `ConfigToml` so that Serde can enforce the
219+
// Step 2: deserialize into `ConfigToml` so that Serde can enforce the
224220
// correct types.
225221
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
226222
tracing::error!("Failed to deserialize overridden config: {e}");
227223
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
228224
})?;
229225

230-
// Step 4: merge with the strongly-typed overrides.
226+
// Step 3: merge with the strongly-typed overrides.
231227
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
232228
}
233229
}
@@ -236,11 +232,7 @@ pub fn load_config_as_toml_with_cli_overrides(
236232
codex_home: &Path,
237233
cli_overrides: Vec<(String, TomlValue)>,
238234
) -> std::io::Result<ConfigToml> {
239-
let mut root_value = crate::config_loader::load_config_as_toml(codex_home)?;
240-
241-
for (path, value) in cli_overrides.into_iter() {
242-
apply_toml_override(&mut root_value, &path, value);
243-
}
235+
let root_value = load_layered_config_with_cli_overrides(codex_home, cli_overrides)?;
244236

245237
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
246238
tracing::error!("Failed to deserialize overridden config: {e}");
@@ -250,6 +242,27 @@ pub fn load_config_as_toml_with_cli_overrides(
250242
Ok(cfg)
251243
}
252244

245+
fn load_layered_config_with_cli_overrides(
246+
codex_home: &Path,
247+
cli_overrides: Vec<(String, TomlValue)>,
248+
) -> std::io::Result<TomlValue> {
249+
let crate::config_loader::LoadedConfigLayers {
250+
mut base,
251+
override_layer,
252+
managed_layer,
253+
} = crate::config_loader::load_config_layers(codex_home)?;
254+
255+
for (path, value) in cli_overrides.into_iter() {
256+
apply_toml_override(&mut base, &path, value);
257+
}
258+
259+
for overlay in [override_layer, managed_layer].into_iter().flatten() {
260+
crate::config_loader::merge_toml_values(&mut base, &overlay);
261+
}
262+
263+
Ok(base)
264+
}
265+
253266
pub use crate::config_loader::load_config_as_toml;
254267

255268
pub fn load_global_mcp_servers(
@@ -1277,6 +1290,29 @@ exclude_slash_tmp = true
12771290
Ok(())
12781291
}
12791292

1293+
#[test]
1294+
fn config_override_wins_over_cli_overrides() -> anyhow::Result<()> {
1295+
let codex_home = TempDir::new()?;
1296+
1297+
std::fs::write(
1298+
codex_home.path().join(CONFIG_TOML_FILE),
1299+
"model = \"base\"\n",
1300+
)?;
1301+
std::fs::write(
1302+
codex_home.path().join("config_override.toml"),
1303+
"model = \"override\"\n",
1304+
)?;
1305+
1306+
let cfg = load_config_as_toml_with_cli_overrides(
1307+
codex_home.path(),
1308+
vec![("model".to_string(), TomlValue::String("cli".to_string()))],
1309+
)?;
1310+
1311+
assert_eq!(cfg.model.as_deref(), Some("override"));
1312+
1313+
Ok(())
1314+
}
1315+
12801316
#[tokio::test]
12811317
async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> {
12821318
let codex_home = TempDir::new()?;

codex-rs/core/src/config_loader.rs

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,49 @@ use std::path::Path;
77
use std::thread;
88
use toml::Value as TomlValue;
99

10+
#[cfg(all(test, target_os = "macos"))]
11+
use std::sync::Mutex;
12+
#[cfg(all(test, target_os = "macos"))]
13+
use std::sync::OnceLock;
14+
1015
const CONFIG_TOML_FILE: &str = "config.toml";
1116
const CONFIG_OVERRIDE_TOML_FILE: &str = "config_override.toml";
1217

18+
#[derive(Debug)]
19+
pub(crate) struct LoadedConfigLayers {
20+
pub base: TomlValue,
21+
pub override_layer: Option<TomlValue>,
22+
pub managed_layer: Option<TomlValue>,
23+
}
24+
25+
#[cfg(all(test, target_os = "macos"))]
26+
static TEST_MANAGED_PREFERENCES_OVERRIDE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
27+
28+
#[cfg(all(test, target_os = "macos"))]
29+
fn test_managed_preferences_override_storage() -> &'static Mutex<Option<String>> {
30+
TEST_MANAGED_PREFERENCES_OVERRIDE.get_or_init(|| Mutex::new(None))
31+
}
32+
33+
#[cfg(all(test, target_os = "macos"))]
34+
fn test_managed_preferences_override() -> Option<String> {
35+
lock_test_managed_preferences_override_storage().clone()
36+
}
37+
38+
#[cfg(all(test, target_os = "macos"))]
39+
fn replace_test_managed_preferences_override(value: Option<String>) -> Option<String> {
40+
let mut guard = lock_test_managed_preferences_override_storage();
41+
std::mem::replace(&mut *guard, value)
42+
}
43+
44+
#[cfg(all(test, target_os = "macos"))]
45+
fn lock_test_managed_preferences_override_storage() -> std::sync::MutexGuard<'static, Option<String>>
46+
{
47+
match test_managed_preferences_override_storage().lock() {
48+
Ok(guard) => guard,
49+
Err(poisoned) => poisoned.into_inner(),
50+
}
51+
}
52+
1353
// Configuration layering pipeline (top overrides bottom):
1454
//
1555
// +-------------------------+
@@ -29,6 +69,20 @@ const CONFIG_OVERRIDE_TOML_FILE: &str = "config_override.toml";
2969
// (*) Only available on macOS via managed device profiles.
3070

3171
pub fn load_config_as_toml(codex_home: &Path) -> io::Result<TomlValue> {
72+
let LoadedConfigLayers {
73+
mut base,
74+
override_layer,
75+
managed_layer,
76+
} = load_config_layers(codex_home)?;
77+
78+
for overlay in [override_layer, managed_layer].into_iter().flatten() {
79+
merge_toml_values(&mut base, &overlay);
80+
}
81+
82+
Ok(base)
83+
}
84+
85+
pub(crate) fn load_config_layers(codex_home: &Path) -> io::Result<LoadedConfigLayers> {
3286
let user_config_path = codex_home.join(CONFIG_TOML_FILE);
3387
let override_config_path = codex_home.join(CONFIG_OVERRIDE_TOML_FILE);
3488

@@ -42,13 +96,11 @@ pub fn load_config_as_toml(codex_home: &Path) -> io::Result<TomlValue> {
4296
let override_config = join_config_result(override_handle, "config_override.toml")?;
4397
let managed_config = join_config_result(managed_handle, "managed preferences")?;
4498

45-
let mut merged = user_config.unwrap_or_else(default_empty_table);
46-
47-
for overlay in [override_config, managed_config].into_iter().flatten() {
48-
merge_toml_values(&mut merged, &overlay);
49-
}
50-
51-
Ok(merged)
99+
Ok(LoadedConfigLayers {
100+
base: user_config.unwrap_or_else(default_empty_table),
101+
override_layer: override_config,
102+
managed_layer: managed_config,
103+
})
52104
})
53105
}
54106

@@ -101,7 +153,7 @@ fn read_config_from_path(path: &Path, log_missing_as_info: bool) -> io::Result<O
101153
}
102154
}
103155

104-
fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) {
156+
pub(crate) fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) {
105157
if let TomlValue::Table(overlay_table) = overlay
106158
&& let TomlValue::Table(base_table) = base
107159
{
@@ -131,7 +183,7 @@ fn load_managed_admin_config_impl() -> io::Result<Option<TomlValue>> {
131183

132184
#[cfg(test)]
133185
{
134-
if let Ok(encoded) = std::env::var("CODEX_TEST_MANAGED_PREFERENCES_BASE64") {
186+
if let Some(encoded) = test_managed_preferences_override() {
135187
let trimmed = encoded.trim();
136188
if trimmed.is_empty() {
137189
return Ok(None);
@@ -206,39 +258,34 @@ mod tests {
206258
use tempfile::tempdir;
207259

208260
#[cfg(target_os = "macos")]
209-
const MANAGED_ENV: &str = "CODEX_TEST_MANAGED_PREFERENCES_BASE64";
210-
211-
#[cfg(target_os = "macos")]
212-
use base64::Engine as _;
213-
214-
#[cfg(target_os = "macos")]
215-
struct ManagedEnvGuard {
261+
struct ManagedPreferencesOverrideGuard {
216262
previous: Option<String>,
217263
}
218264

219265
#[cfg(target_os = "macos")]
220-
impl ManagedEnvGuard {
266+
impl ManagedPreferencesOverrideGuard {
267+
fn clear() -> Self {
268+
Self::set("")
269+
}
270+
221271
fn set(value: &str) -> Self {
222-
let previous = std::env::var(MANAGED_ENV).ok();
223-
std::env::set_var(MANAGED_ENV, value);
272+
let previous =
273+
super::replace_test_managed_preferences_override(Some(value.to_string()));
224274
Self { previous }
225275
}
226276
}
227277

228278
#[cfg(target_os = "macos")]
229-
impl Drop for ManagedEnvGuard {
279+
impl Drop for ManagedPreferencesOverrideGuard {
230280
fn drop(&mut self) {
231-
match &self.previous {
232-
Some(prev) => std::env::set_var(MANAGED_ENV, prev),
233-
None => std::env::remove_var(MANAGED_ENV),
234-
}
281+
super::replace_test_managed_preferences_override(self.previous.clone());
235282
}
236283
}
237284

238285
#[test]
239286
fn merges_override_layer_on_top() {
240287
#[cfg(target_os = "macos")]
241-
let _guard = ManagedEnvGuard::set("");
288+
let _guard = ManagedPreferencesOverrideGuard::clear();
242289

243290
let tmp = tempdir().expect("tempdir");
244291
std::fs::write(
@@ -279,7 +326,7 @@ extra = true
279326
#[test]
280327
fn returns_empty_when_all_layers_missing() {
281328
#[cfg(target_os = "macos")]
282-
let _guard = ManagedEnvGuard::set("");
329+
let _guard = ManagedPreferencesOverrideGuard::clear();
283330

284331
let tmp = tempdir().expect("tempdir");
285332
let loaded = load_config_as_toml(tmp.path()).expect("load config");
@@ -293,14 +340,13 @@ extra = true
293340
#[cfg(target_os = "macos")]
294341
#[test]
295342
fn managed_preferences_take_highest_precedence() {
296-
let _guard = ManagedEnvGuard::set("");
297343
let managed_payload = r#"
298344
[nested]
299345
value = "managed"
300346
flag = false
301347
"#;
302348
let encoded = super::BASE64_STANDARD.encode(managed_payload.as_bytes());
303-
std::env::set_var(MANAGED_ENV, encoded);
349+
let _guard = ManagedPreferencesOverrideGuard::set(&encoded);
304350

305351
let tmp = tempdir().expect("tempdir");
306352
std::fs::write(

0 commit comments

Comments
 (0)