mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Return workspace directory installed plugins (#27098)
## Summary - return installed `workspace-directory` remote plugins by default in `plugin/installed` - keep shared-with-me installed plugins gated behind `plugin_sharing` - filter remote installed plugin marketplaces by canonical marketplace name instead of coarse workspace scope ## Validation - `just fmt` - `just test -p codex-core-plugins` - `just test -p codex-app-server` - `just fix -p codex-core-plugins` - `just fix -p codex-app-server` - `$xin-build` targeted verification: - `just test -p codex-core-plugins build_remote_installed_plugin_marketplaces_from_cache_filters_by_marketplace_name` - `just test -p codex-app-server plugin_installed_includes_workspace_directory_without_plugin_sharing` - `just test -p codex-app-server plugin_installed_includes_remote_shared_with_me_plugins` - `just test -p codex-app-server plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled`
This commit is contained in:
committed by
GitHub
Unverified
parent
14660c22d1
commit
a304569c79
@@ -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<RemotePluginScope> {
|
||||
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<codex_core_plugins::PluginsManager>,
|
||||
plugins_input: &codex_core_plugins::PluginsConfigInput,
|
||||
visible_scopes: &[RemotePluginScope],
|
||||
visible_marketplaces: &[&str],
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Vec<PluginMarketplaceEntry> {
|
||||
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
|
||||
|
||||
@@ -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<_>>(),
|
||||
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()?;
|
||||
|
||||
@@ -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::<HashSet<_>>();
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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<Vec<crate::remote::RemoteMarketplace>> {
|
||||
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<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) -> Result<Vec<crate::remote::RemoteMarketplace>, 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();
|
||||
|
||||
@@ -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<_>>(),
|
||||
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();
|
||||
|
||||
@@ -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<RemoteMarketplace> {
|
||||
let mut plugins_by_marketplace = BTreeMap::<String, Vec<RemotePluginSummary>>::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())
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user