mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] [3/4] Activate endpoint plugin recommendations (#27704)
Summary\n- Await endpoint recommendation selection while constructing each authenticated turn, removing the first-turn cache race.\n- Snapshot and filter endpoint candidates once per turn, then use that same set for the bounded contextual user fragment, tool exposure, and exact install validation.\n- Keep recommendation selection ephemeral: do not persist recommendation state in or gate resumed threads on prior context.\n- Hide the legacy list tool in endpoint mode and preserve legacy discovery unchanged when the endpoint is disabled or unavailable.\n- Keep remote plugin and connector app identities out of model-visible context and attach them only to Codex-owned elicitation metadata.\n\nStack\n- 3/4, based on #28400.\n- Endpoint client and cache: #28399.\n- Generalized suggestion presentation: #28400.\n- Install-schema follow-up: #28403.\n\nValidation\n- \n- \n- \n- \n- Full : 2,649 passed and 88 environment-dependent tests failed because this sandbox cannot write , nest Seatbelt, or locate auxiliary test binaries.
This commit is contained in:
committed by
GitHub
Unverified
parent
587487df9e
commit
a34da3b295
@@ -49,6 +49,7 @@ pub use manager::PluginReadRequest;
|
||||
pub use manager::PluginUninstallError;
|
||||
pub use manager::PluginsConfigInput;
|
||||
pub use manager::PluginsManager;
|
||||
pub use manager::RecommendedPluginCandidatesInput;
|
||||
pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeError as PluginMarketplaceUpgradeError;
|
||||
pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome as PluginMarketplaceUpgradeOutcome;
|
||||
pub use provider::ExecutorPluginProvider;
|
||||
|
||||
@@ -57,6 +57,8 @@ use codex_config::ConfigLayerStack;
|
||||
use codex_config::clear_user_plugin;
|
||||
use codex_config::set_user_plugin_enabled;
|
||||
use codex_config::types::PluginConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_core_skills::SkillMetadata;
|
||||
use codex_core_skills::config_rules::SkillConfigRules;
|
||||
use codex_core_skills::config_rules::skill_config_rules_from_stack;
|
||||
@@ -71,6 +73,9 @@ use codex_plugin::app_connector_ids_from_declarations;
|
||||
use codex_plugin::prompt_safe_plugin_description;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_tools::DiscoverablePluginInfo;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::filter_request_plugin_install_discoverable_tools_for_client;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::PluginSkillRoot;
|
||||
use std::collections::HashMap;
|
||||
@@ -114,6 +119,15 @@ impl PluginsConfigInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inputs used to select endpoint-backed plugin install candidates.
|
||||
pub struct RecommendedPluginCandidatesInput<'a> {
|
||||
pub plugins_config: &'a PluginsConfigInput,
|
||||
pub loaded_plugins: &'a PluginLoadOutcome,
|
||||
pub auth: Option<&'a CodexAuth>,
|
||||
pub disabled_tools: &'a [ToolSuggestDisabledTool],
|
||||
pub app_server_client_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct FeaturedPluginIdsCacheKey {
|
||||
chatgpt_base_url: String,
|
||||
@@ -997,6 +1011,59 @@ impl PluginsManager {
|
||||
mode
|
||||
}
|
||||
|
||||
/// Returns endpoint recommendations eligible for installation in the current client.
|
||||
/// `None` selects the legacy discovery workflow.
|
||||
pub async fn recommended_plugin_candidates_for_config(
|
||||
&self,
|
||||
input: RecommendedPluginCandidatesInput<'_>,
|
||||
) -> Option<Vec<DiscoverableTool>> {
|
||||
let RecommendedPluginsMode::Endpoint { plugins } = self
|
||||
.recommended_plugins_mode_for_config(input.plugins_config, input.auth)
|
||||
.await
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if plugins.is_empty() {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
|
||||
let installed_plugin_ids = input
|
||||
.loaded_plugins
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| plugin.config_name.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let disabled_plugin_ids = input
|
||||
.disabled_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.kind == ToolSuggestDiscoverableType::Plugin)
|
||||
.map(|tool| tool.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let candidates = plugins
|
||||
.into_iter()
|
||||
.filter(|plugin| {
|
||||
!installed_plugin_ids.contains(plugin.config_id.as_str())
|
||||
&& !disabled_plugin_ids.contains(plugin.config_id.as_str())
|
||||
})
|
||||
.map(|plugin| {
|
||||
DiscoverableTool::from(DiscoverablePluginInfo {
|
||||
id: plugin.config_id,
|
||||
remote_plugin_id: Some(plugin.remote_plugin_id),
|
||||
name: plugin.display_name,
|
||||
description: None,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: plugin.app_connector_ids,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Some(filter_request_plugin_install_discoverable_tools_for_client(
|
||||
candidates,
|
||||
input.app_server_client_name,
|
||||
))
|
||||
}
|
||||
|
||||
fn cached_recommended_plugins_mode(
|
||||
&self,
|
||||
cache_key: &RecommendedPluginsCacheKey,
|
||||
|
||||
@@ -3844,6 +3844,79 @@ remote_plugin = true
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recommended_plugin_candidates_filter_installed_and_disabled_plugins() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
"#,
|
||||
);
|
||||
write_cached_plugin(tmp.path(), REMOTE_GLOBAL_MARKETPLACE_NAME, "linear");
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/suggested"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"enabled": true,
|
||||
"plugins": [
|
||||
{
|
||||
"id": "plugin_linear",
|
||||
"name": "linear",
|
||||
"release": {"display_name": "Linear"}
|
||||
},
|
||||
{
|
||||
"id": "plugin_github",
|
||||
"name": "github",
|
||||
"release": {"display_name": "GitHub"}
|
||||
},
|
||||
{
|
||||
"id": "plugin_slack",
|
||||
"name": "slack",
|
||||
"release": {"display_name": "Slack"}
|
||||
}
|
||||
]
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_plugin("linear")]);
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let disabled_tools = [ToolSuggestDisabledTool::plugin(
|
||||
"github@openai-curated-remote",
|
||||
)];
|
||||
let loaded_plugins = manager.plugins_for_config(&config).await;
|
||||
|
||||
let candidates = manager
|
||||
.recommended_plugin_candidates_for_config(RecommendedPluginCandidatesInput {
|
||||
plugins_config: &config,
|
||||
loaded_plugins: &loaded_plugins,
|
||||
auth: Some(&auth),
|
||||
disabled_tools: &disabled_tools,
|
||||
app_server_client_name: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
Some(vec![DiscoverableTool::from(DiscoverablePluginInfo {
|
||||
id: "slack@openai-curated-remote".to_string(),
|
||||
remote_plugin_id: Some("plugin_slack".to_string()),
|
||||
name: "Slack".to_string(),
|
||||
description: None,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
})])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recommended_plugins_mode_caches_explicit_false() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user