diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 5c70cdb9d..4feda2ab0 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -5328,7 +5328,11 @@ impl CodexMessageProcessor { &self, config: &Config, ) -> Result<(), JSONRPCErrorError> { - let configured_servers = self.thread_manager.mcp_manager().configured_servers(config); + let configured_servers = self + .thread_manager + .mcp_manager() + .configured_servers(config) + .await; let mcp_servers = match serde_json::to_value(configured_servers) { Ok(value) => value, Err(err) => { @@ -5388,7 +5392,8 @@ impl CodexMessageProcessor { let configured_servers = self .thread_manager .mcp_manager() - .configured_servers(&config); + .configured_servers(&config) + .await; let Some(server) = configured_servers.get(&name) else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -5490,7 +5495,9 @@ impl CodexMessageProcessor { return; } }; - let mcp_config = config.to_mcp_config(self.thread_manager.plugins_manager().as_ref()); + let mcp_config = config + .to_mcp_config(self.thread_manager.plugins_manager().as_ref()) + .await; let auth = self.auth_manager.auth().await; tokio::spawn(async move { @@ -6315,10 +6322,12 @@ impl CodexMessageProcessor { continue; } }; - let effective_skill_roots = plugins_manager.effective_skill_roots_for_layer_stack( - &config_layer_stack, - config.features.enabled(Feature::Plugins), - ); + let effective_skill_roots = plugins_manager + .effective_skill_roots_for_layer_stack( + &config_layer_stack, + config.features.enabled(Feature::Plugins), + ) + .await; let skills_input = codex_core::skills::SkillsLoadInput::new( cwd_abs, effective_skill_roots, @@ -6544,26 +6553,16 @@ impl CodexMessageProcessor { plugin_name, marketplace_path, }; - let config_for_read = config.clone(); - let outcome = match tokio::task::spawn_blocking(move || { - plugins_manager.read_plugin_for_config(&config_for_read, &request) - }) - .await + let outcome = match plugins_manager + .read_plugin_for_config(&config, &request) + .await { - Ok(Ok(outcome)) => outcome, - Ok(Err(err)) => { + Ok(outcome) => outcome, + Err(err) => { self.send_marketplace_error(request_id, err, "read plugin details") .await; return; } - Err(err) => { - self.send_internal_error( - request_id, - format!("failed to read plugin details: {err}"), - ) - .await; - return; - } }; let app_summaries = plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await; @@ -6704,7 +6703,8 @@ impl CodexMessageProcessor { self.clear_plugin_related_caches(); - let plugin_mcp_servers = load_plugin_mcp_servers(result.installed_path.as_path()); + let plugin_mcp_servers = + load_plugin_mcp_servers(result.installed_path.as_path()).await; if !plugin_mcp_servers.is_empty() { if let Err(err) = self.queue_mcp_server_refresh_for_config(&config).await { @@ -6717,7 +6717,7 @@ impl CodexMessageProcessor { .await; } - let plugin_apps = load_plugin_apps(result.installed_path.as_path()); + let plugin_apps = load_plugin_apps(result.installed_path.as_path()).await; let auth = self.auth_manager.auth().await; let apps_needing_auth = if plugin_apps.is_empty() || !config.features.apps_enabled_for_auth( diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index e85f137bc..7c39f1c44 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -210,7 +210,7 @@ impl ConfigApi { .write_value(params) .await .map_err(map_error)?; - self.emit_plugin_toggle_events(pending_changes); + self.emit_plugin_toggle_events(pending_changes).await; Ok(response) } @@ -230,7 +230,7 @@ impl ConfigApi { .batch_write(params) .await .map_err(map_error)?; - self.emit_plugin_toggle_events(pending_changes); + self.emit_plugin_toggle_events(pending_changes).await; if reload_user_config { self.user_config_reloader.reload_user_config().await; } @@ -299,13 +299,16 @@ impl ConfigApi { Ok(ExperimentalFeatureEnablementSetResponse { enablement }) } - fn emit_plugin_toggle_events(&self, pending_changes: std::collections::BTreeMap) { + async fn emit_plugin_toggle_events( + &self, + pending_changes: std::collections::BTreeMap, + ) { for (plugin_id, enabled) in pending_changes { let Ok(plugin_id) = PluginId::parse(&plugin_id) else { continue; }; let metadata = - installed_plugin_telemetry_metadata(self.codex_home.as_path(), &plugin_id); + installed_plugin_telemetry_metadata(self.codex_home.as_path(), &plugin_id).await; if enabled { self.analytics_events_client.track_plugin_enabled(metadata); } else { diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 5927881a0..f37609be3 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -73,10 +73,9 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option> } let token_data = get_chatgpt_token_data()?; let cache_key = all_connectors_cache_key(config, &token_data); - codex_connectors::cached_all_connectors(&cache_key).map(|connectors| { - let connectors = merge_plugin_apps(connectors, plugin_apps_for_config(config)); - filter_disallowed_connectors(connectors) - }) + let connectors = codex_connectors::cached_all_connectors(&cache_key)?; + let connectors = merge_plugin_apps(connectors, plugin_apps_for_config(config).await); + Some(filter_disallowed_connectors(connectors)) } pub async fn list_all_connectors_with_options( @@ -106,7 +105,7 @@ pub async fn list_all_connectors_with_options( }, ) .await?; - let connectors = merge_plugin_apps(connectors, plugin_apps_for_config(config)); + let connectors = merge_plugin_apps(connectors, plugin_apps_for_config(config).await); Ok(filter_disallowed_connectors(connectors)) } @@ -119,9 +118,10 @@ fn all_connectors_cache_key(config: &Config, token_data: &TokenData) -> AllConne ) } -fn plugin_apps_for_config(config: &Config) -> Vec { +async fn plugin_apps_for_config(config: &Config) -> Vec { PluginsManager::new(config.codex_home.to_path_buf()) .plugins_for_config(config) + .await .effective_apps() } diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index b8e3fc670..cac6ef216 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -394,7 +394,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); - let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None); + let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await; let LoginArgs { name, scopes } = login_args; @@ -447,7 +447,7 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); - let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None); + let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await; let LogoutArgs { name } = logout_args; @@ -479,7 +479,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); - let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None); + let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await; let mut entries: Vec<_> = mcp_servers.iter().collect(); entries.sort_by(|(a, _), (b, _)| a.cmp(b)); @@ -730,7 +730,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( config.codex_home.to_path_buf(), ))); - let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None); + let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await; let Some(server) = mcp_servers.get(&get_args.name) else { bail!("No MCP server named '{name}' found.", name = get_args.name); diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index e1d4d3f8c..2bb10744a 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -655,7 +655,7 @@ enabled = false let plugins_manager = Arc::new(PluginsManager::new(home.path().to_path_buf())); let skills_manager = SkillsManager::new(home.path().abs(), /*bundled_skills_enabled*/ true); - let plugin_outcome = plugins_manager.plugins_for_config(&config); + let plugin_outcome = plugins_manager.plugins_for_config(&config).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&config, effective_skill_roots); let outcome = skills_manager.skills_for_config(&skills_input); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ec3d7c202..bab91b177 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -496,7 +496,7 @@ impl Codex { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); - let plugin_outcome = plugins_manager.plugins_for_config(&config); + let plugin_outcome = plugins_manager.plugins_for_config(&config).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&config, effective_skill_roots); let loaded_skills = skills_manager.skills_for_config(&skills_input); @@ -1746,7 +1746,9 @@ impl Session { let mcp_manager_for_mcp = Arc::clone(&mcp_manager); let auth_and_mcp_fut = async move { let auth = auth_manager_clone.auth().await; - let mcp_servers = mcp_manager_for_mcp.effective_servers(&config_for_mcp, auth.as_ref()); + let mcp_servers = mcp_manager_for_mcp + .effective_servers(&config_for_mcp, auth.as_ref()) + .await; let auth_statuses = compute_auth_statuses( mcp_servers.iter(), config_for_mcp.mcp_oauth_credentials_store_mode, @@ -2161,7 +2163,7 @@ impl Session { required_mcp_servers.sort(); let enabled_mcp_server_count = mcp_servers.values().filter(|server| server.enabled).count(); let required_mcp_server_count = required_mcp_servers.len(); - let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config.as_ref()); + let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config.as_ref()).await; { let mut cancel_guard = sess.services.mcp_startup_cancellation_token.lock().await; cancel_guard.cancel(); @@ -2658,7 +2660,8 @@ impl Session { let plugin_outcome = self .services .plugins_manager - .plugins_for_config(&per_turn_config); + .plugins_for_config(&per_turn_config) + .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots); let skills_outcome = Arc::new( @@ -3852,7 +3855,8 @@ impl Session { let loaded_plugins = self .services .plugins_manager - .plugins_for_config(&turn_context.config); + .plugins_for_config(&turn_context.config) + .await; if let Some(plugin_section) = render_plugins_section(loaded_plugins.capability_summaries()) { developer_sections.push(plugin_section); @@ -4501,11 +4505,14 @@ impl Session { ) { let auth = self.services.auth_manager.auth().await; let config = self.get_config().await; - let mcp_config = config.to_mcp_config(self.services.plugins_manager.as_ref()); + let mcp_config = config + .to_mcp_config(self.services.plugins_manager.as_ref()) + .await; let tool_plugin_provenance = self .services .mcp_manager - .tool_plugin_provenance(config.as_ref()); + .tool_plugin_provenance(config.as_ref()) + .await; let mcp_servers = with_codex_apps_mcp(mcp_servers, auth.as_ref(), &mcp_config); let auth_statuses = compute_auth_statuses(mcp_servers.iter(), store_mode).await; let sandbox_state = SandboxState { @@ -5360,7 +5367,8 @@ mod handlers { let mcp_servers = sess .services .mcp_manager - .effective_servers(config, auth.as_ref()); + .effective_servers(config, auth.as_ref()) + .await; let snapshot = collect_mcp_snapshot_from_manager( &mcp_connection_manager, compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode) @@ -5434,10 +5442,12 @@ mod handlers { continue; } }; - let effective_skill_roots = plugins_manager.effective_skill_roots_for_layer_stack( - &config_layer_stack, - config.features.enabled(Feature::Plugins), - ); + let effective_skill_roots = plugins_manager + .effective_skill_roots_for_layer_stack( + &config_layer_stack, + config.features.enabled(Feature::Plugins), + ) + .await; let skills_input = crate::SkillsLoadInput::new( cwd_abs, effective_skill_roots, @@ -6144,7 +6154,8 @@ pub(crate) async fn run_turn( let loaded_plugins = sess .services .plugins_manager - .plugins_for_config(&turn_context.config); + .plugins_for_config(&turn_context.config) + .await; // Structured plugin:// mentions are resolved from the current session's // enabled plugins, then converted into turn-scoped guidance below. let mentioned_plugins = @@ -7057,7 +7068,8 @@ pub(crate) async fn built_tools( let loaded_plugins = sess .services .plugins_manager - .plugins_for_config(&turn_context.config); + .plugins_for_config(&turn_context.config) + .await; let mut effective_explicitly_enabled_connectors = explicitly_enabled_connectors.clone(); effective_explicitly_enabled_connectors.extend(sess.get_connector_selection().await); diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 34d1b4a62..19ee45f41 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2906,7 +2906,8 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let plugin_outcome = services .plugins_manager - .plugins_for_config(&per_turn_config); + .plugins_for_config(&per_turn_config) + .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots); @@ -3751,7 +3752,8 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( let plugin_outcome = services .plugins_manager - .plugins_for_config(&per_turn_config); + .plugins_for_config(&per_turn_config) + .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots); diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index b10c62ed1..b2931e105 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2192,8 +2192,8 @@ approval_mode = "approve" ); } -#[test] -fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<()> { +#[tokio::test] +async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<()> { let codex_home = TempDir::new()?; let mut config = Config::load_from_base_config_with_overrides( ConfigToml::default(), @@ -2202,15 +2202,15 @@ fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<()> { )?; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); - let mcp_config = config.to_mcp_config(&plugins_manager); + let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(mcp_config.apps_enabled); let _ = config.features.disable(Feature::Apps); - let mcp_config = config.to_mcp_config(&plugins_manager); + let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(!mcp_config.apps_enabled); let _ = config.features.enable(Feature::Apps); - let mcp_config = config.to_mcp_config(&plugins_manager); + let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(mcp_config.apps_enabled); Ok(()) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index a5ddd3e19..88987e416 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -747,8 +747,11 @@ impl Config { } } - pub fn to_mcp_config(&self, plugins_manager: &crate::plugins::PluginsManager) -> McpConfig { - let loaded_plugins = plugins_manager.plugins_for_config(self); + pub async fn to_mcp_config( + &self, + plugins_manager: &crate::plugins::PluginsManager, + ) -> McpConfig { + let loaded_plugins = plugins_manager.plugins_for_config(self).await; let mut configured_mcp_servers = self.mcp_servers.get().clone(); for (name, plugin_server) in loaded_plugins.effective_mcp_servers() { configured_mcp_servers.entry(name).or_insert(plugin_server); diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 14154cffa..eef6d5747 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -124,7 +124,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( ) -> anyhow::Result> { let directory_connectors = list_directory_connectors_for_tool_suggest_with_auth(config, auth).await?; - let connector_ids = tool_suggest_connector_ids(config); + let connector_ids = tool_suggest_connector_ids(config).await; let discoverable_connectors = filter_tool_suggest_discoverable_connectors( directory_connectors, accessible_connectors, @@ -132,7 +132,8 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( ) .into_iter() .map(DiscoverableTool::from); - let discoverable_plugins = list_tool_suggest_discoverable_plugins(config)? + let discoverable_plugins = list_tool_suggest_discoverable_plugins(config) + .await? .into_iter() .map(DiscoverableTool::from); Ok(discoverable_connectors @@ -201,7 +202,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( let cache_key = accessible_connectors_cache_key(config, auth.as_ref()); let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf())); let mcp_manager = McpManager::new(Arc::clone(&plugins_manager)); - let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config); + let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await; if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key) { let cached_connectors = filter_disallowed_connectors(cached_connectors); @@ -212,7 +213,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( }); } - let mcp_config = config.to_mcp_config(plugins_manager.as_ref()); + let mcp_config = config.to_mcp_config(plugins_manager.as_ref()).await; let mcp_servers = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config); if mcp_servers.is_empty() { return Ok(AccessibleConnectorsStatus { @@ -395,9 +396,10 @@ fn filter_tool_suggest_discoverable_connectors( connectors } -fn tool_suggest_connector_ids(config: &Config) -> HashSet { +async fn tool_suggest_connector_ids(config: &Config) -> HashSet { let mut connector_ids = PluginsManager::new(config.codex_home.to_path_buf()) .plugins_for_config(config) + .await .capability_summaries() .iter() .flat_map(|plugin| plugin.app_connector_ids.iter()) diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 3c6504111..c9f77d046 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -1037,7 +1037,7 @@ discoverables = [ .expect("config should load"); assert_eq!( - tool_suggest_connector_ids(&config), + tool_suggest_connector_ids(&config).await, HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()]) ); } diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 83becdc07..0d4c26991 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -20,22 +20,22 @@ impl McpManager { Self { plugins_manager } } - pub fn configured_servers(&self, config: &Config) -> HashMap { - let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()); + pub async fn configured_servers(&self, config: &Config) -> HashMap { + let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await; configured_mcp_servers(&mcp_config) } - pub fn effective_servers( + pub async fn effective_servers( &self, config: &Config, auth: Option<&CodexAuth>, ) -> HashMap { - let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()); + let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await; effective_mcp_servers(&mcp_config, auth) } - pub fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance { - let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()); + pub async fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance { + let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await; collect_tool_plugin_provenance(&mcp_config) } } diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index 536c4eb4b..6cdd3cf08 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -53,7 +53,8 @@ pub(crate) async fn maybe_prompt_and_install_mcp_dependencies( let installed = sess .services .mcp_manager - .configured_servers(config.as_ref()); + .configured_servers(config.as_ref()) + .await; let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed); if missing.is_empty() { return; @@ -86,7 +87,7 @@ pub(crate) async fn maybe_install_mcp_dependencies( } let codex_home = config.codex_home.clone(); - let installed = sess.services.mcp_manager.configured_servers(config); + let installed = sess.services.mcp_manager.configured_servers(config).await; let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed); if missing.is_empty() { return; @@ -197,7 +198,8 @@ pub(crate) async fn maybe_install_mcp_dependencies( let mut refresh_servers = sess .services .mcp_manager - .effective_servers(config, auth.as_ref()); + .effective_servers(config, auth.as_ref()) + .await; for (name, server_config) in &servers { refresh_servers .entry(name.clone()) diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index 91d564bec..e856815a7 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -21,7 +21,7 @@ const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[ "figma@openai-curated", ]; -pub(crate) fn list_tool_suggest_discoverable_plugins( +pub(crate) async fn list_tool_suggest_discoverable_plugins( config: &Config, ) -> anyhow::Result> { if !config.features.enabled(Feature::Plugins) { @@ -59,11 +59,10 @@ pub(crate) fn list_tool_suggest_discoverable_plugins( let plugin_id = plugin.id.clone(); - match plugins_manager.read_plugin_detail_for_marketplace_plugin( - config, - &curated_marketplace_name, - plugin, - ) { + match plugins_manager + .read_plugin_detail_for_marketplace_plugin(config, &curated_marketplace_name, plugin) + .await + { Ok(plugin) => { let plugin: PluginCapabilitySummary = plugin.into(); discoverable_plugins.push(DiscoverablePluginInfo { diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index f17c897fe..b1d0d79e2 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -21,7 +21,9 @@ async fn list_tool_suggest_discoverable_plugins_returns_uninstalled_curated_plug write_plugins_feature_config(codex_home.path()); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); assert_eq!( discoverable_plugins, @@ -51,7 +53,9 @@ plugins = false ); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); assert_eq!(discoverable_plugins, Vec::::new()); } @@ -71,7 +75,9 @@ async fn list_tool_suggest_discoverable_plugins_normalizes_description() { ); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); assert_eq!( discoverable_plugins, @@ -106,7 +112,9 @@ async fn list_tool_suggest_discoverable_plugins_omits_installed_curated_plugins( .expect("plugin should install"); let refreshed_config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config).unwrap(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config) + .await + .unwrap(); assert_eq!(discoverable_plugins, Vec::::new()); } @@ -127,7 +135,9 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }] ); let config = load_plugins_config(codex_home.path()).await; - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); assert_eq!( discoverable_plugins, @@ -182,7 +192,9 @@ async fn list_tool_suggest_discoverable_plugins_does_not_reload_marketplace_per_ .finish(); let _guard = tracing::subscriber::set_default(subscriber); - let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap(); + let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config) + .await + .unwrap(); assert_eq!(discoverable_plugins.len(), 1); assert_eq!(discoverable_plugins[0].id, "slack@openai-curated"); diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index bfae2edf2..4f6090fd7 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -374,11 +374,12 @@ impl PluginsManager { } } - pub fn plugins_for_config(&self, config: &Config) -> PluginLoadOutcome { + pub async fn plugins_for_config(&self, config: &Config) -> PluginLoadOutcome { self.plugins_for_config_with_force_reload(config, /*force_reload*/ false) + .await } - pub(crate) fn plugins_for_config_with_force_reload( + pub(crate) async fn plugins_for_config_with_force_reload( &self, config: &Config, force_reload: bool, @@ -395,7 +396,8 @@ impl PluginsManager { &config.config_layer_stack, &self.store, self.restriction_product, - ); + ) + .await; log_plugin_load_errors(&outcome); let mut cache = match self.cached_enabled_outcome.write() { Ok(cache) => cache, @@ -419,7 +421,7 @@ impl PluginsManager { } /// Resolve plugin skill roots for a config layer stack without touching the plugins cache. - pub fn effective_skill_roots_for_layer_stack( + pub async fn effective_skill_roots_for_layer_stack( &self, config_layer_stack: &ConfigLayerStack, plugins_feature_enabled: bool, @@ -428,6 +430,7 @@ impl PluginsManager { return Vec::new(); } load_plugins_from_layer_stack(config_layer_stack, &self.store, self.restriction_product) + .await .effective_skill_roots() } @@ -585,10 +588,10 @@ impl PluginsManager { Err(err) => err.into_inner().clone(), }; if let Some(analytics_events_client) = analytics_events_client { - analytics_events_client.track_plugin_installed(plugin_telemetry_metadata_from_root( - &result.plugin_id, - &result.installed_path, - )); + analytics_events_client.track_plugin_installed( + plugin_telemetry_metadata_from_root(&result.plugin_id, &result.installed_path) + .await, + ); } Ok(PluginInstallOutcome { @@ -622,10 +625,11 @@ impl PluginsManager { } async fn uninstall_plugin_id(&self, plugin_id: PluginId) -> Result<(), PluginUninstallError> { - let plugin_telemetry = self - .store - .active_plugin_root(&plugin_id) - .map(|_| installed_plugin_telemetry_metadata(self.codex_home.as_path(), &plugin_id)); + let plugin_telemetry = if self.store.active_plugin_root(&plugin_id).is_some() { + Some(installed_plugin_telemetry_metadata(self.codex_home.as_path(), &plugin_id).await) + } else { + None + }; let store = self.store.clone(); let plugin_id_for_store = plugin_id.clone(); tokio::task::spawn_blocking(move || store.uninstall(&plugin_id_for_store)) @@ -931,7 +935,7 @@ impl PluginsManager { }) } - pub fn read_plugin_for_config( + pub async fn read_plugin_for_config( &self, config: &Config, request: &PluginReadRequest, @@ -959,19 +963,21 @@ impl PluginsManager { )?; let plugin_key = plugin_id.as_key(); let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config); - let plugin = self.read_plugin_detail_for_marketplace_plugin( - config, - &marketplace.name, - ConfiguredMarketplacePlugin { - id: plugin_key.clone(), - name: plugin.name, - source: plugin.source, - policy: plugin.policy, - interface: plugin.interface, - installed: installed_plugins.contains(&plugin_key), - enabled: enabled_plugins.contains(&plugin_key), - }, - )?; + let plugin = self + .read_plugin_detail_for_marketplace_plugin( + config, + &marketplace.name, + ConfiguredMarketplacePlugin { + id: plugin_key.clone(), + name: plugin.name, + source: plugin.source, + policy: plugin.policy, + interface: plugin.interface, + installed: installed_plugins.contains(&plugin_key), + enabled: enabled_plugins.contains(&plugin_key), + }, + ) + .await?; Ok(PluginReadOutcome { marketplace_name: if marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME { @@ -984,7 +990,7 @@ impl PluginsManager { }) } - pub(crate) fn read_plugin_detail_for_marketplace_plugin( + pub(crate) async fn read_plugin_detail_for_marketplace_plugin( &self, config: &Config, marketplace_name: &str, @@ -1026,12 +1032,17 @@ impl PluginsManager { self.restriction_product, &skill_config_rules, ); - let apps = load_plugin_apps(source_path.as_path()); + let apps = load_apps_from_paths( + source_path.as_path(), + plugin_app_config_paths(source_path.as_path(), manifest_paths), + ) + .await; let mcp_config_paths = plugin_mcp_config_paths(source_path.as_path(), manifest_paths); let mut mcp_server_names = Vec::new(); for mcp_config_path in mcp_config_paths { mcp_server_names.extend( load_mcp_servers_from_file(source_path.as_path(), &mcp_config_path) + .await .mcp_servers .into_keys(), ); @@ -1374,7 +1385,7 @@ struct PluginAppConfig { id: String, } -pub(crate) fn load_plugins_from_layer_stack( +pub(crate) async fn load_plugins_from_layer_stack( config_layer_stack: &ConfigLayerStack, store: &PluginStore, restriction_product: Option, @@ -1394,7 +1405,8 @@ pub(crate) fn load_plugins_from_layer_stack( store, restriction_product, &skill_config_rules, - ); + ) + .await; for name in loaded_plugin.mcp_servers.keys() { if let Some(previous_plugin) = seen_mcp_server_names.insert(name.clone(), configured_name.clone()) @@ -1663,7 +1675,7 @@ fn non_curated_plugin_ids_from_config_keys( configured_non_curated_plugin_ids } -fn load_plugin( +async fn load_plugin( config_name: String, plugin: &PluginConfig, store: &PluginStore, @@ -1745,7 +1757,7 @@ fn load_plugin( loaded_plugin.has_enabled_skills = has_enabled_skills; let mut mcp_servers = HashMap::new(); for mcp_config_path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) { - let plugin_mcp = load_mcp_servers_from_file(plugin_root.as_path(), &mcp_config_path); + let plugin_mcp = load_mcp_servers_from_file(plugin_root.as_path(), &mcp_config_path).await; for (name, config) in plugin_mcp.mcp_servers { if mcp_servers.insert(name.clone(), config).is_some() { warn!( @@ -1758,7 +1770,11 @@ fn load_plugin( } } loaded_plugin.mcp_servers = mcp_servers; - loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()); + loaded_plugin.apps = load_apps_from_paths( + plugin_root.as_path(), + plugin_app_config_paths(plugin_root.as_path(), manifest_paths), + ) + .await; loaded_plugin } @@ -1853,14 +1869,15 @@ fn default_mcp_config_paths(plugin_root: &Path) -> Vec { paths } -pub fn load_plugin_apps(plugin_root: &Path) -> Vec { +pub async fn load_plugin_apps(plugin_root: &Path) -> Vec { if let Some(manifest) = load_plugin_manifest(plugin_root) { return load_apps_from_paths( plugin_root, plugin_app_config_paths(plugin_root, &manifest.paths), - ); + ) + .await; } - load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)) + load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await } fn plugin_app_config_paths( @@ -1886,13 +1903,13 @@ fn default_app_config_paths(plugin_root: &Path) -> Vec { paths } -fn load_apps_from_paths( +async fn load_apps_from_paths( plugin_root: &Path, app_config_paths: Vec, ) -> Vec { let mut connector_ids = Vec::new(); for app_config_path in app_config_paths { - let Ok(contents) = fs::read_to_string(app_config_path.as_path()) else { + let Ok(contents) = tokio::fs::read_to_string(app_config_path.as_path()).await else { continue; }; let parsed = match serde_json::from_str::(&contents) { @@ -1925,7 +1942,7 @@ fn load_apps_from_paths( connector_ids } -pub fn plugin_telemetry_metadata_from_root( +pub async fn plugin_telemetry_metadata_from_root( plugin_id: &PluginId, plugin_root: &AbsolutePathBuf, ) -> PluginTelemetryMetadata { @@ -1939,6 +1956,7 @@ pub fn plugin_telemetry_metadata_from_root( for path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) { mcp_server_names.extend( load_mcp_servers_from_file(plugin_root.as_path(), &path) + .await .mcp_servers .into_keys(), ); @@ -1954,19 +1972,23 @@ pub fn plugin_telemetry_metadata_from_root( description: None, has_skills, mcp_server_names, - app_connector_ids: load_plugin_apps(plugin_root.as_path()), + app_connector_ids: load_apps_from_paths( + plugin_root.as_path(), + plugin_app_config_paths(plugin_root.as_path(), manifest_paths), + ) + .await, }), } } -pub fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap { +pub async fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap { let Some(manifest) = load_plugin_manifest(plugin_root) else { return HashMap::new(); }; let mut mcp_servers = HashMap::new(); for mcp_config_path in plugin_mcp_config_paths(plugin_root, &manifest.paths) { - let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path); + let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path).await; for (name, config) in plugin_mcp.mcp_servers { mcp_servers.entry(name).or_insert(config); } @@ -1975,7 +1997,7 @@ pub fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap PluginTelemetryMetadata { @@ -1984,14 +2006,14 @@ pub fn installed_plugin_telemetry_metadata( return PluginTelemetryMetadata::from_plugin_id(plugin_id); }; - plugin_telemetry_metadata_from_root(plugin_id, &plugin_root) + plugin_telemetry_metadata_from_root(plugin_id, &plugin_root).await } -fn load_mcp_servers_from_file( +async fn load_mcp_servers_from_file( plugin_root: &Path, mcp_config_path: &AbsolutePathBuf, ) -> PluginMcpDiscovery { - let Ok(contents) = fs::read_to_string(mcp_config_path.as_path()) else { + let Ok(contents) = tokio::fs::read_to_string(mcp_config_path.as_path()).await else { return PluginMcpDiscovery::default(); }; let parsed = match serde_json::from_str::(&contents) { diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index 3b06a08a9..23b202f1a 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -82,10 +82,12 @@ fn plugin_config_toml(enabled: bool, plugins_feature_enabled: bool) -> String { toml::to_string(&Value::Table(root)).expect("plugin test config should serialize") } -fn load_plugins_from_config(config_toml: &str, codex_home: &Path) -> PluginLoadOutcome { +async fn load_plugins_from_config(config_toml: &str, codex_home: &Path) -> PluginLoadOutcome { write_file(&codex_home.join(CONFIG_TOML_FILE), config_toml); - let config = load_config_blocking(codex_home, codex_home); - PluginsManager::new(codex_home.to_path_buf()).plugins_for_config(&config) + let config = load_config(codex_home, codex_home).await; + PluginsManager::new(codex_home.to_path_buf()) + .plugins_for_config(&config) + .await } async fn load_config(codex_home: &Path, cwd: &Path) -> crate::config::Config { @@ -97,16 +99,8 @@ async fn load_config(codex_home: &Path, cwd: &Path) -> crate::config::Config { .expect("config should load") } -fn load_config_blocking(codex_home: &Path, cwd: &Path) -> crate::config::Config { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime should build") - .block_on(load_config(codex_home, cwd)) -} - -#[test] -fn load_plugins_loads_default_skills_and_mcp_servers() { +#[tokio::test] +async fn load_plugins_loads_default_skills_and_mcp_servers() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -153,7 +147,8 @@ fn load_plugins_loads_default_skills_and_mcp_servers() { let outcome = load_plugins_from_config( &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), codex_home.path(), - ); + ) + .await; assert_eq!( outcome.plugins(), @@ -216,8 +211,8 @@ fn load_plugins_loads_default_skills_and_mcp_servers() { ); } -#[test] -fn load_plugins_resolves_disabled_skill_names_against_loaded_plugin_skills() { +#[tokio::test] +async fn load_plugins_resolves_disabled_skill_names_against_loaded_plugin_skills() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -244,7 +239,7 @@ enabled = false [plugins."sample@test"] enabled = true "#; - let outcome = load_plugins_from_config(config_toml, codex_home.path()); + let outcome = load_plugins_from_config(config_toml, codex_home.path()).await; let skill_path = dunce::canonicalize(skill_path) .expect("skill path should canonicalize") .abs(); @@ -257,8 +252,8 @@ enabled = true assert!(outcome.capability_summaries().is_empty()); } -#[test] -fn load_plugins_ignores_unknown_disabled_skill_names() { +#[tokio::test] +async fn load_plugins_ignores_unknown_disabled_skill_names() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -284,7 +279,7 @@ enabled = false [plugins."sample@test"] enabled = true "#; - let outcome = load_plugins_from_config(config_toml, codex_home.path()); + let outcome = load_plugins_from_config(config_toml, codex_home.path()).await; assert!(outcome.plugins()[0].disabled_skill_paths.is_empty()); assert!(outcome.plugins()[0].has_enabled_skills); @@ -301,8 +296,8 @@ enabled = true ); } -#[test] -fn plugin_telemetry_metadata_uses_default_mcp_config_path() { +#[tokio::test] +async fn plugin_telemetry_metadata_uses_default_mcp_config_path() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -330,7 +325,8 @@ fn plugin_telemetry_metadata_uses_default_mcp_config_path() { let metadata = plugin_telemetry_metadata_from_root( &PluginId::parse("sample@test").expect("plugin id should parse"), &plugin_root.abs(), - ); + ) + .await; assert_eq!( metadata.capability_summary, @@ -345,8 +341,8 @@ fn plugin_telemetry_metadata_uses_default_mcp_config_path() { ); } -#[test] -fn capability_summary_sanitizes_plugin_descriptions_to_one_line() { +#[tokio::test] +async fn capability_summary_sanitizes_plugin_descriptions_to_one_line() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -368,7 +364,8 @@ fn capability_summary_sanitizes_plugin_descriptions_to_one_line() { let outcome = load_plugins_from_config( &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), codex_home.path(), - ); + ) + .await; assert_eq!( outcome.plugins()[0].manifest_description.as_deref(), @@ -380,8 +377,8 @@ fn capability_summary_sanitizes_plugin_descriptions_to_one_line() { ); } -#[test] -fn capability_summary_truncates_overlong_plugin_descriptions() { +#[tokio::test] +async fn capability_summary_truncates_overlong_plugin_descriptions() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -406,7 +403,8 @@ fn capability_summary_truncates_overlong_plugin_descriptions() { let outcome = load_plugins_from_config( &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), codex_home.path(), - ); + ) + .await; assert_eq!( outcome.plugins()[0].manifest_description.as_deref(), @@ -418,8 +416,8 @@ fn capability_summary_truncates_overlong_plugin_descriptions() { ); } -#[test] -fn load_plugins_uses_manifest_configured_component_paths() { +#[tokio::test] +async fn load_plugins_uses_manifest_configured_component_paths() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -489,7 +487,8 @@ fn load_plugins_uses_manifest_configured_component_paths() { let outcome = load_plugins_from_config( &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), codex_home.path(), - ); + ) + .await; assert_eq!( outcome.plugins()[0].skill_roots, @@ -529,8 +528,8 @@ fn load_plugins_uses_manifest_configured_component_paths() { ); } -#[test] -fn load_plugins_ignores_manifest_component_paths_without_dot_slash() { +#[tokio::test] +async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -600,7 +599,8 @@ fn load_plugins_ignores_manifest_component_paths_without_dot_slash() { let outcome = load_plugins_from_config( &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), codex_home.path(), - ); + ) + .await; assert_eq!( outcome.plugins()[0].skill_roots, @@ -637,8 +637,8 @@ fn load_plugins_ignores_manifest_component_paths_without_dot_slash() { ); } -#[test] -fn load_plugins_preserves_disabled_plugins_without_effective_contributions() { +#[tokio::test] +async fn load_plugins_preserves_disabled_plugins_without_effective_contributions() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -666,7 +666,8 @@ fn load_plugins_preserves_disabled_plugins_without_effective_contributions() { /*enabled*/ false, /*plugins_feature_enabled*/ true, ), codex_home.path(), - ); + ) + .await; assert_eq!( outcome.plugins(), @@ -688,8 +689,8 @@ fn load_plugins_preserves_disabled_plugins_without_effective_contributions() { assert!(outcome.effective_mcp_servers().is_empty()); } -#[test] -fn effective_apps_dedupes_connector_ids_across_plugins() { +#[tokio::test] +async fn effective_apps_dedupes_connector_ids_across_plugins() { let codex_home = TempDir::new().unwrap(); let plugin_a_root = codex_home .path() @@ -751,7 +752,7 @@ fn effective_apps_dedupes_connector_ids_across_plugins() { let config_toml = toml::to_string(&Value::Table(root)).expect("plugin test config should serialize"); - let outcome = load_plugins_from_config(&config_toml, codex_home.path()); + let outcome = load_plugins_from_config(&config_toml, codex_home.path()).await; assert_eq!( outcome.effective_apps(), @@ -858,8 +859,8 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() { ); } -#[test] -fn load_plugins_returns_empty_when_feature_disabled() { +#[tokio::test] +async fn load_plugins_returns_empty_when_feature_disabled() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -881,14 +882,16 @@ fn load_plugins_returns_empty_when_feature_disabled() { ), ); - let config = load_config_blocking(codex_home.path(), codex_home.path()); - let outcome = PluginsManager::new(codex_home.path().to_path_buf()).plugins_for_config(&config); + let config = load_config(codex_home.path(), codex_home.path()).await; + let outcome = PluginsManager::new(codex_home.path().to_path_buf()) + .plugins_for_config(&config) + .await; assert_eq!(outcome, PluginLoadOutcome::default()); } -#[test] -fn load_plugins_rejects_invalid_plugin_keys() { +#[tokio::test] +async fn load_plugins_rejects_invalid_plugin_keys() { let codex_home = TempDir::new().unwrap(); let plugin_root = codex_home .path() @@ -915,7 +918,8 @@ fn load_plugins_rejects_invalid_plugin_keys() { let outcome = load_plugins_from_config( &toml::to_string(&Value::Table(root)).expect("plugin test config should serialize"), codex_home.path(), - ); + ) + .await; assert_eq!(outcome.plugins().len(), 1); assert_eq!( @@ -1346,6 +1350,7 @@ enabled = true marketplace_path, }, ) + .await .unwrap_err(); assert!(matches!(err, MarketplaceError::PluginsDisabled)); @@ -1410,6 +1415,7 @@ enabled = false .unwrap(), }, ) + .await .unwrap(); assert!(outcome.plugin.disabled_skill_paths.is_empty()); @@ -2727,8 +2733,8 @@ enabled = true ); } -#[test] -fn load_plugins_ignores_project_config_files() { +#[tokio::test] +async fn load_plugins_ignores_project_config_files() { let codex_home = TempDir::new().unwrap(); let project_root = codex_home.path().join("project"); let plugin_root = codex_home @@ -2764,7 +2770,8 @@ fn load_plugins_ignores_project_config_files() { &stack, &PluginStore::new(codex_home.path().to_path_buf()), Some(Product::Codex), - ); + ) + .await; assert_eq!(outcome, PluginLoadOutcome::default()); } diff --git a/codex-rs/core/src/skills_watcher.rs b/codex-rs/core/src/skills_watcher.rs index 07f3d1ebf..dd7d21cfc 100644 --- a/codex-rs/core/src/skills_watcher.rs +++ b/codex-rs/core/src/skills_watcher.rs @@ -54,13 +54,13 @@ impl SkillsWatcher { self.tx.subscribe() } - pub(crate) fn register_config( + pub(crate) async fn register_config( &self, config: &Config, skills_manager: &SkillsManager, plugins_manager: &PluginsManager, ) -> WatchRegistration { - let plugin_outcome = plugins_manager.plugins_for_config(config); + let plugin_outcome = plugins_manager.plugins_for_config(config).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(config, effective_skill_roots); let roots = skills_manager diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index b4658e81d..06aa1bc7b 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -903,11 +903,14 @@ impl ThreadManagerState { parent_trace: Option, user_shell_override: Option, ) -> CodexResult { - let watch_registration = self.skills_watcher.register_config( - &config, - self.skills_manager.as_ref(), - self.plugins_manager.as_ref(), - ); + let watch_registration = self + .skills_watcher + .register_config( + &config, + self.skills_manager.as_ref(), + self.plugins_manager.as_ref(), + ) + .await; let CodexSpawnOk { codex, thread_id, .. } = Codex::spawn(CodexSpawnArgs { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index a1f813e9b..ba8de0bac 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -42,6 +42,7 @@ use crate::legacy_core::config::edit::ConfigEdit; use crate::legacy_core::config::edit::ConfigEditsBuilder; use crate::legacy_core::config_loader::ConfigLayerStackOrdering; use crate::legacy_core::lookup_message_history_entry; +use crate::legacy_core::plugins::PluginsManager; #[cfg(target_os = "windows")] use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::model_catalog::ModelCatalog; @@ -2017,6 +2018,26 @@ impl App { }); } + fn refresh_plugin_mentions(&mut self) { + let config = self.config.clone(); + let app_event_tx = self.app_event_tx.clone(); + if !config.features.enabled(Feature::Plugins) { + app_event_tx.send(AppEvent::PluginMentionsLoaded { plugins: None }); + return; + } + + tokio::spawn(async move { + let plugins = PluginsManager::new(config.codex_home.to_path_buf()) + .plugins_for_config(&config) + .await + .capability_summaries() + .to_vec(); + app_event_tx.send(AppEvent::PluginMentionsLoaded { + plugins: Some(plugins), + }); + }); + } + fn submit_feedback( &mut self, app_server: &AppServerSession, @@ -5044,6 +5065,15 @@ impl App { self.fetch_plugins_list(app_server, cwd); } } + AppEvent::RefreshPluginMentions => { + self.refresh_plugin_mentions(); + } + AppEvent::PluginMentionsLoaded { mut plugins } => { + if !self.config.features.enabled(Feature::Plugins) { + plugins = None; + } + self.chat_widget.on_plugin_mentions_loaded(plugins); + } AppEvent::PersistPersonalitySelection { personality } => { let profile = self.active_profile.as_deref(); match ConfigEditsBuilder::new(&self.config.codex_home) diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index f448a5988..91f00b99b 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -30,6 +30,7 @@ use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::TerminalTitleItem; use crate::history_cell::HistoryCell; +use crate::legacy_core::plugins::PluginCapabilitySummary; use codex_config::types::ApprovalsReviewer; use codex_features::Feature; @@ -270,6 +271,14 @@ pub(crate) enum AppEvent { result: Result, }, + /// Refresh plugin mention bindings from the current config. + RefreshPluginMentions, + + /// Result of refreshing plugin mention bindings. + PluginMentionsLoaded { + plugins: Option>, + }, + /// Advance the post-install plugin app-auth flow. PluginInstallAuthAdvance { refresh_connectors: bool, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6c96e1598..7ae7bbc6c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -62,7 +62,6 @@ use crate::legacy_core::config::Constrained; use crate::legacy_core::config::ConstraintResult; use crate::legacy_core::config_loader::ConfigLayerStackOrdering; use crate::legacy_core::find_thread_name_by_id; -use crate::legacy_core::plugins::PluginsManager; use crate::legacy_core::skills::model::SkillMetadata; #[cfg(target_os = "windows")] use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; @@ -10369,11 +10368,14 @@ impl ChatWidget { return; } - let plugins = PluginsManager::new(self.config.codex_home.to_path_buf()) - .plugins_for_config(&self.config) - .capability_summaries() - .to_vec(); - self.bottom_pane.set_plugin_mentions(Some(plugins)); + self.app_event_tx.send(AppEvent::RefreshPluginMentions); + } + + pub(crate) fn on_plugin_mentions_loaded( + &mut self, + plugins: Option>, + ) { + self.bottom_pane.set_plugin_mentions(plugins); } pub(crate) fn sync_plugin_mentions_config(&mut self, config: &Config) { diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 89869a605..b87dd4ff8 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -19,11 +19,7 @@ use crate::exec_cell::output_lines; use crate::exec_cell::spinner; use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; -#[cfg(test)] -use crate::legacy_core::McpManager; use crate::legacy_core::config::Config; -#[cfg(test)] -use crate::legacy_core::plugins::PluginsManager; use crate::legacy_core::web_search_detail; use crate::live_wrap::take_prefix_by_width; use crate::markdown::append_markdown; @@ -88,8 +84,6 @@ use std::collections::HashMap; use std::io::Cursor; use std::path::Path; use std::path::PathBuf; -#[cfg(test)] -use std::sync::Arc; use std::time::Duration; use std::time::Instant; use tracing::error; @@ -1877,10 +1871,7 @@ pub(crate) fn new_mcp_tools_output( lines.push("".into()); } - let mcp_manager = McpManager::new(Arc::new(PluginsManager::new( - config.codex_home.to_path_buf(), - ))); - let effective_servers = mcp_manager.effective_servers(config, /*auth*/ None); + let effective_servers = config.mcp_servers.get().clone(); let mut servers: Vec<_> = effective_servers.iter().collect(); servers.sort_by(|(a, _), (b, _)| a.cmp(b));