mirror of
https://github.com/openai/codex.git
synced 2026-05-05 13:51:29 +03:00
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:
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user