feat: Split shared workspace plugins by discoverability (#22425)

- Keep shared-with-me as the plugin/list request kind, but return
private plugins under workspace-shared-with-me-private.
- Add workspace-shared-with-me-unlisted for installed workspace plugins
with UNLISTED discoverability,
This commit is contained in:
xl-openai
2026-05-12 21:11:19 -07:00
committed by GitHub
parent 104fc14956
commit 7bf95b39aa
6 changed files with 177 additions and 51 deletions

View File

@@ -1824,13 +1824,27 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
))?;
shared_plugin_body["plugins"][0]["share_principals"] = serde_json::Value::Null;
let shared_plugin_body = serde_json::to_string(&shared_plugin_body)?;
let workspace_installed_body = workspace_remote_plugin_page_body(
"plugins~Plugin_22222222222222222222222222222222",
"shared-linear",
"Shared Linear",
"PRIVATE",
/*enabled*/ Some(true),
);
let mut workspace_installed_body: serde_json::Value =
serde_json::from_str(&workspace_remote_plugin_page_body(
"plugins~Plugin_22222222222222222222222222222222",
"shared-linear",
"Shared Linear",
"PRIVATE",
/*enabled*/ Some(true),
))?;
let unlisted_installed_body: serde_json::Value =
serde_json::from_str(&workspace_remote_plugin_page_body(
"plugins~Plugin_33333333333333333333333333333333",
"unlisted-linear",
"Unlisted Linear",
"UNLISTED",
/*enabled*/ Some(false),
))?;
workspace_installed_body["plugins"]
.as_array_mut()
.expect("installed plugins should be an array")
.push(unlisted_installed_body["plugins"][0].clone());
let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;
mount_shared_workspace_plugins(&server, &shared_plugin_body).await;
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
@@ -1851,9 +1865,12 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
.await??;
let response: PluginListResponse = to_response(response)?;
assert_eq!(response.marketplaces.len(), 1);
let marketplace = &response.marketplaces[0];
assert_eq!(marketplace.name, "shared-with-me");
assert_eq!(response.marketplaces.len(), 2);
let marketplace = response
.marketplaces
.iter()
.find(|marketplace| marketplace.name == "workspace-shared-with-me-private")
.expect("expected private shared-with-me marketplace");
assert_eq!(
marketplace
.interface
@@ -1862,7 +1879,10 @@ 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].id,
"shared-linear@workspace-shared-with-me-private"
);
assert_eq!(
marketplace.plugins[0].remote_plugin_id.as_deref(),
Some("plugins~Plugin_22222222222222222222222222222222")
@@ -1893,6 +1913,44 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
Some("https://chatgpt.example/plugins/share/share-key-1")
);
assert_eq!(share_context.share_principals, None);
let marketplace = response
.marketplaces
.iter()
.find(|marketplace| marketplace.name == "workspace-shared-with-me-unlisted")
.expect("expected unlisted shared-with-me marketplace");
assert_eq!(
marketplace
.interface
.as_ref()
.and_then(|interface| interface.display_name.as_deref()),
Some("Shared with me (unlisted)")
);
assert_eq!(marketplace.plugins.len(), 1);
assert_eq!(
marketplace.plugins[0].id,
"unlisted-linear@workspace-shared-with-me-unlisted"
);
assert_eq!(
marketplace.plugins[0].remote_plugin_id.as_deref(),
Some("plugins~Plugin_33333333333333333333333333333333")
);
assert_eq!(marketplace.plugins[0].name, "unlisted-linear");
assert_eq!(marketplace.plugins[0].installed, true);
assert_eq!(marketplace.plugins[0].enabled, false);
let share_context = marketplace.plugins[0]
.share_context
.as_ref()
.expect("expected share context");
assert_eq!(
share_context.remote_plugin_id,
"plugins~Plugin_33333333333333333333333333333333"
);
assert_eq!(share_context.remote_version.as_deref(), Some("1.2.3"));
assert_eq!(
share_context.discoverability,
Some(PluginShareDiscoverability::Unlisted)
);
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
Ok(())
}

View File

