diff --git a/codex-rs/core-plugins/src/discoverable.rs b/codex-rs/core-plugins/src/discoverable.rs index ccf093d816..bb50cb959f 100644 --- a/codex-rs/core-plugins/src/discoverable.rs +++ b/codex-rs/core-plugins/src/discoverable.rs @@ -64,6 +64,7 @@ pub struct ToolSuggestPluginDiscoveryInput { #[derive(Clone, Debug, PartialEq, Eq)] pub struct ToolSuggestDiscoverablePlugin { pub id: String, + pub remote_plugin_id: Option, pub name: String, pub description: Option, pub has_skills: bool, @@ -145,6 +146,7 @@ impl PluginsManager { discoverable_plugins.push(ToolSuggestDiscoverablePlugin { id: plugin.config_name, + remote_plugin_id: None, name: plugin.display_name, description: plugin.description, has_skills: plugin.has_skills, @@ -200,6 +202,7 @@ impl PluginsManager { discoverable_plugins.push(ToolSuggestDiscoverablePlugin { id: plugin.config_id, + remote_plugin_id: Some(plugin.remote_plugin_id), name: plugin.name, description: plugin.description, has_skills: plugin.has_skills, diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index 8ddec7056e..fc77ae21e6 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -41,6 +41,7 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins( .into_iter() .map(|plugin| DiscoverablePluginInfo { id: plugin.id, + remote_plugin_id: plugin.remote_plugin_id, name: plugin.name, description: plugin.description, has_skills: plugin.has_skills, diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index 992bf93f45..9cdca69de2 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -393,6 +393,7 @@ remote_plugin = true remote_plugins, vec![DiscoverablePluginInfo { id: "github@openai-curated-remote".to_string(), + remote_plugin_id: Some("plugins~Plugin_remote_github".to_string()), name: "Remote GitHub".to_string(), description: Some("Remote GitHub short".to_string()), has_skills: true, @@ -680,6 +681,7 @@ async fn list_tool_suggest_discoverable_plugins_normalizes_description() { discoverable_plugins, vec![DiscoverablePluginInfo { id: "slack@openai-curated".to_string(), + remote_plugin_id: None, name: "slack".to_string(), description: Some("Plugin with extra spacing".to_string()), has_skills: true, @@ -816,6 +818,7 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }] discoverable_plugins, vec![DiscoverablePluginInfo { id: "sample@openai-curated".to_string(), + remote_plugin_id: None, name: "sample".to_string(), description: Some( "Plugin that includes skills, MCP servers, and app connectors".to_string(), diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs b/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs index 830e9d408e..cacc25ff95 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs @@ -130,6 +130,7 @@ async fn persist_disabled_install_request_writes_plugin_config() { let codex_home = tempdir().expect("tempdir should succeed"); let tool = DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { id: "slack@openai-curated".to_string(), + remote_plugin_id: None, name: "Slack".to_string(), description: None, has_skills: true, diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 02a7a21f63..25aae5599e 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -406,6 +406,7 @@ fn dynamic_tool(namespace: Option<&str>, name: &str, defer_loading: bool) -> Dyn fn discoverable_plugin(id: &str, name: &str) -> DiscoverableTool { DiscoverablePluginInfo { id: id.to_string(), + remote_plugin_id: None, name: name.to_string(), description: Some(format!("{name} plugin")), has_skills: false, diff --git a/codex-rs/tools/src/request_plugin_install.rs b/codex-rs/tools/src/request_plugin_install.rs index 70e9cb093d..2db82ae8cc 100644 --- a/codex-rs/tools/src/request_plugin_install.rs +++ b/codex-rs/tools/src/request_plugin_install.rs @@ -46,9 +46,18 @@ pub struct RequestPluginInstallMeta<'a> { pub tool_id: &'a str, pub tool_name: &'a str, #[serde(skip_serializing_if = "Option::is_none")] + pub remote_plugin_id: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub app_ids: Option<&'a [String]>, + #[serde(skip_serializing_if = "Option::is_none")] pub install_url: Option<&'a str>, } +struct RemotePluginInstallMeta<'a> { + remote_plugin_id: &'a str, + app_ids: &'a [String], +} + pub fn build_request_plugin_install_elicitation_request( server_name: &str, thread_id: String, @@ -59,6 +68,18 @@ pub fn build_request_plugin_install_elicitation_request( ) -> McpServerElicitationRequestParams { let tool_name = tool.name().to_string(); let install_url = tool.install_url().map(ToString::to_string); + let remote_plugin = match tool { + DiscoverableTool::Connector(_) => None, + DiscoverableTool::Plugin(plugin) => { + plugin + .remote_plugin_id + .as_deref() + .map(|remote_plugin_id| RemotePluginInstallMeta { + remote_plugin_id, + app_ids: plugin.app_connector_ids.as_slice(), + }) + } + }; let message = suggest_reason.to_string(); McpServerElicitationRequestParams { @@ -72,6 +93,7 @@ pub fn build_request_plugin_install_elicitation_request( suggest_reason, tool.id(), tool_name.as_str(), + remote_plugin, install_url.as_deref(), ))), message, @@ -110,8 +132,17 @@ fn build_request_plugin_install_meta<'a>( suggest_reason: &'a str, tool_id: &'a str, tool_name: &'a str, + remote_plugin: Option>, install_url: Option<&'a str>, ) -> RequestPluginInstallMeta<'a> { + let (remote_plugin_id, app_ids) = match remote_plugin { + Some(remote_plugin) => ( + Some(remote_plugin.remote_plugin_id), + Some(remote_plugin.app_ids), + ), + None => (None, None), + }; + RequestPluginInstallMeta { codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE, persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE, @@ -120,6 +151,8 @@ fn build_request_plugin_install_meta<'a>( suggest_reason, tool_id, tool_name, + remote_plugin_id, + app_ids, install_url, } } diff --git a/codex-rs/tools/src/request_plugin_install_tests.rs b/codex-rs/tools/src/request_plugin_install_tests.rs index ff2370ade7..4c11252c78 100644 --- a/codex-rs/tools/src/request_plugin_install_tests.rs +++ b/codex-rs/tools/src/request_plugin_install_tests.rs @@ -54,6 +54,8 @@ fn build_request_plugin_install_elicitation_request_uses_expected_shape() { suggest_reason: "Plan and reference events from your calendar", tool_id: "connector_2128aebfecb84f64a069897515042a44", tool_name: "Google Calendar", + remote_plugin_id: None, + app_ids: None, install_url: Some( "https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44" ), @@ -80,6 +82,7 @@ fn build_request_plugin_install_elicitation_request_for_plugin_omits_install_url }; let plugin = DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { id: "sample@openai-curated".to_string(), + remote_plugin_id: None, name: "Sample Plugin".to_string(), description: Some("Includes skills, MCP servers, and apps.".to_string()), has_skills: true, @@ -111,6 +114,8 @@ fn build_request_plugin_install_elicitation_request_for_plugin_omits_install_url suggest_reason: "Use the sample plugin's skills and MCP server", tool_id: "sample@openai-curated", tool_name: "Sample Plugin", + remote_plugin_id: None, + app_ids: None, install_url: None, })), message: "Use the sample plugin's skills and MCP server".to_string(), @@ -133,6 +138,7 @@ fn build_request_plugin_install_meta_uses_expected_shape() { "Find and reference emails from your inbox", "connector_68df038e0ba48191908c8434991bbac2", "Gmail", + /*remote_plugin*/ None, Some("https://chatgpt.com/apps/gmail/connector_68df038e0ba48191908c8434991bbac2"), ); @@ -146,6 +152,8 @@ fn build_request_plugin_install_meta_uses_expected_shape() { suggest_reason: "Find and reference emails from your inbox", tool_id: "connector_68df038e0ba48191908c8434991bbac2", tool_name: "Gmail", + remote_plugin_id: None, + app_ids: None, install_url: Some( "https://chatgpt.com/apps/gmail/connector_68df038e0ba48191908c8434991bbac2" ), @@ -153,6 +161,65 @@ fn build_request_plugin_install_meta_uses_expected_shape() { ); } +#[test] +fn build_request_plugin_install_elicitation_request_for_remote_plugin_includes_directory_metadata() +{ + let args = RequestPluginInstallArgs { + tool_type: DiscoverableToolType::Plugin, + action_type: DiscoverableToolAction::Install, + tool_id: "slack@openai-curated-remote".to_string(), + suggest_reason: "Search and reference Slack messages".to_string(), + }; + let plugin = DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { + id: "slack@openai-curated-remote".to_string(), + remote_plugin_id: Some("plugins~Plugin_slack".to_string()), + name: "Slack".to_string(), + description: Some("Search Slack messages".to_string()), + has_skills: true, + mcp_server_names: Vec::new(), + app_connector_ids: vec!["connector_slack".to_string()], + })); + + let request = build_request_plugin_install_elicitation_request( + "codex-apps", + "thread-1".to_string(), + "turn-1".to_string(), + &args, + "Search and reference Slack messages", + &plugin, + ); + + assert_eq!( + request, + McpServerElicitationRequestParams { + thread_id: "thread-1".to_string(), + turn_id: Some("turn-1".to_string()), + server_name: "codex-apps".to_string(), + request: McpServerElicitationRequest::Form { + meta: Some(json!(RequestPluginInstallMeta { + codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE, + persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE, + tool_type: DiscoverableToolType::Plugin, + suggest_type: DiscoverableToolAction::Install, + suggest_reason: "Search and reference Slack messages", + tool_id: "slack@openai-curated-remote", + tool_name: "Slack", + remote_plugin_id: Some("plugins~Plugin_slack"), + app_ids: Some(&["connector_slack".to_string()]), + install_url: None, + })), + message: "Search and reference Slack messages".to_string(), + requested_schema: McpElicitationSchema { + schema_uri: None, + type_: McpElicitationObjectType::Object, + properties: BTreeMap::new(), + required: None, + }, + }, + } + ); +} + #[test] fn verified_connector_install_completed_requires_accessible_connector() { let accessible_connectors = vec![AppInfo { diff --git a/codex-rs/tools/src/tool_discovery.rs b/codex-rs/tools/src/tool_discovery.rs index ac5aac904b..2584d7f17a 100644 --- a/codex-rs/tools/src/tool_discovery.rs +++ b/codex-rs/tools/src/tool_discovery.rs @@ -93,6 +93,7 @@ pub fn filter_request_plugin_install_discoverable_tools_for_client( #[derive(Clone, Debug, PartialEq, Eq)] pub struct DiscoverablePluginInfo { pub id: String, + pub remote_plugin_id: Option, pub name: String, pub description: Option, pub has_skills: bool, diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 6e45260c0e..5a3310057c 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -37,6 +37,7 @@ fn filter_request_plugin_install_discoverable_tools_for_codex_tui_omits_plugins( })), DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { id: "slack@openai-curated".to_string(), + remote_plugin_id: None, name: "Slack".to_string(), description: Some("Search Slack messages".to_string()), has_skills: true,