Compare commits

...

3 Commits

Author SHA1 Message Date
Xin Lin
a73219ed5f Cleanup remote.rs 2026-04-15 22:14:29 -07:00
Xin Lin
4f4a7485d8 Clean up plugin re-exports
Co-authored-by: Codex <noreply@openai.com>
2026-04-15 22:03:58 -07:00
Xin Lin
dc8b2ef0b2 Extract plugin loading and marketplace logic into codex-core-plugins
Co-authored-by: Codex <noreply@openai.com>
2026-04-15 21:54:57 -07:00
24 changed files with 1025 additions and 892 deletions

29
codex-rs/Cargo.lock generated
View File

@@ -1446,6 +1446,7 @@ dependencies = [
"codex-cloud-requirements",
"codex-config",
"codex-core",
"codex-core-plugins",
"codex-exec-server",
"codex-features",
"codex-feedback",
@@ -1913,6 +1914,7 @@ dependencies = [
"codex-code-mode",
"codex-config",
"codex-connectors",
"codex-core-plugins",
"codex-core-skills",
"codex-exec-server",
"codex-execpolicy",
@@ -2013,6 +2015,33 @@ dependencies = [
"zstd 0.13.3",
]
[[package]]
name = "codex-core-plugins"
version = "0.0.0"
dependencies = [
"codex-app-server-protocol",
"codex-config",
"codex-core-skills",
"codex-exec-server",
"codex-git-utils",
"codex-login",
"codex-plugin",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-plugins",
"dirs",
"pretty_assertions",
"reqwest",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.18",
"tokio",
"toml 0.9.11+spec-1.1.0",
"tracing",
"url",
]
[[package]]
name = "codex-core-skills"
version = "0.0.0"

View File

@@ -28,6 +28,7 @@ members = [
"shell-escalation",
"skills",
"core",
"core-plugins",
"core-skills",
"hooks",
"instructions",
@@ -128,6 +129,7 @@ codex-code-mode = { path = "code-mode" }
codex-config = { path = "config" }
codex-connectors = { path = "connectors" }
codex-core = { path = "core" }
codex-core-plugins = { path = "core-plugins" }
codex-core-skills = { path = "core-skills" }
codex-exec = { path = "exec" }
codex-exec-server = { path = "exec-server" }

View File

@@ -34,6 +34,7 @@ codex-arg0 = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-config = { workspace = true }
codex-core = { workspace = true }
codex-core-plugins = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-git-utils = { workspace = true }

View File

@@ -232,22 +232,23 @@ use codex_core::find_thread_names_by_ids;
use codex_core::find_thread_path_by_id_str;
use codex_core::path_utils;
use codex_core::plugins::MarketplaceAddError;
use codex_core::plugins::MarketplaceError;
use codex_core::plugins::MarketplacePluginSource;
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
use codex_core::plugins::PluginInstallRequest;
use codex_core::plugins::PluginReadRequest;
use codex_core::plugins::PluginUninstallError as CorePluginUninstallError;
use codex_core::plugins::add_marketplace as add_marketplace_to_codex_home;
use codex_core::plugins::load_plugin_apps;
use codex_core::plugins::load_plugin_mcp_servers;
use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode;
use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
use codex_core_plugins::loader::load_plugin_apps;
use codex_core_plugins::loader::load_plugin_mcp_servers;
use codex_core_plugins::manifest::PluginManifestInterface;
use codex_core_plugins::marketplace::MarketplaceError;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_exec_server::LOCAL_FS;
use codex_features::FEATURES;
use codex_features::Feature;
@@ -8834,9 +8835,7 @@ fn plugin_skills_to_info(
.collect()
}
fn plugin_interface_to_info(
interface: codex_core::plugins::PluginManifestInterface,
) -> PluginInterface {
fn plugin_interface_to_info(interface: PluginManifestInterface) -> PluginInterface {
PluginInterface {
display_name: interface.display_name,
short_description: interface.short_description,

View File

@@ -27,8 +27,8 @@ use codex_core::config_loader::LoaderOverrides;
use codex_core::config_loader::ResidencyRequirement as CoreResidencyRequirement;
use codex_core::config_loader::SandboxModeRequirement as CoreSandboxModeRequirement;
use codex_core::plugins::PluginId;
use codex_core::plugins::collect_plugin_enabled_candidates;
use codex_core::plugins::installed_plugin_telemetry_metadata;
use codex_core_plugins::loader::installed_plugin_telemetry_metadata;
use codex_core_plugins::toggles::collect_plugin_enabled_candidates;
use codex_features::canonical_feature_for_key;
use codex_features::feature_for_key;
use codex_protocol::config_types::WebSearchMode;

View File

@@ -0,0 +1,15 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "core-plugins",
crate_name = "codex_core_plugins",
compile_data = glob(
include = ["**"],
exclude = [
"**/* *",
"BUILD.bazel",
"Cargo.toml",
],
allow_empty = True,
),
)

View File

@@ -0,0 +1,39 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-core-plugins"
version.workspace = true
[lib]
doctest = false
name = "codex_core_plugins"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
codex-app-server-protocol = { workspace = true }
codex-config = { workspace = true }
codex-core-skills = { workspace = true }
codex-exec-server = { workspace = true }
codex-git-utils = { workspace = true }
codex-login = { workspace = true }
codex-plugin = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-plugins = { workspace = true }
dirs = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
toml = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }

View File

@@ -0,0 +1,6 @@
pub mod loader;
pub mod manifest;
pub mod marketplace;
pub mod remote;
pub mod store;
pub mod toggles;

View File

@@ -0,0 +1,807 @@
use crate::manifest::PluginManifestPaths;
use crate::manifest::load_plugin_manifest;
use crate::marketplace::MarketplacePluginSource;
use crate::marketplace::list_marketplaces;
use crate::marketplace::load_marketplace;
use crate::store::PluginStore;
use crate::store::plugin_version_for_source;
use codex_config::ConfigLayerStack;
use codex_config::types::McpServerConfig;
use codex_config::types::PluginConfig;
use codex_core_skills::SkillMetadata;
use codex_core_skills::config_rules::SkillConfigRules;
use codex_core_skills::config_rules::resolve_disabled_skill_paths;
use codex_core_skills::config_rules::skill_config_rules_from_stack;
use codex_core_skills::loader::SkillRoot;
use codex_core_skills::loader::load_skills_from_roots;
use codex_exec_server::LOCAL_FS;
use codex_plugin::AppConnectorId;
use codex_plugin::LoadedPlugin;
use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginId;
use codex_plugin::PluginIdError;
use codex_plugin::PluginLoadOutcome;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde_json::Map as JsonMap;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use tracing::warn;
const DEFAULT_SKILLS_DIR_NAME: &str = "skills";
const DEFAULT_MCP_CONFIG_FILE: &str = ".mcp.json";
const DEFAULT_APP_CONFIG_FILE: &str = ".app.json";
const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated";
const CONFIG_TOML_FILE: &str = "config.toml";
pub fn log_plugin_load_errors(outcome: &PluginLoadOutcome<McpServerConfig>) {
for plugin in outcome
.plugins()
.iter()
.filter(|plugin| plugin.error.is_some())
{
if let Some(error) = plugin.error.as_deref() {
warn!(
plugin = plugin.config_name,
path = %plugin.root.display(),
"failed to load plugin: {error}"
);
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginMcpFile {
#[serde(default)]
mcp_servers: HashMap<String, JsonValue>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginAppFile {
#[serde(default)]
apps: HashMap<String, PluginAppConfig>,
}
#[derive(Debug, Default, Deserialize)]
struct PluginAppConfig {
id: String,
}
pub async fn load_plugins_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
store: &PluginStore,
restriction_product: Option<Product>,
) -> PluginLoadOutcome<McpServerConfig> {
let skill_config_rules = skill_config_rules_from_stack(config_layer_stack);
let mut configured_plugins: Vec<_> = configured_plugins_from_stack(config_layer_stack)
.into_iter()
.collect();
configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let mut plugins = Vec::with_capacity(configured_plugins.len());
let mut seen_mcp_server_names = HashMap::<String, String>::new();
for (configured_name, plugin) in configured_plugins {
let loaded_plugin = load_plugin(
configured_name.clone(),
&plugin,
store,
restriction_product,
&skill_config_rules,
)
.await;
for name in loaded_plugin.mcp_servers.keys() {
if let Some(previous_plugin) =
seen_mcp_server_names.insert(name.clone(), configured_name.clone())
{
warn!(
plugin = configured_name,
previous_plugin,
server = name,
"skipping duplicate plugin MCP server name"
);
}
}
plugins.push(loaded_plugin);
}
PluginLoadOutcome::from_plugins(plugins)
}
pub fn refresh_curated_plugin_cache(
codex_home: &Path,
plugin_version: &str,
configured_curated_plugin_ids: &[PluginId],
) -> Result<bool, String> {
let store = PluginStore::new(codex_home.to_path_buf());
let curated_marketplace_path = AbsolutePathBuf::try_from(
codex_home
.join(".tmp/plugins")
.join(".agents/plugins/marketplace.json"),
)
.map_err(|_| "local curated marketplace is not available".to_string())?;
let curated_marketplace = load_marketplace(&curated_marketplace_path)
.map_err(|err| format!("failed to load curated marketplace for cache refresh: {err}"))?;
let mut plugin_sources = HashMap::<String, AbsolutePathBuf>::new();
for plugin in curated_marketplace.plugins {
let plugin_name = plugin.name;
if plugin_sources.contains_key(&plugin_name) {
warn!(
plugin = plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
"ignoring duplicate curated plugin entry during cache refresh"
);
continue;
}
let source_path = match plugin.source {
MarketplacePluginSource::Local { path } => path,
};
plugin_sources.insert(plugin_name, source_path);
}
let mut cache_refreshed = false;
for plugin_id in configured_curated_plugin_ids {
if store.active_plugin_version(plugin_id).as_deref() == Some(plugin_version) {
continue;
}
let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else {
warn!(
plugin = plugin_id.plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
"configured curated plugin no longer exists in curated marketplace during cache refresh"
);
continue;
};
store
.install_with_version(source_path, plugin_id.clone(), plugin_version.to_string())
.map_err(|err| {
format!(
"failed to refresh curated plugin cache for {}: {err}",
plugin_id.as_key()
)
})?;
cache_refreshed = true;
}
Ok(cache_refreshed)
}
pub fn refresh_non_curated_plugin_cache(
codex_home: &Path,
additional_roots: &[AbsolutePathBuf],
) -> Result<bool, String> {
let configured_non_curated_plugin_ids =
non_curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home(
codex_home,
"failed to read user config while refreshing non-curated plugin cache",
"failed to parse user config while refreshing non-curated plugin cache",
));
if configured_non_curated_plugin_ids.is_empty() {
return Ok(false);
}
let configured_non_curated_plugin_keys = configured_non_curated_plugin_ids
.iter()
.map(PluginId::as_key)
.collect::<HashSet<_>>();
let store = PluginStore::new(codex_home.to_path_buf());
let marketplace_outcome = list_marketplaces(additional_roots)
.map_err(|err| format!("failed to discover marketplaces for cache refresh: {err}"))?;
let mut plugin_sources = HashMap::<String, (AbsolutePathBuf, String)>::new();
for marketplace in marketplace_outcome.marketplaces {
if marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME {
continue;
}
for plugin in marketplace.plugins {
let plugin_id =
PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err(|err| {
match err {
PluginIdError::Invalid(message) => {
format!("failed to prepare non-curated plugin cache refresh: {message}")
}
}
})?;
let plugin_key = plugin_id.as_key();
if !configured_non_curated_plugin_keys.contains(&plugin_key) {
continue;
}
if plugin_sources.contains_key(&plugin_key) {
warn!(
plugin = plugin.name,
marketplace = marketplace.name,
"ignoring duplicate non-curated plugin entry during cache refresh"
);
continue;
}
let source_path = match plugin.source {
MarketplacePluginSource::Local { path } => path,
};
let plugin_version = plugin_version_for_source(source_path.as_path())
.map_err(|err| format!("failed to read plugin version for {plugin_key}: {err}"))?;
plugin_sources.insert(plugin_key, (source_path, plugin_version));
}
}
let mut cache_refreshed = false;
for plugin_id in configured_non_curated_plugin_ids {
let plugin_key = plugin_id.as_key();
let Some((source_path, plugin_version)) = plugin_sources.get(&plugin_key).cloned() else {
warn!(
plugin = plugin_id.plugin_name,
marketplace = plugin_id.marketplace_name,
"configured non-curated plugin no longer exists in discovered marketplaces during cache refresh"
);
continue;
};
if store.active_plugin_version(&plugin_id).as_deref() == Some(plugin_version.as_str()) {
continue;
}
store
.install_with_version(source_path, plugin_id.clone(), plugin_version)
.map_err(|err| format!("failed to refresh plugin cache for {plugin_key}: {err}"))?;
cache_refreshed = true;
}
Ok(cache_refreshed)
}
fn configured_plugins_from_stack(
config_layer_stack: &ConfigLayerStack,
) -> HashMap<String, PluginConfig> {
let Some(user_layer) = config_layer_stack.get_user_layer() else {
return HashMap::new();
};
configured_plugins_from_user_config_value(&user_layer.config)
}
fn configured_plugins_from_user_config_value(
user_config: &toml::Value,
) -> HashMap<String, PluginConfig> {
let Some(plugins_value) = user_config.get("plugins") else {
return HashMap::new();
};
match plugins_value.clone().try_into() {
Ok(plugins) => plugins,
Err(err) => {
warn!("invalid plugins config: {err}");
HashMap::new()
}
}
}
fn configured_plugins_from_codex_home(
codex_home: &Path,
read_error_message: &str,
parse_error_message: &str,
) -> HashMap<String, PluginConfig> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let user_config = match fs::read_to_string(&config_path) {
Ok(user_config) => user_config,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return HashMap::new(),
Err(err) => {
warn!(
path = %config_path.display(),
error = %err,
"{read_error_message}"
);
return HashMap::new();
}
};
let user_config = match toml::from_str::<toml::Value>(&user_config) {
Ok(user_config) => user_config,
Err(err) => {
warn!(
path = %config_path.display(),
error = %err,
"{parse_error_message}"
);
return HashMap::new();
}
};
configured_plugins_from_user_config_value(&user_config)
}
fn configured_plugin_ids(
configured_plugins: HashMap<String, PluginConfig>,
invalid_plugin_key_message: &str,
) -> Vec<PluginId> {
configured_plugins
.into_keys()
.filter_map(|plugin_key| match PluginId::parse(&plugin_key) {
Ok(plugin_id) => Some(plugin_id),
Err(err) => {
warn!(
plugin_key,
error = %err,
"{invalid_plugin_key_message}"
);
None
}
})
.collect()
}
fn curated_plugin_ids_from_config_keys(
configured_plugins: HashMap<String, PluginConfig>,
) -> Vec<PluginId> {
let mut configured_curated_plugin_ids = configured_plugin_ids(
configured_plugins,
"ignoring invalid configured plugin key during curated sync setup",
)
.into_iter()
.filter(|plugin_id| plugin_id.marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME)
.collect::<Vec<_>>();
configured_curated_plugin_ids.sort_unstable_by_key(PluginId::as_key);
configured_curated_plugin_ids
}
fn non_curated_plugin_ids_from_config_keys(
configured_plugins: HashMap<String, PluginConfig>,
) -> Vec<PluginId> {
let mut configured_non_curated_plugin_ids = configured_plugin_ids(
configured_plugins,
"ignoring invalid plugin key during non-curated cache refresh setup",
)
.into_iter()
.filter(|plugin_id| plugin_id.marketplace_name != OPENAI_CURATED_MARKETPLACE_NAME)
.collect::<Vec<_>>();
configured_non_curated_plugin_ids.sort_unstable_by_key(PluginId::as_key);
configured_non_curated_plugin_ids
}
pub fn configured_curated_plugin_ids_from_codex_home(codex_home: &Path) -> Vec<PluginId> {
curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home(
codex_home,
"failed to read user config while refreshing curated plugin cache",
"failed to parse user config while refreshing curated plugin cache",
))
}
async fn load_plugin(
config_name: String,
plugin: &PluginConfig,
store: &PluginStore,
restriction_product: Option<Product>,
skill_config_rules: &SkillConfigRules,
) -> LoadedPlugin<McpServerConfig> {
let plugin_id = PluginId::parse(&config_name);
let active_plugin_root = plugin_id
.as_ref()
.ok()
.and_then(|plugin_id| store.active_plugin_root(plugin_id));
let root = active_plugin_root
.clone()
.unwrap_or_else(|| match &plugin_id {
Ok(plugin_id) => store.plugin_base_root(plugin_id),
Err(_) => store.root().clone(),
});
let mut loaded_plugin = LoadedPlugin {
config_name,
manifest_name: None,
manifest_description: None,
root,
enabled: plugin.enabled,
skill_roots: Vec::new(),
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
apps: Vec::new(),
error: None,
};
if !plugin.enabled {
return loaded_plugin;
}
let plugin_root = match plugin_id {
Ok(_) => match active_plugin_root {
Some(plugin_root) => plugin_root,
None => {
loaded_plugin.error = Some("plugin is not installed".to_string());
return loaded_plugin;
}
},
Err(err) => {
loaded_plugin.error = Some(err.to_string());
return loaded_plugin;
}
};
if !plugin_root.as_path().is_dir() {
loaded_plugin.error = Some("path does not exist or is not a directory".to_string());
return loaded_plugin;
}
let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else {
loaded_plugin.error = Some("missing or invalid .codex-plugin/plugin.json".to_string());
return loaded_plugin;
};
let manifest_paths = &manifest.paths;
loaded_plugin.manifest_name = manifest
.interface
.as_ref()
.and_then(|interface| interface.display_name.as_deref())
.map(str::trim)
.filter(|display_name| !display_name.is_empty())
.map(str::to_string)
.or_else(|| Some(manifest.name.clone()));
loaded_plugin.manifest_description = manifest.description.clone();
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
let resolved_skills = load_plugin_skills(
&plugin_root,
manifest_paths,
restriction_product,
skill_config_rules,
)
.await;
let has_enabled_skills = resolved_skills.has_enabled_skills();
loaded_plugin.disabled_skill_paths = resolved_skills.disabled_skill_paths;
loaded_plugin.has_enabled_skills = has_enabled_skills;
let mut mcp_servers = HashMap::new();
for mcp_config_path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root.as_path(), &mcp_config_path).await;
for (name, config) in plugin_mcp.mcp_servers {
if mcp_servers.insert(name.clone(), config).is_some() {
warn!(
plugin = %plugin_root.display(),
path = %mcp_config_path.display(),
server = name,
"plugin MCP file overwrote an earlier server definition"
);
}
}
}
loaded_plugin.mcp_servers = mcp_servers;
loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()).await;
loaded_plugin
}
#[derive(Debug, Clone)]
pub struct ResolvedPluginSkills {
pub skills: Vec<SkillMetadata>,
pub disabled_skill_paths: HashSet<AbsolutePathBuf>,
pub had_errors: bool,
}
impl ResolvedPluginSkills {
pub fn has_enabled_skills(&self) -> bool {
self.had_errors
|| self
.skills
.iter()
.any(|skill| !self.disabled_skill_paths.contains(&skill.path_to_skills_md))
}
}
pub async fn load_plugin_skills(
plugin_root: &AbsolutePathBuf,
manifest_paths: &PluginManifestPaths,
restriction_product: Option<Product>,
skill_config_rules: &SkillConfigRules,
) -> ResolvedPluginSkills {
let roots = plugin_skill_roots(plugin_root, manifest_paths)
.into_iter()
.map(|path| SkillRoot {
path,
scope: SkillScope::User,
file_system: Arc::clone(&LOCAL_FS),
})
.collect::<Vec<_>>();
let outcome = load_skills_from_roots(roots).await;
let had_errors = !outcome.errors.is_empty();
let skills = outcome
.skills
.into_iter()
.filter(|skill| skill.matches_product_restriction_for_product(restriction_product))
.collect::<Vec<_>>();
let disabled_skill_paths = resolve_disabled_skill_paths(&skills, skill_config_rules);
ResolvedPluginSkills {
skills,
disabled_skill_paths,
had_errors,
}
}
fn plugin_skill_roots(
plugin_root: &AbsolutePathBuf,
manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf> {
let mut paths = default_skill_roots(plugin_root);
if let Some(path) = &manifest_paths.skills {
paths.push(path.clone());
}
paths.sort_unstable();
paths.dedup();
paths
}
fn default_skill_roots(plugin_root: &AbsolutePathBuf) -> Vec<AbsolutePathBuf> {
let skills_dir = plugin_root.join(DEFAULT_SKILLS_DIR_NAME);
if skills_dir.is_dir() {
vec![skills_dir]
} else {
Vec::new()
}
}
fn plugin_mcp_config_paths(
plugin_root: &Path,
manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf> {
if let Some(path) = &manifest_paths.mcp_servers {
return vec![path.clone()];
}
default_mcp_config_paths(plugin_root)
}
fn default_mcp_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
let mut paths = Vec::new();
let default_path = plugin_root.join(DEFAULT_MCP_CONFIG_FILE);
if default_path.is_file()
&& let Ok(default_path) = AbsolutePathBuf::try_from(default_path)
{
paths.push(default_path);
}
paths.sort_unstable_by(|left, right| left.as_path().cmp(right.as_path()));
paths.dedup_by(|left, right| left.as_path() == right.as_path());
paths
}
pub async fn load_plugin_apps(plugin_root: &Path) -> Vec<AppConnectorId> {
if let Some(manifest) = load_plugin_manifest(plugin_root) {
return load_apps_from_paths(
plugin_root,
plugin_app_config_paths(plugin_root, &manifest.paths),
)
.await;
}
load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await
}
fn plugin_app_config_paths(
plugin_root: &Path,
manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf> {
if let Some(path) = &manifest_paths.apps {
return vec![path.clone()];
}
default_app_config_paths(plugin_root)
}
fn default_app_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
let mut paths = Vec::new();
let default_path = plugin_root.join(DEFAULT_APP_CONFIG_FILE);
if default_path.is_file()
&& let Ok(default_path) = AbsolutePathBuf::try_from(default_path)
{
paths.push(default_path);
}
paths.sort_unstable_by(|left, right| left.as_path().cmp(right.as_path()));
paths.dedup_by(|left, right| left.as_path() == right.as_path());
paths
}
async fn load_apps_from_paths(
plugin_root: &Path,
app_config_paths: Vec<AbsolutePathBuf>,
) -> Vec<AppConnectorId> {
let mut connector_ids = Vec::new();
for app_config_path in app_config_paths {
let Ok(contents) = tokio::fs::read_to_string(app_config_path.as_path()).await else {
continue;
};
let parsed = match serde_json::from_str::<PluginAppFile>(&contents) {
Ok(parsed) => parsed,
Err(err) => {
warn!(
path = %app_config_path.display(),
"failed to parse plugin app config: {err}"
);
continue;
}
};
let mut apps: Vec<PluginAppConfig> = parsed.apps.into_values().collect();
apps.sort_unstable_by(|left, right| left.id.cmp(&right.id));
connector_ids.extend(apps.into_iter().filter_map(|app| {
if app.id.trim().is_empty() {
warn!(
plugin = %plugin_root.display(),
"plugin app config is missing an app id"
);
None
} else {
Some(AppConnectorId(app.id))
}
}));
}
connector_ids.dedup();
connector_ids
}
pub async fn plugin_telemetry_metadata_from_root(
plugin_id: &PluginId,
plugin_root: &AbsolutePathBuf,
) -> PluginTelemetryMetadata {
let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else {
return PluginTelemetryMetadata::from_plugin_id(plugin_id);
};
let manifest_paths = &manifest.paths;
let has_skills = !plugin_skill_roots(plugin_root, manifest_paths).is_empty();
let mut mcp_server_names = Vec::new();
for path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) {
mcp_server_names.extend(
load_mcp_servers_from_file(plugin_root.as_path(), &path)
.await
.mcp_servers
.into_keys(),
);
}
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
PluginTelemetryMetadata {
plugin_id: plugin_id.clone(),
capability_summary: Some(PluginCapabilitySummary {
config_name: plugin_id.as_key(),
display_name: plugin_id.plugin_name.clone(),
description: None,
has_skills,
mcp_server_names,
app_connector_ids: load_apps_from_paths(
plugin_root.as_path(),
plugin_app_config_paths(plugin_root.as_path(), manifest_paths),
)
.await,
}),
}
}
pub async fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap<String, McpServerConfig> {
let Some(manifest) = load_plugin_manifest(plugin_root) else {
return HashMap::new();
};
let mut mcp_servers = HashMap::new();
for mcp_config_path in plugin_mcp_config_paths(plugin_root, &manifest.paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path).await;
for (name, config) in plugin_mcp.mcp_servers {
mcp_servers.entry(name).or_insert(config);
}
}
mcp_servers
}
pub async fn installed_plugin_telemetry_metadata(
codex_home: &Path,
plugin_id: &PluginId,
) -> PluginTelemetryMetadata {
let store = PluginStore::new(codex_home.to_path_buf());
let Some(plugin_root) = store.active_plugin_root(plugin_id) else {
return PluginTelemetryMetadata::from_plugin_id(plugin_id);
};
plugin_telemetry_metadata_from_root(plugin_id, &plugin_root).await
}
async fn load_mcp_servers_from_file(
plugin_root: &Path,
mcp_config_path: &AbsolutePathBuf,
) -> PluginMcpDiscovery {
let Ok(contents) = tokio::fs::read_to_string(mcp_config_path.as_path()).await else {
return PluginMcpDiscovery::default();
};
let parsed = match serde_json::from_str::<PluginMcpFile>(&contents) {
Ok(parsed) => parsed,
Err(err) => {
warn!(
path = %mcp_config_path.display(),
"failed to parse plugin MCP config: {err}"
);
return PluginMcpDiscovery::default();
}
};
normalize_plugin_mcp_servers(
plugin_root,
parsed.mcp_servers,
mcp_config_path.to_string_lossy().as_ref(),
)
}
fn normalize_plugin_mcp_servers(
plugin_root: &Path,
plugin_mcp_servers: HashMap<String, JsonValue>,
source: &str,
) -> PluginMcpDiscovery {
let mut mcp_servers = HashMap::new();
for (name, config_value) in plugin_mcp_servers {
let normalized = normalize_plugin_mcp_server_value(plugin_root, config_value);
match serde_json::from_value::<McpServerConfig>(JsonValue::Object(normalized)) {
Ok(config) => {
mcp_servers.insert(name, config);
}
Err(err) => {
warn!(
plugin = %plugin_root.display(),
server = name,
"failed to parse plugin MCP server from {source}: {err}"
);
}
}
}
PluginMcpDiscovery { mcp_servers }
}
fn normalize_plugin_mcp_server_value(
plugin_root: &Path,
value: JsonValue,
) -> JsonMap<String, JsonValue> {
let mut object = match value {
JsonValue::Object(object) => object,
_ => return JsonMap::new(),
};
if let Some(JsonValue::String(transport_type)) = object.remove("type") {
match transport_type.as_str() {
"http" | "streamable_http" | "streamable-http" => {}
"stdio" => {}
other => {
warn!(
plugin = %plugin_root.display(),
transport = other,
"plugin MCP server uses an unknown transport type"
);
}
}
}
if let Some(JsonValue::Object(oauth)) = object.remove("oauth")
&& oauth.contains_key("callbackPort")
{
warn!(
plugin = %plugin_root.display(),
"plugin MCP server OAuth callbackPort is ignored; Codex uses global MCP OAuth callback settings"
);
}
if let Some(JsonValue::String(cwd)) = object.get("cwd")
&& !Path::new(cwd).is_absolute()
{
object.insert(
"cwd".to_string(),
JsonValue::String(plugin_root.join(cwd).display().to_string()),
);
}
object
}
#[derive(Debug, Default)]
struct PluginMcpDiscovery {
mcp_servers: HashMap<String, McpServerConfig>,
}

