Download codex-rs/app-server/src/request_processors/config_processor.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 46.6 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server/src/request_processors/config_processor.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/app-server/src/request_processors/config_processor.rs
-
curl -L -o config_processor.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server/src/request_processors/config_processor.rs
46.6 kB
| 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", | |
| ]; | |
| 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(/*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<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 }); | |
| } | |
| // 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?; | |
| 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(/*fallback_cwd*/ 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; | |
| } | |
| }; | |
| // Keep runtime refresh state off the request dispatcher's stack. | |
| 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 | |
| } | |
| 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") | |
| } | |
| 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); | |
| } | |
| 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()) | |
| ); | |
| } | |
| 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); | |
| } | |
| 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)); | |
| } | |
| 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")); | |
| } | |
| 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, | |
| }]), | |
| }), | |
| }) | |
| ); | |
| } | |
| 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)); | |
| } | |
| 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), | |
| }) | |
| ); | |
| } | |
| } | |