diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 006235c68..bc2383c21 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -9,8 +9,11 @@ use codex_config::types::McpServerConfig; use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; use codex_core_plugins::PluginListBackgroundTaskOptions; use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; +use codex_core_plugins::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME; +use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME; +use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME; +use codex_core_plugins::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME; use codex_core_plugins::remote::RemoteAppTemplateUnavailableReason; -use codex_core_plugins::remote::RemotePluginScope; use codex_core_plugins::remote::is_valid_remote_plugin_id; use codex_core_plugins::remote::validate_remote_plugin_id; use codex_mcp::McpOAuthLoginSupport; @@ -149,15 +152,18 @@ fn convert_configured_marketplace_plugin_to_plugin_summary( } } -fn remote_installed_plugin_visible_scopes(config: &Config) -> Vec { - let mut scopes = Vec::new(); +fn remote_installed_plugin_visible_marketplaces(config: &Config) -> Vec<&'static str> { + let mut marketplaces = Vec::new(); if config.features.enabled(Feature::RemotePlugin) { - scopes.push(RemotePluginScope::Global); + marketplaces.push(REMOTE_GLOBAL_MARKETPLACE_NAME); } + marketplaces.push(REMOTE_WORKSPACE_MARKETPLACE_NAME); if config.features.enabled(Feature::PluginSharing) { - scopes.push(RemotePluginScope::Workspace); + marketplaces.push(REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME); + marketplaces.push(REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME); + marketplaces.push(REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME); } - scopes + marketplaces } fn filter_openai_curated_installed_conflicts( @@ -776,8 +782,8 @@ impl PluginRequestProcessor { } let plugins_input = config.plugins_config_input(); - let remote_installed_plugin_visible_scopes = - remote_installed_plugin_visible_scopes(&config); + let remote_installed_plugin_visible_marketplaces = + remote_installed_plugin_visible_marketplaces(&config); plugins_manager.maybe_start_remote_installed_plugin_bundle_sync( &plugins_input, auth.clone(), @@ -798,7 +804,7 @@ impl PluginRequestProcessor { self.load_remote_installed_plugins( plugins_manager, &plugins_input, - &remote_installed_plugin_visible_scopes, + &remote_installed_plugin_visible_marketplaces, auth.as_ref(), ) .await, @@ -898,11 +904,11 @@ impl PluginRequestProcessor { &self, plugins_manager: Arc, plugins_input: &codex_core_plugins::PluginsConfigInput, - visible_scopes: &[RemotePluginScope], + visible_marketplaces: &[&str], auth: Option<&CodexAuth>, ) -> Vec { - let remote_marketplaces = if let Some(remote_marketplaces) = - plugins_manager.build_remote_installed_plugin_marketplaces_from_cache(visible_scopes) + let remote_marketplaces = if let Some(remote_marketplaces) = plugins_manager + .build_remote_installed_plugin_marketplaces_from_cache(visible_marketplaces) { Ok(remote_marketplaces) } else { @@ -910,7 +916,7 @@ impl PluginRequestProcessor { .build_and_cache_remote_installed_plugin_marketplaces( plugins_input, auth, - visible_scopes, + visible_marketplaces, Some(self.effective_plugins_changed_callback()), ) .await 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 418f071e9..a49c5d3c7 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -2371,6 +2371,92 @@ plugin_sharing = true Ok(()) } +#[tokio::test] +async fn plugin_installed_includes_workspace_directory_without_plugin_sharing() -> Result<()> { + let codex_home = TempDir::new()?; + let server = MockServer::start().await; + std::fs::write( + codex_home.path().join("config.toml"), + format!( + r#"chatgpt_base_url = "{}/backend-api/" + +[features] +plugins = true +remote_plugin = false +plugin_sharing = false +"#, + server.uri() + ), + )?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .chatgpt_user_id("user-123") + .chatgpt_account_id("account-123"), + AuthCredentialsStoreMode::File, + )?; + let mut workspace_installed_body: serde_json::Value = + serde_json::from_str(&workspace_remote_plugin_page_body( + "plugins~Plugin_11111111111111111111111111111111", + "workspace-linear", + "Workspace Linear", + "LISTED", + /*enabled*/ Some(true), + ))?; + let shared_installed_body: serde_json::Value = + serde_json::from_str(&workspace_remote_plugin_page_body( + "plugins~Plugin_22222222222222222222222222222222", + "shared-linear", + "Shared Linear", + "PRIVATE", + /*enabled*/ Some(true), + ))?; + workspace_installed_body["plugins"] + .as_array_mut() + .expect("installed plugins should be an array") + .push(shared_installed_body["plugins"][0].clone()); + let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?; + mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await; + mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_installed_request(PluginInstalledParams { + cwds: None, + install_suggestion_plugin_names: None, + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: PluginInstalledResponse = to_response(response)?; + + assert_eq!(response.marketplaces.len(), 1); + let marketplace = &response.marketplaces[0]; + assert_eq!(marketplace.name, "workspace-directory"); + assert_eq!( + marketplace + .plugins + .iter() + .map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled)) + .collect::>(), + vec![( + "workspace-linear@workspace-directory".to_string(), + true, + true + )] + ); + wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?; + wait_for_remote_installed_scope_request(&server, "GLOBAL").await?; + Ok(()) +} + #[tokio::test] async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core-plugins/src/discoverable.rs b/codex-rs/core-plugins/src/discoverable.rs index f376d099c..28a90c192 100644 --- a/codex-rs/core-plugins/src/discoverable.rs +++ b/codex-rs/core-plugins/src/discoverable.rs @@ -14,7 +14,6 @@ use crate::PluginsConfigInput; use crate::PluginsManager; use crate::marketplace::MarketplacePluginInstallPolicy; use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; -use crate::remote::RemotePluginScope; const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[ "github@openai-curated", @@ -100,7 +99,9 @@ impl PluginsManager { .collect::>(); installed_app_connector_ids.extend(input.loaded_plugin_app_connector_ids.iter().cloned()); let remote_installed_marketplaces = if input.plugins.remote_plugin_enabled { - self.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global]) + self.build_remote_installed_plugin_marketplaces_from_cache(&[ + REMOTE_GLOBAL_MARKETPLACE_NAME, + ]) } else { None }; diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 59cfd135e..095c5fd69 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -38,7 +38,6 @@ use crate::marketplace_upgrade::configured_git_marketplace_names; use crate::marketplace_upgrade::upgrade_configured_git_marketplaces; use crate::remote::RemoteInstalledPlugin; use crate::remote::RemotePluginCatalogError; -use crate::remote::RemotePluginScope; use crate::remote::RemotePluginServiceConfig; use crate::remote_legacy::RemotePluginFetchError; use crate::remote_legacy::RemotePluginMutationError; @@ -560,14 +559,19 @@ impl PluginsManager { pub fn build_remote_installed_plugin_marketplaces_from_cache( &self, - visible_scopes: &[RemotePluginScope], + visible_marketplaces: &[&str], ) -> Option> { let cache = match self.remote_installed_plugins_cache.read() { Ok(cache) => cache, Err(err) => err.into_inner(), }; let plugins = cache.as_ref()?; - Some(crate::remote::group_remote_installed_plugins_by_marketplaces(plugins, visible_scopes)) + Some( + crate::remote::group_remote_installed_plugins_by_marketplaces( + plugins, + visible_marketplaces, + ), + ) } pub fn cached_global_remote_discoverable_plugins_for_config( @@ -599,7 +603,7 @@ impl PluginsManager { &self, config: &PluginsConfigInput, auth: Option<&CodexAuth>, - visible_scopes: &[RemotePluginScope], + visible_marketplaces: &[&str], on_effective_plugins_changed: Option>, ) -> Result, RemotePluginCatalogError> { let plugins = crate::remote::fetch_remote_installed_plugins( @@ -607,8 +611,10 @@ impl PluginsManager { auth, ) .await?; - let marketplaces = - crate::remote::group_remote_installed_plugins_by_marketplaces(&plugins, visible_scopes); + let marketplaces = crate::remote::group_remote_installed_plugins_by_marketplaces( + &plugins, + visible_marketplaces, + ); let changed = self.write_remote_installed_plugins_cache(plugins); if changed && let Some(on_effective_plugins_changed) = on_effective_plugins_changed { on_effective_plugins_changed(); diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 4fc8456ce..c3961796c 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -6,8 +6,10 @@ use crate::loader::load_plugins_from_layer_stack; use crate::loader::refresh_non_curated_plugin_cache; use crate::loader::refresh_non_curated_plugin_cache_force_reinstall; use crate::marketplace::MarketplacePluginInstallPolicy; +use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; +use crate::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME; +use crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME; use crate::remote::RemoteInstalledPlugin; -use crate::remote::RemotePluginScope; use crate::startup_sync::curated_plugins_repo_path; use crate::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION; use crate::test_support::TEST_CURATED_PLUGIN_SHA; @@ -137,8 +139,15 @@ fn remote_installed_linear_plugin() -> RemoteInstalledPlugin { } fn remote_installed_plugin(name: &str) -> RemoteInstalledPlugin { + remote_installed_plugin_in_marketplace(name, REMOTE_GLOBAL_MARKETPLACE_NAME) +} + +fn remote_installed_plugin_in_marketplace( + name: &str, + marketplace_name: &str, +) -> RemoteInstalledPlugin { RemoteInstalledPlugin { - marketplace_name: "openai-curated-remote".to_string(), + marketplace_name: marketplace_name.to_string(), id: format!("plugins~Plugin_{name}"), name: name.to_string(), enabled: true, @@ -484,7 +493,7 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad manager.write_remote_installed_plugins_cache(vec![plugin]); let marketplaces = manager - .build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global]) + .build_remote_installed_plugin_marketplaces_from_cache(&[REMOTE_GLOBAL_MARKETPLACE_NAME]) .expect("remote installed cache should be present"); assert_eq!(marketplaces.len(), 1); assert_eq!(marketplaces[0].name, "openai-curated-remote"); @@ -521,12 +530,45 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad ); assert_eq!( manager - .build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Workspace]) + .build_remote_installed_plugin_marketplaces_from_cache(&[ + REMOTE_WORKSPACE_MARKETPLACE_NAME + ]) .expect("remote installed cache should be present"), Vec::new() ); } +#[tokio::test] +async fn build_remote_installed_plugin_marketplaces_from_cache_filters_by_marketplace_name() { + let codex_home = TempDir::new().unwrap(); + let manager = PluginsManager::new(codex_home.path().to_path_buf()); + manager.write_remote_installed_plugins_cache(vec![ + remote_installed_plugin_in_marketplace( + "workspace-linear", + REMOTE_WORKSPACE_MARKETPLACE_NAME, + ), + remote_installed_plugin_in_marketplace( + "shared-linear", + REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME, + ), + ]); + + let marketplaces = manager + .build_remote_installed_plugin_marketplaces_from_cache(&[REMOTE_WORKSPACE_MARKETPLACE_NAME]) + .expect("remote installed cache should be present"); + + assert_eq!(marketplaces.len(), 1); + assert_eq!(marketplaces[0].name, REMOTE_WORKSPACE_MARKETPLACE_NAME); + assert_eq!( + marketplaces[0] + .plugins + .iter() + .map(|plugin| plugin.id.as_str()) + .collect::>(), + vec!["workspace-linear@workspace-directory"] + ); +} + #[tokio::test] async fn load_plugins_resolves_disabled_skill_names_against_loaded_plugin_skills() { let codex_home = TempDir::new().unwrap(); diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index ee768e1c9..188531872 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -851,14 +851,12 @@ pub(crate) async fn fetch_remote_installed_plugins( pub fn group_remote_installed_plugins_by_marketplaces( plugins: &[RemoteInstalledPlugin], - visible_scopes: &[RemotePluginScope], + visible_marketplaces: &[&str], ) -> Vec { let mut plugins_by_marketplace = BTreeMap::>::new(); for plugin in plugins { - if !RemotePluginScope::from_marketplace_name(&plugin.marketplace_name) - .is_some_and(|scope| visible_scopes.contains(&scope)) - { + if !visible_marketplaces.contains(&plugin.marketplace_name.as_str()) { continue; } let Ok(plugin_id) = PluginId::new(plugin.name.clone(), plugin.marketplace_name.clone()) diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index 992bf93f4..c1dfe8e63 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -7,7 +7,7 @@ use crate::plugins::test_support::write_plugins_feature_config; use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME; use codex_core_plugins::PluginInstallRequest; use codex_core_plugins::PluginsManager; -use codex_core_plugins::remote::RemotePluginScope; +use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; use codex_core_plugins::remote::RemotePluginServiceConfig; use codex_core_plugins::remote::fetch_and_cache_global_remote_plugin_catalog; use codex_core_plugins::startup_sync::curated_plugins_repo_path; @@ -362,7 +362,7 @@ remote_plugin = true .build_and_cache_remote_installed_plugin_marketplaces( &config.plugins_config_input(), Some(&auth), - &[RemotePluginScope::Global], + &[REMOTE_GLOBAL_MARKETPLACE_NAME], /*on_effective_plugins_changed*/ None, ) .await