mirror of
https://github.com/openai/codex.git
synced 2026-11-03 16:23:51 +00:00
[codex] Add cross-repo plugin sources to marketplace manifests (#18017)
## Summary
- add first-class marketplace support for git-backed plugin sources
- keep the newer marketplace parsing behavior from `main`, including
alternate manifest locations and string local sources
- materialize remote plugin sources during install, detail reads, and
non-curated cache refresh
- expose git plugin source metadata through the app-server protocol
## Details
This teaches the marketplace parser to accept all of the following:
- local string sources such as `"source": "./plugins/foo"`
- local object sources such as
`{"source":"local","path":"./plugins/foo"}`
- remote repo-root sources such as
`{"source":"url","url":"https://github.com/org/repo.git"}`
- remote subdir sources such as
`{"source":"git-subdir","url":"owner/repo","path":"plugins/foo","ref":"main","sha":"..."}`
It also preserves the newer tolerant behavior from `main`: invalid or
unsupported plugin entries are skipped instead of breaking the whole
marketplace.
## Validation
- `cargo test -p codex-core plugins::marketplace::tests`
- `just fix -p codex-core`
- `just fmt`
## Notes
- A full `cargo test -p codex-core` run still hit unrelated existing
failures in agent and multi-agent tests during this session; the
marketplace-focused suite passed after the rebase resolution.
This commit is contained in:
1 parent
1265df0ec2
commit
0e111e08d0
13 files changed
+1336
-113
No files matched your search
@@ -22,6 +22,7 @@ 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::materialize_marketplace_plugin_source;
|
||||
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;
|
||||
@@ -39,6 +40,7 @@ use codex_core_plugins::marketplace::find_installable_marketplace_plugin;
|
||||
use codex_core_plugins::marketplace::find_marketplace_plugin;
|
||||
use codex_core_plugins::marketplace::list_marketplaces;
|
||||
use codex_core_plugins::marketplace::load_marketplace;
|
||||
use codex_core_plugins::marketplace::plugin_interface_with_marketplace_category;
|
||||
use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeError;
|
||||
use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome;
|
||||
use codex_core_plugins::marketplace_upgrade::configured_git_marketplace_names;
|
||||
@@ -188,6 +190,12 @@ pub struct PluginDetail {
|
||||
pub disabled_skill_paths: HashSet<AbsolutePathBuf>,
|
||||
pub apps: Vec<AppConnectorId>,
|
||||
pub mcp_server_names: Vec<String>,
|
||||
pub details_unavailable_reason: Option<PluginDetailsUnavailableReason>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PluginDetailsUnavailableReason {
|
||||
InstallRequiredForRemoteSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -588,8 +596,12 @@ impl PluginsManager {
|
||||
None
|
||||
};
|
||||
let store = self.store.clone();
|
||||
let codex_home = self.codex_home.clone();
|
||||
let result: StorePluginInstallResult = tokio::task::spawn_blocking(move || {
|
||||
let MarketplacePluginSource::Local { path: source_path } = resolved.source;
|
||||
let materialized =
|
||||
materialize_marketplace_plugin_source(codex_home.as_path(), &resolved.source)
|
||||
.map_err(PluginStoreError::Invalid)?;
|
||||
let source_path = materialized.path;
|
||||
if let Some(plugin_version) = plugin_version {
|
||||
store.install_with_version(source_path, resolved.plugin_id, plugin_version)
|
||||
} else {
|
||||
@@ -754,6 +766,14 @@ impl PluginsManager {
|
||||
let plugin_key = plugin_id.as_key();
|
||||
let source_path = match plugin.source {
|
||||
MarketplacePluginSource::Local { path } => path,
|
||||
MarketplacePluginSource::Git { .. } => {
|
||||
warn!(
|
||||
plugin = plugin_name,
|
||||
marketplace = %marketplace_name,
|
||||
"skipping remote plugin source during remote sync"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let current_enabled = configured_plugins
|
||||
.get(&plugin_key)
|
||||
@@ -1032,9 +1052,55 @@ impl PluginsManager {
|
||||
});
|
||||
}
|
||||
|
||||
let source_path = match &plugin.source {
|
||||
MarketplacePluginSource::Local { path } => path.clone(),
|
||||
};
|
||||
let plugin_id =
|
||||
PluginId::new(plugin.name.clone(), marketplace_name.to_string()).map_err(|err| {
|
||||
match err {
|
||||
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
|
||||
}
|
||||
})?;
|
||||
let plugin_key = plugin_id.as_key();
|
||||
if matches!(plugin.source, MarketplacePluginSource::Git { .. }) && !plugin.installed {
|
||||
return Ok(PluginDetail {
|
||||
id: plugin_key,
|
||||
name: plugin.name,
|
||||
description: None,
|
||||
source: plugin.source,
|
||||
policy: plugin.policy,
|
||||
interface: plugin.interface,
|
||||
installed: plugin.installed,
|
||||
enabled: plugin.enabled,
|
||||
skills: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
apps: Vec::new(),
|
||||
mcp_server_names: Vec::new(),
|
||||
details_unavailable_reason: Some(
|
||||
PluginDetailsUnavailableReason::InstallRequiredForRemoteSource,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let source_path =
|
||||
if matches!(plugin.source, MarketplacePluginSource::Git { .. }) && plugin.installed {
|
||||
self.store.active_plugin_root(&plugin_id).ok_or_else(|| {
|
||||
MarketplaceError::InvalidPlugin(format!(
|
||||
"installed plugin cache entry is missing for {plugin_key}"
|
||||
))
|
||||
})?
|
||||
} else {
|
||||
let codex_home = self.codex_home.clone();
|
||||
let source = plugin.source.clone();
|
||||
let materialized = tokio::task::spawn_blocking(move || {
|
||||
materialize_marketplace_plugin_source(codex_home.as_path(), &source)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
MarketplaceError::InvalidPlugin(format!(
|
||||
"failed to materialize plugin source: {err}"
|
||||
))
|
||||
})?
|
||||
.map_err(MarketplaceError::InvalidPlugin)?;
|
||||
materialized.path.clone()
|
||||
};
|
||||
if !source_path.as_path().is_dir() {
|
||||
return Err(MarketplaceError::InvalidPlugin(
|
||||
"path does not exist or is not a directory".to_string(),
|
||||
@@ -1044,6 +1110,14 @@ impl PluginsManager {
|
||||
MarketplaceError::InvalidPlugin("missing or invalid plugin.json".to_string())
|
||||
})?;
|
||||
let description = manifest.description.clone();
|
||||
let marketplace_category = plugin
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.category.clone());
|
||||
let interface = plugin_interface_with_marketplace_category(
|
||||
manifest.interface.clone(),
|
||||
marketplace_category,
|
||||
);
|
||||
let resolved_skills = load_plugin_skills(
|
||||
&source_path,
|
||||
&manifest.paths,
|
||||
@@ -1067,13 +1141,14 @@ impl PluginsManager {
|
||||
description,
|
||||
source: plugin.source,
|
||||
policy: plugin.policy,
|
||||
interface: plugin.interface,
|
||||
interface,
|
||||
installed: plugin.installed,
|
||||
enabled: plugin.enabled,
|
||||
skills: resolved_skills.skills,
|
||||
disabled_skill_paths: resolved_skills.disabled_skill_paths,
|
||||
apps,
|
||||
mcp_server_names,
|
||||
details_unavailable_reason: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user