diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 8321806066..7821bb11b0 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2118,6 +2118,10 @@ "null" ] }, + "forceRefetch": { + "description": "Whether the client requests a fresh remote plugin catalog fetch.", + "type": "boolean" + }, "marketplaceKinds": { "description": "Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", "items": { 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 f6487a69c7..8f5d8087b3 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 @@ -14257,6 +14257,10 @@ "null" ] }, + "forceRefetch": { + "description": "Whether the client requests a fresh remote plugin catalog fetch.", + "type": "boolean" + }, "marketplaceKinds": { "description": "Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", "items": { 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 923226f332..04829826c1 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 @@ -10599,6 +10599,10 @@ "null" ] }, + "forceRefetch": { + "description": "Whether the client requests a fresh remote plugin catalog fetch.", + "type": "boolean" + }, "marketplaceKinds": { "description": "Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", "items": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json index 997c83dc74..0c47b9ac65 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json @@ -27,6 +27,10 @@ "null" ] }, + "forceRefetch": { + "description": "Whether the client requests a fresh remote plugin catalog fetch.", + "type": "boolean" + }, "marketplaceKinds": { "description": "Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", "items": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts index 6dd86b8a41..ecd122542b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts @@ -14,4 +14,8 @@ cwds?: Array | null, * Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus * the default remote catalog when enabled by feature flag. */ -marketplaceKinds?: Array | null, }; +marketplaceKinds?: Array | null, +/** + * Whether the client requests a fresh remote plugin catalog fetch. + */ +forceRefetch?: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index a4d87e202c..91b43db079 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1938,6 +1938,7 @@ mod tests { params: v2::PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }, }; assert_eq!(plugin_list.serialization_scope(), None); 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 dcf2dbf8b3..146da5386c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -135,6 +135,9 @@ pub struct PluginListParams { /// the default remote catalog when enabled by feature flag. #[ts(optional = nullable)] pub marketplace_kinds: Option>, + /// Whether the client requests a fresh remote plugin catalog fetch. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub force_refetch: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 4ab7d6cac0..7fce5e3f02 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3354,6 +3354,22 @@ fn plugin_list_params_ignore_removed_force_remote_sync_field() { PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, + }, + ); +} + +#[test] +fn plugin_list_params_deserializes_force_refetch() { + assert_eq!( + serde_json::from_value::(json!({ + "forceRefetch": true, + })) + .unwrap(), + PluginListParams { + cwds: None, + marketplace_kinds: None, + force_refetch: true, }, ); } @@ -3370,6 +3386,7 @@ fn plugin_list_params_serializes_marketplace_kind_filter() { PluginListMarketplaceKind::SharedWithMe, PluginListMarketplaceKind::CreatedByMeRemote, ]), + force_refetch: false, }) .unwrap(), json!({ diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index eedc4e5ffa..449d9fc5bb 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -216,7 +216,7 @@ Example with notification opt-out: - `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present. - `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists. - `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. `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/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. The optional `forceRefetch` request field is currently accepted but has no effect. `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. 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. diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index a56a5e376f..c9e1b29ce7 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -540,6 +540,7 @@ impl PluginRequestProcessor { let PluginListParams { cwds, marketplace_kinds, + force_refetch: _, } = params; let roots = cwds.unwrap_or_default(); let explicit_marketplace_kinds = marketplace_kinds.is_some(); diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index f9cbe72c94..fd03838e7c 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -499,6 +499,7 @@ source = {:?} .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -1473,6 +1474,7 @@ async fn external_agent_config_import_reinstalls_plugins_from_known_marketplaces .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 91e409e3e1..e056fa60db 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -121,6 +121,7 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -479,6 +480,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ AbsolutePathBuf::try_from(invalid_repo_root.path())?, ]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -598,6 +600,7 @@ async fn plugin_list_returns_empty_when_workspace_codex_plugins_disabled() -> Re .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -692,6 +695,7 @@ async fn plugin_list_reuses_cached_workspace_codex_plugins_setting() -> Result<( .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -777,6 +781,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -899,6 +904,7 @@ async fn plugin_list_accepts_omitted_cwds() -> Result<()> { .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -956,6 +962,7 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<( .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1052,6 +1059,7 @@ enabled = false .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1210,6 +1218,7 @@ enabled = false AbsolutePathBuf::try_from(workspace_default.path())?, ]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1298,6 +1307,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1415,6 +1425,7 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> { .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1507,6 +1518,7 @@ enabled = true .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1700,6 +1712,7 @@ async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles() .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -1891,6 +1904,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -2062,6 +2076,7 @@ async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Res .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: PluginListResponse = to_response( @@ -2099,6 +2114,7 @@ async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Res .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: PluginListResponse = to_response( @@ -2186,6 +2202,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_remote_plugi .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2280,6 +2297,7 @@ async fn plugin_list_propagates_openai_curated_remote_collection_errors_when_rem .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let err = timeout( @@ -2324,6 +2342,7 @@ async fn plugin_list_skips_openai_curated_remote_collection_for_api_auth_when_re .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2367,6 +2386,7 @@ async fn plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_p .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2426,6 +2446,7 @@ async fn plugin_list_does_not_query_openai_curated_remote_collection_by_default( .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2483,6 +2504,7 @@ async fn plugin_list_vertical_kind_noops_when_remote_plugin_enabled() -> Result< .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2541,6 +2563,7 @@ async fn plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_ex .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Local]), + force_refetch: false, }) .await?; @@ -3089,6 +3112,7 @@ async fn plugin_list_fetches_workspace_directory_kind_when_remote_plugin_disable .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::WorkspaceDirectory]), + force_refetch: false, }) .await?; @@ -3219,6 +3243,7 @@ plugin_sharing = false .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::CreatedByMeRemote]), + force_refetch: false, }) .await?; @@ -3350,6 +3375,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::SharedWithMe]), + force_refetch: false, }) .await?; @@ -3511,6 +3537,7 @@ plugin_sharing = false .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::SharedWithMe]), + force_refetch: false, }) .await?; @@ -3575,6 +3602,7 @@ plugin_sharing = true .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::CreatedByMeRemote]), + force_refetch: false, }) .await?; @@ -3709,6 +3737,7 @@ async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()> { .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -3773,6 +3802,7 @@ remote_plugin = true .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -3813,6 +3843,7 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -3858,6 +3889,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_share.rs b/codex-rs/app-server/tests/suite/v2/plugin_share.rs index a3445a562a..5e6196d12f 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -753,6 +753,7 @@ async fn plugin_share_checkout_adds_personal_marketplace_entry() -> Result<()> { marketplace_kinds: Some(vec![ codex_app_server_protocol::PluginListMarketplaceKind::Local, ]), + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index c17a54b844..285c8b4593 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -394,6 +394,7 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; let plugin_list_response: JSONRPCResponse = timeout( diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 0036495a8d..69f0612f18 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -1028,6 +1028,7 @@ async fn request_plugin_list_with_marketplace_kinds( params: PluginListParams { cwds: Some(vec![cwd]), marketplace_kinds, + force_refetch: false, }, }) .await