mirror of
https://github.com/openai/codex.git
synced 2026-04-19 05:51:42 +03:00
Compare commits
3 Commits
pr17693
...
xl/plugins
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73219ed5f | ||
|
|
4f4a7485d8 | ||
|
|
dc8b2ef0b2 |
29
codex-rs/Cargo.lock
generated
29
codex-rs/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
15
codex-rs/core-plugins/BUILD.bazel
Normal file
15
codex-rs/core-plugins/BUILD.bazel
Normal 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,
|
||||
),
|
||||
)
|
||||
39
codex-rs/core-plugins/Cargo.toml
Normal file
39
codex-rs/core-plugins/Cargo.toml
Normal 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 }
|
||||
6
codex-rs/core-plugins/src/lib.rs
Normal file
6
codex-rs/core-plugins/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod loader;
|
||||
pub mod manifest;
|
||||
pub mod marketplace;
|
||||
pub mod remote;
|
||||
pub mod store;
|
||||
pub mod toggles;
|
||||
807
codex-rs/core-plugins/src/loader.rs
Normal file
807
codex-rs/core-plugins/src/loader.rs
Normal 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>,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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> {
|
||||
@@ -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> {
|
||||
@@ -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)?;
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user