Files
codex/codex-rs/core/tests/suite/cloud_config.rs
T
smurching-oai 342ed69a17 Require managed configuration from the configured server (#56055)
## Why

Local administrators need to require managed configuration from a specific customer-hosted server before a session can start.

## What changed

- Add a local managed `config_server` requirement with an HTTPS base URL and optional OAuth settings. Validate endpoints and derive the bundle route as `codex/config/bundle` beneath the base URL.
- Replace the entire `config_server` block when a higher-priority requirements layer supplies one, keeping the endpoint and OAuth settings together. Reject this setting in fetched policy.
- Track the applied server through configuration snapshots, role overrides, and refreshes. Reject session startup and managed refreshes when the applied server does not match the requirement; user refreshes retain the session's managed configuration.

## Testing

Add coverage for missing, mismatched, and matching server configuration at session startup, whole-block replacement, rejection of server settings in fetched policy, and preservation across role overrides and refreshes.

GitOrigin-RevId: 51e8a1738dabdb117ae858120f6fb09ae2ff1261
2026-10-22 02:15:00 +00:00

307 lines
11 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 session_requires_managed_configuration_from_configured_service() -> Result<()> {
let server = start_mock_server().await;
let home = TempDir::new()?;
let requirements_path = home.path().join("requirements.toml");
let required_config_server = codex_config::ConfigServer {
base_url: "https://required.example/managed/settings".into(),
oauth: None,
};
std::fs::write(
&requirements_path,
format!(
"[config_server]\n{}",
toml::to_string(&required_config_server)?
),
)?;
let policy = CloudConfigBundleFixture::enterprise_requirement(
"allowed_approval_policies = ['on-request']",
)
.into_loader();
for (loader, should_start) in [
(CloudConfigBundleLoader::default(), false),
(
policy
.clone()
.with_config_server(codex_config::ConfigServer {
base_url: "https://other.example/managed/settings".into(),
oauth: None,
}),
false,
),
(policy.with_config_server(required_config_server), true),
] {
let managed = ConfigBuilder::default()
.codex_home(home.path().to_path_buf())
.loader_overrides(LoaderOverrides {
system_requirements_path: Some(requirements_path.clone()),
..LoaderOverrides::without_managed_config_for_tests()
})
.cloud_config_bundle(loader)
.build()
.await?;
let mut builder = test_codex().with_config(move |config| {
config.config_layer_stack = managed.config_layer_stack;
config.permissions = managed.permissions;
});
match builder.build_with_auto_env(&server).await {
Ok(test) => {
assert!(
should_start,
"session started without required company policy"
);
assert_eq!(
test.session_configured.approval_policy,
AskForApproval::OnRequest
);
}
Err(error) => {
assert!(
!should_start,
"matching managed configuration should start a session: {error:#}"
);
assert!(
format!("{error:#}")
.contains("Required managed configuration has not been loaded")
);
}
}
}
Ok(())
}
#[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(())
}