Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions codex-rs/core/src/plugins/discoverable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use tracing::warn;

use super::OPENAI_CURATED_MARKETPLACE_NAME;
use super::PluginCapabilitySummary;
use super::PluginReadRequest;
use super::PluginsManager;
use crate::config::Config;
use codex_config::types::ToolSuggestDiscoverableType;
Expand Down Expand Up @@ -47,6 +46,7 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
else {
return Ok(Vec::new());
};
let curated_marketplace_name = curated_marketplace.name;

let mut discoverable_plugins = Vec::<DiscoverablePluginInfo>::new();
for plugin in curated_marketplace.plugins {
Expand All @@ -58,17 +58,14 @@ pub(crate) fn list_tool_suggest_discoverable_plugins(
}

let plugin_id = plugin.id.clone();
let plugin_name = plugin.name.clone();

match plugins_manager.read_plugin_for_config(
match plugins_manager.read_plugin_detail_for_marketplace_plugin(
config,
&PluginReadRequest {
plugin_name,
marketplace_path: curated_marketplace.path.clone(),
},
&curated_marketplace_name,
plugin,
) {
Ok(plugin) => {
let plugin: PluginCapabilitySummary = plugin.plugin.into();
let plugin: PluginCapabilitySummary = plugin.into();
discoverable_plugins.push(DiscoverablePluginInfo {
id: plugin.config_name,
name: plugin.display_name,
Expand Down
60 changes: 60 additions & 0 deletions codex-rs/core/src/plugins/discoverable_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use codex_tools::DiscoverablePluginInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
use tracing::Level;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_test::internal::MockWriter;

#[tokio::test]
async fn list_tool_suggest_discoverable_plugins_returns_uninstalled_curated_plugins() {
Expand Down Expand Up @@ -140,3 +143,60 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }]
}]
);
}

#[tokio::test]
async fn list_tool_suggest_discoverable_plugins_does_not_reload_marketplace_per_plugin() {
let codex_home = tempdir().expect("tempdir should succeed");
let curated_root = crate::plugins::curated_plugins_repo_path(codex_home.path());
write_openai_curated_marketplace(
&curated_root,
&["slack", "build-ios-apps", "life-science-research"],
);
write_plugins_feature_config(codex_home.path());

let too_long_prompt = "x".repeat(129);
for plugin_name in ["build-ios-apps", "life-science-research"] {
write_file(
&curated_root.join(format!("plugins/{plugin_name}/.codex-plugin/plugin.json")),
&format!(
r#"{{
"name": "{plugin_name}",
"description": "Plugin that includes skills, MCP servers, and app connectors",
"interface": {{
"defaultPrompt": "{too_long_prompt}"
}}
}}"#
),
);
}

let config = load_plugins_config(codex_home.path()).await;
let buffer: &'static std::sync::Mutex<Vec<u8>> =
Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
let subscriber = tracing_subscriber::fmt()
.with_level(true)
.with_ansi(false)
.with_max_level(Level::WARN)
.with_span_events(FmtSpan::NONE)
.with_writer(MockWriter::new(buffer))
.finish();
let _guard = tracing::subscriber::set_default(subscriber);

let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config).unwrap();

assert_eq!(discoverable_plugins.len(), 1);
assert_eq!(discoverable_plugins[0].id, "slack@openai-curated");

let logs = String::from_utf8(buffer.lock().expect("buffer lock").clone()).expect("utf8 logs");
assert_eq!(logs.matches("ignoring interface.defaultPrompt").count(), 2);
assert_eq!(
logs.matches("build-ios-apps/.codex-plugin/plugin.json")
.count(),
1
);
assert_eq!(
logs.matches("life-science-research/.codex-plugin/plugin.json")
.count(),
1
);
}
86 changes: 58 additions & 28 deletions codex-rs/core/src/plugins/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,20 +952,58 @@ impl PluginsManager {
marketplace_name,
});
};
if !self.restriction_product_matches(plugin.policy.products.as_deref()) {
return Err(MarketplaceError::PluginNotFound {
plugin_name: request.plugin_name.clone(),
marketplace_name,
});
}

let plugin_id = PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err(
|err| match err {
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
},
)?;
let plugin_key = plugin_id.as_key();
let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);
let plugin = self.read_plugin_detail_for_marketplace_plugin(
config,
&marketplace.name,
ConfiguredMarketplacePlugin {
id: plugin_key.clone(),
name: plugin.name,
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
installed: installed_plugins.contains(&plugin_key),
enabled: enabled_plugins.contains(&plugin_key),
},
)?;

Ok(PluginReadOutcome {
marketplace_name: if marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME {
OPENAI_CURATED_MARKETPLACE_DISPLAY_NAME.to_string()
} else {
marketplace.name
},
marketplace_path: marketplace.path,
plugin,
})
}

pub(crate) fn read_plugin_detail_for_marketplace_plugin(
&self,
config: &Config,
marketplace_name: &str,
plugin: ConfiguredMarketplacePlugin,
) -> Result<PluginDetail, MarketplaceError> {
if !self.restriction_product_matches(plugin.policy.products.as_deref()) {
return Err(MarketplaceError::PluginNotFound {
plugin_name: plugin.name,
marketplace_name: marketplace_name.to_string(),
});
}

let plugin_id =
PluginId::new(plugin.name.clone(), marketplace_name.to_string()).map_err(|err| {
match err {
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
}
})?;
let plugin_key = plugin_id.as_key();
let source_path = match &plugin.source {
MarketplacePluginSource::Local { path } => path.clone(),
};
Expand Down Expand Up @@ -1001,27 +1039,19 @@ impl PluginsManager {
mcp_server_names.sort_unstable();
mcp_server_names.dedup();

Ok(PluginReadOutcome {
marketplace_name: if marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME {
OPENAI_CURATED_MARKETPLACE_DISPLAY_NAME.to_string()
} else {
marketplace.name
},
marketplace_path: marketplace.path,
plugin: PluginDetail {
id: plugin_key.clone(),
name: plugin.name,
description,
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
installed: installed_plugins.contains(&plugin_key),
enabled: enabled_plugins.contains(&plugin_key),
skills: resolved_skills.skills,
disabled_skill_paths: resolved_skills.disabled_skill_paths,
apps,
mcp_server_names,
},
Ok(PluginDetail {
id: plugin_key,
name: plugin.name,
description,
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
installed: plugin.installed,
enabled: plugin.enabled,
skills: resolved_skills.skills,
disabled_skill_paths: resolved_skills.disabled_skill_paths,
apps,
mcp_server_names,
})
}

Expand Down
Loading