chore: wire through plugin policies + category from marketplace.json (#14305)

wire plugin marketplace metadata through app-server endpoints:
- `plugin/list` has `installPolicy` and `authPolicy`
- `plugin/install` has plugin-level `authPolicy`

`plugin/install` also now enforces `NOT_AVAILABLE` `installPolicy` when
installing.


added tests.
This commit is contained in:
sayan-oai
2026-03-11 10:37:40 -07:00
committed by Michael Bolin
parent fa1242c83b
commit 7b2cee53db
18 changed files with 429 additions and 22 deletions

View File

@@ -3,6 +3,8 @@ use super::curated_plugins_repo_path;
use super::load_plugin_manifest;
use super::manifest::PluginManifestInterfaceSummary;
use super::marketplace::MarketplaceError;
use super::marketplace::MarketplacePluginAuthPolicy;
use super::marketplace::MarketplacePluginInstallPolicy;
use super::marketplace::MarketplacePluginSourceSummary;
use super::marketplace::list_marketplaces;
use super::marketplace::load_marketplace_summary;
@@ -12,7 +14,7 @@ use super::plugin_manifest_paths;
use super::store::DEFAULT_PLUGIN_VERSION;
use super::store::PluginId;
use super::store::PluginIdError;
use super::store::PluginInstallResult;
use super::store::PluginInstallResult as StorePluginInstallResult;
use super::store::PluginStore;
use super::store::PluginStoreError;
use super::sync_openai_plugins_repo;
@@ -68,6 +70,14 @@ pub struct PluginInstallRequest {
pub marketplace_path: AbsolutePathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginInstallOutcome {
pub plugin_id: PluginId,
pub plugin_version: String,
pub installed_path: AbsolutePathBuf,
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfiguredMarketplaceSummary {
pub name: String,
@@ -80,6 +90,8 @@ pub struct ConfiguredMarketplacePluginSummary {
pub id: String,
pub name: String,
pub source: MarketplacePluginSourceSummary,
pub install_policy: Option<MarketplacePluginInstallPolicy>,
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
pub interface: Option<PluginManifestInterfaceSummary>,
pub installed: bool,
pub enabled: bool,
@@ -380,10 +392,11 @@ impl PluginsManager {
pub async fn install_plugin(
&self,
request: PluginInstallRequest,
) -> Result<PluginInstallResult, PluginInstallError> {
) -> Result<PluginInstallOutcome, PluginInstallError> {
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
let auth_policy = resolved.auth_policy;
let store = self.store.clone();
let result = tokio::task::spawn_blocking(move || {
let result: StorePluginInstallResult = tokio::task::spawn_blocking(move || {
store.install(resolved.source_path, resolved.plugin_id)
})
.await
@@ -403,7 +416,12 @@ impl PluginsManager {
.map(|_| ())
.map_err(PluginInstallError::from)?;
Ok(result)
Ok(PluginInstallOutcome {
plugin_id: result.plugin_id,
plugin_version: result.plugin_version,
installed_path: result.installed_path,
auth_policy,
})
}
pub async fn uninstall_plugin(&self, plugin_id: String) -> Result<(), PluginUninstallError> {
@@ -634,6 +652,8 @@ impl PluginsManager {
.unwrap_or(false),
name: plugin.name,
source: plugin.source,
install_policy: plugin.install_policy,
auth_policy: plugin.auth_policy,
interface: plugin.interface,
})
})
@@ -760,6 +780,7 @@ impl PluginInstallError {
MarketplaceError::MarketplaceNotFound { .. }
| MarketplaceError::InvalidMarketplaceFile { .. }
| MarketplaceError::PluginNotFound { .. }
| MarketplaceError::PluginNotAvailable { .. }
| MarketplaceError::InvalidPlugin(_)
) | Self::Store(PluginStoreError::Invalid(_))
)
@@ -1925,7 +1946,8 @@ mod tests {
"source": {
"source": "local",
"path": "./sample-plugin"
}
},
"authPolicy": "ON_USE"
}
]
}"#,
@@ -1946,10 +1968,11 @@ mod tests {
let installed_path = tmp.path().join("plugins/cache/debug/sample-plugin/local");
assert_eq!(
result,
PluginInstallResult {
PluginInstallOutcome {
plugin_id: PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap(),
plugin_version: "local".to_string(),
installed_path: AbsolutePathBuf::try_from(installed_path).unwrap(),
auth_policy: Some(MarketplacePluginAuthPolicy::OnUse),
}
);
@@ -2079,6 +2102,8 @@ enabled = false
path: AbsolutePathBuf::try_from(tmp.path().join("repo/enabled-plugin"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
installed: true,
enabled: true,
@@ -2092,6 +2117,8 @@ enabled = false
)
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
installed: true,
enabled: false,
@@ -2157,6 +2184,8 @@ enabled = false
path: AbsolutePathBuf::try_from(curated_root.join("plugins/linear"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
installed: false,
enabled: false,
@@ -2255,6 +2284,8 @@ enabled = false
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(tmp.path().join("repo-a/from-a")).unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
installed: false,
enabled: true,
@@ -2279,6 +2310,8 @@ enabled = false
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(tmp.path().join("repo-b/from-b-only")).unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
installed: false,
enabled: false,
@@ -2356,6 +2389,8 @@ enabled = true
path: AbsolutePathBuf::try_from(tmp.path().join("repo/sample-plugin"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
installed: false,
enabled: true,

View File

@@ -32,7 +32,7 @@ pub struct PluginManifestPaths {
pub apps: Option<AbsolutePathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginManifestInterfaceSummary {
pub display_name: Option<String>,
pub short_description: Option<String>,

View File

@@ -4,6 +4,8 @@ use super::plugin_manifest_interface;
use super::store::PluginId;
use super::store::PluginIdError;
use crate::git_info::get_git_repo_root;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use dirs::home_dir;
use serde::Deserialize;
@@ -19,6 +21,7 @@ const MARKETPLACE_RELATIVE_PATH: &str = ".agents/plugins/marketplace.json";
pub struct ResolvedMarketplacePlugin {
pub plugin_id: PluginId,
pub source_path: AbsolutePathBuf,
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -32,6 +35,8 @@ pub struct MarketplaceSummary {
pub struct MarketplacePluginSummary {
pub name: String,
pub source: MarketplacePluginSourceSummary,
pub install_policy: Option<MarketplacePluginInstallPolicy>,
pub auth_policy: Option<MarketplacePluginAuthPolicy>,
pub interface: Option<PluginManifestInterfaceSummary>,
}
@@ -40,6 +45,43 @@ pub enum MarketplacePluginSourceSummary {
Local { path: AbsolutePathBuf },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum MarketplacePluginInstallPolicy {
#[serde(rename = "NOT_AVAILABLE")]
NotAvailable,
#[serde(rename = "AVAILABLE")]
Available,
#[serde(rename = "INSTALLED_BY_DEFAULT")]
InstalledByDefault,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum MarketplacePluginAuthPolicy {
#[serde(rename = "ON_INSTALL")]
OnInstall,
#[serde(rename = "ON_USE")]
OnUse,
}
impl From<MarketplacePluginInstallPolicy> for PluginInstallPolicy {
fn from(value: MarketplacePluginInstallPolicy) -> Self {
match value {
MarketplacePluginInstallPolicy::NotAvailable => Self::NotAvailable,
MarketplacePluginInstallPolicy::Available => Self::Available,
MarketplacePluginInstallPolicy::InstalledByDefault => Self::InstalledByDefault,
}
}
}
impl From<MarketplacePluginAuthPolicy> for PluginAuthPolicy {
fn from(value: MarketplacePluginAuthPolicy) -> Self {
match value {
MarketplacePluginAuthPolicy::OnInstall => Self::OnInstall,
MarketplacePluginAuthPolicy::OnUse => Self::OnUse,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MarketplaceError {
#[error("{context}: {source}")]
@@ -61,6 +103,14 @@ pub enum MarketplaceError {
marketplace_name: String,
},
#[error(
"plugin `{plugin_name}` is not available for install in marketplace `{marketplace_name}`"
)]
PluginNotAvailable {
plugin_name: String,
marketplace_name: String,
},
#[error("{0}")]
InvalidPlugin(String),
}
@@ -91,12 +141,27 @@ pub fn resolve_marketplace_plugin(
});
};
let plugin_id = PluginId::new(plugin.name, marketplace_name).map_err(|err| match err {
let MarketplacePlugin {
name,
source,
install_policy,
auth_policy,
..
} = plugin;
if install_policy == Some(MarketplacePluginInstallPolicy::NotAvailable) {
return Err(MarketplaceError::PluginNotAvailable {
plugin_name: name,
marketplace_name,
});
}
let plugin_id = PluginId::new(name, marketplace_name).map_err(|err| match err {
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
})?;
Ok(ResolvedMarketplacePlugin {
plugin_id,
source_path: resolve_plugin_source_path(marketplace_path, plugin.source)?,
source_path: resolve_plugin_source_path(marketplace_path, source)?,
auth_policy,
})
}
@@ -113,16 +178,31 @@ pub(crate) fn load_marketplace_summary(
let mut plugins = Vec::new();
for plugin in marketplace.plugins {
let source_path = resolve_plugin_source_path(path, plugin.source)?;
let MarketplacePlugin {
name,
source,
install_policy,
auth_policy,
category,
} = plugin;
let source_path = resolve_plugin_source_path(path, source)?;
let source = MarketplacePluginSourceSummary::Local {
path: source_path.clone(),
};
let interface = load_plugin_manifest(source_path.as_path())
let mut interface = load_plugin_manifest(source_path.as_path())
.and_then(|manifest| plugin_manifest_interface(&manifest, source_path.as_path()));
if let Some(category) = category {
// Marketplace taxonomy wins when both sources provide a category.
interface
.get_or_insert_with(PluginManifestInterfaceSummary::default)
.category = Some(category);
}
plugins.push(MarketplacePluginSummary {
name: plugin.name,
name,
source,
install_policy,
auth_policy,
interface,
});
}
@@ -280,9 +360,16 @@ struct MarketplaceFile {
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MarketplacePlugin {
name: String,
source: MarketplacePluginSource,
#[serde(default)]
install_policy: Option<MarketplacePluginInstallPolicy>,
#[serde(default)]
auth_policy: Option<MarketplacePluginAuthPolicy>,
#[serde(default)]
category: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -333,6 +420,7 @@ mod tests {
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
.unwrap(),
source_path: AbsolutePathBuf::try_from(repo_root.join("plugin-1")).unwrap(),
auth_policy: None,
}
);
}
@@ -439,6 +527,8 @@ mod tests {
path: AbsolutePathBuf::try_from(home_root.join("home-shared"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
},
MarketplacePluginSummary {
@@ -447,6 +537,8 @@ mod tests {
path: AbsolutePathBuf::try_from(home_root.join("home-only"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
},
],
@@ -464,6 +556,8 @@ mod tests {
path: AbsolutePathBuf::try_from(repo_root.join("repo-shared"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
},
MarketplacePluginSummary {
@@ -472,6 +566,8 @@ mod tests {
path: AbsolutePathBuf::try_from(repo_root.join("repo-only"))
.unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
},
],
@@ -542,6 +638,8 @@ mod tests {
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(home_root.join("home-plugin")).unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
}],
},
@@ -553,6 +651,8 @@ mod tests {
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("repo-plugin")).unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
}],
},
@@ -617,6 +717,8 @@ mod tests {
source: MarketplacePluginSourceSummary::Local {
path: AbsolutePathBuf::try_from(repo_root.join("plugin")).unwrap(),
},
install_policy: None,
auth_policy: None,
interface: None,
}],
}]
@@ -641,7 +743,10 @@ mod tests {
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
}
},
"installPolicy": "AVAILABLE",
"authPolicy": "ON_INSTALL",
"category": "Design"
}
]
}"#,
@@ -653,6 +758,7 @@ mod tests {
"name": "demo-plugin",
"interface": {
"displayName": "Demo",
"category": "Productivity",
"capabilities": ["Interactive", "Write"],
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png",
@@ -666,6 +772,14 @@ mod tests {
list_marketplaces_with_home(&[AbsolutePathBuf::try_from(repo_root).unwrap()], None)
.unwrap();
assert_eq!(
marketplaces[0].plugins[0].install_policy,
Some(MarketplacePluginInstallPolicy::Available)
);
assert_eq!(
marketplaces[0].plugins[0].auth_policy,
Some(MarketplacePluginAuthPolicy::OnInstall)
);
assert_eq!(
marketplaces[0].plugins[0].interface,
Some(PluginManifestInterfaceSummary {
@@ -673,7 +787,7 @@ mod tests {
short_description: None,
long_description: None,
developer_name: None,
category: None,
category: Some("Design".to_string()),
capabilities: vec!["Interactive".to_string(), "Write".to_string()],
website_url: None,
privacy_policy_url: None,
@@ -754,6 +868,8 @@ mod tests {
screenshots: Vec::new(),
})
);
assert_eq!(marketplaces[0].plugins[0].install_policy, None);
assert_eq!(marketplaces[0].plugins[0].auth_policy, None);
}
#[test]

View File

@@ -15,6 +15,7 @@ pub use manager::ConfiguredMarketplaceSummary;
pub use manager::LoadedPlugin;
pub use manager::PluginCapabilitySummary;
pub use manager::PluginInstallError;
pub use manager::PluginInstallOutcome;
pub use manager::PluginInstallRequest;
pub use manager::PluginLoadOutcome;
pub use manager::PluginRemoteSyncError;
@@ -30,8 +31,9 @@ pub(crate) use manifest::plugin_manifest_interface;
pub(crate) use manifest::plugin_manifest_name;
pub(crate) use manifest::plugin_manifest_paths;
pub use marketplace::MarketplaceError;
pub use marketplace::MarketplacePluginAuthPolicy;
pub use marketplace::MarketplacePluginInstallPolicy;
pub use marketplace::MarketplacePluginSourceSummary;
pub(crate) use render::render_explicit_plugin_instructions;
pub(crate) use render::render_plugins_section;
pub use store::PluginId;
pub use store::PluginInstallResult;