diff --git a/codex-rs/core/src/config/service_tests.rs b/codex-rs/core/src/config/service_tests.rs index bc3006a9a9c5..a1897c9ddd11 100644 --- a/codex-rs/core/src/config/service_tests.rs +++ b/codex-rs/core/src/config/service_tests.rs @@ -237,6 +237,54 @@ async fn read_includes_origins_and_layers() { )); } +#[cfg(target_os = "macos")] +#[tokio::test] +async fn write_value_succeeds_when_managed_preferences_expand_home_directory_paths() -> Result<()> { + use base64::Engine; + + let tmp = tempdir().expect("tempdir"); + std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n")?; + + let service = ConfigService::new( + tmp.path().to_path_buf(), + vec![], + LoaderOverrides { + managed_config_path: Some(tmp.path().join("managed_config.toml")), + managed_preferences_base64: Some( + base64::prelude::BASE64_STANDARD.encode( + r#" +sandbox_mode = "workspace-write" +[sandbox_workspace_write] +writable_roots = ["~/code"] +"# + .as_bytes(), + ), + ), + macos_managed_config_requirements_base64: None, + }, + CloudRequirementsLoader::default(), + ); + + let response = service + .write_value(ConfigValueWriteParams { + file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), + key_path: "model".to_string(), + value: serde_json::json!("updated"), + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await + .expect("write succeeds"); + + assert_eq!(response.status, WriteStatus::Ok); + assert_eq!( + std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"), + "model = \"updated\"\n" + ); + + Ok(()) +} + #[tokio::test] async fn write_value_reports_override() { let tmp = tempdir().expect("tempdir"); diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 0c426b155f7e..4102c07a82f4 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -285,9 +285,11 @@ pub async fn load_config_layers_state( )); } if let Some(config) = managed_config_from_mdm { + let managed_config = + resolve_relative_paths_in_config_toml(config.managed_config, codex_home)?; layers.push(ConfigLayerEntry::new_with_raw_toml( ConfigLayerSource::LegacyManagedConfigTomlFromMdm, - config.managed_config, + managed_config, config.raw_toml, )); } diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 03be02ebfe09..6a2bc0b6b991 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -380,6 +380,54 @@ flag = false assert!(raw.contains("value = \"managed\"")); } +#[cfg(target_os = "macos")] +#[tokio::test] +async fn managed_preferences_expand_home_directory_in_workspace_write_roots() -> anyhow::Result<()> +{ + use base64::Engine; + + let Some(home) = dirs::home_dir() else { + return Ok(()); + }; + let tmp = tempdir()?; + + let config = ConfigBuilder::default() + .codex_home(tmp.path().to_path_buf()) + .fallback_cwd(Some(tmp.path().to_path_buf())) + .loader_overrides(LoaderOverrides { + managed_config_path: Some(tmp.path().join("managed_config.toml")), + managed_preferences_base64: Some( + base64::prelude::BASE64_STANDARD.encode( + r#" +sandbox_mode = "workspace-write" +[sandbox_workspace_write] +writable_roots = ["~/code"] +"# + .as_bytes(), + ), + ), + macos_managed_config_requirements_base64: None, + }) + .build() + .await?; + + let expected_root = AbsolutePathBuf::from_absolute_path(home.join("code"))?; + match config.permissions.sandbox_policy.get() { + SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + assert_eq!( + writable_roots + .iter() + .filter(|root| **root == expected_root) + .count(), + 1, + ); + } + other => panic!("expected workspace-write policy, got {other:?}"), + } + + Ok(()) +} + #[cfg(target_os = "macos")] #[tokio::test] async fn managed_preferences_requirements_are_applied() -> anyhow::Result<()> {