mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Expose workspace plugin publish capability (#35254)
## What changed - Add nullable `canPublishToWorkspace` metadata to plugin share contexts and `plugin/share/save` responses. - Preserve the remote capability through catalog parsing and app-server responses so clients can decide whether to offer workspace-directory publishing. - Document that clients should fail closed when the capability is unavailable. ## Testing - Cover remote catalog parsing, share-save response propagation, and protocol serialization. GitOrigin-RevId: b0346b3b330169838f800dfdf21dfa5db1a47f4d
This commit is contained in:
+14
@@ -14595,6 +14595,13 @@
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -14797,6 +14804,13 @@
|
||||
"PluginShareSaveResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+14
@@ -10937,6 +10937,13 @@
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -11139,6 +11146,13 @@
|
||||
"PluginShareSaveResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -259,6 +259,13 @@
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -259,6 +259,13 @@
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -409,6 +409,13 @@
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -194,6 +194,13 @@
|
||||
},
|
||||
"PluginShareContext": {
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"creatorAccountUserId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"canPublishToWorkspace": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -8,4 +8,4 @@ export type PluginShareContext = { remotePluginId: string,
|
||||
/**
|
||||
* Version of the remote shared plugin release when available.
|
||||
*/
|
||||
remoteVersion: string | null, discoverability: PluginShareDiscoverability | null, shareUrl: string | null, creatorAccountUserId: string | null, creatorName: string | null, sharePrincipals: Array<PluginSharePrincipal> | null, };
|
||||
remoteVersion: string | null, discoverability: PluginShareDiscoverability | null, shareUrl: string | null, creatorAccountUserId: string | null, creatorName: string | null, sharePrincipals: Array<PluginSharePrincipal> | null, canPublishToWorkspace: boolean | null, };
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginShareSaveResponse = { remotePluginId: string, shareUrl: string, };
|
||||
export type PluginShareSaveResponse = { remotePluginId: string, shareUrl: string, canPublishToWorkspace: boolean | null, };
|
||||
|
||||
@@ -254,6 +254,8 @@ pub struct PluginShareSaveParams {
|
||||
pub struct PluginShareSaveResponse {
|
||||
pub remote_plugin_id: String,
|
||||
pub share_url: String,
|
||||
#[serde(default)]
|
||||
pub can_publish_to_workspace: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -662,6 +664,8 @@ pub struct PluginShareContext {
|
||||
pub creator_account_user_id: Option<String>,
|
||||
pub creator_name: Option<String>,
|
||||
pub share_principals: Option<Vec<PluginSharePrincipal>>,
|
||||
#[serde(default)]
|
||||
pub can_publish_to_workspace: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -3701,11 +3701,13 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
serde_json::to_value(PluginShareSaveResponse {
|
||||
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
share_url: String::new(),
|
||||
can_publish_to_workspace: Some(true),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"shareUrl": "",
|
||||
"canPublishToWorkspace": true,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ Example with notification opt-out:
|
||||
- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors.
|
||||
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Every `PluginSummary` returned by plugin list, installed, read, and share-list methods includes `mustShowInstallationInterstitial`: remote service values preserve `true` or `false`, while local plugins and remote responses that omit the policy return `null`. Clients should fail closed when the value is `null`. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. Set `forceRefetch: true` to bypass TTL-backed remote catalog caches for the requested marketplaces and wait for fresh data; cache entries are replaced only after a successful fetch. When local marketplaces are included, the request also waits for configured plugin caches to reconcile before marketplace summaries are returned. At app-server startup, existing cached catalogs remain available to `plugin/list` while they refresh in the background. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
|
||||
- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Remote rows include nullable `installPolicySource`; local rows return `null`. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
|
||||
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details can include scheduled task summaries from the catalog; `scheduledTasks: null` means the metadata is unavailable, while an empty array means the catalog found no scheduled tasks. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. Remote skill interfaces expose `iconSmallUrl` and `iconLargeUrl` when the catalog supplies icon URLs. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details can include scheduled task summaries from the catalog; `scheduledTasks: null` means the metadata is unavailable, while an empty array means the catalog found no scheduled tasks. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. For owned workspace plugins, `summary.shareContext.canPublishToWorkspace` reports whether the current user may add the plugin to the workspace directory; `plugin/share/save` returns the same capability after creating or updating a share, and clients should fail closed when either value is `null`. Remote skill interfaces expose `iconSmallUrl` and `iconLargeUrl` when the catalog supplies icon URLs. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `app/installed` — read installed connector runtime state from the last committed snapshot, optionally refreshing it first.
|
||||
|
||||
@@ -157,6 +157,7 @@ fn share_context_for_source(
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_principals: None,
|
||||
can_publish_to_workspace: None,
|
||||
}),
|
||||
MarketplacePluginSource::Git { .. } | MarketplacePluginSource::Npm { .. } => None,
|
||||
}
|
||||
@@ -1054,6 +1055,8 @@ impl PluginRequestProcessor {
|
||||
Some(remote_plugin_share_context_to_info(remote_share_context))
|
||||
} else {
|
||||
let remote_version = remote_share_context.remote_version;
|
||||
let can_publish_to_workspace =
|
||||
remote_share_context.can_publish_to_workspace;
|
||||
let remote_plugin_id = context.remote_plugin_id.clone();
|
||||
warn!(
|
||||
remote_plugin_id = %remote_plugin_id,
|
||||
@@ -1061,6 +1064,7 @@ impl PluginRequestProcessor {
|
||||
);
|
||||
Some(PluginShareContext {
|
||||
remote_version,
|
||||
can_publish_to_workspace,
|
||||
..context
|
||||
})
|
||||
}
|
||||
@@ -1288,6 +1292,7 @@ impl PluginRequestProcessor {
|
||||
Ok(PluginShareSaveResponse {
|
||||
remote_plugin_id,
|
||||
share_url: result.share_url.unwrap_or_default(),
|
||||
can_publish_to_workspace: result.can_publish_to_workspace,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2211,6 +2216,7 @@ fn remote_plugin_share_context_to_info(
|
||||
.map(plugin_share_principal_from_remote)
|
||||
.collect()
|
||||
}),
|
||||
can_publish_to_workspace: context.can_publish_to_workspace,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
|
||||
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
||||
"plugin_id": "plugins_123",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"can_publish_to_workspace": true,
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
@@ -123,6 +124,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
|
||||
PluginShareSaveResponse {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
can_publish_to_workspace: Some(true),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -278,6 +280,7 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
|
||||
PluginShareSaveResponse {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
can_publish_to_workspace: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
@@ -1361,6 +1364,7 @@ fn remote_plugin_json(plugin_id: &str) -> serde_json::Value {
|
||||
"name": "demo-plugin",
|
||||
"scope": "WORKSPACE",
|
||||
"discoverability": "PRIVATE",
|
||||
"can_publish_to_workspace": true,
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"share_principals": [
|
||||
{
|
||||
@@ -1454,6 +1458,7 @@ fn expected_share_context(plugin_id: &str) -> PluginShareContext {
|
||||
name: "Reader".to_string(),
|
||||
},
|
||||
]),
|
||||
can_publish_to_workspace: Some(true),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,7 @@ pub struct RemotePluginShareContext {
|
||||
pub creator_account_user_id: Option<String>,
|
||||
pub creator_name: Option<String>,
|
||||
pub share_principals: Option<Vec<RemotePluginSharePrincipal>>,
|
||||
pub can_publish_to_workspace: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -628,6 +629,8 @@ struct RemotePluginDirectoryItem {
|
||||
share_url: Option<String>,
|
||||
#[serde(default)]
|
||||
share_principals: Option<Vec<RemotePluginDirectorySharePrincipal>>,
|
||||
#[serde(default)]
|
||||
can_publish_to_workspace: Option<bool>,
|
||||
installation_policy: PluginInstallPolicy,
|
||||
installation_policy_source: Option<RemotePluginInstallPolicySource>,
|
||||
#[serde(default)]
|
||||
@@ -1690,6 +1693,7 @@ fn remote_plugin_share_context(
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
can_publish_to_workspace: plugin.can_publish_to_workspace,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub use checkout::checkout_remote_plugin_share;
|
||||
pub struct RemotePluginShareSaveResult {
|
||||
pub remote_plugin_id: String,
|
||||
pub share_url: Option<String>,
|
||||
pub can_publish_to_workspace: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
@@ -123,6 +124,8 @@ struct RemoteWorkspacePluginCreateRequest {
|
||||
struct RemoteWorkspacePluginCreateResponse {
|
||||
plugin_id: String,
|
||||
share_url: Option<String>,
|
||||
#[serde(default)]
|
||||
can_publish_to_workspace: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
@@ -201,6 +204,7 @@ pub async fn save_remote_plugin_share(
|
||||
Ok(RemotePluginShareSaveResult {
|
||||
remote_plugin_id: response.plugin_id,
|
||||
share_url: response.share_url,
|
||||
can_publish_to_workspace: response.can_publish_to_workspace,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
||||
"plugin_id": "plugins_123",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
"can_publish_to_workspace": true,
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
@@ -258,6 +259,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
RemotePluginShareSaveResult {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
|
||||
can_publish_to_workspace: Some(true),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -435,6 +437,7 @@ async fn save_remote_plugin_share_updates_existing_workspace_plugin() {
|
||||
RemotePluginShareSaveResult {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
share_url: None,
|
||||
can_publish_to_workspace: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -659,6 +662,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
name: "Reader".to_string(),
|
||||
},
|
||||
]),
|
||||
can_publish_to_workspace: None,
|
||||
}),
|
||||
installed: false,
|
||||
enabled: false,
|
||||
@@ -700,6 +704,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
name: "Editor".to_string(),
|
||||
},
|
||||
]),
|
||||
can_publish_to_workspace: None,
|
||||
}),
|
||||
installed: true,
|
||||
enabled: true,
|
||||
|
||||
@@ -241,6 +241,20 @@ fn scheduled_task_metadata_distinguishes_unavailable_from_empty() {
|
||||
assert_eq!(with_empty_metadata.scheduled_tasks, Some(Vec::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_share_context_preserves_publish_capability() {
|
||||
let mut plugin = directory_plugin("plugin-workspace", "workspace plugin");
|
||||
plugin.scope = RemotePluginScope::Workspace;
|
||||
plugin.discoverability = Some(RemotePluginShareDiscoverability::Private);
|
||||
plugin.can_publish_to_workspace = Some(true);
|
||||
|
||||
let context = remote_plugin_share_context(&plugin)
|
||||
.expect("workspace plugin should be valid")
|
||||
.expect("workspace plugin should have share context");
|
||||
|
||||
assert_eq!(context.can_publish_to_workspace, Some(true));
|
||||
}
|
||||
|
||||
fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
|
||||
RemotePluginDirectoryItem {
|
||||
id: id.to_string(),
|
||||
@@ -251,6 +265,7 @@ fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
|
||||
creator_name: None,
|
||||
share_url: None,
|
||||
share_principals: None,
|
||||
can_publish_to_workspace: None,
|
||||
installation_policy: PluginInstallPolicy::Available,
|
||||
installation_policy_source: None,
|
||||
must_show_installation_interstitial: None,
|
||||
|
||||
@@ -156,6 +156,7 @@ mod tests {
|
||||
creator_account_user_id: None,
|
||||
creator_name: Some("Test User".to_string()),
|
||||
share_principals: None,
|
||||
can_publish_to_workspace: None,
|
||||
}),
|
||||
..plugin_summary(name)
|
||||
}
|
||||
|
||||
@@ -1045,6 +1045,7 @@ async fn plugin_detail_popup_shows_local_share_context_as_read_only_snapshot() {
|
||||
creator_account_user_id: None,
|
||||
creator_name: Some("Test User".to_string()),
|
||||
share_principals: None,
|
||||
can_publish_to_workspace: None,
|
||||
}),
|
||||
..plugins_test_summary(
|
||||
"plugin-docs",
|
||||
@@ -1326,6 +1327,7 @@ async fn plugins_popup_remote_detail_tracks_physical_and_policy_install_state()
|
||||
creator_account_user_id: None,
|
||||
creator_name: None,
|
||||
share_principals: None,
|
||||
can_publish_to_workspace: None,
|
||||
}),
|
||||
..plugins_test_summary(
|
||||
"plugin-docs",
|
||||
|
||||
Reference in New Issue
Block a user