@@ -303,7 +303,7 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: None,
remote_marketplace_name: Some("shared-with-me".to_string()),
remote_marketplace_name: Some("workspace-shared-with-me-private".to_string()),
plugin_name: "plugins~Plugin_11111111111111111111111111111111".to_string(),
})
.await?;
@@ -315,8 +315,14 @@ 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.marketplace_name,
"workspace-shared-with-me-private"
);
assert_eq!(
response.plugin.summary.id,
"shared-linear@workspace-shared-with-me-private"
);
assert_eq!(
response.plugin.summary.remote_plugin_id.as_deref(),
Some("plugins~Plugin_11111111111111111111111111111111")

View File

@@ -162,7 +162,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
PluginShareListResponse {
data: vec![PluginShareListItem {
plugin: PluginSummary {
id: "demo-plugin@shared-with-me".to_string(),
id: "demo-plugin@workspace-shared-with-me-private".to_string(),
remote_plugin_id: Some("plugins_123".to_string()),
local_version: None,
name: "demo-plugin".to_string(),
@@ -566,7 +566,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
PluginShareListResponse {
data: vec![PluginShareListItem {
plugin: PluginSummary {
id: "demo-plugin@shared-with-me".to_string(),
id: "demo-plugin@workspace-shared-with-me-private".to_string(),
remote_plugin_id: Some("plugins_123".to_string()),
local_version: None,
name: "demo-plugin".to_string(),
@@ -787,7 +787,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
PluginShareListResponse {
data: vec![PluginShareListItem {
plugin: PluginSummary {
id: "demo-plugin@shared-with-me".to_string(),
id: "demo-plugin@workspace-shared-with-me-private".to_string(),
remote_plugin_id: Some("plugins_123".to_string()),
local_version: None,
name: "demo-plugin".to_string(),

View File

@@ -47,10 +47,15 @@ pub use share::update_remote_plugin_share_targets;
pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "chatgpt-global";
pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "workspace-directory";
pub const REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME: &str = "shared-with-me";
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME: &str =
"workspace-shared-with-me-private";
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME: &str =
"workspace-shared-with-me-unlisted";
pub const REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME: &str = "ChatGPT Plugins";
pub const REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME: &str = "Workspace Directory";
pub const REMOTE_SHARED_WITH_ME_MARKETPLACE_DISPLAY_NAME: &str = "Shared with me";
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_DISPLAY_NAME: &str = "Shared with me";
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_DISPLAY_NAME: &str =
"Shared with me (unlisted)";
const REMOTE_PLUGIN_CATALOG_TIMEOUT: Duration = Duration::from_secs(30);
const REMOTE_PLUGIN_LIST_PAGE_LIMIT: u32 = 200;
@@ -286,9 +291,9 @@ impl RemotePluginScope {
fn from_marketplace_name(name: &str) -> Option<Self> {
match name {
REMOTE_GLOBAL_MARKETPLACE_NAME => Some(Self::Global),
REMOTE_WORKSPACE_MARKETPLACE_NAME | REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME => {
Some(Self::Workspace)
}
REMOTE_WORKSPACE_MARKETPLACE_NAME
| REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME
| REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME => Some(Self::Workspace),
_ => None,
}
}
@@ -388,9 +393,11 @@ fn remote_plugin_canonical_marketplace_name(
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)
RemotePluginShareDiscoverability::Unlisted => {
Ok(REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME)
}
RemotePluginShareDiscoverability::Private => {
Ok(REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME)
}
},
}
@@ -462,43 +469,81 @@ pub async fn fetch_remote_marketplaces(
};
for source in sources {
let marketplace = match source {
match source {
RemoteMarketplaceSource::Global => {
let scope = RemotePluginScope::Global;
let (directory_plugins, installed_plugins) = tokio::try_join!(
fetch_directory_plugins_for_scope(config, auth, scope),
fetch_installed_plugins_for_scope(config, auth, scope),
)?;
build_remote_marketplace(
if let Some(marketplace) = build_remote_marketplace(
scope.marketplace_name(),
scope.marketplace_display_name(),
directory_plugins,
installed_plugins,
/*include_installed_only*/ true,
)?
)? {
marketplaces.push(marketplace);
}
}
RemoteMarketplaceSource::WorkspaceDirectory => {
let scope = RemotePluginScope::Workspace;
let directory_plugins =
fetch_directory_plugins_for_scope(config, auth, scope).await?;
build_remote_marketplace(
if let Some(marketplace) = build_remote_marketplace(
scope.marketplace_name(),
scope.marketplace_display_name(),
directory_plugins,
workspace_installed_plugins.clone().unwrap_or_default(),
/*include_installed_only*/ false,
)?
)? {
marketplaces.push(marketplace);
}
}
RemoteMarketplaceSource::SharedWithMe => {
let private_plugins = fetch_shared_workspace_plugins(config, auth)
.await?
.into_iter()
.filter_map(|plugin| match workspace_plugin_discoverability(&plugin) {
Ok(RemotePluginShareDiscoverability::Private) => Some(Ok(plugin)),
Ok(RemotePluginShareDiscoverability::Listed)
| Ok(RemotePluginShareDiscoverability::Unlisted) => None,
Err(err) => Some(Err(err)),
})
.collect::<Result<Vec<_>, _>>()?;
if let Some(marketplace) = build_remote_marketplace(
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_DISPLAY_NAME,
private_plugins,
workspace_installed_plugins.clone().unwrap_or_default(),
/*include_installed_only*/ false,
)? {
marketplaces.push(marketplace);
}
let unlisted_installed_plugins = workspace_installed_plugins
.clone()
.unwrap_or_default()
.into_iter()
.filter_map(
|plugin| match workspace_plugin_discoverability(&plugin.plugin) {
Ok(RemotePluginShareDiscoverability::Unlisted) => Some(Ok(plugin)),
Ok(RemotePluginShareDiscoverability::Listed)
| Ok(RemotePluginShareDiscoverability::Private) => None,
Err(err) => Some(Err(err)),
},
)
.collect::<Result<Vec<_>, _>>()?;
if let Some(marketplace) = build_remote_marketplace(
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_DISPLAY_NAME,
Vec::new(),
unlisted_installed_plugins,
/*include_installed_only*/ true,
)? {
marketplaces.push(marketplace);
}
}
RemoteMarketplaceSource::SharedWithMe => build_remote_marketplace(
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME,
REMOTE_SHARED_WITH_ME_MARKETPLACE_DISPLAY_NAME,
fetch_shared_workspace_plugins(config, auth).await?,
workspace_installed_plugins.clone().unwrap_or_default(),
/*include_installed_only*/ false,
)?,
};
if let Some(marketplace) = marketplace {
marketplaces.push(marketplace);
}
}

View File

@@ -1,6 +1,7 @@
use super::REMOTE_GLOBAL_MARKETPLACE_NAME;
use super::REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME;
use super::REMOTE_WORKSPACE_MARKETPLACE_NAME;
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
use super::RemotePluginCatalogError;
use super::RemotePluginScope;
use super::RemotePluginServiceConfig;
@@ -153,7 +154,11 @@ pub async fn sync_remote_installed_plugin_bundles_once(
BTreeSet::new(),
),
(
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
(
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
]);
@@ -298,7 +303,8 @@ fn remove_stale_remote_plugin_caches(
for marketplace_name in [
REMOTE_GLOBAL_MARKETPLACE_NAME,
REMOTE_WORKSPACE_MARKETPLACE_NAME,
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME,
] {
let marketplace_root = codex_home.join(PLUGINS_CACHE_DIR).join(marketplace_name);
if !marketplace_root.exists() {
@@ -457,7 +463,11 @@ mod tests {
BTreeSet::new(),
),
(
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
(
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
]);
@@ -500,12 +510,12 @@ mod tests {
}
#[test]
fn stale_remote_plugin_cleanup_removes_shared_with_me_cache() {
fn stale_remote_plugin_cleanup_removes_private_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(REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME)
.join("private-plugin")
.join("1.2.3")
.join(".codex-plugin")
@@ -522,7 +532,11 @@ mod tests {
BTreeSet::new(),
),
(
REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
(
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
]);
@@ -531,9 +545,12 @@ mod tests {
codex_home.path(),
&installed_plugin_names_by_marketplace,
)
.expect("cleanup shared-with-me cache");
.expect("cleanup private shared-with-me cache");
assert_eq!(removed, vec!["private-plugin@shared-with-me".to_string()]);
assert_eq!(
removed,
vec!["private-plugin@workspace-shared-with-me-private".to_string()]
);
assert!(!cached_manifest.exists());
}
}

View File

@@ -586,7 +586,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
vec![
RemotePluginShareSummary {
summary: RemotePluginSummary {
id: "demo-plugin@shared-with-me".to_string(),
id: "demo-plugin@workspace-shared-with-me-private".to_string(),
remote_plugin_id: "plugins_123".to_string(),
name: "demo-plugin".to_string(),
share_context: Some(RemotePluginShareContext {
@@ -625,7 +625,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
},
RemotePluginShareSummary {
summary: RemotePluginSummary {
id: "demo-plugin@shared-with-me".to_string(),
id: "demo-plugin@workspace-shared-with-me-private".to_string(),
remote_plugin_id: "plugins_456".to_string(),
name: "demo-plugin".to_string(),
share_context: Some(RemotePluginShareContext {