diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 3162839f08..14ef60000b 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -83,6 +83,7 @@ use codex_core::config::Config; use codex_core::config::ThreadStoreConfig; use codex_exec_server::EnvironmentManager; use codex_extension_api::TurnStartAdmission; +use codex_features::Feature; use codex_feedback::CodexFeedback; use codex_goal_extension::GoalService; use codex_home::CodexHomeUserInstructionsProvider; @@ -568,6 +569,7 @@ impl MessageProcessor { .plugins_manager() .maybe_start_plugin_startup_tasks_for_config( &config.plugins_config_input(), + config.features.enabled(Feature::PluginDiscoveryPrefetch), reload_config, Some(on_effective_plugins_changed), startup_warnings_tx, diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index 90b5ef4af9..cf4c6406bc 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -308,6 +308,7 @@ impl AccountRequestProcessor { .maybe_start_remote_plugin_caches_refresh( &plugins_config, auth, + /*prefetch_recommendations*/ true, Some(on_effective_plugins_changed), ); } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index c03c422818..956cb5d652 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -1961,7 +1961,8 @@ enabled = true } #[tokio::test] -async fn app_server_startup_sync_downloads_remote_installed_plugin_bundles() -> Result<()> { +async fn app_server_startup_downloads_installed_plugin_bundles_without_discovery_prefetch() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; write_remote_plugin_catalog_config( @@ -2001,11 +2002,24 @@ async fn app_server_startup_sync_downloads_remote_installed_plugin_bundles() -> .await; mount_empty_user_installed_plugins(&server).await; + for endpoint in [ + "/backend-api/ps/plugins/list", + "/backend-api/plugins/featured", + "/backend-api/ps/plugins/suggested/codex", + ] { + Mock::given(path(endpoint)) + .respond_with(ResponseTemplate::new(404)) + .expect(0) + .mount(&server) + .await; + } + let installed_path = codex_home .path() .join("plugins/cache/openai-curated-remote/linear/1.2.3"); let _mcp = TestAppServer::builder() .with_codex_home(codex_home.path()) + .with_args(&["-c", "features.plugin_discovery_prefetch=false"]) .with_plugin_startup_tasks() .with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))]) .build_initialized_with_timeout(DEFAULT_TIMEOUT) @@ -2137,7 +2151,7 @@ enum ProjectPluginConfiguration { Invalid, } -#[test_case(ProjectPluginConfiguration::None, None, "", true, true; "without project override")] +#[test_case(ProjectPluginConfiguration::None, None, "plugin_discovery_prefetch = false", true, true; "without discovery prefetch")] #[test_case(ProjectPluginConfiguration::Disabled, None, "", true, true; "project disables local plugins")] #[test_case(ProjectPluginConfiguration::Invalid, None, "", true, true; "invalid project preserves remote plugins")] #[test_case(ProjectPluginConfiguration::None, Some("tpp"), "", true, true; "configured product sku")] diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 6d9d676258..4c299c32fc 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -1588,6 +1588,7 @@ impl PluginsManager { self: &Arc, config: &PluginsConfigInput, auth: Option, + prefetch_recommendations: bool, on_effective_plugins_changed: Option, ) { self.maybe_start_remote_installed_plugins_cache_refresh_with_notify( @@ -1598,6 +1599,9 @@ impl PluginsManager { EffectivePluginsChange::default(), ); + if !prefetch_recommendations { + return; + } let manager = Arc::clone(self); let config = config.clone(); tokio::spawn(async move { @@ -1873,6 +1877,7 @@ impl PluginsManager { self.maybe_start_remote_plugin_caches_refresh( &context.global_config, auth.clone(), + /*prefetch_recommendations*/ true, on_effective_plugins_changed.clone(), ); self.maybe_start_remote_installed_plugin_bundle_sync( @@ -2874,6 +2879,7 @@ impl PluginsManager { pub fn maybe_start_plugin_startup_tasks_for_config( self: &Arc, config: &PluginsConfigInput, + prefetch_discovery: bool, reload_config: ConfigLayerReload, on_effective_plugins_changed: Option, upgrade_warnings: mpsc::UnboundedSender, @@ -2966,6 +2972,7 @@ impl PluginsManager { manager.maybe_start_remote_plugin_caches_refresh( &config_for_remote_sync, auth.clone(), + prefetch_discovery, on_effective_plugins_changed.clone(), ); manager.maybe_start_remote_installed_plugin_bundle_sync( @@ -2973,6 +2980,9 @@ impl PluginsManager { auth.clone(), on_effective_plugins_changed, ); + if !prefetch_discovery { + return; + } let mut scopes = crate::remote::cached_remote_plugin_catalog_scopes( manager.codex_home.as_path(), &remote_plugin_service_config(&config_for_remote_sync), @@ -2985,18 +2995,12 @@ impl PluginsManager { } manager.maybe_start_remote_catalog_cache_refresh( &config_for_remote_sync, - auth, + auth.clone(), scopes, RemoteCatalogCacheRefreshMode::Force, ); - }); - - let config_for_featured_plugins = config.clone(); - let manager = Arc::clone(self); - tokio::spawn(async move { - let auth = manager.auth_manager.auth().await; if let Err(err) = manager - .featured_plugin_ids_for_config(&config_for_featured_plugins, auth.as_ref()) + .featured_plugin_ids_for_config(&config_for_remote_sync, auth.as_ref()) .await { warn!( diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 39ac515871..7439f7b038 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -6147,6 +6147,7 @@ plugins = true manager.maybe_start_remote_plugin_caches_refresh( &config, Some(auth.clone()), + /*prefetch_recommendations*/ true, /*on_effective_plugins_changed*/ None, ); diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index d15af0114b..cc12557fcb 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1197,6 +1197,9 @@ "personality": { "type": "boolean" }, + "plugin_discovery_prefetch": { + "type": "boolean" + }, "plugin_hooks": { "type": "boolean" }, @@ -7818,6 +7821,9 @@ "personality": { "type": "boolean" }, + "plugin_discovery_prefetch": { + "type": "boolean" + }, "plugin_hooks": { "type": "boolean" }, diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 83daf42efc..1fdbb4bf5a 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -351,6 +351,9 @@ pub enum Feature { ComputerUse, /// Enable the PS-backed remote plugin catalog. RemotePlugin, + /// Prefetch the plugin directory, featured plugins, and recommendations at app-server startup. + /// Inventory-only hosts can disable this. Legacy suggestions read the cached directory. + PluginDiscoveryPrefetch, /// Enable remote plugin sharing flows. PluginSharing, /// Removed compatibility flag retained as a no-op. @@ -1816,6 +1819,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::PluginDiscoveryPrefetch, + key: "plugin_discovery_prefetch", + stage: Stage::Stable, + default_enabled: true, + }, FeatureSpec { id: Feature::PluginSharing, key: "plugin_sharing",