mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] expose remote plugin share URL (#27890)
## Summary - expose the remote plugin detail endpoint's `share_url` as nullable `PluginDetail.shareUrl` - preserve existing `PluginSummary.shareContext` behavior for local and workspace sharing flows - regenerate the app-server TypeScript and JSON schema fixtures ## Why The remote plugin detail response already includes a canonical `share_url`, but that value was not surfaced by `plugin/read` for global plugins. Global plugins intentionally have no `shareContext`, so using that model for the URL would change the semantics consumed by the existing share modal. ## User impact Codex clients can use `PluginDetail.shareUrl` for a remote plugin's copy-link action, including when the plugin is disabled by an administrator, without changing existing share-modal or ownership behavior. ## Validation - `cargo test -p codex-app-server plugin_read_includes_share_url_for_admin_disabled_remote_plugin` - `cargo test -p codex-app-server-protocol typescript_schema_fixtures_match_generated` - `cargo test -p codex-app-server-protocol json_schema_fixtures_match_generated` - `cargo fmt --all`
This commit is contained in:
committed by
GitHub
Unverified
parent
52a50aec70
commit
9600afb1a1
+6
@@ -12546,6 +12546,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/SkillSummary"
|
||||
|
||||
+6
@@ -9019,6 +9019,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/SkillSummary"
|
||||
|
||||
@@ -192,6 +192,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/SkillSummary"
|
||||
|
||||
@@ -8,4 +8,4 @@ import type { PluginHookSummary } from "./PluginHookSummary";
|
||||
import type { PluginSummary } from "./PluginSummary";
|
||||
import type { SkillSummary } from "./SkillSummary";
|
||||
|
||||
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, appTemplates: Array<AppTemplateSummary>, mcpServers: Array<string>, };
|
||||
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, shareUrl: string | null, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, appTemplates: Array<AppTemplateSummary>, mcpServers: Array<string>, };
|
||||
|
||||
@@ -638,6 +638,7 @@ pub struct PluginDetail {
|
||||
pub marketplace_name: String,
|
||||
pub marketplace_path: Option<AbsolutePathBuf>,
|
||||
pub summary: PluginSummary,
|
||||
pub share_url: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub skills: Vec<SkillSummary>,
|
||||
pub hooks: Vec<PluginHookSummary>,
|
||||
|
||||
@@ -206,7 +206,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, 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. `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. 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. 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 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. 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/list` — list available apps.
|
||||
|
||||
@@ -1070,6 +1070,7 @@ impl PluginRequestProcessor {
|
||||
interface: outcome.plugin.interface.map(local_plugin_interface_to_info),
|
||||
keywords: outcome.plugin.keywords,
|
||||
},
|
||||
share_url: None,
|
||||
description: outcome.plugin.description,
|
||||
skills: plugin_skills_to_info(
|
||||
&visible_skills,
|
||||
@@ -2081,6 +2082,7 @@ fn remote_plugin_detail_to_info(
|
||||
marketplace_name: detail.marketplace_name,
|
||||
marketplace_path: None,
|
||||
summary: remote_plugin_summary_to_info(detail.summary),
|
||||
share_url: detail.share_url,
|
||||
description: detail.description,
|
||||
skills: detail
|
||||
.skills
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
@@ -392,7 +393,7 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() -> Result<()> {
|
||||
async fn plugin_read_includes_share_url_for_admin_disabled_remote_plugin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_catalog_config(
|
||||
@@ -410,29 +411,31 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
|
||||
let detail_body = r#"{
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "linear",
|
||||
"name": "example-plugin",
|
||||
"scope": "GLOBAL",
|
||||
"share_url": "https://chatgpt.example/plugins/share/example-plugin",
|
||||
"status": "DISABLED_BY_ADMIN",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"description": "Track work in Linear",
|
||||
"display_name": "Example Plugin",
|
||||
"description": "Exercise example workflows",
|
||||
"app_ids": [],
|
||||
"app_templates": [
|
||||
{
|
||||
"template_id": "templated_apps_GitHubEnterprise",
|
||||
"name": "GitHub Enterprise",
|
||||
"description": "Connect GitHub Enterprise",
|
||||
"template_id": "templated_apps_SourceControlEnterprise",
|
||||
"name": "Source Control Enterprise",
|
||||
"description": "Connect source control",
|
||||
"category": "Developer Tools",
|
||||
"canonical_connector_id": "github_enterprise",
|
||||
"logo_url": "https://example.com/ghe-light.png",
|
||||
"logo_url_dark": "https://example.com/ghe-dark.png",
|
||||
"materialized_app_ids": ["asdk_app_ghe"],
|
||||
"canonical_connector_id": "source_control_enterprise",
|
||||
"logo_url": "https://example.com/source-control-light.png",
|
||||
"logo_url_dark": "https://example.com/source-control-dark.png",
|
||||
"materialized_app_ids": ["asdk_app_source_control"],
|
||||
"reason": null
|
||||
},
|
||||
{
|
||||
"template_id": "templated_apps_Databricks",
|
||||
"name": "Databricks",
|
||||
"template_id": "templated_apps_DataWarehouse",
|
||||
"name": "Data Warehouse",
|
||||
"description": null,
|
||||
"canonical_connector_id": null,
|
||||
"logo_url": null,
|
||||
@@ -441,19 +444,19 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
"reason": "NOT_CONFIGURED_FOR_WORKSPACE"
|
||||
}
|
||||
],
|
||||
"keywords": ["issue-tracking", "project management"],
|
||||
"keywords": ["workflow", "example"],
|
||||
"interface": {
|
||||
"short_description": "Plan and track work",
|
||||
"short_description": "Run example workflows",
|
||||
"capabilities": ["Read", "Write"],
|
||||
"default_prompt": "Use the legacy Linear prompt",
|
||||
"default_prompts": ["Create a Linear issue", "Review my Linear projects"],
|
||||
"logo_url": "https://example.com/linear.png",
|
||||
"screenshot_urls": ["https://example.com/linear-shot.png"]
|
||||
"default_prompt": "Use the legacy example prompt",
|
||||
"default_prompts": ["Create an example item", "Review example projects"],
|
||||
"logo_url": "https://example.com/example-plugin.png",
|
||||
"screenshot_urls": ["https://example.com/example-plugin-shot.png"]
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"name": "plan-work",
|
||||
"description": "Plan work from Linear issues",
|
||||
"description": "Plan example work",
|
||||
"plugin_release_skill_id": "skill-1",
|
||||
"interface": {
|
||||
"display_name": "Plan Work",
|
||||
@@ -467,24 +470,24 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
"plugins": [
|
||||
{
|
||||
"id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"name": "linear",
|
||||
"name": "example-plugin",
|
||||
"scope": "GLOBAL",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"description": "Track work in Linear",
|
||||
"display_name": "Example Plugin",
|
||||
"description": "Exercise example workflows",
|
||||
"app_ids": [],
|
||||
"interface": {
|
||||
"short_description": "Plan and track work",
|
||||
"short_description": "Run example workflows",
|
||||
"capabilities": ["Read", "Write"],
|
||||
"logo_url": "https://example.com/linear.png",
|
||||
"screenshot_urls": ["https://example.com/linear-shot.png"]
|
||||
"logo_url": "https://example.com/example-plugin.png",
|
||||
"screenshot_urls": ["https://example.com/example-plugin-shot.png"]
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"name": "plan-work",
|
||||
"description": "Plan work from Linear issues",
|
||||
"description": "Plan example work",
|
||||
"plugin_release_skill_id": "skill-1",
|
||||
"interface": {
|
||||
"display_name": "Plan Work",
|
||||
@@ -542,24 +545,33 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
assert_eq!(response.plugin.marketplace_name, "openai-curated-remote");
|
||||
assert_eq!(response.plugin.marketplace_path, None);
|
||||
assert_eq!(response.plugin.summary.source, PluginSource::Remote);
|
||||
assert_eq!(response.plugin.summary.id, "linear@openai-curated-remote");
|
||||
assert_eq!(
|
||||
response.plugin.summary.id,
|
||||
"example-plugin@openai-curated-remote"
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.summary.remote_plugin_id.as_deref(),
|
||||
Some("plugins~Plugin_00000000000000000000000000000000")
|
||||
);
|
||||
assert_eq!(response.plugin.summary.name, "linear");
|
||||
assert_eq!(response.plugin.summary.name, "example-plugin");
|
||||
assert_eq!(response.plugin.summary.installed, true);
|
||||
assert_eq!(response.plugin.summary.enabled, false);
|
||||
assert_eq!(
|
||||
response.plugin.summary.availability,
|
||||
PluginAvailability::DisabledByAdmin
|
||||
);
|
||||
assert_eq!(response.plugin.summary.share_context, None);
|
||||
assert_eq!(
|
||||
response.plugin.share_url.as_deref(),
|
||||
Some("https://chatgpt.example/plugins/share/example-plugin")
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.description.as_deref(),
|
||||
Some("Track work in Linear")
|
||||
Some("Exercise example workflows")
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.summary.keywords,
|
||||
vec![
|
||||
"issue-tracking".to_string(),
|
||||
"project management".to_string()
|
||||
]
|
||||
vec!["workflow".to_string(), "example".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
@@ -569,8 +581,8 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.default_prompt.clone()),
|
||||
Some(vec![
|
||||
"Create a Linear issue".to_string(),
|
||||
"Review my Linear projects".to_string(),
|
||||
"Create an example item".to_string(),
|
||||
"Review example projects".to_string(),
|
||||
])
|
||||
);
|
||||
assert_eq!(response.plugin.skills.len(), 1);
|
||||
@@ -582,19 +594,19 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
|
||||
response.plugin.app_templates,
|
||||
vec![
|
||||
AppTemplateSummary {
|
||||
template_id: "templated_apps_GitHubEnterprise".to_string(),
|
||||
name: "GitHub Enterprise".to_string(),
|
||||
description: Some("Connect GitHub Enterprise".to_string()),
|
||||
template_id: "templated_apps_SourceControlEnterprise".to_string(),
|
||||
name: "Source Control Enterprise".to_string(),
|
||||
description: Some("Connect source control".to_string()),
|
||||
category: Some("Developer Tools".to_string()),
|
||||
canonical_connector_id: Some("github_enterprise".to_string()),
|
||||
logo_url: Some("https://example.com/ghe-light.png".to_string()),
|
||||
logo_url_dark: Some("https://example.com/ghe-dark.png".to_string()),
|
||||
materialized_app_ids: vec!["asdk_app_ghe".to_string()],
|
||||
canonical_connector_id: Some("source_control_enterprise".to_string()),
|
||||
logo_url: Some("https://example.com/source-control-light.png".to_string()),
|
||||
logo_url_dark: Some("https://example.com/source-control-dark.png".to_string()),
|
||||
materialized_app_ids: vec!["asdk_app_source_control".to_string()],
|
||||
reason: None,
|
||||
},
|
||||
AppTemplateSummary {
|
||||
template_id: "templated_apps_Databricks".to_string(),
|
||||
name: "Databricks".to_string(),
|
||||
template_id: "templated_apps_DataWarehouse".to_string(),
|
||||
name: "Data Warehouse".to_string(),
|
||||
description: None,
|
||||
category: None,
|
||||
canonical_connector_id: None,
|
||||
|
||||
@@ -163,6 +163,7 @@ pub struct RemotePluginDetail {
|
||||
pub marketplace_name: String,
|
||||
pub marketplace_display_name: String,
|
||||
pub summary: RemotePluginSummary,
|
||||
pub share_url: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub release_version: Option<String>,
|
||||
pub bundle_download_url: Option<String>,
|
||||
@@ -1048,6 +1049,7 @@ async fn build_remote_plugin_detail(
|
||||
marketplace_name,
|
||||
marketplace_display_name: scope.marketplace_display_name().to_string(),
|
||||
summary: build_remote_plugin_summary(&plugin, installed_plugin.as_ref())?,
|
||||
share_url: plugin.share_url,
|
||||
description: non_empty_string(Some(&plugin.release.description)),
|
||||
release_version: plugin.release.version,
|
||||
bundle_download_url: plugin.release.bundle_download_url,
|
||||
|
||||
@@ -1421,6 +1421,7 @@ pub(super) fn plugins_test_detail(
|
||||
marketplace_name: "ChatGPT Marketplace".to_string(),
|
||||
marketplace_path: Some(plugins_test_absolute_path("marketplaces/chatgpt")),
|
||||
summary,
|
||||
share_url: None,
|
||||
description: description.map(str::to_string),
|
||||
skills: skills
|
||||
.iter()
|
||||
|
||||
@@ -803,6 +803,7 @@ async fn plugin_detail_remote_install_uses_remote_location() {
|
||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
||||
marketplace_path: None,
|
||||
summary,
|
||||
share_url: None,
|
||||
description: Some("Install shared Linear plugin.".to_string()),
|
||||
skills: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
@@ -877,6 +878,7 @@ async fn plugin_detail_remote_uninstall_uses_remote_plugin_id() {
|
||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
||||
marketplace_path: None,
|
||||
summary,
|
||||
share_url: None,
|
||||
description: Some("Installed shared Linear plugin.".to_string()),
|
||||
skills: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
@@ -949,6 +951,7 @@ async fn plugin_detail_remote_without_remote_id_disables_uninstall_action() {
|
||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
||||
marketplace_path: None,
|
||||
summary,
|
||||
share_url: None,
|
||||
description: Some("Installed shared Linear plugin.".to_string()),
|
||||
skills: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
|
||||
Reference in New Issue
Block a user