use crate::ApplicationRequirementsToml; use codex_features::FeatureToml; use codex_model_provider_info::ModelProviderInfo; pub use codex_model_provider_info::ResidencyRequirement; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::WebSearchMode; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use serde::de::Error as _; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::convert::Infallible; use std::fmt; use std::path::PathBuf; use std::str::FromStr; use wildmatch::WildMatchPattern; use super::requirements_exec_policy::RequirementsExecPolicyToml; use crate::Constrained; use crate::ConstraintError; use crate::InAppBrowserRequirementsToml; use crate::ManagedAuthPolicy; use crate::ManagedHooksRequirementsToml; use crate::McpServerRequirement; use crate::PluginRequirementsToml; use crate::RequirementsExecPolicy; use crate::browser_computer_use_requirements::BrowserUseRequirementsToml; use crate::browser_computer_use_requirements::ComputerUseRequirementsToml; use crate::config_toml::ConfigToml; use crate::mcp_requirements::validate_mcp_server_requirement; use crate::mcp_types::AppToolApproval; use crate::permissions_toml::PermissionProfileToml; use crate::types::AuthCredentialsStoreMode; use crate::types::FeedbackConfigToml; use crate::types::WindowsSandboxModeToml; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { Unknown, MdmManagedPreferences { domain: String, key: String, }, /// Multiple requirements layers contributed to the final value. Sources are /// stored highest-priority first, matching the order surfaced in errors. Composite { sources: Vec, }, /// A backend-delivered enterprise-managed layer. `id` is the stable backend /// identifier; `name` is the admin-facing display name. EnterpriseManaged { id: String, name: String, }, SystemRequirementsToml { file: AbsolutePathBuf, }, LegacyManagedConfigTomlFromFile { file: AbsolutePathBuf, }, LegacyManagedConfigTomlFromMdm, } impl RequirementSource { pub fn composite(sources: impl IntoIterator) -> Self { let mut flattened = Vec::new(); for source in sources { source.append_to_composite(&mut flattened); } match flattened.len() { 0 => RequirementSource::Unknown, 1 => flattened.remove(0), _ => RequirementSource::Composite { sources: flattened }, } } fn append_to_composite(self, flattened: &mut Vec) { match self { RequirementSource::Composite { sources } => { for source in sources { source.append_to_composite(flattened); } } source => { if !flattened.contains(&source) { flattened.push(source); } } } } } 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::Composite { sources } => { write!(f, "requirements layers: ")?; for (index, source) in sources.iter().enumerate() { if index > 0 { write!(f, ", ")?; } write!(f, "{source}")?; } Ok(()) } RequirementSource::EnterpriseManaged { id, name } => { write!(f, "enterprise-managed requirements {name} ({id})") } 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 allowed_login_methods: Option>>, pub allowed_chatgpt_workspaces: Option>>, pub cli_auth_credentials_store: Option>, pub chatgpt_base_url: Option>, pub sqlite_home: Option>, pub log_dir: Option>, pub model_catalog_json: Option>, pub model_provider: Option>, pub model_providers: Option>>, pub check_for_update_on_startup: Option>, pub allow_login_shell: Option>, pub feedback: Option>, pub approval_policy: ConstrainedWithSource, pub approvals_reviewer: ConstrainedWithSource, pub auto_review_required_models: Option>>, pub permission_profile: ConstrainedWithSource, pub windows_sandbox_mode: ConstrainedWithSource>, pub web_search_mode: ConstrainedWithSource, pub allow_managed_hooks_only: Option>, pub allow_appshots: Option>, pub allow_remote_control: Option>, pub computer_use: Option>, pub feature_requirements: Option>, pub managed_hooks: Option>, pub mcp_servers: Option>>, pub plugins: Option>>, pub marketplaces: Option>, pub exec_policy: Option>, pub enforce_residency: ConstrainedWithSource>, /// Managed network constraints derived from requirements. pub network: Option>, pub application: Option>, /// Managed filesystem constraints derived from requirements. pub filesystem: Option>, /// Managed instructions included independently of ordinary developer instructions. pub additional_developer_instructions: Option>, /// Source for the managed guardian policy config, when one is configured. pub guardian_policy_config_source: Option, /// Source for the managed extra guardian policy, when one is configured. pub guardian_extra_policy_source: Option, } impl Default for ConfigRequirements { fn default() -> Self { Self { allowed_login_methods: None, allowed_chatgpt_workspaces: None, cli_auth_credentials_store: None, chatgpt_base_url: None, sqlite_home: None, log_dir: None, model_catalog_json: None, model_provider: None, model_providers: None, check_for_update_on_startup: None, allow_login_shell: None, feedback: None, approval_policy: ConstrainedWithSource::new( Constrained::allow_any_from_default(), /*source*/ None, ), approvals_reviewer: ConstrainedWithSource::new( Constrained::allow_any_from_default(), /*source*/ None, ), auto_review_required_models: None, permission_profile: ConstrainedWithSource::new( Constrained::allow_any(PermissionProfile::read_only()), /*source*/ None, ), windows_sandbox_mode: ConstrainedWithSource::new( Constrained::allow_any(/*initial_value*/ None), /*source*/ None, ), web_search_mode: ConstrainedWithSource::new( Constrained::allow_any(WebSearchMode::Cached), /*source*/ None, ), allow_managed_hooks_only: None, allow_appshots: None, allow_remote_control: None, computer_use: None, feature_requirements: None, managed_hooks: None, mcp_servers: None, plugins: None, marketplaces: None, exec_policy: None, enforce_residency: ConstrainedWithSource::new( Constrained::allow_any(/*initial_value*/ None), /*source*/ None, ), network: None, application: None, filesystem: None, additional_developer_instructions: None, guardian_policy_config_source: None, guardian_extra_policy_source: None, } } } impl ConfigRequirements { /// Returns whether a model slug or its supported provider alias requires auto-review. pub fn auto_review_required_for_model(&self, model: &str) -> bool { let Some(protected_models) = self.auto_review_required_models.as_ref() else { return false; }; let model = match model.split_once('/') { Some((namespace, suffix)) if !namespace.is_empty() && !suffix.contains('/') && namespace.chars().all(|character| { character.is_ascii_alphanumeric() || character == '_' || character == '-' }) => { suffix } Some(_) => return false, None => model, }; protected_models.value.contains(model) } pub fn managed_auth_policy(&self) -> ManagedAuthPolicy { ManagedAuthPolicy { allowed_login_methods: self .allowed_login_methods .as_ref() .map(|allowed| allowed.value.clone()), allowed_chatgpt_workspaces: self.allowed_chatgpt_workspaces.as_ref().map(|allowed| { allowed .value .iter() .map(|workspace| workspace.trim()) .filter(|workspace| !workspace.is_empty()) .map(str::to_string) .collect() }), } } pub fn exec_policy_source(&self) -> Option<&RequirementSource> { self.exec_policy.as_ref().map(|policy| &policy.source) } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct MarketplaceRequirementsToml { pub restrict_to_allowed_sources: Option, #[serde(default)] pub allowed_sources: BTreeMap, } impl MarketplaceRequirementsToml { pub fn is_empty(&self) -> bool { self.restrict_to_allowed_sources.is_none() && self.allowed_sources.is_empty() } } /// Raw marketplace source rule whose active fields are interpreted after /// requirements composition. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct MarketplaceAllowedSourceToml { pub source: Option, pub url: Option, #[serde(rename = "ref")] pub ref_name: Option, pub host_pattern: Option, pub path: Option, } #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MarketplaceAllowedSourceKind { Git, HostPattern, Local, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct NetworkDomainPermissionsToml { #[serde(flatten)] pub entries: BTreeMap, } impl NetworkDomainPermissionsToml { pub fn allowed_domains(&self) -> Option> { let allowed_domains: Vec = self .entries .iter() .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Allow)) .map(|(pattern, _)| pattern.clone()) .collect(); (!allowed_domains.is_empty()).then_some(allowed_domains) } pub fn denied_domains(&self) -> Option> { let denied_domains: Vec = self .entries .iter() .filter(|(_, permission)| matches!(permission, NetworkDomainPermissionToml::Deny)) .map(|(pattern, _)| pattern.clone()) .collect(); (!denied_domains.is_empty()).then_some(denied_domains) } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] pub enum NetworkDomainPermissionToml { Allow, Deny, } impl std::fmt::Display for NetworkDomainPermissionToml { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let permission = match self { Self::Allow => "allow", Self::Deny => "deny", }; f.write_str(permission) } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct NetworkUnixSocketPermissionsToml { #[serde(flatten)] pub entries: BTreeMap, } impl NetworkUnixSocketPermissionsToml { pub fn allow_unix_sockets(&self) -> Vec { self.entries .iter() .filter(|(_, permission)| matches!(permission, NetworkUnixSocketPermissionToml::Allow)) .map(|(path, _)| path.clone()) .collect() } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] pub enum NetworkUnixSocketPermissionToml { Allow, Deny, } impl std::fmt::Display for NetworkUnixSocketPermissionToml { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let permission = match self { Self::Allow => "allow", Self::Deny => "deny", }; f.write_str(permission) } } #[derive(Serialize, 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 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 unix_sockets: Option, /// MXC requires true when managed networking is enabled; an explicit false is rejected. pub allow_local_binding: Option, /// Requirements-only header injections. These annotate matching requests /// without changing whether non-matching requests are allowed. pub header_injections: Option>, } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct NetworkHeaderInjectionToml { pub host: String, pub methods: Vec, pub path_prefixes: Vec, pub headers: BTreeMap, } impl std::fmt::Debug for NetworkHeaderInjectionToml { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("NetworkHeaderInjectionToml") .field("host", &self.host) .field("methods", &self.methods) .field("path_prefixes", &self.path_prefixes) .field("header_names", &self.headers.keys().collect::>()) .finish() } } #[derive(Deserialize)] struct RawNetworkRequirementsToml { enabled: Option, http_port: Option, socks_port: Option, allow_upstream_proxy: Option, dangerously_allow_non_loopback_proxy: Option, dangerously_allow_all_unix_sockets: Option, domains: Option, #[serde(default)] allowed_domains: Option>, /// When true, only managed `allowed_domains` are respected while managed /// network enforcement is active. User allowlist entries are ignored. managed_allowed_domains_only: Option, #[serde(default)] denied_domains: Option>, unix_sockets: Option, #[serde(default)] allow_unix_sockets: Option>, allow_local_binding: Option, header_injections: Option>, } impl<'de> Deserialize<'de> for NetworkRequirementsToml { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let raw = RawNetworkRequirementsToml::deserialize(deserializer)?; let RawNetworkRequirementsToml { enabled, http_port, socks_port, allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_all_unix_sockets, domains, allowed_domains, managed_allowed_domains_only, denied_domains, unix_sockets, allow_unix_sockets, allow_local_binding, header_injections, } = raw; if domains.is_some() && (allowed_domains.is_some() || denied_domains.is_some()) { return Err(D::Error::custom( "`experimental_network.domains` cannot be combined with legacy `allowed_domains` or `denied_domains`", )); } if unix_sockets.is_some() && allow_unix_sockets.is_some() { return Err(D::Error::custom( "`experimental_network.unix_sockets` cannot be combined with legacy `allow_unix_sockets`", )); } Ok(Self { enabled, http_port, socks_port, allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_all_unix_sockets, domains: domains .or_else(|| legacy_domain_permissions_from_lists(allowed_domains, denied_domains)), managed_allowed_domains_only, unix_sockets: unix_sockets .or_else(|| legacy_unix_socket_permissions_from_list(allow_unix_sockets)), allow_local_binding, header_injections, }) } } /// Legacy list normalization is intentionally lossy: explicit empty legacy /// lists are treated as unset when converted to the canonical network /// permission shape. fn legacy_domain_permissions_from_lists( allowed_domains: Option>, denied_domains: Option>, ) -> Option { let mut entries = BTreeMap::new(); for pattern in allowed_domains.unwrap_or_default() { entries.insert(pattern, NetworkDomainPermissionToml::Allow); } for pattern in denied_domains.unwrap_or_default() { entries.insert(pattern, NetworkDomainPermissionToml::Deny); } (!entries.is_empty()).then_some(NetworkDomainPermissionsToml { entries }) } fn legacy_unix_socket_permissions_from_list( allow_unix_sockets: Option>, ) -> Option { let entries = allow_unix_sockets .unwrap_or_default() .into_iter() .map(|path| (path, NetworkUnixSocketPermissionToml::Allow)) .collect::>(); (!entries.is_empty()).then_some(NetworkUnixSocketPermissionsToml { entries }) } /// Normalized network constraints derived from requirements TOML. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] 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 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 unix_sockets: Option, pub allow_local_binding: Option, pub header_injections: Option>, } impl<'de> Deserialize<'de> for NetworkConstraints { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let requirements = NetworkRequirementsToml::deserialize(deserializer)?; Ok(requirements.into()) } } 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, domains, managed_allowed_domains_only, unix_sockets, allow_local_binding, header_injections, } = value; Self { enabled, http_port, socks_port, allow_upstream_proxy, dangerously_allow_non_loopback_proxy, dangerously_allow_all_unix_sockets, domains, managed_allowed_domains_only, unix_sockets, allow_local_binding, header_injections, } } } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct FilesystemRequirementsToml { pub deny_read: Option>, } #[derive(Deserialize)] struct RawFilesystemRequirementsToml { deny_read: Option>, description: Option, extends: Option, workspace_roots: Option, filesystem: Option, network: Option, } impl<'de> Deserialize<'de> for FilesystemRequirementsToml { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let raw = RawFilesystemRequirementsToml::deserialize(deserializer)?; let RawFilesystemRequirementsToml { deny_read, description, extends, workspace_roots, filesystem, network, } = raw; if description.is_some() || extends.is_some() || workspace_roots.is_some() || filesystem.is_some() || network.is_some() { return Err(D::Error::custom( "`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile", )); } Ok(Self { deny_read }) } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct PermissionsRequirementsToml { pub filesystem: Option, // For legacy reasons, `filesystem` stays reserved for requirements-level // filesystem constraints and cannot name a profile. #[serde(default, flatten)] pub profiles: BTreeMap, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct FilesystemConstraints { pub deny_read: Vec, } impl From for FilesystemConstraints { fn from(value: PermissionsRequirementsToml) -> Self { let deny_read = value .filesystem .and_then(|filesystem| filesystem.deny_read) .unwrap_or_default(); Self { deny_read } } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] #[serde(transparent)] pub struct FilesystemDenyReadPattern(String); impl FilesystemDenyReadPattern { pub fn as_str(&self) -> &str { &self.0 } pub fn contains_glob(&self) -> bool { self.0.chars().any(is_glob_metacharacter) } pub fn from_input(input: &str) -> Result { codex_utils_path_uri::PathUri::validate_config_path_text( input, crate::path_context::convention(), ) .map_err(|error| error.to_string())?; if !input.chars().any(is_glob_metacharacter) { let path = deserialize_absolute_path(input)?; validate_literal_denial_path(&path)?; return Ok(Self(path)); } let (directory_prefix, suffix) = split_glob_pattern(input); if crate::path_context::convention() == codex_utils_path_uri::PathConvention::Windows && matches!(input.as_bytes(), [b'/' | b'\\', b'/' | b'\\', ..]) && !matches!( directory_prefix.as_bytes(), [b'/' | b'\\', b'/' | b'\\', ..] ) { return Err( "filesystem denial glob requires a literal UNC server and share".to_string(), ); } let normalized_prefix = if directory_prefix.is_empty() { deserialize_absolute_path(".")? } else { deserialize_absolute_path(directory_prefix)? }; // The prefix is literal even when supplied home/base facts contain // glob syntax. Reject it before appending the user's pattern suffix. validate_literal_denial_path(&normalized_prefix)?; let normalized = if suffix.is_empty() { normalized_prefix } else if normalized_prefix == "/" { format!("/{suffix}") } else { format!("{normalized_prefix}/{suffix}") }; codex_utils_path_uri::PathUri::validate_config_path_text( &normalized, crate::path_context::convention(), ) .map_err(|error| error.to_string())?; Ok(Self(normalized)) } } impl From for FilesystemDenyReadPattern { fn from(value: AbsolutePathBuf) -> Self { Self(value.to_string_lossy().into_owned()) } } impl<'de> Deserialize<'de> for FilesystemDenyReadPattern { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let input = String::deserialize(deserializer)?; Self::from_input(&input).map_err(D::Error::custom) } } fn validate_literal_denial_path(path: &str) -> Result<(), String> { let convention = crate::path_context::convention(); codex_utils_path_uri::LegacyAppPathString::from_string(path) .to_path_uri(convention) .and_then(|path| path.validate_glob_directory(convention)) .map_err(|error| error.to_string()) } fn deserialize_absolute_path(input: &str) -> Result { crate::path_context::resolve(input) } fn split_glob_pattern(input: &str) -> (&str, &str) { let Some(first_glob) = input.find(is_glob_metacharacter) else { return ("", input); }; let separator_index = input[..first_glob] .char_indices() .rev() .find(|(_, ch)| is_path_separator(*ch)) .map(|(index, _)| index); match separator_index { Some(0) => ("/", &input[1..]), Some(index) if crate::path_context::convention() == codex_utils_path_uri::PathConvention::Windows && index == 2 && input.as_bytes().get(1) == Some(&b':') && input.as_bytes().get(2).is_some() => { (&input[..=index], &input[index + 1..]) } Some(index) => (&input[..index], &input[index + 1..]), None => ("", input), } } fn is_path_separator(ch: char) -> bool { if crate::path_context::convention() == codex_utils_path_uri::PathConvention::Windows { ch == '/' || ch == '\\' } else { ch == '/' } } fn is_glob_metacharacter(ch: char) -> bool { matches!(ch, '*' | '?' | '[') } #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "snake_case")] pub enum WebSearchModeRequirement { Disabled, Cached, Indexed, Live, } impl From for WebSearchModeRequirement { fn from(mode: WebSearchMode) -> Self { match mode { WebSearchMode::Disabled => WebSearchModeRequirement::Disabled, WebSearchMode::Cached => WebSearchModeRequirement::Cached, WebSearchMode::Indexed => WebSearchModeRequirement::Indexed, WebSearchMode::Live => WebSearchModeRequirement::Live, } } } impl From for WebSearchMode { fn from(mode: WebSearchModeRequirement) -> Self { match mode { WebSearchModeRequirement::Disabled => WebSearchMode::Disabled, WebSearchModeRequirement::Cached => WebSearchMode::Cached, WebSearchModeRequirement::Indexed => WebSearchMode::Indexed, 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::Indexed => write!(f, "indexed"), WebSearchModeRequirement::Live => write!(f, "live"), } } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct WindowsRequirementsToml { pub allowed_sandbox_implementations: Option>, /// False blocks both explicit MXC configuration and automatic selection. pub allow_mxc: Option, /// Require MXC as the selected local Windows backend. pub require_mxc: Option, } #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum WindowsSandboxImplementationToml { Elevated, Unelevated, } impl WindowsRequirementsToml { pub fn is_empty(&self) -> bool { self.allowed_sandbox_implementations.is_none() && self.allow_mxc.is_none() && self.require_mxc.is_none() } } #[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 AppToolRequirementToml { pub approval_mode: Option, /// Opt-in analytics extraction for this exact tool, not a tool argument. /// The highest-priority rule wins as a whole, including unsupported formats. pub analytics_result_source: Option, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct AppToolResultSourceRequirementToml { /// Result format to parse; currently only `detailed_message_search_v1` is supported. pub format: AppToolResultSourceFormat, /// Source kind emitted alongside each extracted ID. #[serde(rename = "type")] pub source_type: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum AppToolResultSourceFormat { DetailedMessageSearchV1, /// Keep unknown formats so higher-priority rules still override lower ones. Unknown(String), } impl FromStr for AppToolResultSourceFormat { type Err = Infallible; fn from_str(value: &str) -> Result { Ok(match value { "detailed_message_search_v1" => Self::DetailedMessageSearchV1, _ => Self::Unknown(value.to_string()), }) } } impl<'de> Deserialize<'de> for AppToolResultSourceFormat { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { String::deserialize(deserializer)? .parse() .map_err(D::Error::custom) } } impl AppToolRequirementToml { pub fn is_empty(&self) -> bool { self.approval_mode.is_none() && self.analytics_result_source.is_none() } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct AppToolsRequirementsToml { #[serde(default, flatten)] pub tools: BTreeMap, } impl AppToolsRequirementsToml { pub fn is_empty(&self) -> bool { self.tools.values().all(AppToolRequirementToml::is_empty) } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct AppRequirementToml { pub enabled: Option, pub tools: Option, } impl AppRequirementToml { pub fn is_empty(&self) -> bool { self.enabled.is_none() && self .tools .as_ref() .is_none_or(AppToolsRequirementsToml::is_empty) } } #[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(AppRequirementToml::is_empty) } } /// Merge app requirements from a lower-precedence source into an existing higher-precedence set. /// This lets managed sources (for example Cloud/MDM) enforce setting disablement across layers, /// while exact tool approval settings keep the higher-precedence value when present. pub(crate) fn merge_app_requirements_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) }; let Some(incoming_tools) = incoming_requirement.tools else { continue; }; let base_tools = base_requirement.tools.get_or_insert_with(Default::default); for (tool_name, incoming_tool) in incoming_tools.tools { let base_tool = base_tools.tools.entry(tool_name).or_default(); if base_tool.approval_mode.is_none() { base_tool.approval_mode = incoming_tool.approval_mode; } if base_tool.analytics_result_source.is_none() { base_tool.analytics_result_source = incoming_tool.analytics_result_source; } } } } /// Base config deserialized from system `requirements.toml` or MDM. #[derive(Deserialize, Debug, Clone, Default, PartialEq)] pub struct ConfigRequirementsToml { pub allowed_login_methods: Option>, pub allowed_chatgpt_workspaces: Option>, pub cli_auth_credentials_store: Option, pub chatgpt_base_url: Option, pub sqlite_home: Option, pub log_dir: Option, pub model_catalog_json: Option, /// Exact provider selection, overriding local and session configuration. pub model_provider: Option, /// Complete provider definitions; each entry replaces the configured provider. pub model_providers: Option>, pub check_for_update_on_startup: Option, pub allow_login_shell: Option, pub feedback: Option, pub allowed_approval_policies: Option>, pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, pub allowed_permission_profiles: Option>, pub default_permissions: Option, pub remote_sandbox_config: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, pub allow_browser_and_computer_use: Option, pub allow_appshots: Option, pub allow_remote_control: Option, pub computer_use: Option, pub browser_use: Option, pub in_app_browser: Option, pub windows: Option, #[serde(rename = "features", alias = "feature_requirements")] pub feature_requirements: Option, pub hooks: Option, pub mcp_servers: Option>, pub plugins: Option>, pub marketplaces: Option, pub apps: Option, pub rules: Option, pub enforce_residency: Option, #[serde(rename = "experimental_network")] pub network: Option, pub application: Option, pub permissions: Option, pub auto_review: Option, pub models: Option, pub additional_developer_instructions: Option, pub guardian_policy_config: Option, pub guardian_extra_policy: Option, } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct AutoReviewRequirementsToml { pub required_on_models: Option>, pub ignore_rules: Option>, } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct ModelsRequirementsToml { pub new_thread: Option, } impl ModelsRequirementsToml { fn is_empty(&self) -> bool { self.new_thread .as_ref() .is_none_or(NewThreadModelDefaultsToml::is_empty) } } #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct NewThreadModelDefaultsToml { pub model: Option, pub model_reasoning_effort: Option, pub service_tier: Option, } impl NewThreadModelDefaultsToml { fn is_empty(&self) -> bool { self.model.is_none() && self.model_reasoning_effort.is_none() && self.service_tier.is_none() } } #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct RemoteSandboxConfigToml { pub hostname_patterns: Vec, pub allowed_sandbox_modes: Vec, } /// 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_login_methods: Option>>, pub allowed_chatgpt_workspaces: Option>>, pub cli_auth_credentials_store: Option>, pub chatgpt_base_url: Option>, pub sqlite_home: Option>, pub log_dir: Option>, pub model_catalog_json: Option>, pub model_provider: Option>, pub model_providers: Option>>, pub check_for_update_on_startup: Option>, pub allow_login_shell: Option>, pub feedback: Option>, pub allowed_approval_policies: Option>>, pub allowed_approvals_reviewers: Option>>, pub allowed_sandbox_modes: Option>>, pub allowed_permission_profiles: Option>>, pub default_permissions: Option>, pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, pub allow_browser_and_computer_use: Option>, pub allow_appshots: Option>, pub allow_remote_control: Option>, pub computer_use: Option>, pub browser_use: Option>, pub in_app_browser: Option>, pub windows: Option>, pub feature_requirements: Option>, pub hooks: Option>, pub mcp_servers: Option>>, pub plugins: Option>>, pub marketplaces: Option>, pub apps: Option>, pub rules: Option>, pub enforce_residency: Option>, pub network: Option>, pub application: Option>, pub permissions: Option>, pub auto_review: Option>, pub models: Option>, pub additional_developer_instructions: Option>, pub guardian_policy_config: Option>, pub guardian_extra_policy: 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_login_methods: _, allowed_chatgpt_workspaces: _, cli_auth_credentials_store: _, chatgpt_base_url: _, sqlite_home: _, log_dir: _, model_catalog_json: _, model_provider: _, model_providers: _, check_for_update_on_startup: _, allow_login_shell: _, feedback: _, allowed_approval_policies: _, allowed_approvals_reviewers: _, allowed_sandbox_modes: _, allowed_permission_profiles: _, default_permissions: _, remote_sandbox_config: _, allowed_web_search_modes: _, allow_managed_hooks_only: _, allow_browser_and_computer_use: _, allow_appshots: _, allow_remote_control: _, computer_use: _, browser_use: _, in_app_browser: _, windows: _, feature_requirements: _, hooks: _, mcp_servers: _, plugins: _, marketplaces: _, apps: _, rules: _, enforce_residency: _, network: _, application: _, permissions: _, auto_review: _, models: _, additional_developer_instructions: _, guardian_policy_config: _, guardian_extra_policy: _, } = &other; let mut other = other; if other .guardian_policy_config .as_deref() .is_some_and(|value| value.trim().is_empty()) { other.guardian_policy_config = None; } if other .guardian_extra_policy .as_deref() .is_some_and(|value| value.trim().is_empty()) { other.guardian_extra_policy = None; } fill_missing_take!( self, other, source, { allowed_login_methods, allowed_chatgpt_workspaces, cli_auth_credentials_store, chatgpt_base_url, sqlite_home, log_dir, model_catalog_json, model_provider, model_providers, check_for_update_on_startup, allow_login_shell, feedback, allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permission_profiles, default_permissions, allowed_web_search_modes, allow_managed_hooks_only, allow_browser_and_computer_use, allow_appshots, allow_remote_control, computer_use, browser_use, in_app_browser, windows, feature_requirements, hooks, mcp_servers, plugins, marketplaces, rules, enforce_residency, network, application, permissions, models, additional_developer_instructions, guardian_policy_config, guardian_extra_policy, } ); if let Some(incoming_auto_review) = other.auto_review.take() { if let Some(existing_auto_review) = self.auto_review.as_mut() { let mut source_contributed = false; if let Some(incoming_slugs) = incoming_auto_review.required_on_models { let protected_slugs = existing_auto_review .value .required_on_models .get_or_insert_default(); for slug in incoming_slugs { if !protected_slugs.contains(&slug) { protected_slugs.push(slug); source_contributed = true; } } } if existing_auto_review.value.ignore_rules.is_none() && let Some(ignore_rules) = incoming_auto_review.ignore_rules { existing_auto_review.value.ignore_rules = Some(ignore_rules); source_contributed = true; } if source_contributed && existing_auto_review.source != source { existing_auto_review.source = RequirementSource::composite([ existing_auto_review.source.clone(), source.clone(), ]); } } else { self.auto_review = Some(Sourced::new(incoming_auto_review, source.clone())); } } if let Some(incoming_apps) = other.apps.take() { if let Some(existing_apps) = self.apps.as_mut() { merge_app_requirements_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_login_methods, allowed_chatgpt_workspaces, cli_auth_credentials_store, chatgpt_base_url, sqlite_home, log_dir, model_catalog_json, model_provider, model_providers, check_for_update_on_startup, allow_login_shell, feedback, allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permission_profiles, default_permissions, allowed_web_search_modes, allow_managed_hooks_only, allow_browser_and_computer_use, allow_appshots, allow_remote_control, computer_use, browser_use, in_app_browser, windows, feature_requirements, hooks, mcp_servers, plugins, marketplaces, apps, rules, enforce_residency, network, application, permissions, auto_review, models, additional_developer_instructions, guardian_policy_config, guardian_extra_policy, } = self; ConfigRequirementsToml { allowed_login_methods: allowed_login_methods.map(|sourced| sourced.value), allowed_chatgpt_workspaces: allowed_chatgpt_workspaces.map(|sourced| sourced.value), cli_auth_credentials_store: cli_auth_credentials_store.map(|sourced| sourced.value), chatgpt_base_url: chatgpt_base_url.map(|sourced| sourced.value), sqlite_home: sqlite_home.map(|sourced| sourced.value), log_dir: log_dir.map(|sourced| sourced.value), model_catalog_json: model_catalog_json.map(|sourced| sourced.value), model_provider: model_provider.map(|sourced| sourced.value), model_providers: model_providers.map(|sourced| sourced.value), check_for_update_on_startup: check_for_update_on_startup.map(|sourced| sourced.value), allow_login_shell: allow_login_shell.map(|sourced| sourced.value), feedback: feedback.map(|sourced| sourced.value), allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_approvals_reviewers: allowed_approvals_reviewers.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), allowed_permission_profiles: allowed_permission_profiles.map(|sourced| sourced.value), default_permissions: default_permissions.map(|sourced| sourced.value), remote_sandbox_config: None, allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value), allow_browser_and_computer_use: allow_browser_and_computer_use .map(|sourced| sourced.value), allow_appshots: allow_appshots.map(|sourced| sourced.value), allow_remote_control: allow_remote_control.map(|sourced| sourced.value), computer_use: computer_use.map(|sourced| sourced.value), browser_use: browser_use.map(|sourced| sourced.value), in_app_browser: in_app_browser.map(|sourced| sourced.value), windows: windows.map(|sourced| sourced.value), feature_requirements: feature_requirements.map(|sourced| sourced.value), hooks: hooks.map(|sourced| sourced.value), mcp_servers: mcp_servers.map(|sourced| sourced.value), plugins: plugins.map(|sourced| sourced.value), marketplaces: marketplaces.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), application: application.map(|sourced| sourced.value), permissions: permissions.map(|sourced| sourced.value), auto_review: auto_review.map(|sourced| sourced.value), models: models.map(|sourced| sourced.value), additional_developer_instructions: additional_developer_instructions .map(|sourced| sourced.value), guardian_policy_config: guardian_policy_config.map(|sourced| sourced.value), guardian_extra_policy: guardian_extra_policy.map(|sourced| sourced.value), } } } fn normalize_hostname(hostname: &str) -> Option { let hostname = hostname.trim().trim_end_matches('.'); (!hostname.is_empty()).then(|| hostname.to_ascii_lowercase()) } fn hostname_matches_any_pattern(hostname: &str, patterns: &[String]) -> bool { patterns.iter().any(|pattern| { normalize_hostname(pattern) .map(|pattern| WildMatchPattern::<'*', '?'>::new_case_insensitive(&pattern)) .is_some_and(|pattern| pattern.matches(hostname)) }) } /// Currently, `external-sandbox` is not supported in config.toml, but it is /// supported through programmatic use. #[derive(Serialize, 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, } } } impl ConfigRequirementsToml { pub fn apply_remote_sandbox_config(&mut self, hostname: Option<&str>) { let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else { return; }; let Some(hostname) = hostname.and_then(normalize_hostname) else { return; }; let Some(matched_config) = remote_sandbox_config .iter() .find(|config| hostname_matches_any_pattern(&hostname, &config.hostname_patterns)) else { return; }; self.allowed_sandbox_modes = Some(matched_config.allowed_sandbox_modes.clone()); } pub fn is_empty(&self) -> bool { self.allowed_login_methods.is_none() && self.allowed_chatgpt_workspaces.is_none() && self.cli_auth_credentials_store.is_none() && self.chatgpt_base_url.is_none() && self.sqlite_home.is_none() && self.log_dir.is_none() && self.model_catalog_json.is_none() && self.model_provider.is_none() && self.model_providers.as_ref().is_none_or(HashMap::is_empty) && self.check_for_update_on_startup.is_none() && self.allow_login_shell.is_none() && self .feedback .as_ref() .is_none_or(|feedback| feedback == &FeedbackConfigToml::default()) && self.allowed_approval_policies.is_none() && self.allowed_approvals_reviewers.is_none() && self.allowed_sandbox_modes.is_none() && self.allowed_permission_profiles.is_none() && self.default_permissions.is_none() && self.remote_sandbox_config.is_none() && self.allowed_web_search_modes.is_none() && self.allow_managed_hooks_only.is_none() && self.allow_browser_and_computer_use.is_none() && self.allow_appshots.is_none() && self.allow_remote_control.is_none() && self .computer_use .as_ref() .is_none_or(ComputerUseRequirementsToml::is_empty) && self .browser_use .as_ref() .is_none_or(BrowserUseRequirementsToml::is_empty) && self .in_app_browser .as_ref() .is_none_or(|requirements| requirements == &InAppBrowserRequirementsToml::default()) && self .windows .as_ref() .is_none_or(WindowsRequirementsToml::is_empty) && self .feature_requirements .as_ref() .is_none_or(FeatureRequirementsToml::is_empty) && self .hooks .as_ref() .is_none_or(ManagedHooksRequirementsToml::is_empty) && self.mcp_servers.is_none() && self .plugins .as_ref() .is_none_or(|plugins| plugins.values().all(PluginRequirementsToml::is_empty)) && self .marketplaces .as_ref() .is_none_or(MarketplaceRequirementsToml::is_empty) && self .apps .as_ref() .is_none_or(AppsRequirementsToml::is_empty) && self.rules.is_none() && self.enforce_residency.is_none() && self.network.is_none() && self .application .as_ref() .is_none_or(|application| application.network.is_none()) && self.permissions.is_none() && self.auto_review.as_ref().is_none_or(|auto_review| { auto_review.ignore_rules.as_ref().is_none_or(Vec::is_empty) && auto_review .required_on_models .as_ref() .is_none_or(Vec::is_empty) }) && self .models .as_ref() .is_none_or(ModelsRequirementsToml::is_empty) && self.additional_developer_instructions.is_none() && self .guardian_policy_config .as_deref() .is_none_or(|value| value.trim().is_empty()) && self .guardian_extra_policy .as_deref() .is_none_or(|value| value.trim().is_empty()) } /// Applies the requirements whose values replace config values. /// /// This projection keeps config/read aligned with the final runtime config. pub fn apply_exact_to_config(&self, config: &mut ConfigToml) { macro_rules! apply_exact { ($field:ident) => { if let Some(value) = self.$field.as_ref() { config.$field = Some(value.clone()); } }; } apply_exact!(cli_auth_credentials_store); apply_exact!(chatgpt_base_url); apply_exact!(sqlite_home); apply_exact!(log_dir); apply_exact!(model_catalog_json); apply_exact!(model_provider); if let Some(providers) = &self.model_providers { config.model_providers.extend(providers.clone()); } apply_exact!(check_for_update_on_startup); apply_exact!(allow_login_shell); if self .allowed_approvals_reviewers .as_ref() .is_some_and(|reviewers| !reviewers.contains(&ApprovalsReviewer::User)) { config.features.get_or_insert_default().guardianv2 = Some(FeatureToml::Enabled(false)); } if let Some(enabled) = self.feedback.as_ref().and_then(|feedback| feedback.enabled) { config.feedback.get_or_insert_default().enabled = Some(enabled); } } /// Returns the exact managed field affected by editing `segments`. pub fn exact_requirement_for_config_path(&self, segments: &[String]) -> Option<&'static str> { if self.model_providers.as_ref().is_some_and(|providers| { providers .keys() .any(|id| config_paths_overlap(segments, &["model_providers", id])) }) { return Some("model_providers"); } let managed_fields: [(bool, &[&str], &'static str); 9] = [ ( self.model_provider.is_some(), &["model_provider"], "model_provider", ), (self.sqlite_home.is_some(), &["sqlite_home"], "sqlite_home"), (self.log_dir.is_some(), &["log_dir"], "log_dir"), ( self.model_catalog_json.is_some(), &["model_catalog_json"], "model_catalog_json", ), ( self.check_for_update_on_startup.is_some(), &["check_for_update_on_startup"], "check_for_update_on_startup", ), ( self.allow_login_shell.is_some(), &["allow_login_shell"], "allow_login_shell", ), ( self.feedback .as_ref() .and_then(|feedback| feedback.enabled) .is_some(), &["feedback", "enabled"], "feedback.enabled", ), ( self.cli_auth_credentials_store.is_some(), &["cli_auth_credentials_store"], "cli_auth_credentials_store", ), ( self.chatgpt_base_url.is_some(), &["chatgpt_base_url"], "chatgpt_base_url", ), ]; managed_fields .into_iter() .find_map(|(is_managed, managed_path, field)| { (is_managed && config_paths_overlap(segments, managed_path)).then_some(field) }) } } fn config_paths_overlap(segments: &[String], managed_path: &[&str]) -> bool { segments .iter() .zip(managed_path) .all(|(segment, managed_segment)| segment == managed_segment) } fn validate_mcp_server_requirements( requirements: &BTreeMap, source: &RequirementSource, plugin_name: Option<&str>, ) -> Result<(), ConstraintError> { for (server_name, requirement) in requirements { validate_mcp_server_requirement(requirement).map_err(|reason| { ConstraintError::McpServerRequirementParse { server_name: plugin_name .map(|plugin_name| format!("{plugin_name}/{server_name}")) .unwrap_or_else(|| server_name.clone()), requirement_source: source.clone(), reason, } })?; } Ok(()) } impl TryFrom for ConfigRequirements { type Error = ConstraintError; fn try_from(toml: ConfigRequirementsWithSources) -> Result { // Profile catalog selection remains on ConfigRequirementsToml for // config loading and requirements API projection. Managed new-thread // defaults also remain there because they are initialization values; // model-specific auto-review requirements are runtime constraints. let ConfigRequirementsWithSources { allowed_login_methods, allowed_chatgpt_workspaces, cli_auth_credentials_store, chatgpt_base_url, sqlite_home, log_dir, model_catalog_json, model_provider, model_providers, check_for_update_on_startup, allow_login_shell, feedback, allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permission_profiles: _, default_permissions: _, allowed_web_search_modes, allow_managed_hooks_only, allow_browser_and_computer_use: _, allow_appshots, allow_remote_control, computer_use, browser_use: _, in_app_browser: _, windows, feature_requirements, hooks, mcp_servers, plugins, marketplaces, apps: _apps, rules, enforce_residency, network, application, permissions, auto_review, models: _, additional_developer_instructions, guardian_policy_config, guardian_extra_policy, } = toml; let auto_review_required_models = auto_review .and_then(|auto_review| { auto_review .value .required_on_models .map(|slugs| Sourced::new(slugs, auto_review.source)) }) .filter(|models| !models.value.is_empty()) .map(|models| { let Sourced { value, source } = models; let mut protected_models = BTreeSet::new(); for slug in value { if slug.trim().is_empty() || slug.trim() != slug || slug.contains('/') { return Err(ConstraintError::InvalidValue { field_name: "auto_review.required_on_models", candidate: format!("{slug:?}"), allowed: "non-empty model slugs without surrounding whitespace or provider namespaces" .to_string(), requirement_source: source, }); } protected_models.insert(slug); } Ok(Sourced::new(protected_models, source)) }) .transpose()?; if let Some(requirements) = &mcp_servers { validate_mcp_server_requirements( &requirements.value, &requirements.source, /*plugin_name*/ None, )?; } if let Some(plugin_requirements) = &plugins { for (plugin_name, plugin) in &plugin_requirements.value { if let Some(requirements) = &plugin.mcp_servers { validate_mcp_server_requirements( requirements, &plugin_requirements.source, Some(plugin_name), )?; } } } 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, ), }; let approvals_reviewer = match allowed_approvals_reviewers { Some(Sourced { value: reviewers, source: requirement_source, }) => { let Some(initial_value) = reviewers.first().copied() else { return Err(ConstraintError::empty_field("allowed_approvals_reviewers")); }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(initial_value, move |candidate| { if reviewers.contains(candidate) { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "approvals_reviewer", candidate: format!("{candidate:?}"), allowed: format!("{reviewers:?}"), requirement_source: requirement_source_for_error.clone(), }) } })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } None => ConstrainedWithSource::new( Constrained::allow_any_from_default(), /*source*/ None, ), }; let default_permission_profile = PermissionProfile::read_only(); let permission_profile = 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 PermissionProfile" .to_string(), requirement_source, }); }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(default_permission_profile, move |candidate| { let mode = sandbox_mode_requirement_for_permission_profile(candidate); 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_permission_profile), /*source*/ None, ), }; let mxc_requirement_source = windows .as_ref() .filter(|windows| windows.value.allow_mxc == Some(false)) .map(|windows| windows.source.clone()); let mut windows_sandbox_mode = match windows { Some(Sourced { value: WindowsRequirementsToml { allowed_sandbox_implementations: Some(implementations), allow_mxc: _, require_mxc: _, }, source: requirement_source, }) => { if implementations.is_empty() { return Err(ConstraintError::empty_field( "windows.allowed_sandbox_implementations", )); } // Prefer elevated when both Windows sandbox implementations are allowed. let initial_value = if implementations.contains(&WindowsSandboxImplementationToml::Elevated) { WindowsSandboxModeToml::Elevated } else { WindowsSandboxModeToml::Unelevated }; let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(Some(initial_value), move |candidate| match candidate { Some(WindowsSandboxModeToml::Mxc) => Ok(()), Some(WindowsSandboxModeToml::Elevated) if implementations .contains(&WindowsSandboxImplementationToml::Elevated) => { Ok(()) } Some(WindowsSandboxModeToml::Unelevated) if implementations .contains(&WindowsSandboxImplementationToml::Unelevated) => { Ok(()) } _ => Err(ConstraintError::InvalidValue { field_name: "windows.sandbox", candidate: format!("{candidate:?}"), allowed: format!("{implementations:?}"), requirement_source: requirement_source_for_error.clone(), }), })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } Some(Sourced { value: WindowsRequirementsToml { allowed_sandbox_implementations: None, allow_mxc: _, require_mxc: _, }, .. }) | None => ConstrainedWithSource::new( Constrained::allow_any(/*initial_value*/ None), /*source*/ None, ), }; if let Some(requirement_source) = mxc_requirement_source { windows_sandbox_mode.add_validator(move |candidate| match candidate { Some(WindowsSandboxModeToml::Mxc) => Err(ConstraintError::InvalidValue { field_name: "windows.sandbox", candidate: format!("{candidate:?}"), allowed: "windows.allow_mxc = false".to_string(), requirement_source: requirement_source.clone(), }), Some(WindowsSandboxModeToml::Elevated | WindowsSandboxModeToml::Unelevated) | None => Ok(()), })?; } 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::Indexed) { WebSearchMode::Indexed } 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 managed_hooks = hooks .filter(|managed_hooks| managed_hooks.value.handler_count() > 0) .map(|sourced_hooks| { let Sourced { value, source: requirement_source, } = sourced_hooks; let allowed = value; let allowed_for_error = format!("{allowed:?}"); let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(allowed.clone(), move |candidate| { if candidate == &allowed { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "hooks", candidate: format!("{candidate:?}"), allowed: allowed_for_error.clone(), requirement_source: requirement_source_for_error.clone(), }) } })?; Ok(ConstrainedWithSource::new( constrained, Some(requirement_source), )) }) .transpose()?; 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) }); let filesystem = permissions.map(|sourced_permissions| { let Sourced { value, source } = sourced_permissions; Sourced::new(FilesystemConstraints::from(value), source) }); let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source); let guardian_extra_policy_source = guardian_extra_policy.map(|sourced| sourced.source); Ok(ConfigRequirements { allowed_login_methods, allowed_chatgpt_workspaces, cli_auth_credentials_store, chatgpt_base_url, sqlite_home, log_dir, model_catalog_json, model_provider, model_providers, check_for_update_on_startup, allow_login_shell, feedback, approval_policy, approvals_reviewer, auto_review_required_models, permission_profile, windows_sandbox_mode, web_search_mode, allow_managed_hooks_only, allow_appshots, allow_remote_control, computer_use, feature_requirements, managed_hooks, mcp_servers, plugins, marketplaces, exec_policy, enforce_residency, network, application, filesystem, additional_developer_instructions, guardian_policy_config_source, guardian_extra_policy_source, }) } } pub fn sandbox_mode_requirement_for_permission_profile( permission_profile: &PermissionProfile, ) -> SandboxModeRequirement { match permission_profile { PermissionProfile::Disabled => SandboxModeRequirement::DangerFullAccess, PermissionProfile::External { .. } => SandboxModeRequirement::ExternalSandbox, PermissionProfile::Managed { .. } => { let file_system_policy = permission_profile.file_system_sandbox_policy(); if file_system_policy.has_full_disk_write_access() { SandboxModeRequirement::DangerFullAccess } else if file_system_policy .entries .iter() .any(|entry| entry.access.can_write()) { SandboxModeRequirement::WorkspaceWrite } else { SandboxModeRequirement::ReadOnly } } } } #[cfg(test)] mod tests { use super::*; use crate::AllowDenyRequirementToml; use crate::BrowserUseAccessApprovalLifetimeToml; use crate::BrowserUseOriginPolicyToml; use crate::ComputerUseMacosRequirementsToml; use crate::ComputerUseWindowsExeRequirementToml; use crate::ComputerUseWindowsRequirementsToml; use crate::HookEventsToml; use crate::McpServerCommandMatcher; use crate::McpServerIdentity; use crate::McpServerValueMatcher; use anyhow::Result; use codex_execpolicy::Decision; use codex_execpolicy::Evaluation; use codex_execpolicy::RuleMatch; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; 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"), )?) } #[test] fn exact_requirement_for_config_path_matches_overlapping_paths() { let managed_path = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed")) .expect("managed path should be absolute"); let requirements = ConfigRequirementsToml { cli_auth_credentials_store: Some(AuthCredentialsStoreMode::Ephemeral), chatgpt_base_url: Some("https://managed.example/backend-api/".to_string()), sqlite_home: Some(managed_path.clone()), log_dir: Some(managed_path.clone()), model_catalog_json: Some(managed_path), check_for_update_on_startup: Some(false), allow_login_shell: Some(false), feedback: Some(FeedbackConfigToml { enabled: Some(false), }), ..Default::default() }; let cases: &[(&[&str], Option<&str>)] = &[ ( &["cli_auth_credentials_store"], Some("cli_auth_credentials_store"), ), (&["chatgpt_base_url"], Some("chatgpt_base_url")), (&["sqlite_home"], Some("sqlite_home")), (&["log_dir"], Some("log_dir")), (&["model_catalog_json"], Some("model_catalog_json")), ( &["check_for_update_on_startup"], Some("check_for_update_on_startup"), ), (&["allow_login_shell"], Some("allow_login_shell")), (&["feedback", "enabled"], Some("feedback.enabled")), (&[], Some("sqlite_home")), (&["feedback"], Some("feedback.enabled")), (&["feedback", "other"], None), (&["windows", "sandbox"], None), ]; for (segments, expected) in cases { let segments = segments.iter().map(ToString::to_string).collect::>(); assert_eq!( requirements.exact_requirement_for_config_path(&segments), *expected, "segments: {segments:?}" ); } } #[test] fn composite_requirement_source_flattens_and_deduplicates_sources() { let mdm_source = RequirementSource::MdmManagedPreferences { domain: "com.openai.codex".to_string(), key: "requirements_toml_base64".to_string(), }; let legacy_source = RequirementSource::LegacyManagedConfigTomlFromMdm; assert_eq!( RequirementSource::composite([ mdm_source.clone(), RequirementSource::composite([legacy_source.clone(), mdm_source.clone()]), ]), RequirementSource::Composite { sources: vec![mdm_source, legacy_source], } ); } fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources { let ConfigRequirementsToml { allowed_login_methods, allowed_chatgpt_workspaces, cli_auth_credentials_store, chatgpt_base_url, sqlite_home, log_dir, model_catalog_json, model_provider, model_providers, check_for_update_on_startup, allow_login_shell, feedback, allowed_approval_policies, allowed_approvals_reviewers, allowed_sandbox_modes, allowed_permission_profiles, default_permissions, remote_sandbox_config: _, allowed_web_search_modes, allow_managed_hooks_only, allow_browser_and_computer_use, allow_appshots, allow_remote_control, computer_use, browser_use, in_app_browser, windows, feature_requirements, hooks, mcp_servers, plugins, marketplaces, apps, rules, enforce_residency, network, application, permissions, auto_review, models, additional_developer_instructions, guardian_policy_config, guardian_extra_policy, } = toml; ConfigRequirementsWithSources { allowed_login_methods: allowed_login_methods .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_chatgpt_workspaces: allowed_chatgpt_workspaces .map(|value| Sourced::new(value, RequirementSource::Unknown)), cli_auth_credentials_store: cli_auth_credentials_store .map(|value| Sourced::new(value, RequirementSource::Unknown)), chatgpt_base_url: chatgpt_base_url .map(|value| Sourced::new(value, RequirementSource::Unknown)), sqlite_home: sqlite_home.map(|value| Sourced::new(value, RequirementSource::Unknown)), log_dir: log_dir.map(|value| Sourced::new(value, RequirementSource::Unknown)), model_catalog_json: model_catalog_json .map(|value| Sourced::new(value, RequirementSource::Unknown)), model_provider: model_provider .map(|value| Sourced::new(value, RequirementSource::Unknown)), model_providers: model_providers .map(|value| Sourced::new(value, RequirementSource::Unknown)), check_for_update_on_startup: check_for_update_on_startup .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_login_shell: allow_login_shell .map(|value| Sourced::new(value, RequirementSource::Unknown)), feedback: feedback.map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_approval_policies: allowed_approval_policies .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_approvals_reviewers: allowed_approvals_reviewers .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_sandbox_modes: allowed_sandbox_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_permission_profiles: allowed_permission_profiles .map(|value| Sourced::new(value, RequirementSource::Unknown)), default_permissions: default_permissions .map(|value| Sourced::new(value, RequirementSource::Unknown)), allowed_web_search_modes: allowed_web_search_modes .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_managed_hooks_only: allow_managed_hooks_only .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_browser_and_computer_use: allow_browser_and_computer_use .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_appshots: allow_appshots .map(|value| Sourced::new(value, RequirementSource::Unknown)), allow_remote_control: allow_remote_control .map(|value| Sourced::new(value, RequirementSource::Unknown)), computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)), browser_use: browser_use.map(|value| Sourced::new(value, RequirementSource::Unknown)), in_app_browser: in_app_browser .map(|value| Sourced::new(value, RequirementSource::Unknown)), windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)), feature_requirements: feature_requirements .map(|value| Sourced::new(value, RequirementSource::Unknown)), hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)), mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)), plugins: plugins.map(|value| Sourced::new(value, RequirementSource::Unknown)), marketplaces: marketplaces.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)), application: application.map(|value| Sourced::new(value, RequirementSource::Unknown)), permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)), auto_review: auto_review.map(|value| Sourced::new(value, RequirementSource::Unknown)), models: models.map(|value| Sourced::new(value, RequirementSource::Unknown)), additional_developer_instructions: additional_developer_instructions .map(|value| Sourced::new(value, RequirementSource::Unknown)), guardian_policy_config: guardian_policy_config .map(|value| Sourced::new(value, RequirementSource::Unknown)), guardian_extra_policy: guardian_extra_policy .map(|value| Sourced::new(value, RequirementSource::Unknown)), } } #[test] fn deserialize_allow_managed_hooks_only() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allow_managed_hooks_only = true "#, )?; assert_eq!(requirements.allow_managed_hooks_only, Some(true)); assert!(!requirements.is_empty()); Ok(()) } #[test] fn allow_managed_hooks_only_false_is_still_configured() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allow_managed_hooks_only = false "#, )?; assert_eq!(requirements.allow_managed_hooks_only, Some(false)); assert!(!requirements.is_empty()); Ok(()) } #[test] fn deserialize_managed_permission_profiles() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" default_permissions = "managed-standard" [allowed_permission_profiles] managed-standard = true managed-build = true [permissions.managed-standard] extends = ":workspace" [permissions.managed-build] extends = "managed-standard" "#, )?; assert_eq!( requirements.allowed_permission_profiles, Some(BTreeMap::from([ ("managed-build".to_string(), true), ("managed-standard".to_string(), true), ])) ); assert_eq!( requirements.default_permissions, Some("managed-standard".to_string()) ); let permissions = requirements .permissions .as_ref() .expect("managed permission profiles"); assert!(permissions.profiles.contains_key("managed-standard")); assert!( permissions .profiles .get("managed-build") .and_then(|profile| profile.extends.as_deref()) .is_some() ); assert!(!requirements.is_empty()); Ok(()) } #[test] fn deserialize_allow_appshots() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allow_appshots = true "#, )?; assert_eq!(requirements.allow_appshots, Some(true)); assert!(!requirements.is_empty()); Ok(()) } #[test] fn filesystem_requirements_table_cannot_define_a_permission_profile() { let err = from_str::( r#" [permissions.filesystem] extends = ":workspace" "#, ) .expect_err("filesystem requirements cannot define a permission profile"); assert!( err.to_string().contains( "`permissions.filesystem` is reserved for requirements-level filesystem constraints and cannot define a profile" ), "unexpected error: {err:#}" ); } #[test] fn allow_appshots_false_is_still_configured() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allow_appshots = false "#, )?; assert_eq!(requirements.allow_appshots, Some(false)); assert!(!requirements.is_empty()); Ok(()) } #[test] fn allow_remote_control_false_is_still_configured() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allow_remote_control = false "#, )?; assert_eq!(requirements.allow_remote_control, Some(false)); assert!(!requirements.is_empty()); Ok(()) } #[test] fn deserialize_browser_and_computer_use_requirements() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allow_browser_and_computer_use = false [browser_use] allow_history_access = false disable_auto_review = true allow_global_persistent_approval = false [browser_use.default_origin_policy] access = "deny" downloads = "allow" uploads = "deny" full_cdp_access = "allow" auto_review = "deny" persistent_approval = false access_approval_lifetime = "turn" [browser_use.origins."https://example.com"] access = "allow" downloads = "deny" uploads = "allow" full_cdp_access = "deny" auto_review = "deny" persistent_approval = true access_approval_lifetime = "thread" [computer_use] allow_locked_computer_use = false allow_persistent_approval = false default_app_access = "deny" [computer_use.macos.bundle_ids] "com.apple.Safari" = "allow" [computer_use.windows.aumids] "Microsoft.Paint_8wekyb3d8bbwe!App" = "allow" [[computer_use.windows.exes]] publisher_name = "CN=Google LLC, O=Google LLC, L=Mountain View, S=California, C=US" product_name = "Google Chrome" binary_name = "chrome.exe" access = "deny" "#, )?; assert_eq!(requirements.allow_browser_and_computer_use, Some(false)); assert_eq!( requirements.browser_use, Some(BrowserUseRequirementsToml { extension: None, allow_webmcp: None, allow_history_access: Some(false), disable_auto_review: Some(true), allow_global_persistent_approval: Some(false), default_origin_policy: Some(BrowserUseOriginPolicyToml { access: Some(AllowDenyRequirementToml::Deny), downloads: Some(AllowDenyRequirementToml::Allow), uploads: Some(AllowDenyRequirementToml::Deny), full_cdp_access: Some(AllowDenyRequirementToml::Allow), auto_review: Some(AllowDenyRequirementToml::Deny), persistent_approval: Some(false), access_approval_lifetime: Some(BrowserUseAccessApprovalLifetimeToml::Turn), }), origins: Some(BTreeMap::from([( "https://example.com".to_string(), BrowserUseOriginPolicyToml { access: Some(AllowDenyRequirementToml::Allow), downloads: Some(AllowDenyRequirementToml::Deny), uploads: Some(AllowDenyRequirementToml::Allow), full_cdp_access: Some(AllowDenyRequirementToml::Deny), auto_review: Some(AllowDenyRequirementToml::Deny), persistent_approval: Some(true), access_approval_lifetime: Some( BrowserUseAccessApprovalLifetimeToml::Thread, ), }, )])), }) ); assert_eq!( requirements.computer_use, Some(ComputerUseRequirementsToml { allow_locked_computer_use: Some(false), allow_persistent_approval: Some(false), default_app_access: Some(AllowDenyRequirementToml::Deny), macos: Some(ComputerUseMacosRequirementsToml { bundle_ids: Some(BTreeMap::from([( "com.apple.Safari".to_string(), AllowDenyRequirementToml::Allow, )])), }), windows: Some(ComputerUseWindowsRequirementsToml { aumids: Some(BTreeMap::from([( "Microsoft.Paint_8wekyb3d8bbwe!App".to_string(), AllowDenyRequirementToml::Allow, )])), exes: Some(vec![ComputerUseWindowsExeRequirementToml { publisher_name: "CN=Google LLC, O=Google LLC, L=Mountain View, S=California, C=US" .to_string(), product_name: "Google Chrome".to_string(), binary_name: Some("chrome.exe".to_string()), access: AllowDenyRequirementToml::Deny, }]), }), }) ); assert!(!requirements.is_empty()); Ok(()) } #[test] fn browser_and_computer_use_leaf_requirements_are_not_empty() -> Result<()> { for (name, requirements_toml) in [ ( "browser history", "[browser_use]\nallow_history_access = false", ), ( "browser auto-review", "[browser_use]\ndisable_auto_review = true", ), ( "global persistent approval", "[browser_use]\nallow_global_persistent_approval = false", ), ( "default origin access", "[browser_use.default_origin_policy]\naccess = \"deny\"", ), ( "default origin downloads", "[browser_use.default_origin_policy]\ndownloads = \"deny\"", ), ( "default origin uploads", "[browser_use.default_origin_policy]\nuploads = \"deny\"", ), ( "default origin full CDP access", "[browser_use.default_origin_policy]\nfull_cdp_access = \"deny\"", ), ( "default origin auto-review", "[browser_use.default_origin_policy]\nauto_review = \"deny\"", ), ( "default origin persistent approval", "[browser_use.default_origin_policy]\npersistent_approval = false", ), ( "default origin access approval lifetime", "[browser_use.default_origin_policy]\naccess_approval_lifetime = \"turn\"", ), ( "origin access", "[browser_use.origins.\"https://example.com\"]\naccess = \"deny\"", ), ( "origin downloads", "[browser_use.origins.\"https://example.com\"]\ndownloads = \"deny\"", ), ( "origin uploads", "[browser_use.origins.\"https://example.com\"]\nuploads = \"deny\"", ), ( "origin full CDP access", "[browser_use.origins.\"https://example.com\"]\nfull_cdp_access = \"deny\"", ), ( "origin auto-review", "[browser_use.origins.\"https://example.com\"]\nauto_review = \"deny\"", ), ( "origin persistent approval", "[browser_use.origins.\"https://example.com\"]\npersistent_approval = false", ), ( "origin access approval lifetime", "[browser_use.origins.\"https://example.com\"]\naccess_approval_lifetime = \"turn\"", ), ( "computer persistent approval", "[computer_use]\nallow_persistent_approval = false", ), ( "default app access", "[computer_use]\ndefault_app_access = \"deny\"", ), ( "macOS bundle identifier", "[computer_use.macos.bundle_ids]\n\"com.example.App\" = \"deny\"", ), ( "Windows AUMID", "[computer_use.windows.aumids]\n\"Example.App_123!Main\" = \"deny\"", ), ( "Windows executable", "[[computer_use.windows.exes]]\npublisher_name = \"CN=Example Corp\"\nproduct_name = \"Example App\"\naccess = \"deny\"", ), ] { let requirements: ConfigRequirementsToml = from_str(requirements_toml)?; assert!(!requirements.is_empty(), "{name} requirement was dropped"); } Ok(()) } #[test] fn deserialize_new_thread_model_defaults() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" [models.new_thread] model = "managed-model" model_reasoning_effort = "medium" service_tier = "fast" "#, )?; assert_eq!( requirements.models, Some(ModelsRequirementsToml { new_thread: Some(NewThreadModelDefaultsToml { model: Some("managed-model".to_string()), model_reasoning_effort: Some(ReasoningEffort::Medium), service_tier: Some("fast".to_string()), }), }) ); assert!(!requirements.is_empty()); Ok(()) } #[test] fn auto_review_required_for_model_matches_exact_provider_aliases() { let requirements = ConfigRequirements { auto_review_required_models: Some(Sourced::new( BTreeSet::from(["protected-model".to_string()]), RequirementSource::Unknown, )), ..Default::default() }; for (model, protected) in [ ("protected-model", true), ("protected-model-preview", false), ("openai-codex/protected-model-preview", false), ("provider_1/protected-model", true), ("protected-modelish", false), ("/protected-model", false), ("bad.provider/protected-model", false), ("provider/nested/protected-model", false), ] { assert_eq!( requirements.auto_review_required_for_model(model), protected, "{model}" ); } } #[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_approvals_reviewers = vec![ApprovalsReviewer::AutoReview, ApprovalsReviewer::User]; 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 browser_use = BrowserUseRequirementsToml { extension: None, allow_webmcp: None, allow_history_access: Some(false), disable_auto_review: Some(true), allow_global_persistent_approval: None, default_origin_policy: None, origins: None, }; let computer_use = ComputerUseRequirementsToml { allow_locked_computer_use: Some(false), allow_persistent_approval: Some(false), default_app_access: None, macos: None, windows: None, }; let auto_review = AutoReviewRequirementsToml { required_on_models: Some(vec!["managed-model".to_string()]), ignore_rules: None, }; let models = ModelsRequirementsToml { new_thread: Some(NewThreadModelDefaultsToml { model: Some("managed-model".to_string()), model_reasoning_effort: Some(ReasoningEffort::Medium), service_tier: Some("fast".to_string()), }), }; let sqlite_home = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-state")) .expect("managed sqlite home should be absolute"); let log_dir = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-logs")) .expect("managed log dir should be absolute"); let model_catalog_json = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-models.json")) .expect("managed model catalog path should be absolute"); let feedback = FeedbackConfigToml { enabled: Some(false), }; let windows = WindowsRequirementsToml { allowed_sandbox_implementations: Some(vec![WindowsSandboxImplementationToml::Elevated]), allow_mxc: None, require_mxc: Some(true), }; let enforce_residency = ResidencyRequirement::Us; let enforce_source = source.clone(); let additional_developer_instructions = "Follow the company policy.".to_string(); let guardian_policy_config = "Use the company-managed guardian policy.".to_string(); let guardian_extra_policy = "Use the company-managed extra 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_login_methods: Some(vec![ForcedLoginMethod::Chatgpt]), allowed_chatgpt_workspaces: Some(vec!["managed-workspace".to_string()]), cli_auth_credentials_store: Some(AuthCredentialsStoreMode::Keyring), chatgpt_base_url: Some("https://managed.example/backend-api/".to_string()), sqlite_home: Some(sqlite_home.clone()), log_dir: Some(log_dir.clone()), model_catalog_json: Some(model_catalog_json.clone()), model_provider: Some("gateway".to_string()), model_providers: Some(HashMap::new()), check_for_update_on_startup: Some(false), allow_login_shell: Some(false), feedback: Some(feedback.clone()), allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_approvals_reviewers: Some(allowed_approvals_reviewers.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), allowed_permission_profiles: Some(BTreeMap::from([("managed".to_string(), true)])), default_permissions: Some("managed".to_string()), remote_sandbox_config: None, allowed_web_search_modes: Some(allowed_web_search_modes.clone()), allow_managed_hooks_only: Some(true), allow_browser_and_computer_use: Some(false), allow_appshots: Some(false), allow_remote_control: Some(false), computer_use: Some(computer_use.clone()), browser_use: Some(browser_use.clone()), in_app_browser: None, windows: Some(windows.clone()), feature_requirements: Some(feature_requirements.clone()), hooks: None, mcp_servers: None, plugins: None, marketplaces: None, apps: None, rules: None, enforce_residency: Some(enforce_residency), network: None, application: None, permissions: None, auto_review: Some(auto_review.clone()), models: Some(models.clone()), additional_developer_instructions: Some(additional_developer_instructions.clone()), guardian_policy_config: Some(guardian_policy_config.clone()), guardian_extra_policy: Some(guardian_extra_policy.clone()), }; target.merge_unset_fields(source.clone(), other); assert_eq!( target, ConfigRequirementsWithSources { allowed_login_methods: Some(Sourced::new( vec![ForcedLoginMethod::Chatgpt], source.clone(), )), allowed_chatgpt_workspaces: Some(Sourced::new( vec!["managed-workspace".to_string()], source.clone(), )), cli_auth_credentials_store: Some(Sourced::new( AuthCredentialsStoreMode::Keyring, source.clone(), )), chatgpt_base_url: Some(Sourced::new( "https://managed.example/backend-api/".to_string(), source.clone(), )), sqlite_home: Some(Sourced::new(sqlite_home, source.clone())), log_dir: Some(Sourced::new(log_dir, source.clone())), model_catalog_json: Some(Sourced::new(model_catalog_json, source.clone())), model_provider: Some(Sourced::new("gateway".to_string(), source.clone())), model_providers: Some(Sourced::new(HashMap::new(), source.clone())), check_for_update_on_startup: Some(Sourced::new( /*value*/ false, source.clone(), )), allow_login_shell: Some(Sourced::new(/*value*/ false, source.clone())), feedback: Some(Sourced::new(feedback, source.clone())), allowed_approval_policies: Some(Sourced::new( allowed_approval_policies, source.clone() )), allowed_approvals_reviewers: Some(Sourced::new( allowed_approvals_reviewers, source.clone(), )), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source.clone(),)), allowed_permission_profiles: Some(Sourced::new( BTreeMap::from([("managed".to_string(), true)]), source.clone(), )), default_permissions: Some(Sourced::new("managed".to_string(), source.clone(),)), allowed_web_search_modes: Some(Sourced::new( allowed_web_search_modes, enforce_source.clone(), )), allow_managed_hooks_only: Some(Sourced::new( /*value*/ true, enforce_source.clone(), )), allow_browser_and_computer_use: Some(Sourced::new( /*value*/ false, enforce_source.clone(), )), allow_appshots: Some(Sourced::new(/*value*/ false, enforce_source.clone(),)), allow_remote_control: Some(Sourced::new( /*value*/ false, enforce_source.clone(), )), computer_use: Some(Sourced::new(computer_use, enforce_source.clone())), browser_use: Some(Sourced::new(browser_use, enforce_source.clone())), in_app_browser: None, windows: Some(Sourced::new(windows, enforce_source.clone())), feature_requirements: Some(Sourced::new( feature_requirements, enforce_source.clone(), )), hooks: None, mcp_servers: None, plugins: None, marketplaces: None, apps: None, rules: None, enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)), network: None, application: None, permissions: None, auto_review: Some(Sourced::new(auto_review, source.clone())), models: Some(Sourced::new(models, source.clone())), additional_developer_instructions: Some(Sourced::new( additional_developer_instructions, source.clone(), )), guardian_policy_config: Some(Sourced::new(guardian_policy_config, source.clone())), guardian_extra_policy: Some(Sourced::new(guardian_extra_policy, 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_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, allow_appshots: None, allow_remote_control: None, computer_use: None, browser_use: None, in_app_browser: None, windows: None, feature_requirements: None, hooks: None, mcp_servers: None, plugins: None, marketplaces: None, apps: None, rules: None, enforce_residency: None, network: None, application: None, permissions: None, models: None, guardian_policy_config: None, ..Default::default() } ); 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"] guardian_extra_policy = "Use the managed extra policy." "#, )?; populated_target.merge_unset_fields(existing_source.clone(), populated_requirements); let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] guardian_extra_policy = "Use the lower-priority extra policy." "#, )?; 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.clone(), )), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, allowed_permission_profiles: None, default_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, allow_appshots: None, allow_remote_control: None, computer_use: None, browser_use: None, in_app_browser: None, windows: None, feature_requirements: None, hooks: None, mcp_servers: None, plugins: None, marketplaces: None, apps: None, rules: None, enforce_residency: None, network: None, application: None, permissions: None, models: None, guardian_policy_config: None, guardian_extra_policy: Some(Sourced::new( "Use the managed extra policy.".to_string(), existing_source, )), ..Default::default() } ); Ok(()) } #[test] fn merge_unset_fields_ignores_blank_guardian_override() { let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields( RequirementSource::LegacyManagedConfigTomlFromMdm, ConfigRequirementsToml { guardian_policy_config: Some(" \n\t".to_string()), guardian_extra_policy: Some(" \n\t".to_string()), ..Default::default() }, ); let source = RequirementSource::SystemRequirementsToml { file: system_requirements_toml_file_for_test().expect("system requirements.toml path"), }; target.merge_unset_fields( source.clone(), ConfigRequirementsToml { guardian_policy_config: Some("Use the system guardian policy.".to_string()), guardian_extra_policy: Some("Use the system extra policy.".to_string()), ..Default::default() }, ); assert_eq!( target, ConfigRequirementsWithSources { guardian_policy_config: Some(Sourced::new( "Use the system guardian policy.".to_string(), source.clone(), )), guardian_extra_policy: Some(Sourced::new( "Use the system extra policy.".to_string(), source, )), ..Default::default() }, ); } #[test] fn deserialize_guardian_policy_config() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" guardian_policy_config = """ Use the cloud-managed guardian policy. """ guardian_extra_policy = """ Use the cloud-managed extra policy. """ "#, )?; assert_eq!( requirements, ConfigRequirementsToml { guardian_policy_config: Some( "Use the cloud-managed guardian policy.\n".to_string() ), guardian_extra_policy: Some("Use the cloud-managed extra policy.\n".to_string()), ..Default::default() } ); Ok(()) } #[test] fn blank_guardian_policy_config_is_empty() -> Result<()> { for key in ["guardian_policy_config", "guardian_extra_policy"] { let requirements: ConfigRequirementsToml = from_str(&format!("{key} = \"\"\"\n\n\"\"\"\n"))?; assert!(requirements.is_empty(), "{key}"); let requirements: ConfigRequirementsToml = from_str(&format!("{key} = \"Use the managed policy.\"\n"))?; assert!(!requirements.is_empty(), "{key}"); } Ok(()) } #[test] fn allowed_approvals_reviewers_is_not_empty() -> Result<()> { let requirements: ConfigRequirementsToml = from_str( r#" allowed_approvals_reviewers = ["user"] "#, )?; assert!(!requirements.is_empty()); Ok(()) } #[test] fn deserialize_filesystem_deny_read_requirements() -> Result<()> { let deny_read_0 = if cfg!(windows) { r"C:\Users\alice\.gitconfig" } else { "/home/alice/.gitconfig" }; let deny_read_1 = if cfg!(windows) { r"C:\Users\alice\.ssh" } else { "/home/alice/.ssh" }; let toml_str = format!( r#" [permissions.filesystem] deny_read = [{deny_read_0:?}, {deny_read_1:?}] "# ); let config: ConfigRequirementsToml = from_str(&toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.filesystem, Some(Sourced::new( FilesystemConstraints { deny_read: vec![ AbsolutePathBuf::from_absolute_path(deny_read_0)?.into(), AbsolutePathBuf::from_absolute_path(deny_read_1)?.into(), ], }, RequirementSource::Unknown, )) ); Ok(()) } #[test] fn deserialize_filesystem_deny_read_glob_requirements() -> Result<()> { let temp_dir = std::env::temp_dir(); let _guard = AbsolutePathBufGuard::new(&temp_dir); let config: ConfigRequirementsToml = from_str( r#" [permissions.filesystem] deny_read = ["./private/**/*.txt"] "#, )?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.filesystem, Some(Sourced::new( FilesystemConstraints { deny_read: vec![ FilesystemDenyReadPattern::from_input("./private/**/*.txt") .expect("normalize glob pattern"), ], }, RequirementSource::Unknown, )) ); 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), tools: None, }, )]), }) ); Ok(()) } #[test] fn deserialize_apps_tool_requirements() -> Result<()> { let toml_str = r#" [apps.connector_123123.tools."calendar/list_events"] approval_mode = "approve" "#; let requirements: ConfigRequirementsToml = from_str(toml_str)?; assert_eq!( requirements.apps, Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: None, tools: Some(AppToolsRequirementsToml { tools: BTreeMap::from([( "calendar/list_events".to_string(), AppToolRequirementToml { approval_mode: Some(AppToolApproval::Approve), analytics_result_source: None, }, )]), }), }, )]), }) ); Ok(()) } #[test] fn app_tool_result_source_requirements_parse_and_merge() -> Result<()> { let rule = r#" [apps.connector_123123.tools."messages/search"] analytics_result_source = { format = "detailed_message_search_v1", type = "message_room" } "#; let requirements: ConfigRequirementsToml = from_str(rule)?; assert!(!requirements.is_empty()); let source = requirements.apps.expect("apps should be present"); for higher_rule in [ None, Some(("unsupported", "other_resource")), Some(("detailed_message_search_v1", "other_resource")), ] { let mut merged = source.clone(); merged .apps .get_mut("connector_123123") .expect("app should be present") .tools .as_mut() .expect("tools should be present") .tools .get_mut("messages/search") .expect("tool should be present") .analytics_result_source = higher_rule.map(|(format, source_type)| { from_str(&format!("format = {format:?}\ntype = {source_type:?}")) .expect("complete source rule should parse, including unknown formats") }); let expected = if higher_rule.is_none() { source.clone() } else { merged.clone() }; merge_app_requirements_descending(&mut merged, source.clone()); assert_eq!(merged, expected); } for incomplete_rule in [ r#"format = "detailed_message_search_v1""#, r#"type = "message_room""#, ] { assert!(from_str::(incomplete_rule).is_err()); } Ok(()) } fn apps_requirements(entries: &[(&str, Option)]) -> AppsRequirementsToml { AppsRequirementsToml { apps: entries .iter() .map(|(app_id, enabled)| { ( (*app_id).to_string(), AppRequirementToml { enabled: *enabled, tools: None, }, ) }) .collect(), } } fn app_tool_requirements( app_id: &str, tool_name: &str, approval_mode: AppToolApproval, ) -> AppsRequirementsToml { AppsRequirementsToml { apps: BTreeMap::from([( app_id.to_string(), AppRequirementToml { enabled: None, tools: Some(AppToolsRequirementsToml { tools: BTreeMap::from([( tool_name.to_string(), AppToolRequirementToml { approval_mode: Some(approval_mode), analytics_result_source: None, }, )]), }), }, )]), } } #[test] fn merge_app_requirements_descending_unions_distinct_apps() { let mut merged = apps_requirements(&[("connector_high", Some(false))]); let lower = apps_requirements(&[("connector_low", Some(true))]); merge_app_requirements_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[ ("connector_high", Some(false)), ("connector_low", Some(true)) ]), ); } #[test] fn merge_app_requirements_descending_prefers_false_from_lower_precedence() { let mut merged = apps_requirements(&[("connector_123123", Some(true))]); let lower = apps_requirements(&[("connector_123123", Some(false))]); merge_app_requirements_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(false))]), ); } #[test] fn merge_app_requirements_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_app_requirements_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(true))]), ); } #[test] fn merge_app_requirements_descending_uses_lower_value_when_higher_missing() { let mut merged = apps_requirements(&[]); let lower = apps_requirements(&[("connector_123123", Some(true))]); merge_app_requirements_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(true))]), ); } #[test] fn merge_app_requirements_descending_preserves_higher_false_when_lower_missing_app() { let mut merged = apps_requirements(&[("connector_123123", Some(false))]); let lower = apps_requirements(&[]); merge_app_requirements_descending(&mut merged, lower); assert_eq!( merged, apps_requirements(&[("connector_123123", Some(false))]), ); } #[test] fn merge_app_requirements_descending_preserves_higher_tool_approval_mode() { let mut merged = app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ); let lower = app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Prompt, ); merge_app_requirements_descending(&mut merged, lower); assert_eq!( merged, app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ) ); } #[test] fn merge_app_requirements_descending_uses_lower_tool_approval_when_higher_missing() { let mut merged = apps_requirements(&[("connector_123123", None)]); let lower = app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ); merge_app_requirements_descending(&mut merged, lower); assert_eq!( merged, app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ) ); } #[test] fn merge_unset_fields_merges_apps_across_sources_with_enabled_evaluation() { let higher_source = RequirementSource::LegacyManagedConfigTomlFromMdm; let lower_source = RequirementSource::MdmManagedPreferences { domain: "com.openai.codex".to_string(), key: "requirements_toml_base64".to_string(), }; 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::LegacyManagedConfigTomlFromMdm, 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_approvals_reviewers = ["auto_review"] 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 .permission_profile .can_set(&PermissionProfile::Disabled), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), allowed: "[ReadOnly]".into(), requirement_source: source_location.clone(), }) ); assert_eq!( requirements .approvals_reviewer .can_set(&ApprovalsReviewer::User), Err(ConstraintError::InvalidValue { field_name: "approvals_reviewer", candidate: "User".into(), allowed: "[AutoReview]".into(), requirement_source: source_location, }) ); Ok(()) } #[test] fn constraint_error_includes_composite_requirement_source() -> Result<()> { let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] "#, )?; let source_location = RequirementSource::composite([ RequirementSource::MdmManagedPreferences { domain: "com.openai.codex".to_string(), key: "requirements_toml_base64".to_string(), }, RequirementSource::LegacyManagedConfigTomlFromMdm, ]); 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_approvals_reviewers = ["auto_review"] allowed_sandbox_modes = ["read-only"] allowed_web_search_modes = ["cached"] enforce_residency = "us" [features] personality = true "#, )?; let source_location = RequirementSource::LegacyManagedConfigTomlFromMdm; 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.approvals_reviewer.source, Some(source_location.clone()) ); assert_eq!( requirements.permission_profile.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 = ["on-request", "never"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.approval_policy.value(), AskForApproval::OnRequest, "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::OnRequest) .is_ok() ); assert_eq!( requirements .approval_policy .can_set(&AskForApproval::UnlessTrusted), Err(ConstraintError::InvalidValue { field_name: "approval_policy", candidate: "UnlessTrusted".into(), allowed: "[OnRequest, Never]".into(), requirement_source: RequirementSource::Unknown, }) ); assert!( requirements .permission_profile .can_set(&PermissionProfile::read_only()) .is_ok() ); Ok(()) } #[test] fn deserialize_allowed_approvals_reviewers() -> Result<()> { let toml_str = r#" allowed_approvals_reviewers = ["auto_review", "user"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.approvals_reviewer.value(), ApprovalsReviewer::AutoReview, "currently, there is no way to specify the default value for approvals reviewer in the toml, so it picks the first allowed value" ); assert!( requirements .approvals_reviewer .can_set(&ApprovalsReviewer::AutoReview) .is_ok() ); assert!( requirements .approvals_reviewer .can_set(&ApprovalsReviewer::User) .is_ok() ); Ok(()) } #[test] fn deserialize_allowed_windows_sandbox_implementations() -> Result<()> { let requirements: ConfigRequirementsToml = from_str("[windows]\nrequire_mxc = true")?; assert!(!requirements.is_empty()); let toml_str = r#" [windows] allowed_sandbox_implementations = ["elevated"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.windows_sandbox_mode.value(), Some(WindowsSandboxModeToml::Elevated) ); assert!( requirements .windows_sandbox_mode .can_set(&Some(WindowsSandboxModeToml::Elevated)) .is_ok() ); assert!( requirements .windows_sandbox_mode .can_set(&Some(WindowsSandboxModeToml::Unelevated)) .is_err() ); assert!( requirements .windows_sandbox_mode .can_set(&Some(WindowsSandboxModeToml::Mxc)) .is_ok() ); assert!(requirements.windows_sandbox_mode.can_set(&None).is_err()); for allow_mxc in [true, false] { let config = from_str(&format!("{toml_str}\nallow_mxc = {allow_mxc}"))?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; let constraint = &requirements.windows_sandbox_mode; assert_eq!( [ Some(WindowsSandboxModeToml::Elevated), Some(WindowsSandboxModeToml::Unelevated), Some(WindowsSandboxModeToml::Mxc), None, ] .map(|mode| constraint.can_set(&mode).is_ok()), [true, false, allow_mxc, false] ); } Ok(()) } #[test] fn empty_allowed_windows_sandbox_implementations_is_rejected() -> Result<()> { let toml_str = r#" [windows] allowed_sandbox_implementations = [] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; assert_eq!( ConfigRequirements::try_from(with_unknown_source(config)), Err(ConstraintError::EmptyField { field_name: "windows.allowed_sandbox_implementations".to_string(), }) ); Ok(()) } #[test] fn allowed_windows_sandbox_implementations_prefer_elevated_fallback() -> Result<()> { let toml_str = r#" [windows] allowed_sandbox_implementations = ["unelevated", "elevated"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.windows_sandbox_mode.value(), Some(WindowsSandboxModeToml::Elevated) ); Ok(()) } #[test] fn deserialize_legacy_allowed_approvals_reviewer() -> Result<()> { let toml_str = r#" allowed_approvals_reviewers = ["guardian_subagent", "user"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!( requirements.approvals_reviewer.value(), ApprovalsReviewer::AutoReview ); Ok(()) } #[test] fn empty_allowed_approvals_reviewers_is_rejected() -> Result<()> { let toml_str = r#" allowed_approvals_reviewers = [] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; let err = ConfigRequirements::try_from(with_unknown_source(config)) .expect_err("empty approvals reviewer allow-list should be rejected"); assert_eq!( err, ConstraintError::EmptyField { field_name: "allowed_approvals_reviewers".to_string(), } ); 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 .permission_profile .can_set(&PermissionProfile::read_only()) .is_ok() ); let workspace_write_profile = PermissionProfile::workspace_write_with( &[AbsolutePathBuf::from_absolute_path(root)?], NetworkSandboxPolicy::Restricted, /*exclude_tmpdir_env_var*/ false, /*exclude_slash_tmp*/ false, ); assert!( requirements .permission_profile .can_set(&workspace_write_profile) .is_ok() ); assert_eq!( requirements .permission_profile .can_set(&PermissionProfile::Disabled), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), allowed: "[ReadOnly, WorkspaceWrite]".into(), requirement_source: RequirementSource::Unknown, }) ); assert_eq!( requirements .permission_profile .can_set(&PermissionProfile::External { network: NetworkSandboxPolicy::Restricted, }), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "ExternalSandbox".into(), allowed: "[ReadOnly, WorkspaceWrite]".into(), requirement_source: RequirementSource::Unknown, }) ); Ok(()) } #[test] fn deserialize_remote_sandbox_config_requires_hostname_patterns_list() -> Result<()> { let toml_str = r#" [[remote_sandbox_config]] hostname_patterns = ["*.org", "runner-??.ci"] allowed_sandbox_modes = ["read-only", "workspace-write"] "#; let config: ConfigRequirementsToml = from_str(toml_str)?; assert_eq!( config.remote_sandbox_config, Some(vec![RemoteSandboxConfigToml { hostname_patterns: vec!["*.org".to_string(), "runner-??.ci".to_string()], allowed_sandbox_modes: vec![ SandboxModeRequirement::ReadOnly, SandboxModeRequirement::WorkspaceWrite, ], }]) ); let err = from_str::( r#" [[remote_sandbox_config]] hostname_patterns = "*.org" allowed_sandbox_modes = ["read-only"] "#, ) .expect_err("hostname_patterns should be list-only"); assert!( err.to_string().contains("invalid type: string"), "unexpected error: {err}" ); Ok(()) } #[test] fn remote_sandbox_config_first_match_overrides_top_level() -> Result<()> { let source = RequirementSource::LegacyManagedConfigTomlFromMdm; let mut requirements_toml: ConfigRequirementsToml = from_str( r#" allowed_sandbox_modes = ["read-only"] [[remote_sandbox_config]] hostname_patterns = ["build-*.example.com"] allowed_sandbox_modes = ["read-only", "workspace-write"] [[remote_sandbox_config]] hostname_patterns = ["build-01.example.com"] allowed_sandbox_modes = ["read-only", "danger-full-access"] "#, )?; requirements_toml.apply_remote_sandbox_config(Some("BUILD-01.EXAMPLE.COM.")); let mut requirements_with_sources = ConfigRequirementsWithSources::default(); requirements_with_sources.merge_unset_fields(source.clone(), requirements_toml); assert_eq!( requirements_with_sources .allowed_sandbox_modes .as_ref() .map(|sourced| sourced.value.clone()), Some(vec![ SandboxModeRequirement::ReadOnly, SandboxModeRequirement::WorkspaceWrite, ]) ); let requirements = ConfigRequirements::try_from(requirements_with_sources)?; let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; let workspace_write_profile = PermissionProfile::workspace_write_with( &[AbsolutePathBuf::from_absolute_path(root)?], NetworkSandboxPolicy::Restricted, /*exclude_tmpdir_env_var*/ false, /*exclude_slash_tmp*/ false, ); assert!( requirements .permission_profile .can_set(&workspace_write_profile) .is_ok() ); assert_eq!( requirements .permission_profile .can_set(&PermissionProfile::Disabled), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), allowed: "[ReadOnly, WorkspaceWrite]".into(), requirement_source: source, }) ); Ok(()) } #[test] fn remote_sandbox_config_non_match_preserves_top_level() -> Result<()> { let mut requirements_toml: ConfigRequirementsToml = from_str( r#" allowed_sandbox_modes = ["read-only"] [[remote_sandbox_config]] hostname_patterns = ["build-*.example.com"] allowed_sandbox_modes = ["read-only", "workspace-write"] "#, )?; requirements_toml.apply_remote_sandbox_config(Some("laptop.example.com")); let mut requirements_with_sources = ConfigRequirementsWithSources::default(); requirements_with_sources.merge_unset_fields(RequirementSource::Unknown, requirements_toml); let requirements = ConfigRequirements::try_from(requirements_with_sources)?; assert_eq!( requirements .permission_profile .can_set(&PermissionProfile::Disabled), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), allowed: "[ReadOnly]".into(), requirement_source: RequirementSource::Unknown, }) ); Ok(()) } #[test] fn remote_sandbox_config_does_not_override_higher_precedence_sandbox_modes() -> Result<()> { let high_source = RequirementSource::LegacyManagedConfigTomlFromMdm; let mut high_precedence: ConfigRequirementsToml = from_str( r#" allowed_sandbox_modes = ["read-only"] "#, )?; high_precedence.apply_remote_sandbox_config(Some("runner-01.ci.example.com")); let mut low_precedence: ConfigRequirementsToml = from_str( r#" [[remote_sandbox_config]] hostname_patterns = ["runner-*.ci.example.com"] allowed_sandbox_modes = ["read-only", "workspace-write"] "#, )?; low_precedence.apply_remote_sandbox_config(Some("runner-01.ci.example.com")); let mut requirements_with_sources = ConfigRequirementsWithSources::default(); requirements_with_sources.merge_unset_fields(high_source.clone(), high_precedence); requirements_with_sources.merge_unset_fields(RequirementSource::Unknown, low_precedence); let requirements = ConfigRequirements::try_from(requirements_with_sources)?; assert_eq!( requirements .permission_profile .can_set(&PermissionProfile::workspace_write()), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "WorkspaceWrite".into(), allowed: "[ReadOnly]".into(), requirement_source: high_source, }) ); 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_supports_indexed() -> Result<()> { let config: ConfigRequirementsToml = from_str( r#" allowed_web_search_modes = ["indexed"] "#, )?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; assert_eq!(requirements.web_search_mode.value(), WebSearchMode::Indexed); for mode in [WebSearchMode::Disabled, WebSearchMode::Indexed] { assert!(requirements.web_search_mode.can_set(&mode).is_ok()); } for mode in [WebSearchMode::Cached, WebSearchMode::Live] { assert_eq!( requirements.web_search_mode.can_set(&mode), Err(ConstraintError::InvalidValue { field_name: "web_search_mode", candidate: format!("{mode:?}"), allowed: "[Disabled, Indexed]".into(), requirement_source: RequirementSource::Unknown, }) ); } 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 deserialize_managed_hooks_requirements() -> Result<()> { let toml_str = r#" managed_dir = "/enterprise/hooks" windows_managed_dir = 'C:\enterprise\hooks' [[PreToolUse]] matcher = "^Bash$" [[PreToolUse.hooks]] type = "command" command = "python3 /enterprise/hooks/pre.py" timeout = 10 statusMessage = "checking" "#; let hooks: ManagedHooksRequirementsToml = from_str(toml_str)?; assert_eq!( hooks.managed_dir.as_deref(), Some(std::path::Path::new("/enterprise/hooks")) ); assert_eq!(hooks.handler_count(), 1); assert_eq!(hooks.hooks.pre_tool_use.len(), 1); Ok(()) } #[test] fn merge_unset_fields_does_not_overwrite_existing_hooks() -> Result<()> { let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields( RequirementSource::LegacyManagedConfigTomlFromMdm, from_str::( r#" [hooks] managed_dir = "/cloud/hooks" [[hooks.PreToolUse]] matcher = "^Bash$" [[hooks.PreToolUse.hooks]] type = "command" command = "python3 /cloud/hooks/pre.py" "#, )?, ); target.merge_unset_fields( RequirementSource::SystemRequirementsToml { file: system_requirements_toml_file_for_test()?, }, from_str::( r#" [hooks] managed_dir = "/system/hooks" [[hooks.PreToolUse]] matcher = "^Bash$" [[hooks.PreToolUse.hooks]] type = "command" command = "python3 /system/hooks/pre.py" "#, )?, ); assert_eq!( target .hooks .as_ref() .and_then(|hooks| hooks.value.managed_dir.as_ref()) .map(std::path::PathBuf::as_path), Some(std::path::Path::new("/cloud/hooks")) ); assert_eq!( target.hooks.as_ref().map(|hooks| hooks.source.clone()), Some(RequirementSource::LegacyManagedConfigTomlFromMdm) ); Ok(()) } #[test] fn managed_hooks_constraint_rejects_drift() -> Result<()> { let config: ConfigRequirementsToml = from_str( r#" [hooks] managed_dir = "/enterprise/hooks" [[hooks.PreToolUse]] matcher = "^Bash$" [[hooks.PreToolUse.hooks]] type = "command" command = "python3 /enterprise/hooks/pre.py" "#, )?; let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; let mut managed_hooks = requirements .managed_hooks .expect("expected managed hooks requirements"); let err = managed_hooks .set(ManagedHooksRequirementsToml { managed_dir: Some(std::path::PathBuf::from("/other/hooks")), windows_managed_dir: None, hooks: HookEventsToml::default(), }) .expect_err("managed hooks should reject drift"); assert!(matches!( err, ConstraintError::InvalidValue { field_name: "hooks", requirement_source: 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 managed_allowed_domains_only = true allow_local_binding = false [experimental_network.domains] "api.example.com" = "allow" "*.openai.com" = "allow" "blocked.example.com" = "deny" [experimental_network.unix_sockets] "/tmp/example.sock" = "allow" "/tmp/blocked.sock" = "deny" [[experimental_network.header_injections]] host = "api.example.com" methods = ["POST"] path_prefixes = ["/console/v1"] [experimental_network.header_injections.headers] "x-statsig-change-source" = "codex" "#; let source = RequirementSource::LegacyManagedConfigTomlFromMdm; 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.domains.as_ref(), Some(&NetworkDomainPermissionsToml { entries: BTreeMap::from([ ( "*.openai.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "api.example.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "blocked.example.com".to_string(), NetworkDomainPermissionToml::Deny, ), ]), }) ); assert_eq!( sourced_network.value.managed_allowed_domains_only, Some(true) ); assert_eq!( sourced_network.value.unix_sockets.as_ref(), Some(&NetworkUnixSocketPermissionsToml { entries: BTreeMap::from([ ( "/tmp/blocked.sock".to_string(), NetworkUnixSocketPermissionToml::Deny, ), ( "/tmp/example.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, ), ]), }) ); assert_eq!(sourced_network.value.allow_local_binding, Some(false)); assert_eq!( sourced_network.value.header_injections, Some(vec![NetworkHeaderInjectionToml { host: "api.example.com".to_string(), methods: vec!["POST".to_string()], path_prefixes: vec!["/console/v1".to_string()], headers: BTreeMap::from([( "x-statsig-change-source".to_string(), "codex".to_string(), )]), }]) ); let debug = format!("{:?}", sourced_network.value.header_injections); assert!(debug.contains("x-statsig-change-source")); assert!(!debug.contains("codex")); Ok(()) } #[test] fn legacy_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::LegacyManagedConfigTomlFromMdm; 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.domains.as_ref(), Some(&NetworkDomainPermissionsToml { entries: BTreeMap::from([ ( "*.openai.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "api.example.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "blocked.example.com".to_string(), NetworkDomainPermissionToml::Deny, ), ]), }) ); assert_eq!( sourced_network.value.managed_allowed_domains_only, Some(true) ); assert_eq!( sourced_network.value.unix_sockets.as_ref(), Some(&NetworkUnixSocketPermissionsToml { entries: BTreeMap::from([( "/tmp/example.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, )]), }) ); assert_eq!(sourced_network.value.allow_local_binding, Some(false)); Ok(()) } #[test] fn mixed_legacy_and_canonical_network_requirements_are_rejected() { let err = from_str::( r#" [experimental_network] allowed_domains = ["api.example.com"] [experimental_network.domains] "*.openai.com" = "allow" "#, ) .expect_err("mixed network domain shapes should fail"); assert!( err.to_string() .contains("`experimental_network.domains` cannot be combined"), "unexpected error: {err:#}" ); let err = from_str::( r#" [experimental_network] allow_unix_sockets = ["/tmp/example.sock"] [experimental_network.unix_sockets] "/tmp/another.sock" = "allow" "#, ) .expect_err("mixed network unix socket shapes should fail"); assert!( err.to_string() .contains("`experimental_network.unix_sockets` cannot be combined"), "unexpected error: {err:#}" ); } #[test] fn network_permission_containers_project_allowed_and_denied_entries() { let domains = NetworkDomainPermissionsToml { entries: BTreeMap::from([ ( "*.openai.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "api.example.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "blocked.example.com".to_string(), NetworkDomainPermissionToml::Deny, ), ]), }; let unix_sockets = NetworkUnixSocketPermissionsToml { entries: BTreeMap::from([ ( "/tmp/example.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, ), ( "/tmp/ignored.sock".to_string(), NetworkUnixSocketPermissionToml::Deny, ), ]), }; assert_eq!( domains.allowed_domains(), Some(vec![ "*.openai.com".to_string(), "api.example.com".to_string() ]) ); assert_eq!( domains.denied_domains(), Some(vec!["blocked.example.com".to_string()]) ); assert_eq!( NetworkDomainPermissionsToml { entries: BTreeMap::from([( "api.example.com".to_string(), NetworkDomainPermissionToml::Allow, )]), } .denied_domains(), None ); assert_eq!( unix_sockets.allow_unix_sockets(), vec!["/tmp/example.sock".to_string()] ); } #[test] fn deserialize_mcp_server_requirements() -> Result<()> { let toml_str = r#" [mcp_servers.docs] description = "ignored legacy field" [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 { identity: McpServerIdentity::Command { command: "codex-mcp".to_string(), }, }, ), ( "remote".to_string(), McpServerRequirement::Identity { identity: McpServerIdentity::Url { url: "https://example.com/mcp".to_string(), }, }, ), ]), RequirementSource::Unknown, )) ); Ok(()) } #[test] fn deserialize_mcp_server_matcher_requirements() -> Result<()> { let toml_str = r#" [mcp_servers.internal_mcp_proxy.identity] command = { executable = "company-cli", args = [ { match = "exact", value = "mcp" }, { match = "exact", value = "proxy" }, { match = "exact", value = "--server" }, { match = "regex", expression = '^https://[A-Za-z0-9-]+\.mcp\.internal\.example\.com(?::443)?(?:/.*)?$' }, ] } "#; let requirements: ConfigRequirements = with_unknown_source(from_str(toml_str)?).try_into()?; assert_eq!( requirements.mcp_servers, Some(Sourced::new( BTreeMap::from([( "internal_mcp_proxy".to_string(), McpServerRequirement::Command(McpServerCommandMatcher { executable: "company-cli".to_string(), args: vec![ McpServerValueMatcher::Exact { value: "mcp".to_string(), }, McpServerValueMatcher::Exact { value: "proxy".to_string(), }, McpServerValueMatcher::Exact { value: "--server".to_string(), }, McpServerValueMatcher::Regex { expression: r"^https://[A-Za-z0-9-]+\.mcp\.internal\.example\.com(?::443)?(?:/.*)?$" .to_string(), }, ], }), )]), RequirementSource::Unknown, )) ); Ok(()) } #[test] fn invalid_mcp_server_requirement_regex_reports_the_server_name_and_source() -> Result<()> { let toml_str = r#" [mcp_servers.broken_rule.identity] url = { match = "regex", expression = "[" } "#; let err = ConfigRequirements::try_from(with_unknown_source(from_str(toml_str)?)) .expect_err("invalid matcher regex should fail requirements normalization"); let ConstraintError::McpServerRequirementParse { server_name, requirement_source, reason, } = err else { panic!("unexpected error: {err:?}"); }; assert_eq!(server_name, "broken_rule"); assert_eq!(requirement_source, RequirementSource::Unknown); assert!(reason.contains("invalid regex `[`"), "{reason}"); Ok(()) } #[test] fn deserialize_plugin_mcp_server_requirements() -> Result<()> { let toml_str = r#" [plugins."sample@test".mcp_servers.sample.identity] command = "sample-mcp" [plugins."remote@test".mcp_servers.remote.identity] url = "https://example.com/mcp" "#; let requirements: ConfigRequirements = with_unknown_source(from_str(toml_str)?).try_into()?; assert_eq!( requirements.plugins, Some(Sourced::new( BTreeMap::from([ ( "remote@test".to_string(), PluginRequirementsToml { mcp_servers: Some(BTreeMap::from([( "remote".to_string(), McpServerRequirement::Identity { identity: McpServerIdentity::Url { url: "https://example.com/mcp".to_string(), }, }, )])), }, ), ( "sample@test".to_string(), PluginRequirementsToml { mcp_servers: Some(BTreeMap::from([( "sample".to_string(), McpServerRequirement::Identity { identity: McpServerIdentity::Command { command: "sample-mcp".to_string(), }, }, )])), }, ), ]), RequirementSource::Unknown, )) ); Ok(()) } #[test] fn deserialize_plugin_mcp_server_matcher_requirement() -> Result<()> { let toml_str = r#" [plugins."sample@test".mcp_servers.internal_proxy.identity] command = { executable = "company-cli", args = [ { match = "exact", value = "mcp" }, { match = "regex", expression = '^https://[a-z]+\.example\.com$' }, ] } "#; let requirements: ConfigRequirements = with_unknown_source(from_str(toml_str)?).try_into()?; assert_eq!( requirements.plugins, Some(Sourced::new( BTreeMap::from([( "sample@test".to_string(), PluginRequirementsToml { mcp_servers: Some(BTreeMap::from([( "internal_proxy".to_string(), McpServerRequirement::Command(McpServerCommandMatcher { executable: "company-cli".to_string(), args: vec![ McpServerValueMatcher::Exact { value: "mcp".to_string(), }, McpServerValueMatcher::Regex { expression: r"^https://[a-z]+\.example\.com$".to_string(), }, ], }), )])), }, )]), RequirementSource::Unknown, )) ); Ok(()) } #[test] fn invalid_plugin_mcp_server_regex_reports_plugin_and_server_name() -> Result<()> { let toml_str = r#" [plugins."sample@test".mcp_servers.broken_rule.identity] url = { match = "regex", expression = "[" } "#; let err = ConfigRequirements::try_from(with_unknown_source(from_str(toml_str)?)) .expect_err("invalid plugin MCP regex should fail requirements normalization"); let ConstraintError::McpServerRequirementParse { server_name, requirement_source, reason, } = err else { panic!("unexpected error: {err:?}"); }; assert_eq!(server_name, "sample@test/broken_rule"); assert_eq!(requirement_source, RequirementSource::Unknown); assert!(reason.contains("invalid regex `[`"), "{reason}"); 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(()) } }