Add cross-repo plugin sources to marketplace manifests

This commit is contained in:
xli-oai
2026-04-16 00:14:37 -07:00
parent 4cd85b28d2
commit 7f5a11a703
12 changed files with 746 additions and 125 deletions
+27 -5
View File
@@ -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;
@@ -560,11 +561,16 @@ impl PluginsManager {
None
};
let store = self.store.clone();
let codex_home = self.codex_home.clone();
let result: StorePluginInstallResult = tokio::task::spawn_blocking(move || {
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(resolved.source_path, resolved.plugin_id, plugin_version)
store.install_with_version(source_path, resolved.plugin_id, plugin_version)
} else {
store.install(resolved.source_path, resolved.plugin_id)
store.install(source_path, resolved.plugin_id)
}
})
.await
@@ -725,6 +731,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)
@@ -1018,9 +1032,17 @@ impl PluginsManager {
}
})?;
let plugin_key = plugin_id.as_key();
let source_path = match &plugin.source {
MarketplacePluginSource::Local { path } => path.clone(),
};
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)?;
let source_path = 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(),
@@ -64,6 +64,31 @@ fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) {
);
}
fn init_git_repo(repo: &Path) {
run_git(repo, &["init"]);
run_git(repo, &["config", "user.email", "codex-test@example.com"]);
run_git(repo, &["config", "user.name", "Codex Test"]);
run_git(repo, &["add", "."]);
run_git(repo, &["commit", "-m", "initial"]);
}
fn run_git(repo: &Path, args: &[&str]) {
let output = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.unwrap_or_else(|err| panic!("git should run: {err}"));
assert!(
output.status.success(),
"git -C {} {} failed\nstdout:\n{}\nstderr:\n{}",
repo.display(),
args.join(" "),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn plugin_config_toml(enabled: bool, plugins_feature_enabled: bool) -> String {
let mut root = toml::map::Map::new();
@@ -1040,6 +1065,60 @@ async fn install_plugin_uses_manifest_version_for_non_curated_plugins() {
);
}
#[tokio::test]
async fn install_plugin_supports_git_subdir_marketplace_sources() {
let tmp = tempfile::tempdir().unwrap();
let repo_root = tmp.path().join("marketplace");
let remote_repo = tmp.path().join("remote-plugin-repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
write_plugin(&remote_repo, "plugins/toolkit", "toolkit");
init_git_repo(&remote_repo);
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
format!(
r#"{{
"name": "debug",
"plugins": [
{{
"name": "toolkit",
"source": {{
"source": "git-subdir",
"url": "{}",
"path": "plugins/toolkit"
}}
}}
]
}}"#,
remote_repo.display()
),
)
.unwrap();
let result = PluginsManager::new(tmp.path().to_path_buf())
.install_plugin(PluginInstallRequest {
plugin_name: "toolkit".to_string(),
marketplace_path: AbsolutePathBuf::try_from(
repo_root.join(".agents/plugins/marketplace.json"),
)
.unwrap(),
})
.await
.unwrap();
let installed_path = tmp.path().join("plugins/cache/debug/toolkit/local");
assert_eq!(
result,
PluginInstallOutcome {
plugin_id: PluginId::new("toolkit".to_string(), "debug".to_string()).unwrap(),
plugin_version: "local".to_string(),
installed_path: AbsolutePathBuf::try_from(installed_path.clone()).unwrap(),
auth_policy: MarketplacePluginAuthPolicy::OnInstall,
}
);
assert!(installed_path.join(".codex-plugin/plugin.json").is_file());
}
#[tokio::test]
async fn uninstall_plugin_removes_cache_and_config_entry() {
let tmp = tempfile::tempdir().unwrap();