Codex Handbook
app-server/src/request_processors/config_processor.rs 669 lines
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::ClientResponsePayload;use codex_app_server_protocol::ComputerUseRequirements;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::JSONRPCErrorError;use codex_app_server_protocol::ManagedHooksRequirements;use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;use codex_app_server_protocol::NetworkDomainPermission;use codex_app_server_protocol::NetworkRequirements;use codex_app_server_protocol::NetworkUnixSocketPermission;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::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 SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[    "auth_elicitation",    "memories",    "mentions_v2",    "remote_control",    "remote_plugin",    "tool_suggest",];#[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)?            .map(map_requirements_toml_to_api);        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> {        self.handle_config_mutation_result(self.batch_write_inner(params).await)            .await            .map(ClientResponsePayload::ConfigBatchWrite)    }    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?;        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_manager().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(            [(&params.key_path, &params.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 reload_user_config = params.reload_user_config;        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;        if reload_user_config {            self.reload_user_config().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 });        }        self.config_manager            .extend_runtime_feature_enablement(                enablement                    .iter()                    .map(|(name, enabled)| (name.clone(), *enabled)),            )            .map_err(|_| internal_error("failed to update feature enablement"))?;        self.load_latest_config(/*fallback_cwd*/ None).await?;        self.reload_user_config().await;        Ok(ExperimentalFeatureEnablementSetResponse { enablement })    }    async fn reload_user_config(&self) {        let next_config = match self.load_latest_config(/*fallback_cwd*/ None).await {            Ok(config) => config,            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;            };            thread.refresh_runtime_config(next_config.clone()).await;        }    }    async fn emit_plugin_toggle_events(        &self,        pending_changes: std::collections::BTreeMap<String, bool>,    ) {        for (plugin_id, enabled) in pending_changes {            let Ok(plugin_id) = PluginId::parse(&plugin_id) else {                continue;            };            let metadata = codex_core_plugins::loader::installed_plugin_telemetry_metadata(                self.config_manager.codex_home(),                &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 {    ConfigRequirements {        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::<Vec<WebSearchMode>>();            if !normalized.contains(&WebSearchMode::Disabled) {                normalized.push(WebSearchMode::Disabled);            }            normalized        }),        allow_managed_hooks_only: requirements.allow_managed_hooks_only,        allow_appshots: requirements.allow_appshots,        allow_remote_control: requirements.allow_remote_control,        computer_use: requirements            .computer_use            .map(map_computer_use_requirements_to_api),        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),    }}fn map_computer_use_requirements_to_api(    computer_use: codex_config::ComputerUseRequirementsToml,) -> ComputerUseRequirements {    ComputerUseRequirements {        allow_locked_computer_use: computer_use.allow_locked_computer_use,    }}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,        user_prompt_submit,        subagent_start,        subagent_stop,        stop,    } = 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),        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),    }}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,        } => ConfiguredHookHandler::Command {            command,            command_windows,            timeout_sec,            r#async,            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,    }}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_toml_to_api;    use codex_app_server_protocol::WindowsSandboxSetupMode;    use codex_config::ComputerUseRequirementsToml;    use codex_config::ConfigRequirementsToml;    use codex_config::WindowsRequirementsToml;    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_computer_use_requirements() {        let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {            computer_use: Some(ComputerUseRequirementsToml {                allow_locked_computer_use: Some(false),            }),            ..ConfigRequirementsToml::default()        });        assert_eq!(            mapped                .computer_use                .and_then(|requirements| requirements.allow_locked_computer_use),            Some(false)        );    }    #[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,                ]),            }),            ..ConfigRequirementsToml::default()        });        assert_eq!(            mapped.allowed_windows_sandbox_implementations,            Some(vec![                WindowsSandboxSetupMode::Elevated,                WindowsSandboxSetupMode::Unelevated,            ])        );    }}