| 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::WindowsSandboxImplementation; |
| 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::ForcedLoginMethod; |
| 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] = &[ |
| "api_key_model_discovery", |
| "auth_elicitation", |
| BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE, |
| "codex_apps_mcp_2026_07_28", |
| "mcp_2026_07_28", |
| "memories", |
| "mentions_v2", |
| "remote_control", |
| "remote_plugin", |
| "tool_suggest", |
| "windows_sandbox_service", |
| ]; |
|
|
| #[derive(Clone)] |
| pub(crate) struct ConfigRequestProcessor { |
| outgoing: Arc<OutgoingMessageSender>, |
| config_manager: ConfigManager, |
| thread_manager: Arc<ThreadManager>, |
| analytics_events_client: AnalyticsEventsClient, |
| } |
|
|
| impl ConfigRequestProcessor { |
| pub(crate) fn new( |
| outgoing: Arc<OutgoingMessageSender>, |
| config_manager: ConfigManager, |
| thread_manager: Arc<ThreadManager>, |
| analytics_events_client: AnalyticsEventsClient, |
| ) -> Self { |
| Self { |
| outgoing, |
| config_manager, |
| thread_manager, |
| analytics_events_client, |
| } |
| } |
|
|
| pub(crate) async fn read( |
| &self, |
| params: ConfigReadParams, |
| ) -> Result<ConfigReadResponse, JSONRPCErrorError> { |
| 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<ConfigRequirementsReadResponse, JSONRPCErrorError> { |
| let requirements = self |
| .config_manager |
| .read_requirements() |
| .await |
| .map_err(map_error)?; |
| let requirements = map_requirements_to_api( |
| requirements, |
| self.thread_manager.auth_manager().allowed_login_methods(), |
| ); |
|
|
| Ok(ConfigRequirementsReadResponse { requirements }) |
| } |
|
|
| pub(crate) async fn value_write( |
| &self, |
| params: ConfigValueWriteParams, |
| ) -> Result<ClientResponsePayload, JSONRPCErrorError> { |
| self.handle_config_mutation_result(self.write_value(params).await) |
| .await |
| .map(ClientResponsePayload::ConfigValueWrite) |
| } |
|
|
| pub(crate) async fn batch_write( |
| &self, |
| params: ConfigBatchWriteParams, |
| ) -> Result<ClientResponsePayload, JSONRPCErrorError> { |
| 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 should_reload = params.reload_user_config; |
| let response = self.batch_write_inner(params).await?; |
| if !session_defaults_only { |
| self.handle_config_mutation().await; |
| if should_reload { |
| reload_user_config(&self.config_manager, &self.thread_manager).await; |
| } |
| } |
| Ok(ClientResponsePayload::ConfigBatchWrite(response)) |
| } |
|
|
| pub(crate) async fn experimental_feature_enablement_set( |
| &self, |
| request_id: ConnectionRequestId, |
| params: ExperimentalFeatureEnablementSetParams, |
| ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> { |
| let response = self |
| .handle_config_mutation_result(self.set_experimental_feature_enablement(params).await) |
| .await?; |
| if !response.enablement.is_empty() { |
| reload_user_config(&self.config_manager, &self.thread_manager).await; |
| } |
| self.outgoing |
| .send_response_as( |
| request_id, |
| ClientResponsePayload::ExperimentalFeatureEnablementSet(response), |
| ) |
| .await; |
| Ok(None) |
| } |
|
|
| pub(crate) async fn model_provider_capabilities_read( |
| &self, |
| ) -> Result<ModelProviderCapabilitiesReadResponse, JSONRPCErrorError> { |
| let config = self.load_latest_config( None).await?; |
| let provider = create_model_provider(config.model_provider, 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<T>( |
| &self, |
| result: std::result::Result<T, JSONRPCErrorError>, |
| ) -> Result<T, JSONRPCErrorError> { |
| let response = result?; |
| self.handle_config_mutation().await; |
| Ok(response) |
| } |
|
|
| async fn load_latest_config( |
| &self, |
| fallback_cwd: Option<PathBuf>, |
| ) -> Result<codex_core::config::Config, JSONRPCErrorError> { |
| 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<ConfigWriteResponse, JSONRPCErrorError> { |
| 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<ConfigWriteResponse, JSONRPCErrorError> { |
| 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<ExperimentalFeatureEnablementSetResponse, JSONRPCErrorError> { |
| 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 }); |
| } |
|
|
| |
| |
| 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( 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( None).await?; |
| self.thread_manager |
| .get_models_manager() |
| .set_api_key_model_discovery_enabled( |
| config.features.enabled(Feature::ApiKeyModelDiscovery), |
| ); |
| if should_start_background_rollout_migration && config.features.enabled(feature) { |
| self.thread_manager.start_background_rollout_migration(); |
| } |
|
|
| Ok(ExperimentalFeatureEnablementSetResponse { enablement }) |
| } |
|
|
| async fn emit_plugin_toggle_events( |
| &self, |
| pending_changes: std::collections::BTreeMap<String, bool>, |
| ) { |
| 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); |
| } |
| } |
| } |
| } |
|
|
| pub(super) async fn reload_user_config( |
| config_manager: &ConfigManager, |
| thread_manager: &ThreadManager, |
| ) { |
| if let Err(err) = config_manager |
| .load_latest_config( None) |
| .await |
| { |
| tracing::warn!("failed to rebuild user config for runtime refresh: {err}"); |
| return; |
| } |
| let thread_ids = thread_manager.list_thread_ids().await; |
| for thread_id in thread_ids { |
| let Ok(thread) = thread_manager.get_thread(thread_id).await else { |
| continue; |
| }; |
| let current_config = thread.config().await; |
| let next_config = match config_manager |
| .load_latest_config_with_session_layers( |
| ¤t_config.config_layer_stack, |
| ¤t_config.cwd, |
| ) |
| .await |
| { |
| Ok(config) => config, |
| Err(err) => { |
| tracing::warn!(%thread_id, %err, "failed to reload thread configuration"); |
| continue; |
| } |
| }; |
| |
| Box::pin(thread.refresh_runtime_config(next_config)).await; |
| } |
| } |
|
|
| fn map_requirements_to_api( |
| requirements: Option<ConfigRequirementsToml>, |
| allowed_login_methods: Vec<ForcedLoginMethod>, |
| ) -> Option<ConfigRequirements> { |
| let requirements = match requirements { |
| Some(requirements) => requirements, |
| None if allowed_login_methods == [ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt] => { |
| return None; |
| } |
| None => ConfigRequirementsToml::default(), |
| }; |
| let windows_sandbox_private_desktop = requirements |
| .windows |
| .as_ref() |
| .and_then(|windows| windows.sandbox_private_desktop); |
|
|
| Some(ConfigRequirements { |
| model_provider: requirements.model_provider, |
| model_providers: requirements.model_providers.map(|providers| { |
| providers |
| .into_iter() |
| .map(|(id, provider)| (id, serde_json::json!(provider))) |
| .collect() |
| }), |
| allowed_login_methods: Some(allowed_login_methods), |
| 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 => { |
| WindowsSandboxImplementation::Elevated |
| } |
| codex_config::types::WindowsSandboxModeToml::Unelevated => { |
| WindowsSandboxImplementation::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::<Vec<WebSearchMode>>(); |
| 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_webmcp: browser_use.allow_webmcp, |
| 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<CoreMatcherGroup>, |
| ) -> Vec<ConfiguredHookMatcherGroup> { |
| 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<SandboxMode> { |
| 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<String>) -> 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_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::WindowsSandboxImplementation; |
| 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::config_types::ForcedLoginMethod; |
| 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; |
|
|
| fn map_test_requirements( |
| requirements: ConfigRequirementsToml, |
| ) -> codex_app_server_protocol::ConfigRequirements { |
| map_requirements_to_api( |
| Some(requirements), |
| vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt], |
| ) |
| .expect("requirements") |
| } |
|
|
| #[test] |
| fn requirements_api_includes_allow_managed_hooks_only() { |
| let mapped = map_test_requirements(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_test_requirements(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_test_requirements(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_test_requirements(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_test_requirements(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_test_requirements(ConfigRequirementsToml { |
| allow_browser_and_computer_use: Some(false), |
| browser_use: Some(BrowserUseRequirementsToml { |
| allow_webmcp: Some(true), |
| 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_webmcp: Some(true), |
| 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_test_requirements(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![ |
| WindowsSandboxImplementation::Elevated, |
| WindowsSandboxImplementation::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_test_requirements(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), |
| }) |
| ); |
| } |
| } |
|
|