diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 68c242504f..dae773dad9 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1905,6 +1905,10 @@ "null" ] }, + "forceRefetch": { + "description": "When true, bypass app/tool caches that can stay stale after external app re-auth.", + "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 4bbf58a12e..36a8742a79 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 @@ -13525,6 +13525,10 @@ "null" ] }, + "forceRefetch": { + "description": "When true, bypass app/tool caches that can stay stale after external app re-auth.", + "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 9cae10233e..9c4be3f545 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 @@ -9929,6 +9929,10 @@ "null" ] }, + "forceRefetch": { + "description": "When true, bypass app/tool caches that can stay stale after external app re-auth.", + "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..55253a085d 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": "When true, bypass app/tool caches that can stay stale after external app re-auth.", + "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..708ef62369 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, +/** + * When true, bypass app/tool caches that can stay stale after external app re-auth. + */ +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 51a37fbaa9..24cdd5a27d 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1875,6 +1875,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 dbe0b3d55d..d5a7886cc4 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>, + /// When true, bypass app/tool caches that can stay stale after external app re-auth. + #[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 7c78fee4c8..cee7f0089e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3032,6 +3032,7 @@ fn plugin_list_params_ignore_removed_force_remote_sync_field() { PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }, ); } @@ -3048,6 +3049,7 @@ fn plugin_list_params_serializes_marketplace_kind_filter() { PluginListMarketplaceKind::SharedWithMe, PluginListMarketplaceKind::CreatedByMeRemote, ]), + force_refetch: false, }) .unwrap(), json!({ @@ -3063,6 +3065,23 @@ fn plugin_list_params_serializes_marketplace_kind_filter() { ); } +#[test] +fn plugin_list_params_serializes_force_refetch_when_enabled() { + assert_eq!( + serde_json::to_value(PluginListParams { + cwds: None, + marketplace_kinds: None, + force_refetch: true, + }) + .unwrap(), + json!({ + "cwds": null, + "marketplaceKinds": null, + "forceRefetch": true, + }), + ); +} + #[test] fn plugin_installed_params_serializes_install_suggestion_names() { assert_eq!( diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index a98fddb5bc..b8bdb168d5 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -533,6 +533,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(); @@ -560,6 +561,10 @@ impl PluginRequestProcessor { let auth_mode = auth.as_ref().map(CodexAuth::api_auth_mode); plugins_manager.set_auth_mode(auth_mode); let plugins_input = config.plugins_config_input(); + if force_refetch { + self.force_refresh_app_caches_for_plugin_list(&config, auth.as_ref()) + .await; + } let include_shared_with_me = marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe); let include_created_by_me_remote = marketplace_kinds @@ -738,6 +743,7 @@ impl PluginRequestProcessor { || include_shared_with_me || include_global_remote { + let on_effective_plugins_changed = Some(self.effective_plugins_changed_callback()); plugins_manager.maybe_start_plugin_list_background_tasks_for_config( &plugins_input, auth.clone(), @@ -745,8 +751,15 @@ impl PluginRequestProcessor { PluginListBackgroundTaskOptions { refresh_global_remote_catalog_cache, }, - Some(self.effective_plugins_changed_callback()), + on_effective_plugins_changed.clone(), ); + if force_refetch { + plugins_manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation( + &plugins_input, + auth.clone(), + on_effective_plugins_changed, + ); + } } let featured_plugin_ids = if data.iter().any(|marketplace| { @@ -777,6 +790,64 @@ impl PluginRequestProcessor { }) } + async fn force_refresh_app_caches_for_plugin_list( + &self, + config: &Config, + auth: Option<&CodexAuth>, + ) { + if !config + .features + .apps_enabled_for_auth(auth.is_some_and(CodexAuth::uses_codex_backend)) + { + return; + } + + let plugins_manager = self.thread_manager.plugins_manager(); + let plugin_apps = plugins_manager + .plugins_for_config(&config.plugins_config_input()) + .await + .effective_apps(); + let environment_manager = self.thread_manager.environment_manager(); + let (all_connectors_result, accessible_connectors_result) = tokio::join!( + connectors::list_all_connectors_with_options( + config, + /*force_refetch*/ true, + &plugin_apps, + ), + connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager( + config, + /*force_refetch*/ true, + Arc::clone(&environment_manager), + self.thread_manager.mcp_manager(), + ), + ); + + match all_connectors_result { + Ok(_) => {} + Err(err) => { + warn!("plugin/list forceRefetch failed to refresh app directory cache: {err:#}"); + } + } + let mut refreshed_codex_apps_tools_cache = false; + match accessible_connectors_result { + Ok(status) => { + refreshed_codex_apps_tools_cache = true; + if !status.codex_apps_ready { + warn!( + "plugin/list forceRefetch refreshed Codex Apps cache before codex-apps was ready" + ); + } + } + Err(err) => { + warn!("plugin/list forceRefetch failed to refresh Codex Apps tools cache: {err:#}"); + } + } + + if refreshed_codex_apps_tools_cache { + self.on_effective_plugins_changed(); + } + } + async fn plugin_installed_response( &self, params: PluginInstalledParams, 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 a5d918cbaf..4f41cdc12b 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 @@ -504,6 +504,7 @@ async fn external_agent_config_import_sends_completion_notification_for_local_pl .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 4d143109fa..5fdbd55a79 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -97,6 +97,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?; @@ -417,6 +418,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?; @@ -531,6 +533,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?; @@ -623,6 +626,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?; @@ -707,6 +711,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?; @@ -822,6 +827,7 @@ async fn plugin_list_accepts_omitted_cwds() -> Result<()> { .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -875,6 +881,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?; @@ -967,6 +974,7 @@ enabled = false .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1124,6 +1132,7 @@ enabled = false AbsolutePathBuf::try_from(workspace_default.path())?, ]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1208,6 +1217,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?; @@ -1321,6 +1331,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?; @@ -1409,6 +1420,7 @@ enabled = true .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -1599,6 +1611,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( @@ -1779,6 +1792,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?; @@ -1922,6 +1936,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( @@ -1955,6 +1970,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( @@ -2036,6 +2052,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_requested() .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2124,6 +2141,7 @@ async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let err = timeout( @@ -2163,6 +2181,7 @@ async fn plugin_list_skips_explicit_openai_curated_remote_collection_for_api_aut .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]), + force_refetch: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -2202,6 +2221,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( @@ -2257,6 +2277,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( @@ -2310,6 +2331,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( @@ -2364,6 +2386,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?; @@ -2785,6 +2808,7 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::WorkspaceDirectory]), + force_refetch: false, }) .await?; @@ -2912,6 +2936,7 @@ plugin_sharing = false .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::CreatedByMeRemote]), + force_refetch: false, }) .await?; @@ -3039,6 +3064,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?; @@ -3196,6 +3222,7 @@ plugin_sharing = false .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::SharedWithMe]), + force_refetch: false, }) .await?; @@ -3256,6 +3283,7 @@ plugin_sharing = true .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: Some(vec![PluginListMarketplaceKind::CreatedByMeRemote]), + force_refetch: false, }) .await?; @@ -3386,6 +3414,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?; @@ -3446,6 +3475,7 @@ remote_plugin = true .send_plugin_list_request(PluginListParams { cwds: None, marketplace_kinds: None, + force_refetch: false, }) .await?; @@ -3482,6 +3512,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?; @@ -3522,6 +3553,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 ba142feb90..8adee5c533 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -719,6 +719,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 075cc007ac..4dfebdb65d 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -390,6 +390,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(