use std::sync::Arc; use crate::config_manager::ConfigManager; use crate::config_manager_service::ConfigManagerError; use crate::error_code::internal_error; use crate::error_code::invalid_request; use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::AllowDenyRequirement; use codex_app_server_protocol::AutoReviewRequirements; use codex_app_server_protocol::BrowserUseAccessApprovalLifetime; use codex_app_server_protocol::BrowserUseOriginPolicy; use codex_app_server_protocol::BrowserUseRequirements; use codex_app_server_protocol::CliAuthCredentialsStoreMode; use codex_app_server_protocol::ClientResponsePayload; use codex_app_server_protocol::ComputerUseMacosRequirements; use codex_app_server_protocol::ComputerUseRequirements; use codex_app_server_protocol::ComputerUseWindowsExeRequirement; use codex_app_server_protocol::ComputerUseWindowsRequirements; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigReadParams; use codex_app_server_protocol::ConfigReadResponse; use codex_app_server_protocol::ConfigRequirements; use codex_app_server_protocol::ConfigRequirementsReadResponse; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWriteErrorCode; use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::ConfiguredHookHandler; use codex_app_server_protocol::ConfiguredHookMatcherGroup; use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams; use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse; use codex_app_server_protocol::FeedbackRequirements; use codex_app_server_protocol::InAppBrowserRequirements; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ManagedHooksRequirements; use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse; use codex_app_server_protocol::ModelsRequirements; use codex_app_server_protocol::NetworkDomainPermission; use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::NetworkUnixSocketPermission; use codex_app_server_protocol::NewThreadModelDefaults; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ConfigRequirementsToml; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig as CoreHookHandlerConfig; use codex_config::ManagedHooksRequirementsToml; use codex_config::MatcherGroup as CoreMatcherGroup; use codex_config::ResidencyRequirement as CoreResidencyRequirement; use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement; use codex_core::ThreadManager; use codex_features::Feature; use codex_features::canonical_feature_for_key; use codex_features::feature_for_key; use codex_model_provider::create_model_provider; use codex_plugin::PluginId; use codex_protocol::config_types::WebSearchMode; use serde_json::json; use std::path::PathBuf; const BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE: &str = "background_paginated_rollout_migration"; const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ "auth_elicitation", BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE, "mcp_2026_07_28", "memories", "mentions_v2", "remote_control", "remote_plugin", "tool_suggest", "windows_sandbox_service", ]; #[derive(Clone)] pub(crate) struct ConfigRequestProcessor { outgoing: Arc, config_manager: ConfigManager, thread_manager: Arc, analytics_events_client: AnalyticsEventsClient, } impl ConfigRequestProcessor { pub(crate) fn new( outgoing: Arc, config_manager: ConfigManager, thread_manager: Arc, analytics_events_client: AnalyticsEventsClient, ) -> Self { Self { outgoing, config_manager, thread_manager, analytics_events_client, } } pub(crate) async fn read( &self, params: ConfigReadParams, ) -> Result { let fallback_cwd = params.cwd.as_ref().map(PathBuf::from); let mut response = self.config_manager.read(params).await.map_err(map_error)?; let config = self.load_latest_config(fallback_cwd).await?; for feature_key in SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT { let Some(feature) = feature_for_key(feature_key) else { continue; }; let features = response .config .additional .entry("features".to_string()) .or_insert_with(|| json!({})); if !features.is_object() { *features = json!({}); } if let Some(features) = features.as_object_mut() { features.insert( (*feature_key).to_string(), json!(config.features.enabled(feature)), ); } } Ok(response) } pub(crate) async fn config_requirements_read( &self, ) -> Result { let requirements = self .config_manager .read_requirements() .await .map_err(map_error)? .map(map_requirements_toml_to_api); Ok(ConfigRequirementsReadResponse { requirements }) } pub(crate) async fn value_write( &self, params: ConfigValueWriteParams, ) -> Result { self.handle_config_mutation_result(self.write_value(params).await) .await .map(ClientResponsePayload::ConfigValueWrite) } pub(crate) async fn batch_write( &self, params: ConfigBatchWriteParams, ) -> Result { let session_defaults_only = !params.edits.is_empty() && params.edits.iter().all(|edit| { matches!( edit.key_path.as_str(), "model" | "model_reasoning_effort" | "plan_mode_reasoning_effort" | "service_tier" | "personality" ) }); let reload_user_config = params.reload_user_config; let response = self.batch_write_inner(params).await?; if !session_defaults_only { self.handle_config_mutation().await; if reload_user_config { self.reload_user_config().await; } } Ok(ClientResponsePayload::ConfigBatchWrite(response)) } pub(crate) async fn experimental_feature_enablement_set( &self, request_id: ConnectionRequestId, params: ExperimentalFeatureEnablementSetParams, ) -> Result, JSONRPCErrorError> { let response = self .handle_config_mutation_result(self.set_experimental_feature_enablement(params).await) .await?; if !response.enablement.is_empty() { self.reload_user_config().await; } self.outgoing .send_response_as( request_id, ClientResponsePayload::ExperimentalFeatureEnablementSet(response), ) .await; Ok(None) } pub(crate) async fn model_provider_capabilities_read( &self, ) -> Result { let config = self.load_latest_config(/*fallback_cwd*/ None).await?; let provider = create_model_provider(config.model_provider, /*auth_manager*/ None); let capabilities = provider.capabilities(); Ok(ModelProviderCapabilitiesReadResponse { namespace_tools: capabilities.namespace_tools, image_generation: capabilities.image_generation, web_search: capabilities.web_search, }) } pub(crate) async fn handle_config_mutation(&self) { self.thread_manager.plugins_manager().clear_cache(); self.thread_manager.skills_service().clear_cache(); } async fn handle_config_mutation_result( &self, result: std::result::Result, ) -> Result { let response = result?; self.handle_config_mutation().await; Ok(response) } async fn load_latest_config( &self, fallback_cwd: Option, ) -> Result { self.config_manager .load_latest_config(fallback_cwd) .await .map_err(|err| { internal_error(format!( "failed to resolve feature override precedence: {err}" )) }) } async fn write_value( &self, params: ConfigValueWriteParams, ) -> Result { let pending_changes = codex_core_plugins::toggles::collect_plugin_enabled_candidates( [(¶ms.key_path, ¶ms.value)].into_iter(), ); let response = self .config_manager .write_value(params) .await .map_err(map_error)?; self.emit_plugin_toggle_events(pending_changes).await; Ok(response) } async fn batch_write_inner( &self, params: ConfigBatchWriteParams, ) -> Result { let pending_changes = codex_core_plugins::toggles::collect_plugin_enabled_candidates( params .edits .iter() .map(|edit| (&edit.key_path, &edit.value)), ); let response = self .config_manager .batch_write(params) .await .map_err(map_error)?; self.emit_plugin_toggle_events(pending_changes).await; Ok(response) } async fn set_experimental_feature_enablement( &self, params: ExperimentalFeatureEnablementSetParams, ) -> Result { let ExperimentalFeatureEnablementSetParams { mut enablement } = params; let mut invalid_keys = Vec::new(); enablement.retain(|key, _| { let valid = canonical_feature_for_key(key).is_some() && SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT.contains(&key.as_str()); if !valid { invalid_keys.push(key.clone()); } valid }); if !invalid_keys.is_empty() { let invalid_keys = invalid_keys.join(", "); tracing::warn!("ignoring invalid experimental feature enablement keys: {invalid_keys}"); } if enablement.is_empty() { return Ok(ExperimentalFeatureEnablementSetResponse { enablement }); } // Most runtime features are read later from config. Background migration is a one-shot // process-scoped task, so start it when runtime enablement first changes it to on. let feature = Feature::BackgroundPaginatedRolloutMigration; let should_start_background_rollout_migration = enablement .get(BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE) .is_some_and(|enabled| *enabled) && !self .load_latest_config(/*fallback_cwd*/ None) .await? .features .enabled(feature); self.config_manager .extend_runtime_feature_enablement( enablement .iter() .map(|(name, enabled)| (name.clone(), *enabled)), ) .map_err(|_| internal_error("failed to update feature enablement"))?; let config = self.load_latest_config(/*fallback_cwd*/ None).await?; if should_start_background_rollout_migration && config.features.enabled(feature) { self.thread_manager.start_background_rollout_migration(); } Ok(ExperimentalFeatureEnablementSetResponse { enablement }) } async fn reload_user_config(&self) { match self.load_latest_config(/*fallback_cwd*/ None).await { Ok(_) => {} Err(err) => { tracing::warn!( "failed to rebuild user config for runtime refresh: {}", err.message ); return; } }; let thread_ids = self.thread_manager.list_thread_ids().await; for thread_id in thread_ids { let Ok(thread) = self.thread_manager.get_thread(thread_id).await else { continue; }; let current_config = thread.config().await; let next_config = match self .config_manager .load_latest_config_for_thread(current_config.as_ref()) .await { Ok(config) => config, Err(err) => { tracing::warn!(%thread_id, %err, "failed to reload thread configuration"); continue; } }; thread.refresh_runtime_config(next_config).await; } } async fn emit_plugin_toggle_events( &self, pending_changes: std::collections::BTreeMap, ) { let plugins_manager = self.thread_manager.plugins_manager(); for (plugin_id, enabled) in pending_changes { let Ok(plugin_id) = PluginId::parse(&plugin_id) else { continue; }; let metadata = plugins_manager .telemetry_metadata_for_installed_plugin(&plugin_id) .await; if enabled { self.analytics_events_client.track_plugin_enabled(metadata); } else { self.analytics_events_client.track_plugin_disabled(metadata); } } } } fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements { let windows_sandbox_private_desktop = requirements .windows .as_ref() .and_then(|windows| windows.sandbox_private_desktop); ConfigRequirements { application: requirements.application.map(|application| { codex_app_server_protocol::ApplicationRequirements { network: application.network.map(|network| { codex_app_server_protocol::ApplicationNetworkRequirements { enabled: network.enabled, domains: network .domains .into_iter() .map(|(domain, permission)| { (domain, map_network_domain_permission_to_api(permission)) }) .collect(), } }), } }), cli_auth_credentials_store: requirements.cli_auth_credentials_store.map( |mode| match mode { codex_config::types::AuthCredentialsStoreMode::File => { CliAuthCredentialsStoreMode::File } codex_config::types::AuthCredentialsStoreMode::Keyring => { CliAuthCredentialsStoreMode::Keyring } codex_config::types::AuthCredentialsStoreMode::Auto => { CliAuthCredentialsStoreMode::Auto } codex_config::types::AuthCredentialsStoreMode::Ephemeral => { CliAuthCredentialsStoreMode::Ephemeral } }, ), chatgpt_base_url: requirements.chatgpt_base_url, additional_developer_instructions: requirements.additional_developer_instructions, allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| { policies .into_iter() .map(codex_app_server_protocol::AskForApproval::from) .collect() }), allowed_approvals_reviewers: requirements.allowed_approvals_reviewers.map(|reviewers| { reviewers .into_iter() .map(codex_app_server_protocol::ApprovalsReviewer::from) .collect() }), allowed_sandbox_modes: requirements.allowed_sandbox_modes.map(|modes| { modes .into_iter() .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), allowed_windows_sandbox_implementations: requirements.windows.and_then(|windows| { windows .allowed_sandbox_implementations .map(|implementations| { implementations .into_iter() .map(|implementation| match implementation { codex_config::types::WindowsSandboxModeToml::Elevated => { WindowsSandboxSetupMode::Elevated } codex_config::types::WindowsSandboxModeToml::Unelevated => { WindowsSandboxSetupMode::Unelevated } }) .collect() }) }), allowed_permission_profiles: requirements.allowed_permission_profiles, default_permissions: requirements.default_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes .into_iter() .map(Into::into) .collect::>(); if !normalized.contains(&WebSearchMode::Disabled) { normalized.push(WebSearchMode::Disabled); } normalized }), allow_managed_hooks_only: requirements.allow_managed_hooks_only, allow_browser_and_computer_use: requirements.allow_browser_and_computer_use, allow_appshots: requirements.allow_appshots, allow_remote_control: requirements.allow_remote_control, computer_use: requirements .computer_use .map(map_computer_use_requirements_to_api), browser_use: requirements .browser_use .map(map_browser_use_requirements_to_api), in_app_browser: requirements.in_app_browser.map(|in_app_browser| { InAppBrowserRequirements { allow_external_browser_settings_import: in_app_browser .allow_external_browser_settings_import, } }), feature_requirements: requirements .feature_requirements .map(|requirements| requirements.entries), hooks: requirements.hooks.map(map_hooks_requirements_to_api), enforce_residency: requirements .enforce_residency .map(map_residency_requirement_to_api), network: requirements.network.map(map_network_requirements_to_api), auto_review: requirements .auto_review .map(|auto_review| AutoReviewRequirements { required_on_models: auto_review.required_on_models, ignore_rules: auto_review.ignore_rules, }), models: requirements.models.map(|models| ModelsRequirements { new_thread: models.new_thread.map(|new_thread| NewThreadModelDefaults { model: new_thread.model, model_reasoning_effort: new_thread.model_reasoning_effort, service_tier: new_thread.service_tier, }), }), sqlite_home: requirements.sqlite_home.map(Into::into), log_dir: requirements.log_dir.map(Into::into), model_catalog_json: requirements.model_catalog_json.map(Into::into), check_for_update_on_startup: requirements.check_for_update_on_startup, allow_login_shell: requirements.allow_login_shell, feedback: requirements.feedback.map(|feedback| FeedbackRequirements { enabled: feedback.enabled, }), windows_sandbox_private_desktop, } } fn map_computer_use_requirements_to_api( computer_use: codex_config::ComputerUseRequirementsToml, ) -> ComputerUseRequirements { ComputerUseRequirements { allow_locked_computer_use: computer_use.allow_locked_computer_use, allow_persistent_approval: computer_use.allow_persistent_approval, default_app_access: computer_use .default_app_access .map(map_allow_deny_requirement_to_api), macos: computer_use .macos .map(|macos| ComputerUseMacosRequirements { bundle_ids: macos.bundle_ids.map(|bundle_ids| { bundle_ids .into_iter() .map(|(bundle_id, requirement)| { (bundle_id, map_allow_deny_requirement_to_api(requirement)) }) .collect() }), }), windows: computer_use .windows .map(|windows| ComputerUseWindowsRequirements { aumids: windows.aumids.map(|aumids| { aumids .into_iter() .map(|(aumid, requirement)| { (aumid, map_allow_deny_requirement_to_api(requirement)) }) .collect() }), exes: windows.exes.map(|exes| { exes.into_iter() .map(|exe| ComputerUseWindowsExeRequirement { publisher_name: exe.publisher_name, product_name: exe.product_name, binary_name: exe.binary_name, access: map_allow_deny_requirement_to_api(exe.access), }) .collect() }), }), } } fn map_browser_use_requirements_to_api( browser_use: codex_config::BrowserUseRequirementsToml, ) -> BrowserUseRequirements { BrowserUseRequirements { allow_history_access: browser_use.allow_history_access, disable_auto_review: browser_use.disable_auto_review, allow_global_persistent_approval: browser_use.allow_global_persistent_approval, default_origin_policy: browser_use .default_origin_policy .map(map_browser_use_origin_policy_to_api), origins: browser_use.origins.map(|origins| { origins .into_iter() .map(|(pattern, policy)| (pattern, map_browser_use_origin_policy_to_api(policy))) .collect() }), } } fn map_browser_use_origin_policy_to_api( policy: codex_config::BrowserUseOriginPolicyToml, ) -> BrowserUseOriginPolicy { BrowserUseOriginPolicy { access: policy.access.map(map_allow_deny_requirement_to_api), downloads: policy.downloads.map(map_allow_deny_requirement_to_api), uploads: policy.uploads.map(map_allow_deny_requirement_to_api), full_cdp_access: policy .full_cdp_access .map(map_allow_deny_requirement_to_api), auto_review: policy.auto_review.map(map_allow_deny_requirement_to_api), persistent_approval: policy.persistent_approval, access_approval_lifetime: policy .access_approval_lifetime .map(map_browser_use_access_approval_lifetime_to_api), } } fn map_allow_deny_requirement_to_api( requirement: codex_config::AllowDenyRequirementToml, ) -> AllowDenyRequirement { match requirement { codex_config::AllowDenyRequirementToml::Allow => AllowDenyRequirement::Allow, codex_config::AllowDenyRequirementToml::Deny => AllowDenyRequirement::Deny, } } fn map_browser_use_access_approval_lifetime_to_api( lifetime: codex_config::BrowserUseAccessApprovalLifetimeToml, ) -> BrowserUseAccessApprovalLifetime { match lifetime { codex_config::BrowserUseAccessApprovalLifetimeToml::Turn => { BrowserUseAccessApprovalLifetime::Turn } codex_config::BrowserUseAccessApprovalLifetimeToml::Thread => { BrowserUseAccessApprovalLifetime::Thread } } } fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> ManagedHooksRequirements { let ManagedHooksRequirementsToml { managed_dir, windows_managed_dir, hooks, } = hooks; let HookEventsToml { pre_tool_use, permission_request, post_tool_use, pre_compact, post_compact, session_start, session_end, user_prompt_submit, subagent_start, subagent_stop, stop, interrupt, } = hooks; ManagedHooksRequirements { managed_dir, windows_managed_dir, pre_tool_use: map_hook_matcher_groups_to_api(pre_tool_use), permission_request: map_hook_matcher_groups_to_api(permission_request), post_tool_use: map_hook_matcher_groups_to_api(post_tool_use), pre_compact: map_hook_matcher_groups_to_api(pre_compact), post_compact: map_hook_matcher_groups_to_api(post_compact), session_start: map_hook_matcher_groups_to_api(session_start), session_end: map_hook_matcher_groups_to_api(session_end), user_prompt_submit: map_hook_matcher_groups_to_api(user_prompt_submit), subagent_start: map_hook_matcher_groups_to_api(subagent_start), subagent_stop: map_hook_matcher_groups_to_api(subagent_stop), stop: map_hook_matcher_groups_to_api(stop), interrupt: map_hook_matcher_groups_to_api(interrupt), } } fn map_hook_matcher_groups_to_api( groups: Vec, ) -> Vec { groups .into_iter() .map(map_hook_matcher_group_to_api) .collect() } fn map_hook_matcher_group_to_api(group: CoreMatcherGroup) -> ConfiguredHookMatcherGroup { ConfiguredHookMatcherGroup { matcher: group.matcher, hooks: group .hooks .into_iter() .map(map_hook_handler_to_api) .collect(), } } fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHandler { match handler { CoreHookHandlerConfig::Command { command, command_windows, timeout_sec, r#async, status_message, additional_context_limit, } => ConfiguredHookHandler::Command { command, command_windows, timeout_sec, r#async, status_message, additional_context_limit, }, CoreHookHandlerConfig::McpTool { server, tool, input, timeout_sec, status_message, } => ConfiguredHookHandler::McpTool { server, tool, input, timeout_sec, status_message, }, CoreHookHandlerConfig::Prompt {} => ConfiguredHookHandler::Prompt {}, CoreHookHandlerConfig::Agent {} => ConfiguredHookHandler::Agent {}, } } fn map_sandbox_mode_requirement_to_api(mode: CoreSandboxModeRequirement) -> Option { match mode { CoreSandboxModeRequirement::ReadOnly => Some(SandboxMode::ReadOnly), CoreSandboxModeRequirement::WorkspaceWrite => Some(SandboxMode::WorkspaceWrite), CoreSandboxModeRequirement::DangerFullAccess => Some(SandboxMode::DangerFullAccess), CoreSandboxModeRequirement::ExternalSandbox => None, } } fn map_residency_requirement_to_api( residency: CoreResidencyRequirement, ) -> codex_app_server_protocol::ResidencyRequirement { match residency { CoreResidencyRequirement::Us => codex_app_server_protocol::ResidencyRequirement::Us, } } fn map_network_requirements_to_api( network: codex_config::NetworkRequirementsToml, ) -> NetworkRequirements { let allowed_domains = network .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains); let denied_domains = network .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::denied_domains); let allow_unix_sockets = network .unix_sockets .as_ref() .map(codex_config::NetworkUnixSocketPermissionsToml::allow_unix_sockets) .filter(|entries| !entries.is_empty()); NetworkRequirements { enabled: network.enabled, http_port: network.http_port, socks_port: network.socks_port, allow_upstream_proxy: network.allow_upstream_proxy, dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy, dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets, domains: network.domains.map(|domains| { domains .entries .into_iter() .map(|(pattern, permission)| { (pattern, map_network_domain_permission_to_api(permission)) }) .collect() }), managed_allowed_domains_only: network.managed_allowed_domains_only, allowed_domains, denied_domains, unix_sockets: network.unix_sockets.map(|unix_sockets| { unix_sockets .entries .into_iter() .map(|(path, permission)| { (path, map_network_unix_socket_permission_to_api(permission)) }) .collect() }), allow_unix_sockets, allow_local_binding: network.allow_local_binding, } } fn map_network_domain_permission_to_api( permission: codex_config::NetworkDomainPermissionToml, ) -> NetworkDomainPermission { match permission { codex_config::NetworkDomainPermissionToml::Allow => NetworkDomainPermission::Allow, codex_config::NetworkDomainPermissionToml::Deny => NetworkDomainPermission::Deny, } } fn map_network_unix_socket_permission_to_api( permission: codex_config::NetworkUnixSocketPermissionToml, ) -> NetworkUnixSocketPermission { match permission { codex_config::NetworkUnixSocketPermissionToml::Allow => NetworkUnixSocketPermission::Allow, codex_config::NetworkUnixSocketPermissionToml::Deny => NetworkUnixSocketPermission::Deny, } } pub(super) fn map_error(err: ConfigManagerError) -> JSONRPCErrorError { if let Some(code) = err.write_error_code() { return config_write_error(code, err.to_string()); } internal_error(err.to_string()) } fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> JSONRPCErrorError { let mut error = invalid_request(message); error.data = Some(json!({ "config_write_error_code": code, })); error } #[cfg(test)] mod tests { use super::map_requirements_toml_to_api; use codex_app_server_protocol::AllowDenyRequirement; use codex_app_server_protocol::AutoReviewRequirements; use codex_app_server_protocol::BrowserUseAccessApprovalLifetime; use codex_app_server_protocol::BrowserUseOriginPolicy; use codex_app_server_protocol::BrowserUseRequirements; use codex_app_server_protocol::ComputerUseMacosRequirements; use codex_app_server_protocol::ComputerUseRequirements; use codex_app_server_protocol::ComputerUseWindowsExeRequirement; use codex_app_server_protocol::ComputerUseWindowsRequirements; use codex_app_server_protocol::FeedbackRequirements; use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::AllowDenyRequirementToml; use codex_config::AutoReviewRequirementsToml; use codex_config::BrowserUseAccessApprovalLifetimeToml; use codex_config::BrowserUseOriginPolicyToml; use codex_config::BrowserUseRequirementsToml; use codex_config::ComputerUseMacosRequirementsToml; use codex_config::ComputerUseRequirementsToml; use codex_config::ComputerUseWindowsExeRequirementToml; use codex_config::ComputerUseWindowsRequirementsToml; use codex_config::ConfigRequirementsToml; use codex_config::ModelsRequirementsToml; use codex_config::NewThreadModelDefaultsToml; use codex_config::WindowsRequirementsToml; use codex_config::types::FeedbackConfigToml; use codex_protocol::openai_models::ReasoningEffort; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; use std::collections::BTreeMap; #[test] fn requirements_api_includes_allow_managed_hooks_only() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { allow_managed_hooks_only: Some(true), ..ConfigRequirementsToml::default() }); assert_eq!(mapped.allow_managed_hooks_only, Some(true)); assert_eq!(mapped.hooks, None); } #[test] fn requirements_api_includes_permission_default_and_allowlist() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { allowed_permission_profiles: Some(BTreeMap::from([ ("managed-build".to_string(), false), ("managed-standard".to_string(), true), ])), default_permissions: Some("managed-standard".to_string()), ..ConfigRequirementsToml::default() }); assert_eq!( mapped.allowed_permission_profiles, Some(BTreeMap::from([ ("managed-build".to_string(), false), ("managed-standard".to_string(), true), ])) ); assert_eq!( mapped.default_permissions, Some("managed-standard".to_string()) ); } #[test] fn requirements_api_includes_allow_appshots() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { allow_appshots: Some(false), ..ConfigRequirementsToml::default() }); assert_eq!(mapped.allow_appshots, Some(false)); assert_eq!(mapped.hooks, None); } #[test] fn requirements_api_includes_allow_remote_control() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { allow_remote_control: Some(false), ..ConfigRequirementsToml::default() }); assert_eq!(mapped.allow_remote_control, Some(false)); } #[test] fn requirements_api_includes_model_auto_review_and_new_thread_defaults() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { auto_review: Some(AutoReviewRequirementsToml { required_on_models: Some(vec!["gpt-protected".to_string()]), ignore_rules: Some(vec!["gpt-protected".to_string()]), }), models: Some(ModelsRequirementsToml { new_thread: Some(NewThreadModelDefaultsToml { model: Some("gpt-managed".to_string()), model_reasoning_effort: Some(ReasoningEffort::Medium), service_tier: Some("fast".to_string()), }), }), ..ConfigRequirementsToml::default() }); assert_eq!( mapped.auto_review, Some(AutoReviewRequirements { required_on_models: Some(vec!["gpt-protected".to_string()]), ignore_rules: Some(vec!["gpt-protected".to_string()]), }) ); let models = mapped.models.expect("managed model requirements"); let defaults = models.new_thread.expect("new-thread defaults"); assert_eq!(defaults.model.as_deref(), Some("gpt-managed")); assert_eq!( defaults.model_reasoning_effort, Some(ReasoningEffort::Medium) ); assert_eq!(defaults.service_tier.as_deref(), Some("fast")); } #[test] fn requirements_api_includes_browser_and_computer_use_requirements() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { allow_browser_and_computer_use: Some(false), browser_use: Some(BrowserUseRequirementsToml { 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, ), }, )])), }), 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".to_string(), product_name: "Google Chrome".to_string(), binary_name: Some("chrome.exe".to_string()), access: AllowDenyRequirementToml::Deny, }]), }), }), ..ConfigRequirementsToml::default() }); assert_eq!(mapped.allow_browser_and_computer_use, Some(false)); assert_eq!( mapped.browser_use, Some(BrowserUseRequirements { allow_history_access: Some(false), disable_auto_review: Some(true), allow_global_persistent_approval: Some(false), default_origin_policy: Some(BrowserUseOriginPolicy { access: Some(AllowDenyRequirement::Deny), downloads: Some(AllowDenyRequirement::Allow), uploads: Some(AllowDenyRequirement::Deny), full_cdp_access: Some(AllowDenyRequirement::Allow), auto_review: Some(AllowDenyRequirement::Deny), persistent_approval: Some(false), access_approval_lifetime: Some(BrowserUseAccessApprovalLifetime::Turn), }), origins: Some(BTreeMap::from([( "https://example.com".to_string(), BrowserUseOriginPolicy { access: Some(AllowDenyRequirement::Allow), downloads: Some(AllowDenyRequirement::Deny), uploads: Some(AllowDenyRequirement::Allow), full_cdp_access: Some(AllowDenyRequirement::Deny), auto_review: Some(AllowDenyRequirement::Deny), persistent_approval: Some(true), access_approval_lifetime: Some(BrowserUseAccessApprovalLifetime::Thread), }, )])), }) ); assert_eq!( mapped.computer_use, Some(ComputerUseRequirements { allow_locked_computer_use: Some(false), allow_persistent_approval: Some(false), default_app_access: Some(AllowDenyRequirement::Deny), macos: Some(ComputerUseMacosRequirements { bundle_ids: Some(BTreeMap::from([( "com.apple.Safari".to_string(), AllowDenyRequirement::Allow, )])), }), windows: Some(ComputerUseWindowsRequirements { aumids: Some(BTreeMap::from([( "Microsoft.Paint_8wekyb3d8bbwe!App".to_string(), AllowDenyRequirement::Allow, )])), exes: Some(vec![ComputerUseWindowsExeRequirement { publisher_name: "CN=Google LLC".to_string(), product_name: "Google Chrome".to_string(), binary_name: Some("chrome.exe".to_string()), access: AllowDenyRequirement::Deny, }]), }), }) ); } #[test] fn requirements_api_includes_allowed_windows_sandbox_implementations() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { windows: Some(WindowsRequirementsToml { allowed_sandbox_implementations: Some(vec![ codex_config::types::WindowsSandboxModeToml::Elevated, codex_config::types::WindowsSandboxModeToml::Unelevated, ]), sandbox_private_desktop: Some(false), }), ..ConfigRequirementsToml::default() }); assert_eq!( mapped.allowed_windows_sandbox_implementations, Some(vec![ WindowsSandboxSetupMode::Elevated, WindowsSandboxSetupMode::Unelevated, ]) ); assert_eq!(mapped.windows_sandbox_private_desktop, Some(false)); } #[test] fn requirements_api_includes_exact_managed_values() { 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 mapped = map_requirements_toml_to_api(ConfigRequirementsToml { sqlite_home: Some(sqlite_home.clone()), log_dir: Some(log_dir.clone()), model_catalog_json: Some(model_catalog_json.clone()), check_for_update_on_startup: Some(false), allow_login_shell: Some(false), feedback: Some(FeedbackConfigToml { enabled: Some(false), }), ..ConfigRequirementsToml::default() }); assert_eq!(mapped.sqlite_home, Some(PathUri::from(sqlite_home))); assert_eq!(mapped.log_dir, Some(PathUri::from(log_dir))); assert_eq!( mapped.model_catalog_json, Some(PathUri::from(model_catalog_json)) ); assert_eq!(mapped.check_for_update_on_startup, Some(false)); assert_eq!(mapped.allow_login_shell, Some(false)); assert_eq!( mapped.feedback, Some(FeedbackRequirements { enabled: Some(false), }) ); } }