mirror of
https://github.com/openai/codex.git
synced 2026-05-29 15:30:22 +00:00
feat: Normalize remote plugin summary identities. (#22265)
Makes plugin summaries use config-style plugin@marketplace IDs while exposing backend remote IDs separately as remotePluginId. Also fix the consistency issue of REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME
This commit is contained in:
@@ -12708,6 +12708,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"description": "Backend remote plugin identifier when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -9257,6 +9257,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"description": "Backend remote plugin identifier when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -452,6 +452,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"description": "Backend remote plugin identifier when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -506,6 +506,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"description": "Backend remote plugin identifier when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -408,6 +408,13 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remotePluginId": {
|
||||
"description": "Backend remote plugin identifier when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareContext": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -8,7 +8,11 @@ import type { PluginInterface } from "./PluginInterface";
|
||||
import type { PluginShareContext } from "./PluginShareContext";
|
||||
import type { PluginSource } from "./PluginSource";
|
||||
|
||||
export type PluginSummary = { id: string, name: string,
|
||||
export type PluginSummary = { id: string,
|
||||
/**
|
||||
* Backend remote plugin identifier when available.
|
||||
*/
|
||||
remotePluginId: string | null, name: string,
|
||||
/**
|
||||
* Remote sharing context associated with this plugin when available.
|
||||
*/
|
||||
|
||||
@@ -538,6 +538,8 @@ pub enum PluginAvailability {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginSummary {
|
||||
pub id: String,
|
||||
/// Backend remote plugin identifier when available.
|
||||
pub remote_plugin_id: Option<String>,
|
||||
pub name: String,
|
||||
/// Remote sharing context associated with this plugin when available.
|
||||
pub share_context: Option<PluginShareContext>,
|
||||
|
||||
@@ -3003,7 +3003,10 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
serde_json::to_value(PluginShareListResponse {
|
||||
data: vec![PluginShareListItem {
|
||||
plugin: PluginSummary {
|
||||
id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
id: "gmail@chatgpt-global".to_string(),
|
||||
remote_plugin_id: Some(
|
||||
"plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
),
|
||||
name: "gmail".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Remote,
|
||||
@@ -3022,7 +3025,8 @@ fn plugin_share_list_response_serializes_share_items() {
|
||||
json!({
|
||||
"data": [{
|
||||
"plugin": {
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"id": "gmail@chatgpt-global",
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "gmail",
|
||||
"shareContext": null,
|
||||
"source": { "type": "remote" },
|
||||
|
||||
@@ -482,6 +482,7 @@ impl PluginRequestProcessor {
|
||||
);
|
||||
PluginSummary {
|
||||
id: plugin.id,
|
||||
remote_plugin_id: None,
|
||||
installed: plugin.installed,
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
@@ -723,6 +724,7 @@ impl PluginRequestProcessor {
|
||||
marketplace_path: outcome.marketplace_path,
|
||||
summary: PluginSummary {
|
||||
id: outcome.plugin.id,
|
||||
remote_plugin_id: None,
|
||||
name: outcome.plugin.name,
|
||||
share_context,
|
||||
source: marketplace_plugin_source_to_info(outcome.plugin.source),
|
||||
@@ -1107,7 +1109,6 @@ impl PluginRequestProcessor {
|
||||
)
|
||||
})?;
|
||||
if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin {
|
||||
let remote_plugin_id = &remote_detail.summary.id;
|
||||
return Err(invalid_request(format!(
|
||||
"remote plugin {remote_plugin_id} is disabled by admin"
|
||||
)));
|
||||
@@ -1587,6 +1588,7 @@ fn remote_marketplace_to_info(marketplace: RemoteMarketplace) -> PluginMarketpla
|
||||
fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginSummary {
|
||||
PluginSummary {
|
||||
id: summary.id,
|
||||
remote_plugin_id: Some(summary.remote_plugin_id),
|
||||
name: summary.name,
|
||||
share_context: summary
|
||||
.share_context
|
||||
|
||||
@@ -240,6 +240,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
|
||||
interface: None,
|
||||
plugins: vec![PluginSummary {
|
||||
id: "valid-plugin@valid-marketplace".to_string(),
|
||||
remote_plugin_id: None,
|
||||
name: "valid-plugin".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
@@ -529,6 +530,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
plugins: vec![
|
||||
PluginSummary {
|
||||
id: "valid-plugin@alternate-marketplace".to_string(),
|
||||
remote_plugin_id: None,
|
||||
name: "valid-plugin".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
@@ -562,6 +564,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
},
|
||||
PluginSummary {
|
||||
id: "missing-plugin@alternate-marketplace".to_string(),
|
||||
remote_plugin_id: None,
|
||||
name: "missing-plugin".to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
@@ -688,6 +691,7 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.flat_map(|marketplace| marketplace.plugins.iter())
|
||||
.find(|plugin| plugin.name == "demo-plugin")
|
||||
.expect("expected demo-plugin entry");
|
||||
assert_eq!(plugin.remote_plugin_id, None);
|
||||
let share_context = plugin
|
||||
.share_context
|
||||
.as_ref()
|
||||
@@ -1464,11 +1468,7 @@ async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles()
|
||||
.into_iter()
|
||||
.map(|plugin| (plugin.id, plugin.installed, plugin.enabled))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(
|
||||
"plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
true,
|
||||
true
|
||||
)]
|
||||
vec![("linear@chatgpt-global".to_string(), true, true)]
|
||||
);
|
||||
|
||||
wait_for_path_exists(&new_path.join(".codex-plugin/plugin.json")).await?;
|
||||
@@ -1629,9 +1629,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
Some("ChatGPT Plugins")
|
||||
);
|
||||
assert_eq!(remote_marketplace.plugins.len(), 1);
|
||||
assert_eq!(remote_marketplace.plugins[0].id, "linear@chatgpt-global");
|
||||
assert_eq!(
|
||||
remote_marketplace.plugins[0].id,
|
||||
"plugins~Plugin_00000000000000000000000000000000"
|
||||
remote_marketplace.plugins[0].remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_00000000000000000000000000000000")
|
||||
);
|
||||
assert_eq!(remote_marketplace.plugins[0].name, "linear");
|
||||
assert_eq!(remote_marketplace.plugins[0].source, PluginSource::Remote);
|
||||
@@ -1725,12 +1726,14 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag
|
||||
"plugins~Plugin_11111111111111111111111111111111",
|
||||
"workspace-linear",
|
||||
"Workspace Linear",
|
||||
"LISTED",
|
||||
/*enabled*/ None,
|
||||
);
|
||||
let workspace_installed_body = workspace_remote_plugin_page_body(
|
||||
"plugins~Plugin_11111111111111111111111111111111",
|
||||
"workspace-linear",
|
||||
"Workspace Linear",
|
||||
"LISTED",
|
||||
/*enabled*/ Some(false),
|
||||
);
|
||||
mount_remote_plugin_list(&server, "WORKSPACE", &workspace_plugin_body).await;
|
||||
@@ -1764,6 +1767,14 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag
|
||||
Some("Workspace Directory")
|
||||
);
|
||||
assert_eq!(marketplace.plugins.len(), 1);
|
||||
assert_eq!(
|
||||
marketplace.plugins[0].id,
|
||||
"workspace-linear@workspace-directory"
|
||||
);
|
||||
assert_eq!(
|
||||
marketplace.plugins[0].remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_11111111111111111111111111111111")
|
||||
);
|
||||
assert_eq!(marketplace.plugins[0].name, "workspace-linear");
|
||||
assert_eq!(marketplace.plugins[0].installed, true);
|
||||
assert_eq!(marketplace.plugins[0].enabled, false);
|
||||
@@ -1803,6 +1814,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
"plugins~Plugin_22222222222222222222222222222222",
|
||||
"shared-linear",
|
||||
"Shared Linear",
|
||||
"PRIVATE",
|
||||
/*enabled*/ None,
|
||||
))?;
|
||||
shared_plugin_body["plugins"][0]["share_principals"] = serde_json::Value::Null;
|
||||
@@ -1811,6 +1823,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
"plugins~Plugin_22222222222222222222222222222222",
|
||||
"shared-linear",
|
||||
"Shared Linear",
|
||||
"PRIVATE",
|
||||
/*enabled*/ Some(true),
|
||||
);
|
||||
mount_shared_workspace_plugins(&server, &shared_plugin_body).await;
|
||||
@@ -1844,6 +1857,11 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
Some("Shared with me")
|
||||
);
|
||||
assert_eq!(marketplace.plugins.len(), 1);
|
||||
assert_eq!(marketplace.plugins[0].id, "shared-linear@shared-with-me");
|
||||
assert_eq!(
|
||||
marketplace.plugins[0].remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_22222222222222222222222222222222")
|
||||
);
|
||||
assert_eq!(marketplace.plugins[0].name, "shared-linear");
|
||||
assert_eq!(marketplace.plugins[0].installed, true);
|
||||
assert_eq!(marketplace.plugins[0].enabled, true);
|
||||
@@ -2381,6 +2399,7 @@ fn workspace_remote_plugin_page_body(
|
||||
remote_plugin_id: &str,
|
||||
plugin_name: &str,
|
||||
display_name: &str,
|
||||
discoverability: &str,
|
||||
enabled: Option<bool>,
|
||||
) -> String {
|
||||
let enabled_field = enabled
|
||||
@@ -2393,7 +2412,7 @@ fn workspace_remote_plugin_page_body(
|
||||
"id": "{remote_plugin_id}",
|
||||
"name": "{plugin_name}",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "PRIVATE",
|
||||
"discoverability": "{discoverability}",
|
||||
"creator_account_user_id": "user-gavin__account-123",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"installation_policy": "AVAILABLE",
|
||||
|
||||
@@ -209,9 +209,10 @@ plugins = true
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.plugin.marketplace_name, "chatgpt-global");
|
||||
assert_eq!(response.plugin.summary.id, "linear@chatgpt-global");
|
||||
assert_eq!(
|
||||
response.plugin.summary.id,
|
||||
"plugins~Plugin_00000000000000000000000000000000"
|
||||
response.plugin.summary.remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_00000000000000000000000000000000")
|
||||
);
|
||||
assert_eq!(response.plugin.summary.name, "linear");
|
||||
assert_eq!(response.plugin.summary.source, PluginSource::Remote);
|
||||
@@ -313,6 +314,12 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
.await??;
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.plugin.marketplace_name, "shared-with-me");
|
||||
assert_eq!(response.plugin.summary.id, "shared-linear@shared-with-me");
|
||||
assert_eq!(
|
||||
response.plugin.summary.remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_11111111111111111111111111111111")
|
||||
);
|
||||
let share_context = response
|
||||
.plugin
|
||||
.summary
|
||||
@@ -482,9 +489,10 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
assert_eq!(response.plugin.marketplace_name, "chatgpt-global");
|
||||
assert_eq!(response.plugin.marketplace_path, None);
|
||||
assert_eq!(response.plugin.summary.source, PluginSource::Remote);
|
||||
assert_eq!(response.plugin.summary.id, "linear@chatgpt-global");
|
||||
assert_eq!(
|
||||
response.plugin.summary.id,
|
||||
"plugins~Plugin_00000000000000000000000000000000"
|
||||
response.plugin.summary.remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_00000000000000000000000000000000")
|
||||
);
|
||||
assert_eq!(response.plugin.summary.name, "linear");
|
||||
assert_eq!(response.plugin.summary.installed, true);
|
||||
@@ -856,6 +864,7 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<(
|
||||
.await??;
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.plugin.summary.remote_plugin_id, None);
|
||||
let share_context = response
|
||||
.plugin
|
||||
.summary
|
||||
@@ -931,6 +940,7 @@ async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() ->
|
||||
.await??;
|
||||
let response: PluginReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response.plugin.summary.remote_plugin_id, None);
|
||||
let share_context = response
|
||||
.plugin
|
||||
.summary
|
||||
|
||||
@@ -162,7 +162,8 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
|
||||
PluginShareListResponse {
|
||||
data: vec![PluginShareListItem {
|
||||
plugin: PluginSummary {
|
||||
id: "plugins_123".to_string(),
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: Some("plugins_123".to_string()),
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(expected_share_context("plugins_123")),
|
||||
source: PluginSource::Remote,
|
||||
@@ -506,7 +507,8 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
|
||||
PluginShareListResponse {
|
||||
data: vec![PluginShareListItem {
|
||||
plugin: PluginSummary {
|
||||
id: "plugins_123".to_string(),
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: Some("plugins_123".to_string()),
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(expected_share_context("plugins_123")),
|
||||
source: PluginSource::Remote,
|
||||
@@ -725,7 +727,8 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
|
||||
PluginShareListResponse {
|
||||
data: vec![PluginShareListItem {
|
||||
plugin: PluginSummary {
|
||||
id: "plugins_123".to_string(),
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: Some("plugins_123".to_string()),
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(expected_share_context("plugins_123")),
|
||||
source: PluginSource::Remote,
|
||||
|
||||
@@ -617,11 +617,17 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
release_version: &str,
|
||||
scope: &str,
|
||||
) {
|
||||
let discoverability = if scope == "WORKSPACE" {
|
||||
r#"
|
||||
"discoverability": "LISTED","#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let detail_body = format!(
|
||||
r#"{{
|
||||
"id": "{remote_plugin_id}",
|
||||
"name": "{plugin_name}",
|
||||
"scope": "{scope}",
|
||||
"scope": "{scope}",{discoverability}
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {{
|
||||
|
||||
@@ -87,6 +87,7 @@ pub struct RemoteInstalledPlugin {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemotePluginSummary {
|
||||
pub id: String,
|
||||
pub remote_plugin_id: String,
|
||||
pub name: String,
|
||||
pub share_context: Option<RemotePluginShareContext>,
|
||||
pub installed: bool,
|
||||
@@ -379,6 +380,32 @@ struct RemotePluginDirectoryItem {
|
||||
release: RemotePluginReleaseResponse,
|
||||
}
|
||||
|
||||
fn remote_plugin_canonical_marketplace_name(
|
||||
plugin: &RemotePluginDirectoryItem,
|
||||
) -> Result<&'static str, RemotePluginCatalogError> {
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => Ok(REMOTE_GLOBAL_MARKETPLACE_NAME),
|
||||
RemotePluginScope::Workspace => match workspace_plugin_discoverability(plugin)? {
|
||||
RemotePluginShareDiscoverability::Listed => Ok(REMOTE_WORKSPACE_MARKETPLACE_NAME),
|
||||
RemotePluginShareDiscoverability::Unlisted
|
||||
| RemotePluginShareDiscoverability::Private => {
|
||||
Ok(REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_plugin_discoverability(
|
||||
plugin: &RemotePluginDirectoryItem,
|
||||
) -> Result<RemotePluginShareDiscoverability, RemotePluginCatalogError> {
|
||||
plugin.discoverability.ok_or_else(|| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"workspace plugin `{}` did not include discoverability",
|
||||
plugin.id
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct RemotePluginDirectorySharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType,
|
||||
@@ -550,12 +577,9 @@ pub async fn fetch_remote_installed_plugins(
|
||||
let (global, workspace) = tokio::try_join!(global, workspace)?;
|
||||
let mut installed_plugins = [global, workspace]
|
||||
.into_iter()
|
||||
.flat_map(|(scope, plugins)| {
|
||||
plugins
|
||||
.into_iter()
|
||||
.map(move |plugin| remote_installed_plugin_to_info(scope, &plugin))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.flat_map(|(_scope, plugins)| plugins)
|
||||
.map(|plugin| remote_installed_plugin_to_info(&plugin))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
installed_plugins.sort_by(|left, right| {
|
||||
left.marketplace_name
|
||||
.cmp(&right.marketplace_name)
|
||||
@@ -655,7 +679,7 @@ async fn fetch_remote_plugin_detail_with_download_url_option(
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin = fetch_plugin_detail(config, auth, plugin_id, include_download_urls).await?;
|
||||
let scope = plugin.scope;
|
||||
let marketplace_name = scope.marketplace_name().to_string();
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
// Remote plugin IDs uniquely identify remote plugins, so the caller-provided
|
||||
// marketplace name is not validated here. The backend detail response is the
|
||||
// source of truth for the plugin's actual scope/marketplace.
|
||||
@@ -756,7 +780,7 @@ pub async fn uninstall_remote_plugin(
|
||||
config, auth, plugin_id, /*include_download_urls*/ false,
|
||||
)
|
||||
.await?;
|
||||
let marketplace_name = plugin.scope.marketplace_name().to_string();
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
let plugin_name = plugin.name;
|
||||
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
@@ -841,8 +865,17 @@ fn build_remote_plugin_summary(
|
||||
plugin: &RemotePluginDirectoryItem,
|
||||
installed_plugin: Option<&RemotePluginInstalledItem>,
|
||||
) -> Result<RemotePluginSummary, RemotePluginCatalogError> {
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(plugin)?;
|
||||
let plugin_id =
|
||||
PluginId::new(plugin.name.clone(), marketplace_name.to_string()).map_err(|err| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"invalid remote plugin config id for `{}` in `{marketplace_name}`: {err}",
|
||||
plugin.name
|
||||
))
|
||||
})?;
|
||||
Ok(RemotePluginSummary {
|
||||
id: plugin.id.clone(),
|
||||
id: plugin_id.as_key(),
|
||||
remote_plugin_id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
share_context: remote_plugin_share_context(plugin)?,
|
||||
installed: installed_plugin.is_some(),
|
||||
@@ -861,12 +894,7 @@ fn remote_plugin_share_context(
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => Ok(None),
|
||||
RemotePluginScope::Workspace => {
|
||||
let discoverability = plugin.discoverability.ok_or_else(|| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"workspace plugin `{}` did not include discoverability",
|
||||
plugin.id
|
||||
))
|
||||
})?;
|
||||
let discoverability = workspace_plugin_discoverability(plugin)?;
|
||||
Ok(Some(RemotePluginShareContext {
|
||||
remote_plugin_id: plugin.id.clone(),
|
||||
discoverability,
|
||||
@@ -890,19 +918,18 @@ fn remote_plugin_share_context(
|
||||
}
|
||||
|
||||
fn remote_installed_plugin_to_info(
|
||||
scope: RemotePluginScope,
|
||||
installed_plugin: &RemotePluginInstalledItem,
|
||||
) -> RemoteInstalledPlugin {
|
||||
) -> Result<RemoteInstalledPlugin, RemotePluginCatalogError> {
|
||||
let plugin = &installed_plugin.plugin;
|
||||
// Remote per-skill disabled state (`disabled_skill_names`) is intentionally
|
||||
// not projected into skills/list yet; local skills.config remains the
|
||||
// supported source for skill enablement.
|
||||
RemoteInstalledPlugin {
|
||||
marketplace_name: scope.marketplace_name().to_string(),
|
||||
Ok(RemoteInstalledPlugin {
|
||||
marketplace_name: remote_plugin_canonical_marketplace_name(plugin)?.to_string(),
|
||||
id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
enabled: installed_plugin.enabled,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option<PluginInterface> {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use super::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use super::REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||
use super::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
use super::RemotePluginCatalogError;
|
||||
use super::RemotePluginScope;
|
||||
use super::RemotePluginServiceConfig;
|
||||
use super::ensure_chatgpt_auth;
|
||||
use super::fetch_installed_plugins_for_scope_with_download_url;
|
||||
use super::remote_plugin_canonical_marketplace_name;
|
||||
use crate::store::PLUGINS_CACHE_DIR;
|
||||
use crate::store::PluginStore;
|
||||
use crate::store::PluginStoreError;
|
||||
@@ -150,14 +152,18 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
(
|
||||
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
]);
|
||||
let mut installed_plugin_ids = BTreeSet::new();
|
||||
let mut failed_remote_plugin_ids = BTreeSet::new();
|
||||
|
||||
for (scope, installed_plugins) in [global, workspace] {
|
||||
let marketplace_name = scope.marketplace_name().to_string();
|
||||
for (_scope, installed_plugins) in [global, workspace] {
|
||||
for installed_plugin in installed_plugins {
|
||||
let plugin = installed_plugin.plugin;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
installed_plugin_names_by_marketplace
|
||||
.entry(marketplace_name.clone())
|
||||
.or_default()
|
||||
@@ -292,6 +298,7 @@ fn remove_stale_remote_plugin_caches(
|
||||
for marketplace_name in [
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME,
|
||||
] {
|
||||
let marketplace_root = codex_home.join(PLUGINS_CACHE_DIR).join(marketplace_name);
|
||||
if !marketplace_root.exists() {
|
||||
@@ -449,6 +456,10 @@ mod tests {
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
(
|
||||
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
]);
|
||||
|
||||
let guard = mark_remote_plugin_cache_mutation_in_flight(
|
||||
@@ -487,4 +498,42 @@ mod tests {
|
||||
assert_eq!(removed, vec!["linear@chatgpt-global".to_string()]);
|
||||
assert!(!cached_manifest.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_remote_plugin_cleanup_removes_shared_with_me_cache() {
|
||||
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||
let cached_manifest = codex_home
|
||||
.path()
|
||||
.join(PLUGINS_CACHE_DIR)
|
||||
.join(REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME)
|
||||
.join("private-plugin")
|
||||
.join("1.2.3")
|
||||
.join(".codex-plugin")
|
||||
.join("plugin.json");
|
||||
std::fs::create_dir_all(cached_manifest.parent().expect("manifest parent"))
|
||||
.expect("create cached plugin manifest parent");
|
||||
std::fs::write(&cached_manifest, r#"{"name":"private-plugin"}"#)
|
||||
.expect("write cached plugin manifest");
|
||||
let installed_plugin_names_by_marketplace =
|
||||
BTreeMap::<String, BTreeSet<String>>::from_iter([
|
||||
(REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new()),
|
||||
(
|
||||
REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
(
|
||||
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
|
||||
BTreeSet::new(),
|
||||
),
|
||||
]);
|
||||
|
||||
let removed = remove_stale_remote_plugin_caches(
|
||||
codex_home.path(),
|
||||
&installed_plugin_names_by_marketplace,
|
||||
)
|
||||
.expect("cleanup shared-with-me cache");
|
||||
|
||||
assert_eq!(removed, vec!["private-plugin@shared-with-me".to_string()]);
|
||||
assert!(!cached_manifest.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ fn remote_plugin_json(plugin_id: &str) -> serde_json::Value {
|
||||
"id": plugin_id,
|
||||
"name": "demo-plugin",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "PRIVATE",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
@@ -584,7 +585,8 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
vec![
|
||||
RemotePluginShareSummary {
|
||||
summary: RemotePluginSummary {
|
||||
id: "plugins_123".to_string(),
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(RemotePluginShareContext {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
@@ -621,7 +623,8 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
},
|
||||
RemotePluginShareSummary {
|
||||
summary: RemotePluginSummary {
|
||||
id: "plugins_456".to_string(),
|
||||
id: "demo-plugin@shared-with-me".to_string(),
|
||||
remote_plugin_id: "plugins_456".to_string(),
|
||||
name: "demo-plugin".to_string(),
|
||||
share_context: Some(RemotePluginShareContext {
|
||||
remote_plugin_id: "plugins_456".to_string(),
|
||||
|
||||
@@ -1314,6 +1314,7 @@ pub(super) fn plugins_test_summary(
|
||||
) -> PluginSummary {
|
||||
PluginSummary {
|
||||
id: id.to_string(),
|
||||
remote_plugin_id: None,
|
||||
name: name.to_string(),
|
||||
share_context: None,
|
||||
source: PluginSource::Local {
|
||||
|
||||
Reference in New Issue
Block a user