mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
[codex] Cache remote plugin catalog for suggestions (#25457)
## Summary - cache the global remote plugin catalog when remote plugin listing runs and warm it during startup - use the cached remote catalog in plugin install recommendations with canonical `plugin@openai-curated-remote` ids - reuse the session `PluginsManager` for plugin recommendations so remote cache state is visible on the recommend path - skip core installed-state verification for remote plugin install suggestions while leaving local plugin and connector verification unchanged ## Testing - `just fmt` - `git diff --check` - `cargo test -p codex-core list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins` - `cargo test -p codex-core remote_plugin_install_suggestions_skip_core_installed_verification` - `cargo test -p codex-app-server plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled` Earlier focused checks during the same branch: codex-tools TUI filter test, request_plugin_install tests, and codex-app-server build.
This commit is contained in:
1 parent
cb63ee7f5d
commit
f2b725102b
13 files changed
+771
-62
No files matched your search
@@ -662,6 +662,7 @@ impl PluginRequestProcessor {
|
|||||||
&remote_plugin_service_config,
|
&remote_plugin_service_config,
|
||||||
auth.as_ref(),
|
auth.as_ref(),
|
||||||
&remote_sources,
|
&remote_sources,
|
||||||
|
/*global_catalog_cache_path*/ Some(config.codex_home.as_path()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1900,6 +1900,23 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
|||||||
"project management".to_string()
|
"project management".to_string()
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
let cache_files = std::fs::read_dir(codex_home.path().join("cache/remote_plugin_catalog"))?
|
||||||
|
.map(|entry| entry.map(|entry| entry.path()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
assert_eq!(cache_files.len(), 1);
|
||||||
|
let cached_catalog: serde_json::Value =
|
||||||
|
serde_json::from_slice(&std::fs::read(&cache_files[0])?)?;
|
||||||
|
assert_eq!(cached_catalog["schema_version"], serde_json::json!(1));
|
||||||
|
let cached_plugin_ids = cached_catalog["plugins"]
|
||||||
|
.as_array()
|
||||||
|
.expect("cached plugins should be an array")
|
||||||
|
.iter()
|
||||||
|
.map(|plugin| plugin["id"].as_str().expect("cached plugin id").to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
cached_plugin_ids,
|
||||||
|
vec!["plugins~Plugin_00000000000000000000000000000000".to_string()]
|
||||||
|
);
|
||||||
assert_eq!(response.featured_plugin_ids, Vec::<String>::new());
|
assert_eq!(response.featured_plugin_ids, Vec::<String>::new());
|
||||||
assert!(
|
assert!(
|
||||||
!server
|
!server
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ pub const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
|
|||||||
"outlook-calendar@openai-curated",
|
"outlook-calendar@openai-curated",
|
||||||
"linear@openai-curated",
|
"linear@openai-curated",
|
||||||
"figma@openai-curated",
|
"figma@openai-curated",
|
||||||
|
"github@openai-curated-remote",
|
||||||
|
"notion@openai-curated-remote",
|
||||||
|
"slack@openai-curated-remote",
|
||||||
|
"gmail@openai-curated-remote",
|
||||||
|
"google-calendar@openai-curated-remote",
|
||||||
|
"google-drive@openai-curated-remote",
|
||||||
|
"openai-developers@openai-curated-remote",
|
||||||
|
"canva@openai-curated-remote",
|
||||||
|
"teams@openai-curated-remote",
|
||||||
|
"sharepoint@openai-curated-remote",
|
||||||
|
"outlook-email@openai-curated-remote",
|
||||||
|
"outlook-calendar@openai-curated-remote",
|
||||||
|
"linear@openai-curated-remote",
|
||||||
|
"figma@openai-curated-remote",
|
||||||
"chrome@openai-bundled",
|
"chrome@openai-bundled",
|
||||||
"computer-use@openai-bundled",
|
"computer-use@openai-bundled",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -592,6 +592,31 @@ impl PluginsManager {
|
|||||||
Some(crate::remote::group_remote_installed_plugins_by_marketplaces(plugins, visible_scopes))
|
Some(crate::remote::group_remote_installed_plugins_by_marketplaces(plugins, visible_scopes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cached_global_remote_discoverable_plugins_for_config(
|
||||||
|
&self,
|
||||||
|
config: &PluginsConfigInput,
|
||||||
|
auth: Option<&CodexAuth>,
|
||||||
|
) -> Vec<crate::remote::RemoteDiscoverablePlugin> {
|
||||||
|
if !config.plugins_enabled || !config.remote_plugin_enabled {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(account_id) = auth.get_account_id() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
if account_id.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::remote::cached_global_remote_discoverable_plugins(
|
||||||
|
self.codex_home.as_path(),
|
||||||
|
&remote_plugin_service_config(config),
|
||||||
|
auth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn build_and_cache_remote_installed_plugin_marketplaces(
|
pub async fn build_and_cache_remote_installed_plugin_marketplaces(
|
||||||
&self,
|
&self,
|
||||||
config: &PluginsConfigInput,
|
config: &PluginsConfigInput,
|
||||||
@@ -1548,9 +1573,30 @@ impl PluginsManager {
|
|||||||
);
|
);
|
||||||
manager.maybe_start_remote_installed_plugin_bundle_sync(
|
manager.maybe_start_remote_installed_plugin_bundle_sync(
|
||||||
&config_for_remote_sync,
|
&config_for_remote_sync,
|
||||||
auth,
|
auth.clone(),
|
||||||
on_effective_plugins_changed,
|
on_effective_plugins_changed,
|
||||||
);
|
);
|
||||||
|
if config_for_remote_sync.remote_plugin_enabled {
|
||||||
|
match crate::remote::fetch_and_cache_global_remote_plugin_catalog(
|
||||||
|
manager.codex_home.as_path(),
|
||||||
|
&remote_plugin_service_config(&config_for_remote_sync),
|
||||||
|
auth.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(
|
||||||
|
RemotePluginCatalogError::AuthRequired
|
||||||
|
| RemotePluginCatalogError::UnsupportedAuthMode,
|
||||||
|
) => {}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
error = %err,
|
||||||
|
"failed to warm remote plugin catalog cache"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ use codex_plugin::PluginId;
|
|||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use reqwest::RequestBuilder;
|
use reqwest::RequestBuilder;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
mod catalog_cache;
|
||||||
mod remote_installed_plugin_sync;
|
mod remote_installed_plugin_sync;
|
||||||
mod share;
|
mod share;
|
||||||
|
|
||||||
@@ -179,6 +182,18 @@ pub struct RemotePluginSkillDetail {
|
|||||||
pub contents: Option<String>,
|
pub contents: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RemoteDiscoverablePlugin {
|
||||||
|
pub config_id: String,
|
||||||
|
pub remote_plugin_id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub has_skills: bool,
|
||||||
|
pub app_ids: Vec<String>,
|
||||||
|
pub install_policy: PluginInstallPolicy,
|
||||||
|
pub availability: PluginAvailability,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_valid_remote_plugin_id(plugin_id: &str) -> bool {
|
pub fn is_valid_remote_plugin_id(plugin_id: &str) -> bool {
|
||||||
!plugin_id.is_empty()
|
!plugin_id.is_empty()
|
||||||
&& plugin_id
|
&& plugin_id
|
||||||
@@ -293,7 +308,7 @@ pub enum RemotePluginCatalogError {
|
|||||||
CacheRemove(String),
|
CacheRemove(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||||
pub enum RemotePluginScope {
|
pub enum RemotePluginScope {
|
||||||
#[serde(rename = "GLOBAL")]
|
#[serde(rename = "GLOBAL")]
|
||||||
Global,
|
Global,
|
||||||
@@ -340,7 +355,7 @@ struct RemotePluginPagination {
|
|||||||
next_page_token: Option<String>,
|
next_page_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
struct RemotePluginSkillInterfaceResponse {
|
struct RemotePluginSkillInterfaceResponse {
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
short_description: Option<String>,
|
short_description: Option<String>,
|
||||||
@@ -350,7 +365,7 @@ struct RemotePluginSkillInterfaceResponse {
|
|||||||
icon_large_url: Option<String>,
|
icon_large_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
struct RemotePluginSkillResponse {
|
struct RemotePluginSkillResponse {
|
||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
@@ -364,7 +379,7 @@ struct RemotePluginSkillDetailResponse {
|
|||||||
skill_md_contents: Option<String>,
|
skill_md_contents: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
struct RemotePluginReleaseInterfaceResponse {
|
struct RemotePluginReleaseInterfaceResponse {
|
||||||
short_description: Option<String>,
|
short_description: Option<String>,
|
||||||
long_description: Option<String>,
|
long_description: Option<String>,
|
||||||
@@ -383,7 +398,7 @@ struct RemotePluginReleaseInterfaceResponse {
|
|||||||
screenshot_urls: Vec<String>,
|
screenshot_urls: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
struct RemotePluginReleaseResponse {
|
struct RemotePluginReleaseResponse {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
@@ -402,7 +417,7 @@ struct RemotePluginReleaseResponse {
|
|||||||
skills: Vec<RemotePluginSkillResponse>,
|
skills: Vec<RemotePluginSkillResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
struct RemotePluginDirectoryItem {
|
struct RemotePluginDirectoryItem {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -450,7 +465,7 @@ fn workspace_plugin_discoverability(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
struct RemotePluginDirectorySharePrincipal {
|
struct RemotePluginDirectorySharePrincipal {
|
||||||
principal_type: RemotePluginSharePrincipalType,
|
principal_type: RemotePluginSharePrincipalType,
|
||||||
principal_id: String,
|
principal_id: String,
|
||||||
@@ -489,6 +504,7 @@ pub async fn fetch_remote_marketplaces(
|
|||||||
config: &RemotePluginServiceConfig,
|
config: &RemotePluginServiceConfig,
|
||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
sources: &[RemoteMarketplaceSource],
|
sources: &[RemoteMarketplaceSource],
|
||||||
|
global_catalog_cache_path: Option<&Path>,
|
||||||
) -> Result<Vec<RemoteMarketplace>, RemotePluginCatalogError> {
|
) -> Result<Vec<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||||
let auth = ensure_chatgpt_auth(auth)?;
|
let auth = ensure_chatgpt_auth(auth)?;
|
||||||
let mut marketplaces = Vec::new();
|
let mut marketplaces = Vec::new();
|
||||||
@@ -512,6 +528,8 @@ pub async fn fetch_remote_marketplaces(
|
|||||||
fetch_directory_plugins_for_scope(config, auth, scope),
|
fetch_directory_plugins_for_scope(config, auth, scope),
|
||||||
fetch_installed_plugins_for_scope(config, auth, scope),
|
fetch_installed_plugins_for_scope(config, auth, scope),
|
||||||
)?;
|
)?;
|
||||||
|
let directory_plugins_for_cache =
|
||||||
|
global_catalog_cache_path.map(|_| directory_plugins.clone());
|
||||||
if let Some(marketplace) = build_remote_marketplace(
|
if let Some(marketplace) = build_remote_marketplace(
|
||||||
scope.marketplace_name(),
|
scope.marketplace_name(),
|
||||||
scope.marketplace_display_name(),
|
scope.marketplace_display_name(),
|
||||||
@@ -521,6 +539,16 @@ pub async fn fetch_remote_marketplaces(
|
|||||||
)? {
|
)? {
|
||||||
marketplaces.push(marketplace);
|
marketplaces.push(marketplace);
|
||||||
}
|
}
|
||||||
|
if let (Some(codex_home), Some(directory_plugins)) =
|
||||||
|
(global_catalog_cache_path, directory_plugins_for_cache)
|
||||||
|
{
|
||||||
|
catalog_cache::write_cached_global_directory_plugins(
|
||||||
|
codex_home,
|
||||||
|
config,
|
||||||
|
auth,
|
||||||
|
&directory_plugins,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RemoteMarketplaceSource::WorkspaceDirectory => {
|
RemoteMarketplaceSource::WorkspaceDirectory => {
|
||||||
let scope = RemotePluginScope::Workspace;
|
let scope = RemotePluginScope::Workspace;
|
||||||
@@ -600,6 +628,36 @@ pub async fn fetch_remote_marketplaces(
|
|||||||
Ok(marketplaces)
|
Ok(marketplaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_and_cache_global_remote_plugin_catalog(
|
||||||
|
codex_home: &Path,
|
||||||
|
config: &RemotePluginServiceConfig,
|
||||||
|
auth: Option<&CodexAuth>,
|
||||||
|
) -> Result<(), RemotePluginCatalogError> {
|
||||||
|
let auth = ensure_chatgpt_auth(auth)?;
|
||||||
|
let plugins =
|
||||||
|
fetch_directory_plugins_for_scope(config, auth, RemotePluginScope::Global).await?;
|
||||||
|
catalog_cache::write_cached_global_directory_plugins(codex_home, config, auth, &plugins);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cached_global_remote_discoverable_plugins(
|
||||||
|
codex_home: &Path,
|
||||||
|
config: &RemotePluginServiceConfig,
|
||||||
|
auth: &CodexAuth,
|
||||||
|
) -> Vec<RemoteDiscoverablePlugin> {
|
||||||
|
catalog_cache::load_cached_global_directory_plugins(codex_home, config, auth)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|plugin| match remote_discoverable_plugin_from_directory_item(&plugin) {
|
||||||
|
Ok(plugin) => Some(plugin),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(error = %err, "ignoring cached remote plugin recommendation entry");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn fetch_openai_curated_remote_collection_marketplace(
|
pub async fn fetch_openai_curated_remote_collection_marketplace(
|
||||||
config: &RemotePluginServiceConfig,
|
config: &RemotePluginServiceConfig,
|
||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
@@ -1053,6 +1111,34 @@ fn build_remote_plugin_summary(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remote_discoverable_plugin_from_directory_item(
|
||||||
|
plugin: &RemotePluginDirectoryItem,
|
||||||
|
) -> Result<RemoteDiscoverablePlugin, RemotePluginCatalogError> {
|
||||||
|
let marketplace_name = remote_plugin_canonical_marketplace_name(plugin)?;
|
||||||
|
let plugin_id =
|
||||||
|
PluginId::new(plugin.name.clone(), marketplace_name.to_string()).map_err(|err| {
|
||||||
|
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||||
|
"invalid remote plugin config id for `{}` in `{marketplace_name}`: {err}",
|
||||||
|
plugin.name
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let display_name =
|
||||||
|
non_empty_string(Some(&plugin.release.display_name)).unwrap_or_else(|| plugin.name.clone());
|
||||||
|
let description = non_empty_string(plugin.release.interface.short_description.as_deref())
|
||||||
|
.or_else(|| non_empty_string(Some(&plugin.release.description)));
|
||||||
|
|
||||||
|
Ok(RemoteDiscoverablePlugin {
|
||||||
|
config_id: plugin_id.as_key(),
|
||||||
|
remote_plugin_id: plugin.id.clone(),
|
||||||
|
name: display_name,
|
||||||
|
description,
|
||||||
|
has_skills: !plugin.release.skills.is_empty(),
|
||||||
|
app_ids: plugin.release.app_ids.clone(),
|
||||||
|
install_policy: plugin.installation_policy,
|
||||||
|
availability: plugin.availability,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn remote_plugin_share_context(
|
fn remote_plugin_share_context(
|
||||||
plugin: &RemotePluginDirectoryItem,
|
plugin: &RemotePluginDirectoryItem,
|
||||||
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError> {
|
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError> {
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use super::RemotePluginDirectoryItem;
|
||||||
|
use super::RemotePluginServiceConfig;
|
||||||
|
use codex_login::CodexAuth;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
const REMOTE_PLUGIN_CATALOG_DISK_CACHE_SCHEMA_VERSION: u8 = 1;
|
||||||
|
const REMOTE_PLUGIN_CATALOG_DISK_CACHE_DIR: &str = "cache/remote_plugin_catalog";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
struct RemotePluginCatalogCacheKey {
|
||||||
|
chatgpt_base_url: String,
|
||||||
|
account_id: Option<String>,
|
||||||
|
chatgpt_user_id: Option<String>,
|
||||||
|
is_workspace_account: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemotePluginCatalogCacheKey {
|
||||||
|
fn global(config: &RemotePluginServiceConfig, auth: &CodexAuth) -> Self {
|
||||||
|
Self {
|
||||||
|
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||||
|
account_id: auth.get_account_id(),
|
||||||
|
chatgpt_user_id: auth.get_chatgpt_user_id(),
|
||||||
|
is_workspace_account: auth.is_workspace_account(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct RemotePluginCatalogDiskCache {
|
||||||
|
schema_version: u8,
|
||||||
|
plugins: Vec<RemotePluginDirectoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_cached_global_directory_plugins(
|
||||||
|
codex_home: &Path,
|
||||||
|
config: &RemotePluginServiceConfig,
|
||||||
|
auth: &CodexAuth,
|
||||||
|
) -> Option<Vec<RemotePluginDirectoryItem>> {
|
||||||
|
let cache_path = cache_path(
|
||||||
|
codex_home,
|
||||||
|
&RemotePluginCatalogCacheKey::global(config, auth),
|
||||||
|
);
|
||||||
|
let bytes = match std::fs::read(&cache_path) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
cache_path = %cache_path.display(),
|
||||||
|
"failed to read remote plugin catalog disk cache: {err}"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cache: RemotePluginCatalogDiskCache = match serde_json::from_slice(&bytes) {
|
||||||
|
Ok(cache) => cache,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
cache_path = %cache_path.display(),
|
||||||
|
"failed to parse remote plugin catalog disk cache: {err}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(cache_path);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if cache.schema_version != REMOTE_PLUGIN_CATALOG_DISK_CACHE_SCHEMA_VERSION {
|
||||||
|
let _ = std::fs::remove_file(cache_path);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(cache.plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_cached_global_directory_plugins(
|
||||||
|
codex_home: &Path,
|
||||||
|
config: &RemotePluginServiceConfig,
|
||||||
|
auth: &CodexAuth,
|
||||||
|
plugins: &[RemotePluginDirectoryItem],
|
||||||
|
) {
|
||||||
|
let cache_path = cache_path(
|
||||||
|
codex_home,
|
||||||
|
&RemotePluginCatalogCacheKey::global(config, auth),
|
||||||
|
);
|
||||||
|
if let Some(parent) = cache_path.parent()
|
||||||
|
&& std::fs::create_dir_all(parent).is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(bytes) = serde_json::to_vec_pretty(&RemotePluginCatalogDiskCache {
|
||||||
|
schema_version: REMOTE_PLUGIN_CATALOG_DISK_CACHE_SCHEMA_VERSION,
|
||||||
|
plugins: plugins.to_vec(),
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = std::fs::write(cache_path, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_path(codex_home: &Path, cache_key: &RemotePluginCatalogCacheKey) -> PathBuf {
|
||||||
|
let cache_key_json = serde_json::to_vec(cache_key).unwrap_or_default();
|
||||||
|
let mut cache_key_hash = 0xcbf29ce484222325_u64;
|
||||||
|
for byte in cache_key_json {
|
||||||
|
cache_key_hash ^= u64::from(byte);
|
||||||
|
cache_key_hash = cache_key_hash.wrapping_mul(0x100000001b3);
|
||||||
|
}
|
||||||
|
codex_home
|
||||||
|
.join(REMOTE_PLUGIN_CATALOG_DISK_CACHE_DIR)
|
||||||
|
.join(format!("{cache_key_hash:016x}.json"))
|
||||||
|
}
|
||||||
@@ -111,6 +111,7 @@ pub(crate) async fn list_accessible_and_enabled_connectors_from_manager(
|
|||||||
|
|
||||||
pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
|
pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
plugins_manager: &PluginsManager,
|
||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
accessible_connectors: &[AppInfo],
|
accessible_connectors: &[AppInfo],
|
||||||
loaded_plugin_app_connector_ids: &[String],
|
loaded_plugin_app_connector_ids: &[String],
|
||||||
@@ -129,11 +130,15 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
|
|||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(DiscoverableTool::from);
|
.map(DiscoverableTool::from);
|
||||||
let discoverable_plugins =
|
let discoverable_plugins = list_tool_suggest_discoverable_plugins(
|
||||||
list_tool_suggest_discoverable_plugins(config, loaded_plugin_app_connector_ids)
|
config,
|
||||||
.await?
|
plugins_manager,
|
||||||
.into_iter()
|
auth,
|
||||||
.map(DiscoverableTool::from);
|
loaded_plugin_app_connector_ids,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(DiscoverableTool::from);
|
||||||
Ok(discoverable_connectors
|
Ok(discoverable_connectors
|
||||||
.chain(discoverable_plugins)
|
.chain(discoverable_plugins)
|
||||||
.collect())
|
.collect())
|
||||||
|
|||||||
@@ -1236,11 +1236,17 @@ discoverables = [
|
|||||||
.await
|
.await
|
||||||
.expect("config should load");
|
.expect("config should load");
|
||||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||||
|
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||||
|
|
||||||
let discoverable_tools =
|
let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
|
||||||
list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[], &[])
|
&config,
|
||||||
.await
|
&plugins_manager,
|
||||||
.expect("discoverable tools should load");
|
Some(&auth),
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("discoverable tools should load");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_tools,
|
discoverable_tools,
|
||||||
@@ -1268,9 +1274,11 @@ apps = true
|
|||||||
.expect("config should load");
|
.expect("config should load");
|
||||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||||
let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()];
|
let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()];
|
||||||
|
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||||
|
|
||||||
let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
|
let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(
|
||||||
&config,
|
&config,
|
||||||
|
&plugins_manager,
|
||||||
Some(&auth),
|
Some(&auth),
|
||||||
&[],
|
&[],
|
||||||
&loaded_plugin_app_connector_ids,
|
&loaded_plugin_app_connector_ids,
|
||||||
|
|||||||
@@ -4,29 +4,36 @@ use tracing::warn;
|
|||||||
|
|
||||||
use super::PluginCapabilitySummary;
|
use super::PluginCapabilitySummary;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use codex_app_server_protocol::PluginAvailability;
|
||||||
|
use codex_app_server_protocol::PluginInstallPolicy;
|
||||||
use codex_config::types::ToolSuggestDiscoverableType;
|
use codex_config::types::ToolSuggestDiscoverableType;
|
||||||
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||||
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||||
use codex_core_plugins::PluginsManager;
|
use codex_core_plugins::PluginsManager;
|
||||||
use codex_core_plugins::TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST as TOOL_SUGGEST_DISCOVERABLE_PLUGIN_FALLBACK_ALLOWLIST;
|
use codex_core_plugins::TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST;
|
||||||
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
|
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
|
||||||
|
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||||
|
use codex_core_plugins::remote::RemotePluginScope;
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
|
use codex_login::CodexAuth;
|
||||||
use codex_tools::DiscoverablePluginInfo;
|
use codex_tools::DiscoverablePluginInfo;
|
||||||
|
|
||||||
const TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST: &[&str] = &[
|
const TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST: &[&str] = &[
|
||||||
OPENAI_BUNDLED_MARKETPLACE_NAME,
|
OPENAI_BUNDLED_MARKETPLACE_NAME,
|
||||||
OPENAI_CURATED_MARKETPLACE_NAME,
|
OPENAI_CURATED_MARKETPLACE_NAME,
|
||||||
|
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) async fn list_tool_suggest_discoverable_plugins(
|
pub(crate) async fn list_tool_suggest_discoverable_plugins(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
plugins_manager: &PluginsManager,
|
||||||
|
auth: Option<&CodexAuth>,
|
||||||
loaded_plugin_app_connector_ids: &[String],
|
loaded_plugin_app_connector_ids: &[String],
|
||||||
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
||||||
if !config.features.enabled(Feature::Plugins) {
|
if !config.features.enabled(Feature::Plugins) {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
|
||||||
let plugins_input = config.plugins_config_input();
|
let plugins_input = config.plugins_config_input();
|
||||||
let configured_plugin_ids = config
|
let configured_plugin_ids = config
|
||||||
.tool_suggest
|
.tool_suggest
|
||||||
@@ -55,17 +62,28 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins(
|
|||||||
.map(|connector_id| connector_id.0.clone())
|
.map(|connector_id| connector_id.0.clone())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
installed_app_connector_ids.extend(loaded_plugin_app_connector_ids.iter().cloned());
|
installed_app_connector_ids.extend(loaded_plugin_app_connector_ids.iter().cloned());
|
||||||
|
let remote_installed_marketplaces = if plugins_input.remote_plugin_enabled {
|
||||||
|
plugins_manager
|
||||||
|
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut discoverable_plugins = Vec::<DiscoverablePluginInfo>::new();
|
let mut discoverable_plugins = Vec::<DiscoverablePluginInfo>::new();
|
||||||
for marketplace in marketplaces {
|
for marketplace in marketplaces {
|
||||||
let marketplace_name = marketplace.name;
|
let marketplace_name = marketplace.name;
|
||||||
|
if plugins_input.remote_plugin_enabled
|
||||||
|
&& marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let is_allowlisted_marketplace =
|
let is_allowlisted_marketplace =
|
||||||
TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST.contains(&marketplace_name.as_str());
|
TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST.contains(&marketplace_name.as_str());
|
||||||
|
|
||||||
for plugin in marketplace.plugins {
|
for plugin in marketplace.plugins {
|
||||||
let is_configured_plugin = configured_plugin_ids.contains(plugin.id.as_str());
|
let is_configured_plugin = configured_plugin_ids.contains(plugin.id.as_str());
|
||||||
let is_fallback_plugin =
|
let is_fallback_plugin =
|
||||||
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_FALLBACK_ALLOWLIST.contains(&plugin.id.as_str());
|
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str());
|
||||||
if plugin.installed
|
if plugin.installed
|
||||||
|| plugin.policy.installation == MarketplacePluginInstallPolicy::NotAvailable
|
|| plugin.policy.installation == MarketplacePluginInstallPolicy::NotAvailable
|
||||||
|| disabled_plugin_ids.contains(plugin.id.as_str())
|
|| disabled_plugin_ids.contains(plugin.id.as_str())
|
||||||
@@ -113,6 +131,44 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(remote_installed_marketplaces) = remote_installed_marketplaces.as_ref() {
|
||||||
|
let installed_remote_plugin_ids = remote_installed_marketplaces
|
||||||
|
.iter()
|
||||||
|
.flat_map(|marketplace| marketplace.plugins.iter())
|
||||||
|
.map(|plugin| plugin.remote_plugin_id.clone())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
for plugin in plugins_manager
|
||||||
|
.cached_global_remote_discoverable_plugins_for_config(&plugins_input, auth)
|
||||||
|
{
|
||||||
|
let is_configured_plugin = configured_plugin_ids.contains(plugin.config_id.as_str())
|
||||||
|
|| configured_plugin_ids.contains(plugin.remote_plugin_id.as_str());
|
||||||
|
let is_fallback_plugin =
|
||||||
|
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.config_id.as_str());
|
||||||
|
let matches_installed_app = plugin
|
||||||
|
.app_ids
|
||||||
|
.iter()
|
||||||
|
.any(|app_id| installed_app_connector_ids.contains(app_id.as_str()));
|
||||||
|
let is_disabled = disabled_plugin_ids.contains(plugin.config_id.as_str())
|
||||||
|
|| disabled_plugin_ids.contains(plugin.remote_plugin_id.as_str());
|
||||||
|
if installed_remote_plugin_ids.contains(&plugin.remote_plugin_id)
|
||||||
|
|| plugin.install_policy == PluginInstallPolicy::NotAvailable
|
||||||
|
|| plugin.availability == PluginAvailability::DisabledByAdmin
|
||||||
|
|| is_disabled
|
||||||
|
|| (!is_configured_plugin && !is_fallback_plugin && !matches_installed_app)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
discoverable_plugins.push(DiscoverablePluginInfo {
|
||||||
|
id: plugin.config_id,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
has_skills: plugin.has_skills,
|
||||||
|
mcp_server_names: Vec::new(),
|
||||||
|
app_connector_ids: plugin.app_ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
discoverable_plugins.sort_by(|left, right| {
|
discoverable_plugins.sort_by(|left, right| {
|
||||||
left.name
|
left.name
|
||||||
.cmp(&right.name)
|
.cmp(&right.name)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use super::*;
|
|
||||||
use crate::plugins::test_support::load_plugins_config;
|
use crate::plugins::test_support::load_plugins_config;
|
||||||
use crate::plugins::test_support::write_curated_plugin;
|
use crate::plugins::test_support::write_curated_plugin;
|
||||||
use crate::plugins::test_support::write_curated_plugin_sha;
|
use crate::plugins::test_support::write_curated_plugin_sha;
|
||||||
@@ -7,6 +6,10 @@ use crate::plugins::test_support::write_openai_curated_marketplace;
|
|||||||
use crate::plugins::test_support::write_plugins_feature_config;
|
use crate::plugins::test_support::write_plugins_feature_config;
|
||||||
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||||
use codex_core_plugins::PluginInstallRequest;
|
use codex_core_plugins::PluginInstallRequest;
|
||||||
|
use codex_core_plugins::PluginsManager;
|
||||||
|
use codex_core_plugins::remote::RemotePluginScope;
|
||||||
|
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;
|
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
|
||||||
use codex_tools::DiscoverablePluginInfo;
|
use codex_tools::DiscoverablePluginInfo;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
@@ -17,6 +20,44 @@ use tracing::Level;
|
|||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
use tracing_test::internal::MockWriter;
|
use tracing_test::internal::MockWriter;
|
||||||
|
|
||||||
|
async fn list_discoverable_plugins(
|
||||||
|
config: &crate::config::Config,
|
||||||
|
loaded_plugin_app_connector_ids: &[String],
|
||||||
|
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
||||||
|
list_discoverable_plugins_with_auth(config, /*auth*/ None, loaded_plugin_app_connector_ids)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_discoverable_plugins_with_auth(
|
||||||
|
config: &crate::config::Config,
|
||||||
|
auth: Option<&codex_login::CodexAuth>,
|
||||||
|
loaded_plugin_app_connector_ids: &[String],
|
||||||
|
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
||||||
|
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||||
|
list_discoverable_plugins_with_manager_and_auth(
|
||||||
|
config,
|
||||||
|
&plugins_manager,
|
||||||
|
auth,
|
||||||
|
loaded_plugin_app_connector_ids,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_discoverable_plugins_with_manager_and_auth(
|
||||||
|
config: &crate::config::Config,
|
||||||
|
plugins_manager: &PluginsManager,
|
||||||
|
auth: Option<&codex_login::CodexAuth>,
|
||||||
|
loaded_plugin_app_connector_ids: &[String],
|
||||||
|
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
||||||
|
super::list_tool_suggest_discoverable_plugins(
|
||||||
|
config,
|
||||||
|
plugins_manager,
|
||||||
|
auth,
|
||||||
|
loaded_plugin_app_connector_ids,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_tool_suggest_discoverable_plugins_returns_fallback_plugins_without_installed_apps() {
|
async fn list_tool_suggest_discoverable_plugins_returns_fallback_plugins_without_installed_apps() {
|
||||||
let codex_home = tempdir().expect("tempdir should succeed");
|
let codex_home = tempdir().expect("tempdir should succeed");
|
||||||
@@ -25,9 +66,7 @@ async fn list_tool_suggest_discoverable_plugins_returns_fallback_plugins_without
|
|||||||
write_plugins_feature_config(codex_home.path());
|
write_plugins_feature_config(codex_home.path());
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
@@ -51,9 +90,7 @@ async fn list_tool_suggest_discoverable_plugins_filters_non_fallback_by_installe
|
|||||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await;
|
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "slack").await;
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
@@ -76,10 +113,9 @@ async fn list_tool_suggest_discoverable_plugins_filters_by_loaded_plugin_apps()
|
|||||||
write_plugins_feature_config(codex_home.path());
|
write_plugins_feature_config(codex_home.path());
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins =
|
let discoverable_plugins = list_discoverable_plugins(&config, &[hubspot_app_id.to_string()])
|
||||||
list_tool_suggest_discoverable_plugins(&config, &[hubspot_app_id.to_string()])
|
.await
|
||||||
.await
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
@@ -102,9 +138,7 @@ async fn list_tool_suggest_discoverable_plugins_filters_microsoft_by_installed_a
|
|||||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "teams").await;
|
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "teams").await;
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
@@ -119,6 +153,283 @@ async fn list_tool_suggest_discoverable_plugins_filters_microsoft_by_installed_a
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins() {
|
||||||
|
use codex_login::CodexAuth;
|
||||||
|
use serde_json::json;
|
||||||
|
use wiremock::Mock;
|
||||||
|
use wiremock::MockServer;
|
||||||
|
use wiremock::ResponseTemplate;
|
||||||
|
use wiremock::matchers::method;
|
||||||
|
use wiremock::matchers::path;
|
||||||
|
use wiremock::matchers::query_param;
|
||||||
|
|
||||||
|
let codex_home = tempdir().expect("tempdir should succeed");
|
||||||
|
write_file(
|
||||||
|
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
||||||
|
r#"[features]
|
||||||
|
plugins = true
|
||||||
|
remote_plugin = true
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/list"))
|
||||||
|
.and(query_param("scope", "GLOBAL"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_github",
|
||||||
|
"name": "github",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "AVAILABLE",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote GitHub",
|
||||||
|
"description": "Remote GitHub long",
|
||||||
|
"app_ids": ["github"],
|
||||||
|
"interface": {
|
||||||
|
"short_description": "Remote GitHub short",
|
||||||
|
"long_description": null,
|
||||||
|
"developer_name": null,
|
||||||
|
"category": null,
|
||||||
|
"capabilities": [],
|
||||||
|
"website_url": null,
|
||||||
|
"privacy_policy_url": null,
|
||||||
|
"terms_of_service_url": null,
|
||||||
|
"brand_color": null,
|
||||||
|
"default_prompt": null,
|
||||||
|
"composer_icon_url": null,
|
||||||
|
"logo_url": null,
|
||||||
|
"screenshot_urls": []
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "github",
|
||||||
|
"description": "Use GitHub",
|
||||||
|
"interface": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_unlisted",
|
||||||
|
"name": "remote-unlisted",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "AVAILABLE",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote Unlisted",
|
||||||
|
"description": "Remote Unlisted long",
|
||||||
|
"app_ids": [],
|
||||||
|
"interface": {
|
||||||
|
"short_description": "Remote Unlisted short",
|
||||||
|
"long_description": null,
|
||||||
|
"developer_name": null,
|
||||||
|
"category": null,
|
||||||
|
"capabilities": [],
|
||||||
|
"website_url": null,
|
||||||
|
"privacy_policy_url": null,
|
||||||
|
"terms_of_service_url": null,
|
||||||
|
"brand_color": null,
|
||||||
|
"default_prompt": null,
|
||||||
|
"composer_icon_url": null,
|
||||||
|
"logo_url": null,
|
||||||
|
"screenshot_urls": []
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "remote-unlisted",
|
||||||
|
"description": "Use unlisted remote plugin",
|
||||||
|
"interface": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_slack_not_available",
|
||||||
|
"name": "slack",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "NOT_AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "AVAILABLE",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote Slack",
|
||||||
|
"description": "Remote Slack long",
|
||||||
|
"app_ids": [],
|
||||||
|
"interface": {
|
||||||
|
"short_description": "Remote Slack short",
|
||||||
|
"long_description": null,
|
||||||
|
"developer_name": null,
|
||||||
|
"category": null,
|
||||||
|
"capabilities": [],
|
||||||
|
"website_url": null,
|
||||||
|
"privacy_policy_url": null,
|
||||||
|
"terms_of_service_url": null,
|
||||||
|
"brand_color": null,
|
||||||
|
"default_prompt": null,
|
||||||
|
"composer_icon_url": null,
|
||||||
|
"logo_url": null,
|
||||||
|
"screenshot_urls": []
|
||||||
|
},
|
||||||
|
"skills": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_figma_admin_disabled",
|
||||||
|
"name": "figma",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "DISABLED_BY_ADMIN",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote Figma",
|
||||||
|
"description": "Remote Figma long",
|
||||||
|
"app_ids": [],
|
||||||
|
"interface": {
|
||||||
|
"short_description": "Remote Figma short",
|
||||||
|
"long_description": null,
|
||||||
|
"developer_name": null,
|
||||||
|
"category": null,
|
||||||
|
"capabilities": [],
|
||||||
|
"website_url": null,
|
||||||
|
"privacy_policy_url": null,
|
||||||
|
"terms_of_service_url": null,
|
||||||
|
"brand_color": null,
|
||||||
|
"default_prompt": null,
|
||||||
|
"composer_icon_url": null,
|
||||||
|
"logo_url": null,
|
||||||
|
"screenshot_urls": []
|
||||||
|
},
|
||||||
|
"skills": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"next_page_token": null
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||||
|
let mut config = load_plugins_config(codex_home.path()).await;
|
||||||
|
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||||
|
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||||
|
fetch_and_cache_global_remote_plugin_catalog(
|
||||||
|
codex_home.path(),
|
||||||
|
&RemotePluginServiceConfig {
|
||||||
|
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||||
|
},
|
||||||
|
Some(&auth),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("remote plugin catalog cache should write");
|
||||||
|
|
||||||
|
let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(
|
||||||
|
&config,
|
||||||
|
&plugins_manager,
|
||||||
|
Some(&auth),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
discoverable_plugins
|
||||||
|
.iter()
|
||||||
|
.all(|plugin| plugin.id != "github@openai-curated-remote")
|
||||||
|
);
|
||||||
|
|
||||||
|
for scope in ["GLOBAL", "WORKSPACE"] {
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param("scope", scope))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"plugins": [],
|
||||||
|
"pagination": {
|
||||||
|
"next_page_token": null
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
plugins_manager
|
||||||
|
.build_and_cache_remote_installed_plugin_marketplaces(
|
||||||
|
&config.plugins_config_input(),
|
||||||
|
Some(&auth),
|
||||||
|
&[RemotePluginScope::Global],
|
||||||
|
/*on_effective_plugins_changed*/ None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("remote installed plugin cache should write");
|
||||||
|
|
||||||
|
let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(
|
||||||
|
&config,
|
||||||
|
&plugins_manager,
|
||||||
|
Some(&auth),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
discoverable_plugins
|
||||||
|
.iter()
|
||||||
|
.filter(|plugin| plugin.id.ends_with("@openai-curated-remote"))
|
||||||
|
.map(|plugin| plugin.id.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["github@openai-curated-remote"]
|
||||||
|
);
|
||||||
|
let remote_plugins = discoverable_plugins
|
||||||
|
.into_iter()
|
||||||
|
.filter(|plugin| plugin.id == "github@openai-curated-remote")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
remote_plugins,
|
||||||
|
vec![DiscoverablePluginInfo {
|
||||||
|
id: "github@openai-curated-remote".to_string(),
|
||||||
|
name: "Remote GitHub".to_string(),
|
||||||
|
description: Some("Remote GitHub short".to_string()),
|
||||||
|
has_skills: true,
|
||||||
|
mcp_server_names: Vec::new(),
|
||||||
|
app_connector_ids: vec!["github".to_string()],
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
|
||||||
|
write_file(
|
||||||
|
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
||||||
|
r#"[features]
|
||||||
|
plugins = true
|
||||||
|
remote_plugin = true
|
||||||
|
|
||||||
|
[tool_suggest]
|
||||||
|
disabled_tools = [
|
||||||
|
{ type = "plugin", id = "github@openai-curated-remote" }
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let mut config_with_disabled_remote_plugin = load_plugins_config(codex_home.path()).await;
|
||||||
|
config_with_disabled_remote_plugin.chatgpt_base_url = config.chatgpt_base_url.clone();
|
||||||
|
let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(
|
||||||
|
&config_with_disabled_remote_plugin,
|
||||||
|
&plugins_manager,
|
||||||
|
Some(&auth),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
discoverable_plugins
|
||||||
|
.iter()
|
||||||
|
.all(|plugin| plugin.id != "github@openai-curated-remote")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_tool_suggest_discoverable_plugins_filters_sales_apps_by_marketplace() {
|
async fn list_tool_suggest_discoverable_plugins_filters_sales_apps_by_marketplace() {
|
||||||
let hubspot_app_id = "asdk_app_697acb8e53d88191bf7a79e62012ae14";
|
let hubspot_app_id = "asdk_app_697acb8e53d88191bf7a79e62012ae14";
|
||||||
@@ -179,9 +490,7 @@ source = "/tmp/{sales_marketplace_name}"
|
|||||||
install_marketplace_plugin(codex_home.path(), sales_marketplace_root.as_path(), "sales").await;
|
install_marketplace_plugin(codex_home.path(), sales_marketplace_root.as_path(), "sales").await;
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
@@ -195,6 +504,55 @@ source = "/tmp/{sales_marketplace_name}"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_tool_suggest_discoverable_plugins_omits_openai_curated_when_remote_enabled() {
|
||||||
|
let codex_home = tempdir().expect("tempdir should succeed");
|
||||||
|
let curated_root = curated_plugins_repo_path(codex_home.path());
|
||||||
|
write_openai_curated_marketplace(&curated_root, &["slack"]);
|
||||||
|
|
||||||
|
let bundled_marketplace_name = OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||||
|
let bundled_marketplace_root = codex_home
|
||||||
|
.path()
|
||||||
|
.join(format!(".tmp/marketplaces/{bundled_marketplace_name}"));
|
||||||
|
write_file(
|
||||||
|
&bundled_marketplace_root.join(".agents/plugins/marketplace.json"),
|
||||||
|
&format!(
|
||||||
|
r#"{{
|
||||||
|
"name": "{bundled_marketplace_name}",
|
||||||
|
"plugins": [
|
||||||
|
{{"name": "chrome", "source": {{"source": "local", "path": "./plugins/chrome"}}}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
);
|
||||||
|
write_curated_plugin(&bundled_marketplace_root, "chrome");
|
||||||
|
write_file(
|
||||||
|
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
||||||
|
&format!(
|
||||||
|
r#"[features]
|
||||||
|
plugins = true
|
||||||
|
remote_plugin = true
|
||||||
|
|
||||||
|
[marketplaces.{bundled_marketplace_name}]
|
||||||
|
source_type = "git"
|
||||||
|
source = "/tmp/{bundled_marketplace_name}"
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
discoverable_plugins
|
||||||
|
.into_iter()
|
||||||
|
.map(|plugin| plugin.id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["chrome@openai-bundled".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_tool_suggest_discoverable_plugins_deduplicates_configured_marketplace_plugin() {
|
async fn list_tool_suggest_discoverable_plugins_deduplicates_configured_marketplace_plugin() {
|
||||||
let codex_home = tempdir().expect("tempdir should succeed");
|
let codex_home = tempdir().expect("tempdir should succeed");
|
||||||
@@ -234,9 +592,7 @@ discoverables = [{{ type = "plugin", id = "{plugin_id}" }}]
|
|||||||
);
|
);
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(discoverable_plugins.len(), 1);
|
assert_eq!(discoverable_plugins.len(), 1);
|
||||||
assert_eq!(discoverable_plugins[0].id, plugin_id.as_str());
|
assert_eq!(discoverable_plugins[0].id, plugin_id.as_str());
|
||||||
@@ -278,9 +634,7 @@ source = "/tmp/{marketplace_name}"
|
|||||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(discoverable_plugins.len(), 1);
|
assert_eq!(discoverable_plugins.len(), 1);
|
||||||
assert_eq!(discoverable_plugins[0].id, "slack@openai-curated");
|
assert_eq!(discoverable_plugins[0].id, "slack@openai-curated");
|
||||||
@@ -299,9 +653,7 @@ plugins = false
|
|||||||
);
|
);
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
||||||
}
|
}
|
||||||
@@ -322,9 +674,7 @@ async fn list_tool_suggest_discoverable_plugins_normalizes_description() {
|
|||||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins,
|
discoverable_plugins,
|
||||||
@@ -359,7 +709,7 @@ async fn list_tool_suggest_discoverable_plugins_omits_installed_curated_plugins(
|
|||||||
.expect("plugin should install");
|
.expect("plugin should install");
|
||||||
|
|
||||||
let refreshed_config = load_plugins_config(codex_home.path()).await;
|
let refreshed_config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&refreshed_config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&refreshed_config, &[])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -384,9 +734,7 @@ disabled_tools = [
|
|||||||
);
|
);
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
||||||
}
|
}
|
||||||
@@ -435,9 +783,7 @@ async fn list_tool_suggest_discoverable_plugins_omits_not_available_curated_plug
|
|||||||
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
install_marketplace_plugin(codex_home.path(), curated_root.as_path(), "installed").await;
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
@@ -464,9 +810,7 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }]
|
|||||||
);
|
);
|
||||||
|
|
||||||
let config = load_plugins_config(codex_home.path()).await;
|
let config = load_plugins_config(codex_home.path()).await;
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins,
|
discoverable_plugins,
|
||||||
@@ -519,9 +863,7 @@ async fn list_tool_suggest_discoverable_plugins_does_not_reload_marketplace_per_
|
|||||||
.finish();
|
.finish();
|
||||||
let _guard = tracing::subscriber::set_default(subscriber);
|
let _guard = tracing::subscriber::set_default(subscriber);
|
||||||
|
|
||||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config, &[])
|
let discoverable_plugins = list_discoverable_plugins(&config, &[]).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins
|
discoverable_plugins
|
||||||
|
|||||||
@@ -1080,6 +1080,7 @@ pub(crate) async fn built_tools(
|
|||||||
if let Some(accessible_connectors) = accessible_connectors_with_enabled_state.as_ref() {
|
if let Some(accessible_connectors) = accessible_connectors_with_enabled_state.as_ref() {
|
||||||
match connectors::list_tool_suggest_discoverable_tools_with_auth(
|
match connectors::list_tool_suggest_discoverable_tools_with_auth(
|
||||||
&turn_context.config,
|
&turn_context.config,
|
||||||
|
sess.services.plugins_manager.as_ref(),
|
||||||
auth.as_ref(),
|
auth.as_ref(),
|
||||||
accessible_connectors.as_slice(),
|
accessible_connectors.as_slice(),
|
||||||
&loaded_plugin_app_connector_ids,
|
&loaded_plugin_app_connector_ids,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use codex_app_server_protocol::AppInfo;
|
use codex_app_server_protocol::AppInfo;
|
||||||
use codex_config::types::ToolSuggestDisabledTool;
|
use codex_config::types::ToolSuggestDisabledTool;
|
||||||
|
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||||
use codex_rmcp_client::ElicitationAction;
|
use codex_rmcp_client::ElicitationAction;
|
||||||
use codex_rmcp_client::ElicitationResponse;
|
use codex_rmcp_client::ElicitationResponse;
|
||||||
@@ -273,6 +274,10 @@ async fn verify_request_plugin_install_completed(
|
|||||||
verified_connector_install_completed(connector.id.as_str(), &accessible_connectors)
|
verified_connector_install_completed(connector.id.as_str(), &accessible_connectors)
|
||||||
}),
|
}),
|
||||||
DiscoverableTool::Plugin(plugin) => {
|
DiscoverableTool::Plugin(plugin) => {
|
||||||
|
if is_remote_plugin_install_suggestion(&plugin.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
session.reload_user_config_layer().await;
|
session.reload_user_config_layer().await;
|
||||||
let config = session.get_config().await;
|
let config = session.get_config().await;
|
||||||
let completed = verified_plugin_install_completed(
|
let completed = verified_plugin_install_completed(
|
||||||
@@ -293,6 +298,12 @@ async fn verify_request_plugin_install_completed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_remote_plugin_install_suggestion(plugin_id: &str) -> bool {
|
||||||
|
plugin_id
|
||||||
|
.rsplit_once('@')
|
||||||
|
.is_some_and(|(_, marketplace_name)| marketplace_name == REMOTE_GLOBAL_MARKETPLACE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
#[expect(
|
#[expect(
|
||||||
clippy::await_holding_invalid_type,
|
clippy::await_holding_invalid_type,
|
||||||
reason = "connector cache refresh reads through the session-owned manager guard"
|
reason = "connector cache refresh reads through the session-owned manager guard"
|
||||||
|
|||||||
@@ -57,6 +57,17 @@ async fn verified_plugin_install_completed_requires_installed_plugin() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remote_plugin_install_suggestions_skip_core_installed_verification() {
|
||||||
|
assert!(is_remote_plugin_install_suggestion(
|
||||||
|
"snowflake@openai-curated-remote"
|
||||||
|
));
|
||||||
|
assert!(!is_remote_plugin_install_suggestion(
|
||||||
|
"snowflake@openai-curated"
|
||||||
|
));
|
||||||
|
assert!(!is_remote_plugin_install_suggestion("Plugin_123"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_plugin_install_response_persists_only_decline_always_mode() {
|
fn request_plugin_install_response_persists_only_decline_always_mode() {
|
||||||
assert!(request_plugin_install_response_requests_persistent_disable(
|
assert!(request_plugin_install_response_requests_persistent_disable(
|
||||||
|
|||||||
Reference in new issue
Block a user