use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::WebSearchMode; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; use std::fmt; use super::requirements_exec_policy::RequirementsExecPolicy; use super::requirements_exec_policy::RequirementsExecPolicyToml; use crate::Constrained; use crate::ConstraintError; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { Unknown, MdmManagedPreferences { domain: String, key: String }, CloudRequirements, SystemRequirementsToml { file: AbsolutePathBuf }, LegacyManagedConfigTomlFromFile { file: AbsolutePathBuf }, LegacyManagedConfigTomlFromMdm, } impl fmt::Display for RequirementSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RequirementSource::Unknown => write!(f, ""), RequirementSource::MdmManagedPreferences { domain, key } => { write!(f, "MDM {domain}:{key}") } RequirementSource::CloudRequirements => { write!(f, "cloud requirements") } RequirementSource::SystemRequirementsToml { file } => { write!(f, "{}", file.as_path().display()) } RequirementSource::LegacyManagedConfigTomlFromFile { file } => { write!(f, "{}", file.as_path().display()) } RequirementSource::LegacyManagedConfigTomlFromMdm => { write!(f, "MDM managed_config.toml (legacy)") } } } } #[derive(Debug, Clone, PartialEq)] pub struct ConstrainedWithSource { pub value: Constrained, pub source: Option, } impl ConstrainedWithSource { pub fn new(value: Constrained, source: Option) -> Self { Self { value, source } } } impl std::ops::Deref for ConstrainedWithSource { type Target = Constrained; fn deref(&self) -> &Self::Target { &self.value } } impl std::ops::DerefMut for ConstrainedWithSource { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.value } } /// Normalized version of [`ConfigRequirementsToml`] after deserialization and /// normalization. #[derive(Debug, Clone, PartialEq)] pub struct ConfigRequirements { pub approval_policy: ConstrainedWithSource, pub sandbox_policy: ConstrainedWithSource, pub web_search_mode: ConstrainedWithSource, pub feature_requirements: Option>, pub mcp_servers: Option>>, pub exec_policy: Option>, pub enforce_residency: ConstrainedWithSource>, /// Managed network constraints derived from requirements. pub network: Option>, } impl Default for ConfigRequirements { fn default() -> Self { Self { approval_policy: ConstrainedWithSource::new( Constrained::allow_any_from_default(), /*source*/ None, ), sandbox_policy: ConstrainedWithSource::new( Constrained::allow_any(SandboxPolicy::new_read_only_policy()), /*source*/ None, ), web_search_mode: ConstrainedWithSource::new( Constrained::allow_any(WebSearchMode::Cached), /*source*/ None, ), feature_requirements: None, mcp_servers: None, exec_policy: None, enforce_residency: ConstrainedWithSource::new( Constrained::allow_any(/*initial_value*/ None), /*source*/ None, ), network: None, } } } impl ConfigRequirements { pub fn exec_policy_source(&self) -> Option<&RequirementSource> { self.exec_policy.as_ref().map(|policy| &policy.source) } } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum McpServerIdentity { Command { command: String }, Url { url: String }, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct McpServerRequirement { pub identity: McpServerIdentity, } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct NetworkRequirementsToml { pub enabled: Option, pub http_port: Option, pub socks_port: Option, pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_all_unix_sockets: Option, pub allowed_domains: Option>, /// When true, only managed `allowed_domains` are respected while managed /// network enforcement is active. User allowlist entries are ignored. pub managed_allowed_domains_only: Option, pub denied_domains: Option>, pub allow_unix_sockets: Option>, pub allow_local_binding: Option, } /// Normalized network constraints derived from requirements TOML. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct NetworkConstraints { pub enabled: Option, pub http_port: Option, pub socks_port: Option, pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_all_unix_sockets: Option, pub allowed_domains: Option>, /// When true, only managed `allowed_domains` are respected while managed /// network enforcement is active. User allowlist entries are ignored. pub managed_allowed_domains_only: Option, pub denied_domains: Option>, pub allow_unix_sockets: Option>, pub allow_local_binding: Option, } impl From for NetworkConstraints { fn from(value: NetworkRequirementsToml) -> Self { let NetworkRequirementsToml { enabled, http_port, socks_port, allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_all_unix_sockets, allowed_domains, managed_allowed_domains_only, denied_domains, allow_unix_sockets, allow_local_binding, } = value; Self { enabled, http_port, socks_port, allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_all_unix_sockets, allowed_domains, managed_allowed_domains_only, denied_domains, allow_unix_sockets, allow_local_binding, } } } #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "lowercase")] pub enum WebSearchModeRequirement { Disabled, Cached, Live, } impl From for WebSearchModeRequirement { fn from(mode: WebSearchMode) -> Self { match mode { WebSearchMode::Disabled => WebSearchModeRequirement::Disabled, WebSearchMode::Cached => WebSearchModeRequirement::Cached, WebSearchMode::Live => WebSearchModeRequirement::Live, } } } impl From for WebSearchMode { fn from(mode: WebSearchModeRequirement) -> Self { match mode { WebSearchModeRequirement::Disabled => WebSearchMode::Disabled, WebSearchModeRequirement::Cached => WebSearchMode::Cached, WebSearchModeRequirement::Live => WebSearchMode::Live, } } } impl fmt::Display for WebSearchModeRequirement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { WebSearchModeRequirement::Disabled => write!(f, "disabled"), WebSearchModeRequirement::Cached => write!(f, "cached"), WebSearchModeRequirement::Live => write!(f, "live"), } } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct FeatureRequirementsToml { #[serde(flatten)] pub entries: BTreeMap, } impl FeatureRequirementsToml { pub fn is_empty(&self) -> bool { self.entries.is_empty() } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct AppRequirementToml { pub enabled: Option, } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct AppsRequirementsToml { #[serde(default, flatten)] pub apps: BTreeMap, } impl AppsRequirementsToml { pub fn is_empty(&self) -> bool { self.apps.values().all(|app| app.enabled.is_none()) } } /// Merge `enabled` configs from a lower-precedence source into an existing higher-precedence set. /// This lets managed sources (for example Cloud/MDM) enforce setting disablement across layers. /// Implemented with AppsRequirementsToml for now, could be abstracted if we have more enablement-style configs in the future. pub(crate) fn merge_enablement_settings_descending( base: &mut AppsRequirementsToml, incoming: AppsRequirementsToml, ) { for (app_id, incoming_requirement) in incoming.apps { let base_requirement = base.apps.entry(app_id).or_default(); let higher_precedence = base_requirement.enabled; let lower_precedence = incoming_requirement.enabled; base_requirement.enabled = if higher_precedence == Some(false) || lower_precedence == Some(false) { Some(false) } else { higher_precedence.or(lower_precedence) }; } } /// Base config deserialized from system `requirements.toml` or MDM. #[derive(Deserialize, Debug, Clone, Default, PartialEq)] pub struct ConfigRequirementsToml { pub allowed_approval_policies: Option>, pub allowed_sandbox_modes: Option>, pub allowed_web_search_modes: Option>, #[serde(rename = "features", alias = "feature_requirements")] pub feature_requirements: Option, pub mcp_servers: Option>, pub apps: Option, pub rules: Option, pub enforce_residency: Option, #[serde(rename = "experimental_network")] pub network: Option, pub guardian_developer_instructions: Option, } /// Value paired with the requirement source it came from, for better error /// messages. #[derive(Debug, Clone, PartialEq)] pub struct Sourced { pub value: T, pub source: RequirementSource, } impl Sourced { pub fn new(value: T, source: RequirementSource) -> Self { Self { value, source } } } impl std::ops::Deref for Sourced { type Target = T; fn deref(&self) -> &Self::Target { &self.value } } #[derive(Debug, Clone, Default, PartialEq)] pub struct ConfigRequirementsWithSources { pub allowed_approval_policies: Option>>, pub allowed_sandbox_modes: Option>>, pub allowed_web_search_modes: Option>>, pub feature_requirements: Option>, pub mcp_servers: Option>>, pub apps: Option>, pub rules: Option>, pub enforce_residency: Option>, pub network: Option>, pub guardian_developer_instructions: Option>, } impl ConfigRequirementsWithSources { pub fn merge_unset_fields(&mut self, source: RequirementSource, other: ConfigRequirementsToml) { // For every field in `other` that is `Some`, if the corresponding field // in `self` is `None`, copy the value from `other` into `self`. macro_rules! fill_missing_take { ($base:expr, $other:expr, $source:expr, { $($field:ident),+ $(,)? }) => { $( if $base.$field.is_none() && let Some(value) = $other.$field.take() { $base.$field = Some(Sourced::new(value, $source.clone())); } )+ }; } // Destructure without `..` so adding fields to `ConfigRequirementsToml` // forces this merge logic to be updated. let ConfigRequirementsToml { allowed_approval_policies: _, allowed_sandbox_modes: _, allowed_web_search_modes: _, feature_requirements: _, mcp_servers: _, apps: _, rules: _, enforce_residency: _, network: _, guardian_developer_instructions: _, } = &other; let mut other = other; if other .guardian_developer_instructions .as_deref() .is_some_and(|value| value.trim().is_empty()) { other.guardian_developer_instructions = None; } fill_missing_take!( self, other, source, { allowed_approval_policies, allowed_sandbox_modes, allowed_web_search_modes, feature_requirements, mcp_servers, rules, enforce_residency, network, guardian_developer_instructions, } ); if let Some(incoming_apps) = other.apps.take() { if let Some(existing_apps) = self.apps.as_mut() { merge_enablement_settings_descending(&mut existing_apps.value, incoming_apps); } else { self.apps = Some(Sourced::new(incoming_apps, source)); } } } pub fn into_toml(self) -> ConfigRequirementsToml { let ConfigRequirementsWithSources { allowed_approval_policies, allowed_sandbox_modes, allowed_web_search_modes, feature_requirements, mcp_servers, apps, rules, enforce_residency, network, guardian_developer_instructions, } = self; ConfigRequirementsToml { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), feature_requirements: feature_requirements.map(|sourced| sourced.value), mcp_servers: mcp_servers.map(|sourced| sourced.value), apps: apps.map(|sourced| sourced.value), rules: rules.map(|sourced| sourced.value), enforce_residency: enforce_residency.map(|sourced| sourced.value), network: network.map(|sourced| sourced.value), guardian_developer_instructions: guardian_developer_instructions .map(|sourced| sourced.value), } } } /// Currently, `external-sandbox` is not supported in config.toml, but it is /// supported through programmatic use. #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub enum SandboxModeRequirement { #[serde(rename = "read-only")] ReadOnly, #[serde(rename = "workspace-write")] WorkspaceWrite, #[serde(rename = "danger-full-access")] DangerFullAccess, #[serde(rename = "external-sandbox")] ExternalSandbox, } impl From for SandboxModeRequirement { fn from(mode: SandboxMode) -> Self { match mode { SandboxMode::ReadOnly => SandboxModeRequirement::ReadOnly, SandboxMode::WorkspaceWrite => SandboxModeRequirement::WorkspaceWrite, SandboxMode::DangerFullAccess => SandboxModeRequirement::DangerFullAccess, } } } #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ResidencyRequirement { Us, } impl ConfigRequirementsToml { pub fn is_empty(&self) -> bool { self.allowed_approval_policies.is_none() && self.allowed_sandbox_modes.is_none() && self.allowed_web_search_modes.is_none() && self .feature_requirements .as_ref() .is_none_or(FeatureRequirementsToml::is_empty) && self.mcp_servers.is_none() && self .apps .as_ref() .is_none_or(AppsRequirementsToml::is_empty) && self.rules.is_none() && self.enforce_residency.is_none() && self.network.is_none() && self .guardian_developer_instructions .as_deref() .is_none_or(|value| value.trim().is_empty()) } } impl TryFrom for ConfigRequirements { type Error = ConstraintError; fn try_from(toml: ConfigRequirementsWithSources) -> Result { let ConfigRequirementsWithSources { allowed_approval_policies, allowed_sandbox_modes, allowed_web_search_modes, feature_requirements, mcp_servers, apps: _apps, rules, enforce_residency, network, guardian_developer_instructions: _guardian_developer_instructions, } = toml; let approval_policy = match allowed_approval_policies { Some(Sourced { value: policies, source: requirement_source, }) => { let Some(initial_value) = policies.first().copied() else { return Err(ConstraintError::empty_field("allowed_approval_policies")); }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(initial_value, move |candidate| { if policies.contains(candidate) { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "approval_policy", candidate: format!("{candidate:?}"), allowed: format!("{policies:?}"), requirement_source: requirement_source_for_error.clone(), }) } })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } None => ConstrainedWithSource::new( Constrained::allow_any_from_default(), /*source*/ None, ), }; // TODO(gt): `ConfigRequirementsToml` should let the author specify the // default `SandboxPolicy`? Should do this for `AskForApproval` too? // // Currently, we force ReadOnly as the default policy because two of // the other variants (WorkspaceWrite, ExternalSandbox) require // additional parameters. Ultimately, we should expand the config // format to allow specifying those parameters. let default_sandbox_policy = SandboxPolicy::new_read_only_policy(); let sandbox_policy = match allowed_sandbox_modes { Some(Sourced { value: modes, source: requirement_source, }) => { if !modes.contains(&SandboxModeRequirement::ReadOnly) { return Err(ConstraintError::InvalidValue { field_name: "allowed_sandbox_modes", candidate: format!("{modes:?}"), allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(), requirement_source, }); }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(default_sandbox_policy, move |candidate| { let mode = match candidate { SandboxPolicy::ReadOnly { .. } => SandboxModeRequirement::ReadOnly, SandboxPolicy::WorkspaceWrite { .. } => { SandboxModeRequirement::WorkspaceWrite } SandboxPolicy::DangerFullAccess => SandboxModeRequirement::DangerFullAccess, SandboxPolicy::ExternalSandbox { .. } => { SandboxModeRequirement::ExternalSandbox } }; if modes.contains(&mode) { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: format!("{mode:?}"), allowed: format!("{modes:?}"), requirement_source: requirement_source_for_error.clone(), }) } })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } None => { ConstrainedWithSource::new( Constrained::allow_any(default_sandbox_policy), /*source*/ None, ) } }; let exec_policy = match rules { Some(Sourced { value, source }) => { let policy = value.to_requirements_policy().map_err(|err| { ConstraintError::ExecPolicyParse { requirement_source: source.clone(), reason: err.to_string(), } })?; Some(Sourced::new(policy, source)) } None => None, }; let web_search_mode = match allowed_web_search_modes { Some(Sourced { value: modes, source: requirement_source, }) => { let mut accepted = modes.into_iter().collect::>(); accepted.insert(WebSearchModeRequirement::Disabled); let allowed_for_error = format!( "{:?}", accepted .iter() .copied() .map(WebSearchMode::from) .collect::>() ); let initial_value = if accepted.contains(&WebSearchModeRequirement::Cached) { WebSearchMode::Cached } else if accepted.contains(&WebSearchModeRequirement::Live) { WebSearchMode::Live } else { WebSearchMode::Disabled }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(initial_value, move |candidate| { if accepted.contains(&(*candidate).into()) { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "web_search_mode", candidate: format!("{candidate:?}"), allowed: allowed_for_error.clone(), requirement_source: requirement_source_for_error.clone(), }) } })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } None => ConstrainedWithSource::new( Constrained::allow_any(WebSearchMode::Cached), /*source*/ None, ), }; let feature_requirements = feature_requirements.filter(|requirements| !requirements.value.is_empty()); let enforce_residency = match enforce_residency { Some(Sourced { value: residency, source: requirement_source, }) => { let required = Some(residency); let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(required, move |candidate| { if candidate == &required { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "enforce_residency", candidate: format!("{candidate:?}"), allowed: format!("{required:?}"), requirement_source: requirement_source_for_error.clone(), }) } })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } None => ConstrainedWithSource::new( Constrained::allow_any(/*initial_value*/ None), /*source*/ None, ), }; let network = network.map(|sourced_network| { let Sourced { value, source } = sourced_network; Sourced::new(NetworkConstraints::from(value), source) }); Ok(ConfigRequirements { approval_policy, sandbox_policy, web_search_mode, feature_requirements, mcp_servers, exec_policy, enforce_residency, network, }) } } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use codex_execpolicy::Decision; use codex_execpolicy::Evaluation; use codex_execpolicy::RuleMatch; use codex_protocol::protocol::NetworkAccess; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use toml::from_str; fn tokens(cmd: &[&str]) -> Vec { cmd.iter().map(std::string::ToString::to_string).collect() } fn system_requirements_toml_file_for_test() -> Result { Ok(AbsolutePathBuf::try_from( std::env::temp_dir().join("requirements.toml"), )?) } fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources { let ConfigRequirementsToml { allowed_approval_policies, allowed_sandbox_modes, allowed_web_search_modes, feature_requirements, mcp_servers, apps, rules, enforce_residency, network, guardian_developer_instructions, } = toml; ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), feature_requirements: feature_requirements .map(|value| Sourced::new(value, RequirementSource::Unknown)), mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)), apps: apps.map(|value| Sourced::new(value, RequirementSource::Unknown)), rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)), enforce_residency: enforce_residency .map(|value| Sourced::new(value, RequirementSource::Unknown)), network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)), guardian_developer_instructions: guardian_developer_instructions .map(|value| Sourced::new(value, RequirementSource::Unknown)), } } #[test] fn merge_unset_fields_copies_every_field_and_sets_sources() { let mut target = ConfigRequirementsWithSources::default(); let source = RequirementSource::LegacyManagedConfigTomlFromMdm; let allowed_approval_policies = vec![AskForApproval::UnlessTrusted, AskForApproval::Never]; let allowed_sandbox_modes = vec![ SandboxModeRequirement::WorkspaceWrite, SandboxModeRequirement::DangerFullAccess, ]; let allowed_web_search_modes = vec![ WebSearchModeRequirement::Cached, WebSearchModeRequirement::Live, ]; let feature_requirements = FeatureRequirementsToml { entries: BTreeMap::from([("personality".to_string(), true)]), }; let enforce_residency = ResidencyRequirement::Us; let enforce_source = source.clone(); let guardian_developer_instructions = "Use the company-managed guardian policy.".to_string(); // Intentionally constructed without `..Default::default()` so adding a new field to // `ConfigRequirementsToml` forces this test to be updated. let other = ConfigRequirementsToml { allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), allowed_web_search_modes: Some(allowed_web_search_modes.clone()), feature_requirements: Some(feature_requirements.clone()), mcp_servers: None, apps: None, rules: None, enforce_residency: Some(enforce_residency), network: None, guardian_developer_instructions: Some(guardian_developer_instructions.clone()), }; target.merge_unset_fields(source.clone(), other); assert_eq!( target, ConfigRequirementsWithSources { allowed_approval_policies: Some(Sourced::new( allowed_approval_policies, source.clone() )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), allowed_web_search_modes: Some(Sourced::new( allowed_web_search_modes, enforce_source.clone(), )), feature_requirements: Some(Sourced::new( feature_requirements, enforce_source.clone(), )), mcp_servers: None, apps: None, rules: None, enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)), network: None, guardian_developer_instructions: Some(Sourced::new( guardian_developer_instructions, source, )), } ); } #[test] fn merge_unset_fields_fills_missing_values() -> Result<()> { let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] "#, )?; let source_location = RequirementSource::MdmManagedPreferences { domain: "com.codex".to_string(), key: "allowed_approval_policies".to_string(), }; let mut empty_target = ConfigRequirementsWithSources::default(); empty_target.merge_unset_fields(source_location.clone(), source); assert_eq!( empty_target, ConfigRequirementsWithSources { allowed_approval_policies: Some(Sourced::new( vec![AskForApproval::OnRequest], source_location, )), allowed_sandbox_modes: None, allowed_web_search_modes: None, feature_requirements: None, mcp_servers: None, apps: None, rules: None, enforce_residency: None, network: None, guardian_developer_instructions: None, } ); Ok(()) } #[test] fn merge_unset_fields_does_not_overwrite_existing_values() -> Result<()> { let existing_source = RequirementSource::LegacyManagedConfigTomlFromMdm; let mut populated_target = ConfigRequirementsWithSources::default(); let populated_requirements: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["never"] "#, )?; populated_target.merge_unset_fields(existing_source.clone(), populated_requirements); let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] "#, )?; let source_location = RequirementSource::MdmManagedPreferences { domain: "com.codex".to_string(), key: "allowed_approval_policies".to_string(), }; populated_target.merge_unset_fields(source_location, source); assert_eq!( populated_target, ConfigRequirementsWithSources { allowed_approval_policies: Some(Sourced::new( vec![AskForApproval::Never], existing_source, )), allowed_sandbox_modes: None, allowed_web_search_modes: None, feature_requirements: None, mcp_servers: None, apps: None, rules: None, enforce_residency: None, network: None, guardian_developer_instructions: None, } ); Ok(()) } #[test] fn merge_unset_fields_ignores_blank_guardian_override() { let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields( RequirementSource::CloudRequirements, ConfigRequirementsToml { guardian_developer_instructions: Some(" \n\t".to_string()), ..Default::default() }, ); target.merge_unset_fields( RequirementSource::SystemRequirementsToml { file: system_requirements_toml_file_for_test() .expect("system requirements.toml path"), }, ConfigRequirementsToml { guardian_developer_instructions: Some( "Use the system guardian policy.".to_string(), ), ..Default::default() }, ); assert_eq!( target.guardian_developer_instructions, Some(Sourced::new( "Use the system guardian policy.".to_string(), RequirementSource::SystemRequirementsToml { file: system_requirements_toml_file_for_test() .expect("system requirements.toml path"), }, )), ); } #[test] fn deserialize_guardian_developer_instructions() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" guardian_developer_instructions = """ Use the cloud-managed guardian policy. """ "#, )?; assert_eq!( requirements.guardian_developer_instructions.as_deref(), Some("Use the cloud-managed guardian policy.\n") ); Ok(()) } #[test] fn blank_guardian_developer_instructions_is_empty() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" guardian_developer_instructions = """ """ "#, )?; assert!(requirements.is_empty()); Ok(()) } #[test] fn deserialize_apps_requirements() -> Result<()> { let toml_str = r#" [apps.connector_123123] enabled = false "#; let requirements: ConfigRequirementsToml = from_str(toml_str)?; assert_eq!( requirements.apps, Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(false), }, )]), }) ); Ok(()) } fn apps_requirements(entries: &[(&str, Option)]) -> AppsRequirementsToml { AppsRequirementsToml { apps: entries .iter() .map(|(app_id, enabled)| { ( (*app_id).to_string(), AppRequirementToml { enabled: *enabled }, ) }) .collect(), } } #[test] fn merge_enablement_settings_descending_unions_distinct_apps() { let mut merged = apps_requirements(&[("connector_high", Some(false))]); let lower = apps_requirements(&[("connector_low", Some(true))]); merge_enablement_settings_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[ ("connector_high", Some(false)), ("connector_low", Some(true)) ]), ); } #[test] fn merge_enablement_settings_descending_prefers_false_from_lower_precedence() { let mut merged = apps_requirements(&[("connector_123123", Some(true))]); let lower = apps_requirements(&[("connector_123123", Some(false))]); merge_enablement_settings_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(false))]), ); } #[test] fn merge_enablement_settings_descending_keeps_higher_true_when_lower_is_unset() { let mut merged = apps_requirements(&[("connector_123123", Some(true))]); let lower = apps_requirements(&[("connector_123123", None)]); merge_enablement_settings_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(true))]), ); } #[test] fn merge_enablement_settings_descending_uses_lower_value_when_higher_missing() { let mut merged = apps_requirements(&[]); let lower = apps_requirements(&[("connector_123123", Some(true))]); merge_enablement_settings_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(true))]), ); } #[test] fn merge_enablement_settings_descending_preserves_higher_false_when_lower_missing_app() { let mut merged = apps_requirements(&[("connector_123123", Some(false))]); let lower = apps_requirements(&[]); merge_enablement_settings_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(false))]), ); } #[test] fn merge_unset_fields_merges_apps_across_sources_with_enabled_evaluation() { let higher_source = RequirementSource::CloudRequirements; let lower_source = RequirementSource::LegacyManagedConfigTomlFromMdm; let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields( higher_source.clone(), ConfigRequirementsToml { apps: Some(apps_requirements(&[ ("connector_high", Some(true)), ("connector_shared", Some(true)), ])), ..Default::default() }, ); target.merge_unset_fields( lower_source, ConfigRequirementsToml { apps: Some(apps_requirements(&[ ("connector_low", Some(false)), ("connector_shared", Some(false)), ])), ..Default::default() }, ); let apps = target.apps.expect("apps should be present"); assert_eq!( apps.value, apps_requirements(&[ ("connector_high", Some(true)), ("connector_low", Some(false)), ("connector_shared", Some(false)), ]) ); assert_eq!(apps.source, higher_source); } #[test] fn merge_unset_fields_apps_empty_higher_source_does_not_block_lower_disables() { let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields( RequirementSource::CloudRequirements, ConfigRequirementsToml { apps: Some(apps_requirements(&[])), ..Default::default() }, ); target.merge_unset_fields( RequirementSource::LegacyManagedConfigTomlFromMdm, ConfigRequirementsToml { apps: Some(apps_requirements(&[("connector_123123", Some(false))])), ..Default::default() }, ); assert_eq!( target.apps.map(|apps| apps.value), Some(apps_requirements(&[("connector_123123", Some(false))])), ); } #[test] fn constraint_error_includes_requirement_source() -> Result<()> { let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] allowed_sandbox_modes = ["read-only"] "#, )?; let requirements_toml_file = system_requirements_toml_file_for_test()?; let source_location = RequirementSource::SystemRequirementsToml { file: requirements_toml_file, }; let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields(source_location.clone(), source); let requirements = ConfigRequirements::try_from(target)?; assert_eq!( requirements.approval_policy.can_set(&AskForApproval::Never), Err(ConstraintError::InvalidValue { field_name: "approval_policy", candidate: "Never".into(), allowed: "[OnRequest]".into(), requirement_source: source_location.clone(), }) ); assert_eq!( requirements .sandbox_policy .can_set(&SandboxPolicy::DangerFullAccess), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), allowed: "[ReadOnly]".into(), requirement_source: source_location, }) ); Ok(()) } #[test] fn constraint_error_includes_cloud_requirements_source() -> Result<()> { let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] "#, )?; let source_location = RequirementSource::CloudRequirements; let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields(source_location.clone(), source); let requirements = ConfigRequirements::try_from(target)?; assert_eq!( requirements.approval_policy.can_set(&AskForApproval::Never), Err(ConstraintError::InvalidValue { field_name: "approval_policy", candidate: "Never".into(), allowed: "[OnRequest]".into(), requirement_source: source_location, }) ); Ok(()) } #[test] fn constrained_fields_store_requirement_source() -> Result<()> { let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] allowed_sandbox_modes = ["read-only"] allowed_web_search_modes = ["cached"] enforce_residency = "us" [features] personality = true "#, )?; let source_location = RequirementSource::CloudRequirements; let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields(source_location.clone(), source); let requirements = ConfigRequirements::try_from(target)?; assert_eq!( requirements.approval_policy.source, Some(source_location.clone()) ); assert_eq!( requirements.sandbox_policy.source, Some(source_location.clone()) ); assert_eq!( requirements.web_search_mode.source, Some(source_location.clone()) ); assert_eq!( requirements .feature_requirements .as_ref() .map(|requirements| requirements.source.clone()), Some(source_location.clone()) ); assert_eq!(requirements.enforce_residency.source, Some(source_location)); Ok(()) } #[test] fn deserialize_allowed_approval_policies() -> Result<()> { let toml_str = r#" allowed_approval_policies = ["untrusted", "on-request"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.approval_policy.value(), AskForApproval::UnlessTrusted, "currently, there is no way to specify the default value for approval policy in the toml, so it picks the first allowed value" ); assert!( requirements .approval_policy .can_set(&AskForApproval::UnlessTrusted) .is_ok() ); assert_eq!( requirements .approval_policy .can_set(&AskForApproval::OnFailure), Err(ConstraintError::InvalidValue { field_name: "approval_policy", candidate: "OnFailure".into(), allowed: "[UnlessTrusted, OnRequest]".into(), requirement_source: RequirementSource::Unknown, }) ); assert!( requirements .approval_policy .can_set(&AskForApproval::OnRequest) .is_ok() ); assert_eq!( requirements.approval_policy.can_set(&AskForApproval::Never), Err(ConstraintError::InvalidValue { field_name: "approval_policy", candidate: "Never".into(), allowed: "[UnlessTrusted, OnRequest]".into(), requirement_source: RequirementSource::Unknown, }) ); assert!( requirements .sandbox_policy .can_set(&SandboxPolicy::new_read_only_policy()) .is_ok() ); Ok(()) } #[test] fn deserialize_allowed_sandbox_modes() -> Result<()> { let toml_str = r#" allowed_sandbox_modes = ["read-only", "workspace-write"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; assert!( requirements .sandbox_policy .can_set(&SandboxPolicy::new_read_only_policy()) .is_ok() ); assert!( requirements .sandbox_policy .can_set(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }) .is_ok() ); assert_eq!( requirements .sandbox_policy .can_set(&SandboxPolicy::DangerFullAccess), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), allowed: "[ReadOnly, WorkspaceWrite]".into(), requirement_source: RequirementSource::Unknown, }) ); assert_eq!( requirements .sandbox_policy .can_set(&SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, }), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "ExternalSandbox".into(), allowed: "[ReadOnly, WorkspaceWrite]".into(), requirement_source: RequirementSource::Unknown, }) ); Ok(()) } #[test] fn deserialize_allowed_web_search_modes() -> Result<()> { let toml_str = r#" allowed_web_search_modes = ["cached"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!(requirements.web_search_mode.value(), WebSearchMode::Cached); assert!( requirements .web_search_mode .can_set(&WebSearchMode::Disabled) .is_ok() ); assert_eq!( requirements.web_search_mode.can_set(&WebSearchMode::Live), Err(ConstraintError::InvalidValue { field_name: "web_search_mode", candidate: "Live".into(), allowed: "[Disabled, Cached]".into(), requirement_source: RequirementSource::Unknown, }) ); assert!( requirements .web_search_mode .can_set(&WebSearchMode::Cached) .is_ok() ); Ok(()) } #[test] fn allowed_web_search_modes_allows_disabled() -> Result<()> { let toml_str = r#" allowed_web_search_modes = ["disabled"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.web_search_mode.value(), WebSearchMode::Disabled ); assert!( requirements .web_search_mode .can_set(&WebSearchMode::Disabled) .is_ok() ); assert_eq!( requirements.web_search_mode.can_set(&WebSearchMode::Cached), Err(ConstraintError::InvalidValue { field_name: "web_search_mode", candidate: "Cached".into(), allowed: "[Disabled]".into(), requirement_source: RequirementSource::Unknown, }) ); Ok(()) } #[test] fn allowed_web_search_modes_empty_restricts_to_disabled() -> Result<()> { let toml_str = r#" allowed_web_search_modes = [] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.web_search_mode.value(), WebSearchMode::Disabled ); assert!( requirements .web_search_mode .can_set(&WebSearchMode::Disabled) .is_ok() ); assert_eq!( requirements.web_search_mode.can_set(&WebSearchMode::Cached), Err(ConstraintError::InvalidValue { field_name: "web_search_mode", candidate: "Cached".into(), allowed: "[Disabled]".into(), requirement_source: RequirementSource::Unknown, }) ); Ok(()) } #[test] fn deserialize_feature_requirements() -> Result<()> { let toml_str = r#" [features] apps = false personality = true "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.feature_requirements, Some(Sourced::new( FeatureRequirementsToml { entries: BTreeMap::from([ ("apps".to_string(), false), ("personality".to_string(), true), ]), }, RequirementSource::Unknown, )) ); Ok(()) } #[test] fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> { let toml_str = r#" [experimental_network] enabled = true allow_upstream_proxy = false dangerously_allow_all_unix_sockets = true allowed_domains = ["api.example.com", "*.openai.com"] managed_allowed_domains_only = true denied_domains = ["blocked.example.com"] allow_unix_sockets = ["/tmp/example.sock"] allow_local_binding = false "#; let source = RequirementSource::CloudRequirements; let mut requirements_with_sources = ConfigRequirementsWithSources::default(); requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?); let requirements = ConfigRequirements::try_from(requirements_with_sources)?; let sourced_network = requirements .network .expect("network requirements should be preserved as constraints"); assert_eq!(sourced_network.source, source); assert_eq!(sourced_network.value.enabled, Some(true)); assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false)); assert_eq!( sourced_network.value.dangerously_allow_all_unix_sockets, Some(true) ); assert_eq!( sourced_network.value.allowed_domains.as_ref(), Some(&vec![ "api.example.com".to_string(), "*.openai.com".to_string() ]) ); assert_eq!( sourced_network.value.managed_allowed_domains_only, Some(true) ); assert_eq!( sourced_network.value.denied_domains.as_ref(), Some(&vec!["blocked.example.com".to_string()]) ); assert_eq!( sourced_network.value.allow_unix_sockets.as_ref(), Some(&vec!["/tmp/example.sock".to_string()]) ); assert_eq!(sourced_network.value.allow_local_binding, Some(false)); Ok(()) } #[test] fn deserialize_mcp_server_requirements() -> Result<()> { let toml_str = r#" [mcp_servers.docs.identity] command = "codex-mcp" [mcp_servers.remote.identity] url = "https://example.com/mcp" "#; let requirements: ConfigRequirements = with_unknown_source(from_str(toml_str)?).try_into()?; assert_eq!( requirements.mcp_servers, Some(Sourced::new( BTreeMap::from([ ( "docs".to_string(), McpServerRequirement { identity: McpServerIdentity::Command { command: "codex-mcp".to_string(), }, }, ), ( "remote".to_string(), McpServerRequirement { identity: McpServerIdentity::Url { url: "https://example.com/mcp".to_string(), }, }, ), ]), RequirementSource::Unknown, )) ); Ok(()) } #[test] fn deserialize_exec_policy_requirements() -> Result<()> { let toml_str = r#" [rules] prefix_rules = [ { pattern = [{ token = "rm" }], decision = "forbidden" }, ] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; let policy = requirements.exec_policy.expect("exec policy").value; assert_eq!( policy.as_ref().check(&tokens(&["rm", "-rf"]), &|_| { panic!("rule should match so heuristic should not be called"); }), Evaluation { decision: Decision::Forbidden, matched_rules: vec![RuleMatch::PrefixRuleMatch { matched_prefix: tokens(&["rm"]), decision: Decision::Forbidden, resolved_program: None, justification: None, }], } ); Ok(()) } #[test] fn exec_policy_error_includes_requirement_source() -> Result<()> { let toml_str = r#" [rules] prefix_rules = [ { pattern = [{ token = "rm" }] }, ] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements_toml_file = system_requirements_toml_file_for_test()?; let source_location = RequirementSource::SystemRequirementsToml { file: requirements_toml_file, }; let mut requirements_with_sources = ConfigRequirementsWithSources::default(); requirements_with_sources.merge_unset_fields(source_location.clone(), config); let err = ConfigRequirements::try_from(requirements_with_sources) .expect_err("invalid exec policy"); assert_eq!( err, ConstraintError::ExecPolicyParse { requirement_source: source_location, reason: "rules prefix_rule at index 0 is missing a decision".to_string(), } ); Ok(()) } }