diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8dd77d38d..f8ddf9329 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12546,6 +12546,12 @@ }, "type": "array" }, + "shareUrl": { + "type": [ + "string", + "null" + ] + }, "skills": { "items": { "$ref": "#/definitions/v2/SkillSummary" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index de2d8ad74..e04aca08f 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9019,6 +9019,12 @@ }, "type": "array" }, + "shareUrl": { + "type": [ + "string", + "null" + ] + }, "skills": { "items": { "$ref": "#/definitions/SkillSummary" diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json index 04919f859..bef85afdd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -192,6 +192,12 @@ }, "type": "array" }, + "shareUrl": { + "type": [ + "string", + "null" + ] + }, "skills": { "items": { "$ref": "#/definitions/SkillSummary" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts index cc2042dd5..ab2e3fd5f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginDetail.ts @@ -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, hooks: Array, apps: Array, appTemplates: Array, mcpServers: Array, }; +export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, shareUrl: string | null, description: string | null, skills: Array, hooks: Array, apps: Array, appTemplates: Array, mcpServers: Array, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs index c196fba91..2bf187210 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -638,6 +638,7 @@ pub struct PluginDetail { pub marketplace_name: String, pub marketplace_path: Option, pub summary: PluginSummary, + pub share_url: Option, pub description: Option, pub skills: Vec, pub hooks: Vec, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 0e0e9b856..956bbb49c 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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. diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index e4971b36f..cd4fccdca 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -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 diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 51bbd75bf..7b253cd68 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -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, diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index b948c5173..a17dc2586 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -163,6 +163,7 @@ pub struct RemotePluginDetail { pub marketplace_name: String, pub marketplace_display_name: String, pub summary: RemotePluginSummary, + pub share_url: Option, pub description: Option, pub release_version: Option, pub bundle_download_url: Option, @@ -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, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index c8d8fdc15..517f8e439 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -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() diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 736be9fa2..f530ef9f3 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -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(),