View File

@@ -30,12 +30,12 @@ struct RawPluginManifest {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PluginManifest {
pub(crate) name: String,
pub(crate) version: Option<String>,
pub(crate) description: Option<String>,
pub(crate) paths: PluginManifestPaths,
pub(crate) interface: Option<PluginManifestInterface>,
pub struct PluginManifest {
pub name: String,
pub version: Option<String>,
pub description: Option<String>,
pub paths: PluginManifestPaths,
pub interface: Option<PluginManifestInterface>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -114,7 +114,7 @@ enum RawPluginManifestDefaultPromptEntry {
Invalid(JsonValue),
}
pub(crate) fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
let manifest_path = plugin_root.join(PLUGIN_MANIFEST_PATH);
if !manifest_path.is_file() {
return None;

View File

@@ -1,5 +1,5 @@
use super::PluginManifestInterface;
use super::load_plugin_manifest;
use crate::manifest::PluginManifestInterface;
use crate::manifest::load_plugin_manifest;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_git_utils::get_git_repo_root;
@@ -231,7 +231,7 @@ pub fn validate_marketplace_root(root: &Path) -> Result<String, MarketplaceError
Ok(marketplace.name)
}
pub(crate) fn find_marketplace_manifest_path(root: &Path) -> Option<AbsolutePathBuf> {
pub fn find_marketplace_manifest_path(root: &Path) -> Option<AbsolutePathBuf> {
MARKETPLACE_MANIFEST_RELATIVE_PATHS
.iter()
.find_map(|relative_path| {
@@ -265,7 +265,7 @@ fn marketplace_root_from_layout(marketplace_path: &Path, relative_path: &str) ->
Some(current.to_path_buf())
}
pub(crate) fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, MarketplaceError> {
pub fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, MarketplaceError> {
let marketplace = load_raw_marketplace_manifest(path)?;
let mut plugins = Vec::new();
@@ -311,7 +311,8 @@ pub(crate) fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, Ma
})
}
fn list_marketplaces_with_home(
#[doc(hidden)]
pub fn list_marketplaces_with_home(
additional_roots: &[AbsolutePathBuf],
home_dir: Option<&Path>,
) -> Result<MarketplaceListOutcome, MarketplaceError> {

View File

@@ -1,4 +1,3 @@
use crate::config::Config;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_protocol::protocol::Product;
@@ -11,12 +10,17 @@ const REMOTE_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(30);
const REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(10);
const REMOTE_PLUGIN_MUTATION_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemotePluginServiceConfig {
pub chatgpt_base_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct RemotePluginStatusSummary {
pub(crate) name: String,
pub struct RemotePluginStatusSummary {
pub name: String,
#[serde(default = "default_remote_marketplace_name")]
pub(crate) marketplace_name: String,
pub(crate) enabled: bool,
pub marketplace_name: String,
pub enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -116,8 +120,8 @@ pub enum RemotePluginFetchError {
},
}
pub(crate) async fn fetch_remote_plugin_status(
config: &Config,
pub async fn fetch_remote_plugin_status(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
) -> Result<Vec<RemotePluginStatusSummary>, RemotePluginFetchError> {
let Some(auth) = auth else {
@@ -161,7 +165,7 @@ pub(crate) async fn fetch_remote_plugin_status(
}
pub async fn fetch_remote_featured_plugin_ids(
config: &Config,
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
product: Option<Product>,
) -> Result<Vec<String>, RemotePluginFetchError> {
@@ -205,8 +209,8 @@ pub async fn fetch_remote_featured_plugin_ids(
})
}
pub(crate) async fn enable_remote_plugin(
config: &Config,
pub async fn enable_remote_plugin(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
plugin_id: &str,
) -> Result<(), RemotePluginMutationError> {
@@ -214,8 +218,8 @@ pub(crate) async fn enable_remote_plugin(
Ok(())
}
pub(crate) async fn uninstall_remote_plugin(
config: &Config,
pub async fn uninstall_remote_plugin(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
plugin_id: &str,
) -> Result<(), RemotePluginMutationError> {
@@ -238,7 +242,7 @@ fn default_remote_marketplace_name() -> String {
}
async fn post_remote_plugin_mutation(
config: &Config,
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
plugin_id: &str,
action: &str,
@@ -294,7 +298,7 @@ async fn post_remote_plugin_mutation(
}
fn remote_plugin_mutation_url(
config: &Config,
config: &RemotePluginServiceConfig,
plugin_id: &str,
action: &str,
) -> Result<String, RemotePluginMutationError> {

View File

@@ -1,5 +1,5 @@
use super::load_plugin_manifest;
use super::manifest::PluginManifest;
use crate::manifest::PluginManifest;
use crate::manifest::load_plugin_manifest;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -11,8 +11,8 @@ use std::io;
use std::path::Path;
use std::path::PathBuf;
pub(crate) const DEFAULT_PLUGIN_VERSION: &str = "local";
pub(crate) const PLUGINS_CACHE_DIR: &str = "plugins/cache";
pub const DEFAULT_PLUGIN_VERSION: &str = "local";
pub const PLUGINS_CACHE_DIR: &str = "plugins/cache";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginInstallResult {
@@ -157,7 +157,7 @@ impl PluginStoreError {
}
}
pub(crate) fn plugin_version_for_source(source_path: &Path) -> Result<String, PluginStoreError> {
pub fn plugin_version_for_source(source_path: &Path) -> Result<String, PluginStoreError> {
let plugin_version = plugin_manifest_version_for_source(source_path)?
.unwrap_or_else(|| DEFAULT_PLUGIN_VERSION.to_string());
validate_plugin_version_segment(&plugin_version).map_err(PluginStoreError::Invalid)?;

View File

@@ -33,6 +33,7 @@ codex-async-utils = { workspace = true }
codex-code-mode = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }
codex-core-plugins = { workspace = true }
codex-core-skills = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }

View File

@@ -98,9 +98,7 @@ pub(crate) use skills::build_skill_injections;
pub(crate) use skills::build_skill_name_counts;
pub(crate) use skills::collect_env_var_dependencies;
pub(crate) use skills::collect_explicit_skill_mentions;
pub(crate) use skills::config_rules;
pub(crate) use skills::injection;
pub(crate) use skills::loader;
pub(crate) use skills::manager;
pub(crate) use skills::maybe_emit_implicit_skill_invocation;
pub(crate) use skills::render_skills_section;

View File

@@ -1,10 +1,10 @@
use crate::config::Config;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use std::path::PathBuf;
use tracing::warn;
use super::marketplace::find_marketplace_manifest_path;
use super::validate_plugin_segment;
pub const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";

View File

@@ -1,52 +1,48 @@
use super::LoadedPlugin;
use super::PluginLoadOutcome;
use super::PluginManifestPaths;
use super::curated_plugins_repo_path;
use super::installed_marketplaces::installed_marketplace_roots_from_config;
use super::load_plugin_manifest;
use super::manifest::PluginManifestInterface;
use super::marketplace::MarketplaceError;
use super::marketplace::MarketplaceInterface;
use super::marketplace::MarketplaceListError;
use super::marketplace::MarketplacePluginAuthPolicy;
use super::marketplace::MarketplacePluginPolicy;
use super::marketplace::MarketplacePluginSource;
use super::marketplace::ResolvedMarketplacePlugin;
use super::marketplace::list_marketplaces;
use super::marketplace::load_marketplace;
use super::marketplace::resolve_marketplace_plugin;
use super::read_curated_plugins_sha;
use super::remote::RemotePluginFetchError;
use super::remote::RemotePluginMutationError;
use super::remote::enable_remote_plugin;
use super::remote::fetch_remote_featured_plugin_ids;
use super::remote::fetch_remote_plugin_status;
use super::remote::uninstall_remote_plugin;
use super::startup_sync::start_startup_remote_plugin_sync_once;
use super::store::PluginInstallResult as StorePluginInstallResult;
use super::store::PluginStore;
use super::store::PluginStoreError;
use super::store::plugin_version_for_source;
use super::sync_openai_plugins_repo;
use crate::SkillMetadata;
use crate::config::CONFIG_TOML_FILE;
use crate::config::Config;
use crate::config::ConfigService;
use crate::config::ConfigServiceError;
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use crate::config_loader::ConfigLayerStack;
use crate::config_rules::SkillConfigRules;
use crate::config_rules::resolve_disabled_skill_paths;
use crate::config_rules::skill_config_rules_from_stack;
use crate::loader::SkillRoot;
use crate::loader::load_skills_from_roots;
use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::MergeStrategy;
use codex_config::types::McpServerConfig;
use codex_config::types::PluginConfig;
use codex_exec_server::LOCAL_FS;
use codex_core_plugins::loader::configured_curated_plugin_ids_from_codex_home;
use codex_core_plugins::loader::installed_plugin_telemetry_metadata;
use codex_core_plugins::loader::load_plugin_apps;
use codex_core_plugins::loader::load_plugin_mcp_servers;
use codex_core_plugins::loader::load_plugin_skills;
use codex_core_plugins::loader::load_plugins_from_layer_stack;
use codex_core_plugins::loader::log_plugin_load_errors;
use codex_core_plugins::loader::plugin_telemetry_metadata_from_root;
use codex_core_plugins::loader::refresh_curated_plugin_cache;
use codex_core_plugins::loader::refresh_non_curated_plugin_cache;
use codex_core_plugins::manifest::PluginManifestInterface;
use codex_core_plugins::manifest::load_plugin_manifest;
use codex_core_plugins::marketplace::MarketplaceError;
use codex_core_plugins::marketplace::MarketplaceInterface;
use codex_core_plugins::marketplace::MarketplaceListError;
use codex_core_plugins::marketplace::MarketplacePluginAuthPolicy;
use codex_core_plugins::marketplace::MarketplacePluginPolicy;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_core_plugins::marketplace::ResolvedMarketplacePlugin;
use codex_core_plugins::marketplace::list_marketplaces;
use codex_core_plugins::marketplace::load_marketplace;
use codex_core_plugins::marketplace::resolve_marketplace_plugin;
use codex_core_plugins::remote::RemotePluginFetchError;
use codex_core_plugins::remote::RemotePluginMutationError;
use codex_core_plugins::remote::RemotePluginServiceConfig;
use codex_core_plugins::store::PluginInstallResult as StorePluginInstallResult;
use codex_core_plugins::store::PluginStore;
use codex_core_plugins::store::PluginStoreError;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_login::CodexAuth;
@@ -54,19 +50,12 @@ use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginId;
use codex_plugin::PluginIdError;
use codex_plugin::PluginTelemetryMetadata;
use codex_plugin::prompt_safe_plugin_description;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde_json::Map as JsonMap;
use serde_json::Value as JsonValue;
use serde_json::json;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
@@ -78,9 +67,6 @@ use toml_edit::value;
use tracing::info;
use tracing::warn;
const DEFAULT_SKILLS_DIR_NAME: &str = "skills";
const DEFAULT_MCP_CONFIG_FILE: &str = ".mcp.json";
const DEFAULT_APP_CONFIG_FILE: &str = ".app.json";
pub const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated";
pub const OPENAI_CURATED_MARKETPLACE_DISPLAY_NAME: &str = "OpenAI Curated";
static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false);
@@ -109,6 +95,12 @@ struct NonCuratedCacheRefreshState {
in_flight: bool,
}
fn remote_plugin_service_config(config: &Config) -> RemotePluginServiceConfig {
RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
}
}
fn featured_plugin_ids_cache_key(
config: &Config,
auth: Option<&CodexAuth>,
@@ -503,8 +495,12 @@ impl PluginsManager {
if let Some(featured_plugin_ids) = self.cached_featured_plugin_ids(&cache_key) {
return Ok(featured_plugin_ids);
}
let featured_plugin_ids =
fetch_remote_featured_plugin_ids(config, auth, self.restriction_product).await?;
let featured_plugin_ids = codex_core_plugins::remote::fetch_remote_featured_plugin_ids(
&remote_plugin_service_config(config),
auth,
self.restriction_product,
)
.await?;
self.write_featured_plugin_ids_cache(cache_key, &featured_plugin_ids);
Ok(featured_plugin_ids)
}
@@ -536,9 +532,13 @@ impl PluginsManager {
// This only forwards the backend mutation before the local install flow. We rely on
// `plugin/list(forceRemoteSync=true)` to sync local state rather than doing an extra
// reconcile pass here.
enable_remote_plugin(config, auth, &plugin_id)
.await
.map_err(PluginInstallError::from)?;
codex_core_plugins::remote::enable_remote_plugin(
&remote_plugin_service_config(config),
auth,
&plugin_id,
)
.await
.map_err(PluginInstallError::from)?;
self.install_resolved_plugin(resolved).await
}
@@ -619,9 +619,13 @@ impl PluginsManager {
// This only forwards the backend mutation before the local uninstall flow. We rely on
// `plugin/list(forceRemoteSync=true)` to sync local state rather than doing an extra
// reconcile pass here.
uninstall_remote_plugin(config, auth, &plugin_key)
.await
.map_err(PluginUninstallError::from)?;
codex_core_plugins::remote::uninstall_remote_plugin(
&remote_plugin_service_config(config),
auth,
&plugin_key,
)
.await
.map_err(PluginUninstallError::from)?;
self.uninstall_plugin_id(plugin_id).await
}
@@ -670,9 +674,12 @@ impl PluginsManager {
}
info!("starting remote plugin sync");
let remote_plugins = fetch_remote_plugin_status(config, auth)
.await
.map_err(PluginRemoteSyncError::from)?;
let remote_plugins = codex_core_plugins::remote::fetch_remote_plugin_status(
&remote_plugin_service_config(config),
auth,
)
.await
.map_err(PluginRemoteSyncError::from)?;
let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack);
let curated_marketplace_root = curated_plugins_repo_path(self.codex_home.as_path());
let curated_marketplace_path = AbsolutePathBuf::try_from(
@@ -1026,7 +1033,9 @@ impl PluginsManager {
})?;
let description = manifest.description.clone();
let manifest_paths = &manifest.paths;
let skill_config_rules = skill_config_rules_from_stack(&config.config_layer_stack);
let skill_config_rules = codex_core_skills::config_rules::skill_config_rules_from_stack(
&config.config_layer_stack,
);
let resolved_skills = load_plugin_skills(
&source_path,
manifest_paths,
@@ -1034,21 +1043,11 @@ impl PluginsManager {
&skill_config_rules,
)
.await;
let apps = load_apps_from_paths(
source_path.as_path(),
plugin_app_config_paths(source_path.as_path(), manifest_paths),
)
.await;
let mcp_config_paths = plugin_mcp_config_paths(source_path.as_path(), manifest_paths);
let mut mcp_server_names = Vec::new();
for mcp_config_path in mcp_config_paths {
mcp_server_names.extend(
load_mcp_servers_from_file(source_path.as_path(), &mcp_config_path)
.await
.mcp_servers
.into_keys(),
);
}
let apps = load_plugin_apps(source_path.as_path()).await;
let mut mcp_server_names = load_plugin_mcp_servers(source_path.as_path())
.await
.into_keys()
.collect::<Vec<_>>();
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
@@ -1160,13 +1159,8 @@ impl PluginsManager {
.spawn(
move || match sync_openai_plugins_repo(codex_home.as_path()) {
Ok(curated_plugin_version) => {
let configured_curated_plugin_ids = curated_plugin_ids_from_config_keys(
configured_plugins_from_codex_home(
codex_home.as_path(),
"failed to read user config while refreshing curated plugin cache",
"failed to parse user config while refreshing curated plugin cache",
),
);
let configured_curated_plugin_ids =
configured_curated_plugin_ids_from_codex_home(codex_home.as_path());
match refresh_curated_plugin_cache(
codex_home.as_path(),
&curated_plugin_version,
@@ -1352,224 +1346,6 @@ impl PluginUninstallError {
}
}
fn log_plugin_load_errors(outcome: &PluginLoadOutcome) {
for plugin in outcome
.plugins()
.iter()
.filter(|plugin| plugin.error.is_some())
{
if let Some(error) = plugin.error.as_deref() {
warn!(
plugin = plugin.config_name,
path = %plugin.root.display(),
"failed to load plugin: {error}"
);
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginMcpFile {
#[serde(default)]
mcp_servers: HashMap<String, JsonValue>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginAppFile {
#[serde(default)]
apps: HashMap<String, PluginAppConfig>,
}
#[derive(Debug, Default, Deserialize)]
struct PluginAppConfig {
id: String,
}
pub(crate) async fn load_plugins_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
store: &PluginStore,
restriction_product: Option<Product>,
) -> PluginLoadOutcome {
let skill_config_rules = skill_config_rules_from_stack(config_layer_stack);
let mut configured_plugins: Vec<_> = configured_plugins_from_stack(config_layer_stack)
.into_iter()
.collect();
configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let mut plugins = Vec::with_capacity(configured_plugins.len());
let mut seen_mcp_server_names = HashMap::<String, String>::new();
for (configured_name, plugin) in configured_plugins {
let loaded_plugin = load_plugin(
configured_name.clone(),
&plugin,
store,
restriction_product,
&skill_config_rules,
)
.await;
for name in loaded_plugin.mcp_servers.keys() {
if let Some(previous_plugin) =
seen_mcp_server_names.insert(name.clone(), configured_name.clone())
{
warn!(
plugin = configured_name,
previous_plugin,
server = name,
"skipping duplicate plugin MCP server name"
);
}
}
plugins.push(loaded_plugin);
}
PluginLoadOutcome::from_plugins(plugins)
}
fn refresh_curated_plugin_cache(
codex_home: &Path,
plugin_version: &str,
configured_curated_plugin_ids: &[PluginId],
) -> Result<bool, String> {
let store = PluginStore::new(codex_home.to_path_buf());
let curated_marketplace_path = AbsolutePathBuf::try_from(
curated_plugins_repo_path(codex_home).join(".agents/plugins/marketplace.json"),
)
.map_err(|_| "local curated marketplace is not available".to_string())?;
let curated_marketplace = load_marketplace(&curated_marketplace_path)
.map_err(|err| format!("failed to load curated marketplace for cache refresh: {err}"))?;
let mut plugin_sources = HashMap::<String, AbsolutePathBuf>::new();
for plugin in curated_marketplace.plugins {
let plugin_name = plugin.name;
if plugin_sources.contains_key(&plugin_name) {
warn!(
plugin = plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
"ignoring duplicate curated plugin entry during cache refresh"
);
continue;
}
let source_path = match plugin.source {
MarketplacePluginSource::Local { path } => path,
};
plugin_sources.insert(plugin_name, source_path);
}
let mut cache_refreshed = false;
for plugin_id in configured_curated_plugin_ids {
if store.active_plugin_version(plugin_id).as_deref() == Some(plugin_version) {
continue;
}
let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else {
warn!(
plugin = plugin_id.plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
"configured curated plugin no longer exists in curated marketplace during cache refresh"
);
continue;
};
store
.install_with_version(source_path, plugin_id.clone(), plugin_version.to_string())
.map_err(|err| {
format!(
"failed to refresh curated plugin cache for {}: {err}",
plugin_id.as_key()
)
})?;
cache_refreshed = true;
}
Ok(cache_refreshed)
}
fn refresh_non_curated_plugin_cache(
codex_home: &Path,
additional_roots: &[AbsolutePathBuf],
) -> Result<bool, String> {
let configured_non_curated_plugin_ids =
non_curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home(
codex_home,
"failed to read user config while refreshing non-curated plugin cache",
"failed to parse user config while refreshing non-curated plugin cache",
));
if configured_non_curated_plugin_ids.is_empty() {
return Ok(false);
}
let configured_non_curated_plugin_keys = configured_non_curated_plugin_ids
.iter()
.map(PluginId::as_key)
.collect::<HashSet<_>>();
let store = PluginStore::new(codex_home.to_path_buf());
let marketplace_outcome = list_marketplaces(additional_roots)
.map_err(|err| format!("failed to discover marketplaces for cache refresh: {err}"))?;
let mut plugin_sources = HashMap::<String, (AbsolutePathBuf, String)>::new();
for marketplace in marketplace_outcome.marketplaces {
if marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME {
continue;
}
for plugin in marketplace.plugins {
let plugin_id =
PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err(|err| {
match err {
PluginIdError::Invalid(message) => {
format!("failed to prepare non-curated plugin cache refresh: {message}")
}
}
})?;
let plugin_key = plugin_id.as_key();
if !configured_non_curated_plugin_keys.contains(&plugin_key) {
continue;
}
if plugin_sources.contains_key(&plugin_key) {
warn!(
plugin = plugin.name,
marketplace = marketplace.name,
"ignoring duplicate non-curated plugin entry during cache refresh"
);
continue;
}
let source_path = match plugin.source {
MarketplacePluginSource::Local { path } => path,
};
let plugin_version = plugin_version_for_source(source_path.as_path())
.map_err(|err| format!("failed to read plugin version for {plugin_key}: {err}"))?;
plugin_sources.insert(plugin_key, (source_path, plugin_version));
}
}
let mut cache_refreshed = false;
for plugin_id in configured_non_curated_plugin_ids {
let plugin_key = plugin_id.as_key();
let Some((source_path, plugin_version)) = plugin_sources.get(&plugin_key).cloned() else {
warn!(
plugin = plugin_id.plugin_name,
marketplace = plugin_id.marketplace_name,
"configured non-curated plugin no longer exists in discovered marketplaces during cache refresh"
);
continue;
};
if store.active_plugin_version(&plugin_id).as_deref() == Some(plugin_version.as_str()) {
continue;
}
store
.install_with_version(source_path, plugin_id.clone(), plugin_version)
.map_err(|err| format!("failed to refresh plugin cache for {plugin_key}: {err}"))?;
cache_refreshed = true;
}
Ok(cache_refreshed)
}
fn configured_plugins_from_stack(
config_layer_stack: &ConfigLayerStack,
) -> HashMap<String, PluginConfig> {
@@ -1595,523 +1371,6 @@ fn configured_plugins_from_user_config_value(
}
}
fn configured_plugins_from_codex_home(
codex_home: &Path,
read_error_message: &str,
parse_error_message: &str,
) -> HashMap<String, PluginConfig> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let user_config = match fs::read_to_string(&config_path) {
Ok(user_config) => user_config,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return HashMap::new(),
Err(err) => {
warn!(
path = %config_path.display(),
error = %err,
"{read_error_message}"
);
return HashMap::new();
}
};
let user_config = match toml::from_str::<toml::Value>(&user_config) {
Ok(user_config) => user_config,
Err(err) => {
warn!(
path = %config_path.display(),
error = %err,
"{parse_error_message}"
);
return HashMap::new();
}
};
configured_plugins_from_user_config_value(&user_config)
}
fn configured_plugin_ids(
configured_plugins: HashMap<String, PluginConfig>,
invalid_plugin_key_message: &str,
) -> Vec<PluginId> {
configured_plugins
.into_keys()
.filter_map(|plugin_key| match PluginId::parse(&plugin_key) {
Ok(plugin_id) => Some(plugin_id),
Err(err) => {
warn!(
plugin_key,
error = %err,
"{invalid_plugin_key_message}"
);
None
}
})
.collect()
}
fn curated_plugin_ids_from_config_keys(
configured_plugins: HashMap<String, PluginConfig>,
) -> Vec<PluginId> {
let mut configured_curated_plugin_ids = configured_plugin_ids(
configured_plugins,
"ignoring invalid configured plugin key during curated sync setup",
)
.into_iter()
.filter(|plugin_id| plugin_id.marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME)
.collect::<Vec<_>>();
configured_curated_plugin_ids.sort_unstable_by_key(PluginId::as_key);
configured_curated_plugin_ids
}
fn non_curated_plugin_ids_from_config_keys(
configured_plugins: HashMap<String, PluginConfig>,
) -> Vec<PluginId> {
let mut configured_non_curated_plugin_ids = configured_plugin_ids(
configured_plugins,
"ignoring invalid plugin key during non-curated cache refresh setup",
)
.into_iter()
.filter(|plugin_id| plugin_id.marketplace_name != OPENAI_CURATED_MARKETPLACE_NAME)
.collect::<Vec<_>>();
configured_non_curated_plugin_ids.sort_unstable_by_key(PluginId::as_key);
configured_non_curated_plugin_ids
}
async fn load_plugin(
config_name: String,
plugin: &PluginConfig,
store: &PluginStore,
restriction_product: Option<Product>,
skill_config_rules: &SkillConfigRules,
) -> LoadedPlugin {
let plugin_id = PluginId::parse(&config_name);
let active_plugin_root = plugin_id
.as_ref()
.ok()
.and_then(|plugin_id| store.active_plugin_root(plugin_id));
let root = active_plugin_root
.clone()
.unwrap_or_else(|| match &plugin_id {
Ok(plugin_id) => store.plugin_base_root(plugin_id),
Err(_) => store.root().clone(),
});
let mut loaded_plugin = LoadedPlugin {
config_name,
manifest_name: None,
manifest_description: None,
root,
enabled: plugin.enabled,
skill_roots: Vec::new(),
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
apps: Vec::new(),
error: None,
};
if !plugin.enabled {
return loaded_plugin;
}
let plugin_root = match plugin_id {
Ok(_) => match active_plugin_root {
Some(plugin_root) => plugin_root,
None => {
loaded_plugin.error = Some("plugin is not installed".to_string());
return loaded_plugin;
}
},
Err(err) => {
loaded_plugin.error = Some(err.to_string());
return loaded_plugin;
}
};
if !plugin_root.as_path().is_dir() {
loaded_plugin.error = Some("path does not exist or is not a directory".to_string());
return loaded_plugin;
}
let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else {
loaded_plugin.error = Some("missing or invalid .codex-plugin/plugin.json".to_string());
return loaded_plugin;
};
let manifest_paths = &manifest.paths;
loaded_plugin.manifest_name = manifest
.interface
.as_ref()
.and_then(|interface| interface.display_name.as_deref())
.map(str::trim)
.filter(|display_name| !display_name.is_empty())
.map(str::to_string)
.or_else(|| Some(manifest.name.clone()));
loaded_plugin.manifest_description = manifest.description.clone();
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
let resolved_skills = load_plugin_skills(
&plugin_root,
manifest_paths,
restriction_product,
skill_config_rules,
)
.await;
let has_enabled_skills = resolved_skills.has_enabled_skills();
loaded_plugin.disabled_skill_paths = resolved_skills.disabled_skill_paths;
loaded_plugin.has_enabled_skills = has_enabled_skills;
let mut mcp_servers = HashMap::new();
for mcp_config_path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root.as_path(), &mcp_config_path).await;
for (name, config) in plugin_mcp.mcp_servers {
if mcp_servers.insert(name.clone(), config).is_some() {
warn!(
plugin = %plugin_root.display(),
path = %mcp_config_path.display(),
server = name,
"plugin MCP file overwrote an earlier server definition"
);
}
}
}
loaded_plugin.mcp_servers = mcp_servers;
loaded_plugin.apps = load_apps_from_paths(
plugin_root.as_path(),
plugin_app_config_paths(plugin_root.as_path(), manifest_paths),
)
.await;
loaded_plugin
}
struct ResolvedPluginSkills {
skills: Vec<SkillMetadata>,
disabled_skill_paths: HashSet<AbsolutePathBuf>,
had_errors: bool,
}
impl ResolvedPluginSkills {
fn has_enabled_skills(&self) -> bool {
// Keep the plugin visible in capability summaries if skill loading was partial.
self.had_errors
|| self
.skills
.iter()
.any(|skill| !self.disabled_skill_paths.contains(&skill.path_to_skills_md))
}
}
async fn load_plugin_skills(
plugin_root: &AbsolutePathBuf,
manifest_paths: &PluginManifestPaths,
restriction_product: Option<Product>,
skill_config_rules: &SkillConfigRules,
) -> ResolvedPluginSkills {
let roots = plugin_skill_roots(plugin_root, manifest_paths)
.into_iter()
.map(|path| SkillRoot {
path,
scope: SkillScope::User,
file_system: Arc::clone(&LOCAL_FS),
})
.collect::<Vec<_>>();
let outcome = load_skills_from_roots(roots).await;
let had_errors = !outcome.errors.is_empty();
let skills = outcome
.skills
.into_iter()
.filter(|skill| skill.matches_product_restriction_for_product(restriction_product))
.collect::<Vec<_>>();
let disabled_skill_paths = resolve_disabled_skill_paths(&skills, skill_config_rules);
ResolvedPluginSkills {
skills,
disabled_skill_paths,
had_errors,
}
}
fn plugin_skill_roots(
plugin_root: &AbsolutePathBuf,
manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf> {
let mut paths = default_skill_roots(plugin_root);
if let Some(path) = &manifest_paths.skills {
paths.push(path.clone());
}
paths.sort_unstable();
paths.dedup();
paths
}
fn default_skill_roots(plugin_root: &AbsolutePathBuf) -> Vec<AbsolutePathBuf> {
let skills_dir = plugin_root.join(DEFAULT_SKILLS_DIR_NAME);
if skills_dir.is_dir() {
vec![skills_dir]
} else {
Vec::new()
}
}
fn plugin_mcp_config_paths(
plugin_root: &Path,
manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf> {
if let Some(path) = &manifest_paths.mcp_servers {
return vec![path.clone()];
}
default_mcp_config_paths(plugin_root)
}
fn default_mcp_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
let mut paths = Vec::new();
let default_path = plugin_root.join(DEFAULT_MCP_CONFIG_FILE);
if default_path.is_file()
&& let Ok(default_path) = AbsolutePathBuf::try_from(default_path)
{
paths.push(default_path);
}
paths.sort_unstable();
paths.dedup();
paths
}
pub async fn load_plugin_apps(plugin_root: &Path) -> Vec<AppConnectorId> {
if let Some(manifest) = load_plugin_manifest(plugin_root) {
return load_apps_from_paths(
plugin_root,
plugin_app_config_paths(plugin_root, &manifest.paths),
)
.await;
}
load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await
}
fn plugin_app_config_paths(
plugin_root: &Path,
manifest_paths: &PluginManifestPaths,
) -> Vec<AbsolutePathBuf> {
if let Some(path) = &manifest_paths.apps {
return vec![path.clone()];
}
default_app_config_paths(plugin_root)
}
fn default_app_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
let mut paths = Vec::new();
let default_path = plugin_root.join(DEFAULT_APP_CONFIG_FILE);
if default_path.is_file()
&& let Ok(default_path) = AbsolutePathBuf::try_from(default_path)
{
paths.push(default_path);
}
paths.sort_unstable();
paths.dedup();
paths
}
async fn load_apps_from_paths(
plugin_root: &Path,
app_config_paths: Vec<AbsolutePathBuf>,
) -> Vec<AppConnectorId> {
let mut connector_ids = Vec::new();
for app_config_path in app_config_paths {
let Ok(contents) = tokio::fs::read_to_string(app_config_path.as_path()).await else {
continue;
};
let parsed = match serde_json::from_str::<PluginAppFile>(&contents) {
Ok(parsed) => parsed,
Err(err) => {
warn!(
path = %app_config_path.display(),
"failed to parse plugin app config: {err}"
);
continue;
}
};
let mut apps: Vec<PluginAppConfig> = parsed.apps.into_values().collect();
apps.sort_unstable_by(|left, right| left.id.cmp(&right.id));
connector_ids.extend(apps.into_iter().filter_map(|app| {
if app.id.trim().is_empty() {
warn!(
plugin = %plugin_root.display(),
"plugin app config is missing an app id"
);
None
} else {
Some(AppConnectorId(app.id))
}
}));
}
connector_ids.dedup();
connector_ids
}
pub async fn plugin_telemetry_metadata_from_root(
plugin_id: &PluginId,
plugin_root: &AbsolutePathBuf,
) -> PluginTelemetryMetadata {
let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else {
return PluginTelemetryMetadata::from_plugin_id(plugin_id);
};
let manifest_paths = &manifest.paths;
let has_skills = !plugin_skill_roots(plugin_root, manifest_paths).is_empty();
let mut mcp_server_names = Vec::new();
for path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) {
mcp_server_names.extend(
load_mcp_servers_from_file(plugin_root.as_path(), &path)
.await
.mcp_servers
.into_keys(),
);
}
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
PluginTelemetryMetadata {
plugin_id: plugin_id.clone(),
capability_summary: Some(PluginCapabilitySummary {
config_name: plugin_id.as_key(),
display_name: plugin_id.plugin_name.clone(),
description: None,
has_skills,
mcp_server_names,
app_connector_ids: load_apps_from_paths(
plugin_root.as_path(),
plugin_app_config_paths(plugin_root.as_path(), manifest_paths),
)
.await,
}),
}
}
pub async fn load_plugin_mcp_servers(plugin_root: &Path) -> HashMap<String, McpServerConfig> {
let Some(manifest) = load_plugin_manifest(plugin_root) else {
return HashMap::new();
};
let mut mcp_servers = HashMap::new();
for mcp_config_path in plugin_mcp_config_paths(plugin_root, &manifest.paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path).await;
for (name, config) in plugin_mcp.mcp_servers {
mcp_servers.entry(name).or_insert(config);
}
}
mcp_servers
}
pub async fn installed_plugin_telemetry_metadata(
codex_home: &Path,
plugin_id: &PluginId,
) -> PluginTelemetryMetadata {
let store = PluginStore::new(codex_home.to_path_buf());
let Some(plugin_root) = store.active_plugin_root(plugin_id) else {
return PluginTelemetryMetadata::from_plugin_id(plugin_id);
};
plugin_telemetry_metadata_from_root(plugin_id, &plugin_root).await
}
async fn load_mcp_servers_from_file(
plugin_root: &Path,
mcp_config_path: &AbsolutePathBuf,
) -> PluginMcpDiscovery {
let Ok(contents) = tokio::fs::read_to_string(mcp_config_path.as_path()).await else {
return PluginMcpDiscovery::default();
};
let parsed = match serde_json::from_str::<PluginMcpFile>(&contents) {
Ok(parsed) => parsed,
Err(err) => {
warn!(
path = %mcp_config_path.display(),
"failed to parse plugin MCP config: {err}"
);
return PluginMcpDiscovery::default();
}
};
normalize_plugin_mcp_servers(
plugin_root,
parsed.mcp_servers,
mcp_config_path.to_string_lossy().as_ref(),
)
}
fn normalize_plugin_mcp_servers(
plugin_root: &Path,
plugin_mcp_servers: HashMap<String, JsonValue>,
source: &str,
) -> PluginMcpDiscovery {
let mut mcp_servers = HashMap::new();
for (name, config_value) in plugin_mcp_servers {
let normalized = normalize_plugin_mcp_server_value(plugin_root, config_value);
match serde_json::from_value::<McpServerConfig>(JsonValue::Object(normalized)) {
Ok(config) => {
mcp_servers.insert(name, config);
}
Err(err) => {
warn!(
plugin = %plugin_root.display(),
server = name,
"failed to parse plugin MCP server from {source}: {err}"
);
}
}
}
PluginMcpDiscovery { mcp_servers }
}
fn normalize_plugin_mcp_server_value(
plugin_root: &Path,
value: JsonValue,
) -> JsonMap<String, JsonValue> {
let mut object = match value {
JsonValue::Object(object) => object,
_ => return JsonMap::new(),
};
if let Some(JsonValue::String(transport_type)) = object.remove("type") {
match transport_type.as_str() {
"http" | "streamable_http" | "streamable-http" => {}
"stdio" => {}
other => {
warn!(
plugin = %plugin_root.display(),
transport = other,
"plugin MCP server uses an unknown transport type"
);
}
}
}
if let Some(JsonValue::Object(oauth)) = object.remove("oauth")
&& oauth.contains_key("callbackPort")
{
warn!(
plugin = %plugin_root.display(),
"plugin MCP server OAuth callbackPort is ignored; Codex uses global MCP OAuth callback settings"
);
}
if let Some(JsonValue::String(cwd)) = object.get("cwd")
&& !Path::new(cwd).is_absolute()
{
object.insert(
"cwd".to_string(),
JsonValue::String(plugin_root.join(cwd).display().to_string()),
);
}
object
}
#[derive(Debug, Default)]
struct PluginMcpDiscovery {
mcp_servers: HashMap<String, McpServerConfig>,
}
#[cfg(test)]
#[path = "manager_tests.rs"]
mod tests;

View File

@@ -6,7 +6,6 @@ use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use crate::plugins::LoadedPlugin;
use crate::plugins::MarketplacePluginInstallPolicy;
use crate::plugins::PluginLoadOutcome;
use crate::plugins::marketplace_install_root;
use crate::plugins::test_support::TEST_CURATED_PLUGIN_SHA;
@@ -14,12 +13,15 @@ use crate::plugins::test_support::write_curated_plugin_sha_with as write_curated
use crate::plugins::test_support::write_file;
use crate::plugins::test_support::write_openai_curated_marketplace;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_login::CodexAuth;
use codex_protocol::protocol::Product;
use codex_utils_absolute_path::test_support::PathBufExt;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use toml::Value;
use wiremock::Mock;
@@ -2477,14 +2479,10 @@ enabled = true
);
assert_eq!(
curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home(
tmp.path(),
"failed to read user config while refreshing curated plugin cache",
"failed to parse user config while refreshing curated plugin cache",
))
.into_iter()
.map(|plugin_id| plugin_id.as_key())
.collect::<Vec<_>>(),
configured_curated_plugin_ids_from_codex_home(tmp.path())
.into_iter()
.map(|plugin_id| plugin_id.as_key())
.collect::<Vec<_>>(),
vec!["slack@openai-curated".to_string()]
);
@@ -2496,11 +2494,7 @@ plugins = true
);
assert_eq!(
curated_plugin_ids_from_config_keys(configured_plugins_from_codex_home(
tmp.path(),
"failed to read user config while refreshing curated plugin cache",
"failed to parse user config while refreshing curated plugin cache",
)),
configured_curated_plugin_ids_from_codex_home(tmp.path()),
Vec::<PluginId>::new()
);
}

View File

@@ -1,10 +1,10 @@
use super::MarketplaceAddError;
use super::MarketplaceSource;
use crate::plugins::installed_marketplaces::resolve_configured_marketplace_root;
use crate::plugins::validate_marketplace_root;
use codex_config::CONFIG_TOML_FILE;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use codex_core_plugins::marketplace::validate_marketplace_root;
use std::fs;
use std::io::ErrorKind;
use std::path::Path;

View File

@@ -1,6 +1,6 @@
use super::MarketplaceAddError;
use crate::plugins::validate_marketplace_root;
use crate::plugins::validate_plugin_segment;
use codex_core_plugins::marketplace::validate_marketplace_root;
use std::path::Path;
use std::path::PathBuf;

View File

@@ -4,17 +4,12 @@ mod discoverable;
mod injection;
mod installed_marketplaces;
mod manager;
mod manifest;
mod marketplace;
mod marketplace_add;
mod mentions;
mod remote;
mod render;
mod startup_sync;
mod store;
#[cfg(test)]
pub(crate) mod test_support;
mod toggles;
pub use codex_plugin::AppConnectorId;
pub use codex_plugin::EffectiveSkillRoots;
@@ -45,32 +40,15 @@ pub use manager::PluginRemoteSyncError;
pub use manager::PluginUninstallError;
pub use manager::PluginsManager;
pub use manager::RemotePluginSyncResult;
pub use manager::installed_plugin_telemetry_metadata;
pub use manager::load_plugin_apps;
pub use manager::load_plugin_mcp_servers;
pub use manager::plugin_telemetry_metadata_from_root;
pub use manifest::PluginManifestInterface;
pub(crate) use manifest::PluginManifestPaths;
pub(crate) use manifest::load_plugin_manifest;
pub use marketplace::MarketplaceError;
pub use marketplace::MarketplaceListError;
pub use marketplace::MarketplacePluginAuthPolicy;
pub use marketplace::MarketplacePluginInstallPolicy;
pub use marketplace::MarketplacePluginPolicy;
pub use marketplace::MarketplacePluginSource;
pub use marketplace::validate_marketplace_root;
pub use marketplace_add::MarketplaceAddError;
pub use marketplace_add::MarketplaceAddOutcome;
pub use marketplace_add::MarketplaceAddRequest;
pub use marketplace_add::add_marketplace;
pub use remote::RemotePluginFetchError;
pub use remote::fetch_remote_featured_plugin_ids;
pub(crate) use render::render_explicit_plugin_instructions;
pub(crate) use render::render_plugins_section;
pub(crate) use startup_sync::curated_plugins_repo_path;
pub(crate) use startup_sync::read_curated_plugins_sha;
pub(crate) use startup_sync::sync_openai_plugins_repo;
pub use toggles::collect_plugin_enabled_candidates;
pub(crate) use mentions::build_connector_slug_counts;
pub(crate) use mentions::build_skill_name_counts;