mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why `windows.allowed_sandbox_implementations` permits MXC regardless of the listed implementations. Managed requirements need a separate way to block both explicit MXC configuration and automatic selection through `prefer_mxc`. ## What changed Add the optional managed requirement `windows.allow_mxc`. Setting it to `false` disables automatic MXC selection and rejects explicit `windows.sandbox = "mxc"` configuration with an error identifying the requirement. Leaving it unset or setting it to `true` preserves existing behavior. ## Testing Add coverage for the requirement alongside allowed sandbox implementations, preference resolution, and cloud-managed opt-out behavior for implicit, elevated, and explicit MXC configurations. GitOrigin-RevId: a458a7b4c92cf17e687dbbad8208884d1c097da5
235 lines
8.2 KiB
Rust
235 lines
8.2 KiB
Rust
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use codex_config::CloudConfigBundleLoader;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_config::test_support::CloudConfigBundleFixture;
|
|
use codex_config::types::WindowsSandboxModeToml;
|
|
use codex_core::CodexThreadSettingsOverrides;
|
|
use codex_core::config::ConfigBuilder;
|
|
use codex_features::Feature;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::ThreadSettingsOverrides;
|
|
use codex_protocol::sandbox::SandboxType;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use core_test_support::responses::start_mock_server;
|
|
use core_test_support::skip_if_target_windows;
|
|
use core_test_support::submit_thread_settings;
|
|
use core_test_support::test_codex::test_codex;
|
|
use pretty_assertions::assert_eq;
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
use tempfile::TempDir;
|
|
|
|
#[tokio::test]
|
|
async fn managed_mxc_opt_out_blocks_preferred_and_explicit_selection() -> Result<()> {
|
|
for mode in [None, Some("elevated"), Some("mxc")] {
|
|
let home = TempDir::new()?;
|
|
let sandbox = mode
|
|
.map(|mode| format!("sandbox = {mode:?}\n"))
|
|
.unwrap_or_default();
|
|
std::fs::write(
|
|
home.path().join("config.toml"),
|
|
format!("[windows]\n{sandbox}[features]\nprefer_mxc = true\n"),
|
|
)?;
|
|
let result = ConfigBuilder::default()
|
|
.loader_overrides(LoaderOverrides::without_managed_config_for_tests())
|
|
.codex_home(home.path().to_path_buf())
|
|
.fallback_cwd(Some(home.path().to_path_buf()))
|
|
.cloud_config_bundle(
|
|
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
|
"[windows]\nallow_mxc = false\n[features]\nelevated_windows_sandbox = true\n",
|
|
),
|
|
)
|
|
.build()
|
|
.await;
|
|
if mode == Some("mxc") {
|
|
let error =
|
|
result.expect_err("explicit MXC should be rejected by managed requirements");
|
|
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert!(error.to_string().contains("windows.allow_mxc = false"));
|
|
} else {
|
|
let config = result?;
|
|
assert_eq!(
|
|
(
|
|
config.permissions.windows_sandbox_mode,
|
|
config.prefer_mxc,
|
|
config.effective_local_windows_sandbox_type()
|
|
),
|
|
(
|
|
mode.map(|_| WindowsSandboxModeToml::Elevated),
|
|
false,
|
|
SandboxType::WindowsRestrictedToken,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn refreshed_cloud_bundle_updates_later_sessions() -> Result<()> {
|
|
let server = start_mock_server().await;
|
|
let home = Arc::new(TempDir::new()?);
|
|
let initial_bundle = CloudConfigBundleFixture::enterprise_requirement(
|
|
r#"allowed_approval_policies = ["never"]"#,
|
|
)
|
|
.add_enterprise_config(r#"developer_instructions = "initial managed instructions""#)
|
|
.into_bundle();
|
|
let latest = Arc::new(RwLock::new(initial_bundle));
|
|
let getter_latest = Arc::clone(&latest);
|
|
let loader = CloudConfigBundleLoader::from_getter(move || {
|
|
let latest = Arc::clone(&getter_latest);
|
|
async move { Ok(Some(latest.read().expect("bundle state lock").clone())) }
|
|
});
|
|
|
|
let mut initial_builder = test_codex()
|
|
.with_home(Arc::clone(&home))
|
|
.with_cloud_config_bundle(loader.clone());
|
|
let initial = initial_builder.build_with_auto_env(&server).await?;
|
|
assert_eq!(
|
|
initial.session_configured.approval_policy,
|
|
AskForApproval::Never
|
|
);
|
|
assert_eq!(
|
|
initial
|
|
.codex
|
|
.config()
|
|
.await
|
|
.developer_instructions
|
|
.as_deref(),
|
|
Some("initial managed instructions")
|
|
);
|
|
|
|
*latest.write().expect("bundle state lock") = CloudConfigBundleFixture::enterprise_requirement(
|
|
r#"allowed_approval_policies = ["on-request"]"#,
|
|
)
|
|
.add_enterprise_config(r#"developer_instructions = "refreshed managed instructions""#)
|
|
.into_bundle();
|
|
|
|
let mut refreshed_builder = test_codex()
|
|
.with_home(home)
|
|
.with_cloud_config_bundle(loader);
|
|
let refreshed = refreshed_builder.build_with_auto_env(&server).await?;
|
|
assert_eq!(
|
|
refreshed.session_configured.approval_policy,
|
|
AskForApproval::OnRequest
|
|
);
|
|
assert_eq!(
|
|
refreshed
|
|
.codex
|
|
.config()
|
|
.await
|
|
.developer_instructions
|
|
.as_deref(),
|
|
Some("refreshed managed instructions")
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn managed_deny_read_requirements_follow_thread_permission_updates() -> Result<()> {
|
|
skip_if_target_windows!(
|
|
Ok(()),
|
|
"Windows restricted-token sandbox cannot enforce deny-read policies"
|
|
);
|
|
|
|
let server = start_mock_server().await;
|
|
let home = Arc::new(TempDir::new()?);
|
|
let denied_root = home.path().join("managed-private");
|
|
let nested_root = denied_root.join("nested");
|
|
std::fs::create_dir_all(&nested_root)?;
|
|
let nested_root = AbsolutePathBuf::from_absolute_path(nested_root)?;
|
|
|
|
let mut builder = test_codex()
|
|
.with_home(home)
|
|
.with_cloud_config_bundle(
|
|
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
|
|
"[permissions.filesystem]\ndeny_read = [{denied_root:?}]\n"
|
|
)),
|
|
)
|
|
.with_config(|config| {
|
|
config
|
|
.permissions
|
|
.set_permission_profile(PermissionProfile::workspace_write())
|
|
.expect("safe workspace permissions should preserve managed denies");
|
|
});
|
|
let test = builder.build_with_auto_env(&server).await?;
|
|
|
|
submit_thread_settings(
|
|
&test.codex,
|
|
ThreadSettingsOverrides {
|
|
permission_profile: Some(PermissionProfile::read_only()),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await?;
|
|
let snapshot = test.codex.config_snapshot().await;
|
|
assert!(
|
|
!snapshot
|
|
.permission_profile
|
|
.file_system_sandbox_policy()
|
|
.can_read_local_path_with_cwd(nested_root.as_path(), snapshot.cwd().as_path()),
|
|
"a live permission change must preserve managed deny-read rules"
|
|
);
|
|
|
|
let conflicting_profile = PermissionProfile::workspace_write_with(
|
|
std::slice::from_ref(&nested_root),
|
|
NetworkSandboxPolicy::Restricted,
|
|
/*exclude_tmpdir_env_var*/ false,
|
|
/*exclude_slash_tmp*/ false,
|
|
);
|
|
let error = test
|
|
.codex
|
|
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
|
|
permission_profile: Some(conflicting_profile),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.err()
|
|
.context("a concrete writable root must not override a managed deny")?;
|
|
assert!(error.to_string().contains("permissions.filesystem"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn managed_guardian_v1_requirements_disable_guardian_v2() -> Result<()> {
|
|
let server = start_mock_server().await;
|
|
|
|
for (requirements, guardian_v2_enabled) in [
|
|
(r#"allowed_approvals_reviewers = ["auto_review"]"#, false),
|
|
(
|
|
r#"allowed_approvals_reviewers = ["guardian_subagent"]"#,
|
|
false,
|
|
),
|
|
(
|
|
r#"allowed_approvals_reviewers = ["auto_review", "user"]"#,
|
|
true,
|
|
),
|
|
("[features]\nguardian_approval = true\n", true),
|
|
("[features]\nauto_review = true\n", true),
|
|
] {
|
|
let mut builder = test_codex()
|
|
.with_pre_build_hook(|home| {
|
|
std::fs::write(home.join("config.toml"), "[features]\nguardianv2 = true\n")
|
|
.expect("Guardian v2 configuration should be written");
|
|
})
|
|
.with_cloud_config_bundle(
|
|
CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements),
|
|
);
|
|
let test = builder.build_with_auto_env(&server).await?;
|
|
|
|
assert!(test.config.features.enabled(Feature::GuardianApproval));
|
|
assert_eq!(
|
|
test.config.features.enabled(Feature::GuardianV2),
|
|
guardian_v2_enabled,
|
|
"{requirements}"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|