plugin: support local-based marketplace.json + install endpoint. (#13422)

Support marketplace.json that points to a local file, with
```
    "source":
    {
        "source": "local",
        "path": "./plugin-1"
    },
 ```
 
 Add a new plugin/install endpoint which add the plugin to the cache folder and enable it in config.toml.
This commit is contained in:
xl-openai
2026-03-04 19:08:18 -05:00
committed by GitHub
parent 294079b0b1
commit 1e877ccdd2
17 changed files with 756 additions and 69 deletions

View File

@@ -1,8 +1,9 @@
use super::load_plugin_manifest;
use super::marketplace::MarketplaceError;
use super::marketplace::resolve_marketplace_plugin;
use super::plugin_manifest_name;
use super::store::DEFAULT_PLUGIN_VERSION;
use super::store::PluginId;
use super::store::PluginInstallRequest;
use super::store::PluginInstallResult;
use super::store::PluginStore;
use super::store::PluginStoreError;
@@ -38,6 +39,13 @@ const DEFAULT_APP_CONFIG_FILE: &str = ".app.json";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AppConnectorId(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginInstallRequest {
pub plugin_name: String,
pub marketplace_name: String,
pub cwd: PathBuf,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LoadedPlugin {
pub config_name: String,
@@ -169,10 +177,17 @@ impl PluginsManager {
&self,
request: PluginInstallRequest,
) -> Result<PluginInstallResult, PluginInstallError> {
let resolved = resolve_marketplace_plugin(
&request.cwd,
&request.plugin_name,
&request.marketplace_name,
)?;
let store = self.store.clone();
let result = tokio::task::spawn_blocking(move || store.install(request))
.await
.map_err(PluginInstallError::join)??;
let result = tokio::task::spawn_blocking(move || {
store.install(resolved.source_path.into_path_buf(), resolved.plugin_id)
})
.await
.map_err(PluginInstallError::join)??;
ConfigService::new_with_defaults(self.codex_home.clone())
.write_value(ConfigValueWriteParams {
@@ -194,6 +209,9 @@ impl PluginsManager {
#[derive(Debug, thiserror::Error)]
pub enum PluginInstallError {
#[error("{0}")]
Marketplace(#[from] MarketplaceError),
#[error("{0}")]
Store(#[from] PluginStoreError),
@@ -208,6 +226,18 @@ impl PluginInstallError {
fn join(source: tokio::task::JoinError) -> Self {
Self::Join(source)
}
pub fn is_invalid_request(&self) -> bool {
matches!(
self,
Self::Marketplace(
MarketplaceError::InvalidMarketplaceFile { .. }
| MarketplaceError::PluginNotFound { .. }
| MarketplaceError::DuplicatePlugin { .. }
| MarketplaceError::InvalidPlugin(_)
) | Self::Store(PluginStoreError::Invalid(_))
)
}
}
fn plugins_feature_enabled_from_stack(config_layer_stack: &ConfigLayerStack) -> bool {
@@ -879,12 +909,36 @@ mod tests {
#[tokio::test]
async fn install_plugin_updates_config_with_relative_path_and_plugin_key() {
let tmp = tempfile::tempdir().unwrap();
write_plugin(tmp.path(), "sample-plugin", "sample-plugin");
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
write_plugin(
&repo_root.join(".agents/plugins"),
"sample-plugin",
"sample-plugin",
);
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "debug",
"plugins": [
{
"name": "sample-plugin",
"source": {
"source": "local",
"path": "./sample-plugin"
}
}
]
}"#,
)
.unwrap();
let result = PluginsManager::new(tmp.path().to_path_buf())
.install_plugin(PluginInstallRequest {
source_path: tmp.path().join("sample-plugin"),
marketplace_name: None,
plugin_name: "sample-plugin".to_string(),
marketplace_name: "debug".to_string(),
cwd: repo_root.clone(),
})
.await
.unwrap();