Refresh installed plugin cache for workspace directory

This commit is contained in:
Alex Phonpradith
2026-06-24 12:11:37 -07:00
parent 54460489d0
commit cbb4d694f7
3 changed files with 110 additions and 6 deletions

View File

@@ -212,10 +212,24 @@ 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, 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. 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, 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. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. Pass `forceRefetch: true` for user-initiated refreshes that should bypass app/tool caches and schedule a fresh remote installed-plugin cache load after external app authentication changes. `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. 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.
Example `plugin/list` refresh request after external app authentication changes:
```json
{
"method": "plugin/list",
"id": 41,
"params": {
"marketplaceKinds": ["workspace-directory"],
"forceRefetch": true
}
}
```
- `skills/changed` — notification emitted when watched local skill files change.
- `app/list` — list available apps.
- `remoteControl/enable` — experimental; enable remote control for the current app-server process and return the current remote-control status snapshot. By default, any missing enrollment is completed before the response and the preference is persisted for the current app-server client scope. Pass `ephemeral: true` to enable remote control only for the current process without changing the persisted preference.

View File

@@ -738,11 +738,8 @@ impl PluginRequestProcessor {
}
}
}
if include_local
|| include_created_by_me_remote
|| include_shared_with_me
|| include_global_remote
{
let include_remote_sources = !remote_sources.is_empty();
if include_local || include_remote_sources {
let on_effective_plugins_changed = Some(self.effective_plugins_changed_callback());
plugins_manager.maybe_start_plugin_list_background_tasks_for_config(
&plugins_input,

View File

@@ -2855,6 +2855,68 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag
Ok(())
}
#[tokio::test]
async fn plugin_list_force_refetch_refreshes_installed_cache_for_workspace_directory() -> Result<()>
{
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugins_enabled_config_with_base_url(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let workspace_plugin_body = workspace_remote_plugin_page_body(
"plugins~Plugin_11111111111111111111111111111111",
"workspace-linear",
"Workspace Linear",
"LISTED",
/*enabled*/ None,
);
let workspace_installed_body = workspace_remote_plugin_page_body(
"plugins~Plugin_11111111111111111111111111111111",
"workspace-linear",
"Workspace Linear",
"LISTED",
/*enabled*/ Some(true),
);
mount_remote_plugin_list(&server, "WORKSPACE", &workspace_plugin_body).await;
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
mount_empty_user_installed_plugins(&server).await;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: None,
marketplace_kinds: Some(vec![PluginListMarketplaceKind::WorkspaceDirectory]),
force_refetch: true,
})
.await?;
let response: PluginListResponse = to_response(
timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??,
)?;
assert_eq!(response.marketplaces.len(), 1);
assert_eq!(response.marketplaces[0].name, "workspace-directory");
wait_for_remote_installed_scope_request_count_at_least(&server, "WORKSPACE", 2).await?;
Ok(())
}
#[tokio::test]
async fn plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -3640,6 +3702,37 @@ async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &st
Ok(())
}
async fn wait_for_remote_installed_scope_request_count_at_least(
server: &MockServer,
scope: &str,
expected_count: usize,
) -> Result<()> {
timeout(DEFAULT_TIMEOUT, async {
loop {
let Some(requests) = server.received_requests().await else {
bail!("wiremock did not record requests");
};
let request_count = requests
.iter()
.filter(|request| {
request.method == "GET"
&& request.url.path().ends_with("/ps/plugins/installed")
&& request
.url
.query_pairs()
.any(|(name, value)| name == "scope" && value == scope)
})
.count();
if request_count >= expected_count {
return Ok::<(), anyhow::Error>(());
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await??;
Ok(())
}
async fn wait_for_cached_remote_catalog_plugin_ids(
codex_home: &std::path::Path,
expected_plugin_ids: &[&str],