diff --git a/codex-rs/connectors/src/lib.rs b/codex-rs/connectors/src/lib.rs index c2bf891115..d1129e4644 100644 --- a/codex-rs/connectors/src/lib.rs +++ b/codex-rs/connectors/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::HashSet; use std::future::Future; use std::sync::LazyLock; use std::sync::Mutex as StdMutex; @@ -77,6 +78,8 @@ pub struct DirectoryApp { logo_url_dark: Option, #[serde(alias = "distributionChannel")] distribution_channel: Option, + #[serde(alias = "templateId")] + template_id: Option, visibility: Option, } @@ -152,16 +155,7 @@ where .into_iter() .map(directory_app_to_app_info) .collect::>(); - for connector in &mut connectors { - let install_url = match connector.install_url.take() { - Some(install_url) => install_url, - None => connector_install_url(&connector.name, &connector.id), - }; - connector.name = normalize_connector_name(&connector.name, &connector.id); - connector.description = normalize_connector_value(connector.description.as_deref()); - connector.install_url = Some(install_url); - connector.is_accessible = false; - } + normalize_directory_app_infos(&mut connectors); connectors.sort_by(|left, right| { left.name .cmp(&right.name) @@ -248,6 +242,35 @@ where } } +pub async fn list_workspace_template_connectors( + template_ids: &HashSet, + mut fetch_page: F, +) -> anyhow::Result> +where + F: FnMut(String) -> Fut, + Fut: Future>, +{ + let mut connectors = list_workspace_connectors(&mut fetch_page) + .await? + .into_iter() + .filter(|app| { + app.template_id + .as_deref() + .is_some_and(|template_id| template_ids.contains(template_id)) + }) + .map(directory_app_to_app_info) + .collect::>(); + for connector in &mut connectors { + normalize_directory_app_info(connector); + } + connectors.sort_by(|left, right| { + left.name + .cmp(&right.name) + .then_with(|| left.id.cmp(&right.id)) + }); + Ok(connectors) +} + fn merge_directory_apps(apps: Vec) -> Vec { let mut merged: HashMap = HashMap::new(); for app in apps { @@ -271,6 +294,7 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { logo_url, logo_url_dark, distribution_channel, + template_id, visibility: _, } = incoming; @@ -296,6 +320,9 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { if existing.distribution_channel.is_none() && distribution_channel.is_some() { existing.distribution_channel = distribution_channel; } + if existing.template_id.is_none() && template_id.is_some() { + existing.template_id = template_id; + } if let Some(incoming_branding) = branding { if let Some(existing_branding) = existing.branding.as_mut() { @@ -422,6 +449,23 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo { } } +fn normalize_directory_app_infos(connectors: &mut [AppInfo]) { + for connector in connectors { + normalize_directory_app_info(connector); + } +} + +fn normalize_directory_app_info(connector: &mut AppInfo) { + let install_url = match connector.install_url.take() { + Some(install_url) => install_url, + None => connector_install_url(&connector.name, &connector.id), + }; + connector.name = normalize_connector_name(&connector.name, &connector.id); + connector.description = normalize_connector_value(connector.description.as_deref()); + connector.install_url = Some(install_url); + connector.is_accessible = false; +} + fn connector_install_url(name: &str, connector_id: &str) -> String { let slug = connector_name_slug(name); format!("https://chatgpt.com/apps/{slug}/{connector_id}") @@ -504,6 +548,7 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + template_id: None, visibility: None, } } diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index aacbf34d50..d3609d2b99 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -489,9 +489,17 @@ struct RemotePluginMutationResponse { } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -struct RemoteTemplateConnectorIdsResponse { +struct RemoteWorkspaceConnectorDirectoryResponse { #[serde(default)] - connector_ids: Vec, + apps: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +struct RemoteWorkspaceConnectorDirectoryApp { + id: String, + #[serde(alias = "templateId")] + template_id: Option, + visibility: Option, } pub async fn fetch_remote_marketplaces( @@ -1453,11 +1461,20 @@ async fn fetch_template_connector_ids( auth: &CodexAuth, template_id: &str, ) -> Result, RemotePluginCatalogError> { - let url = remote_template_connector_ids_url(config, template_id)?; + let url = remote_workspace_connector_directory_url(config)?; let client = build_reqwest_client(); let request = authenticated_request(client.get(&url), auth)?; - let response: RemoteTemplateConnectorIdsResponse = send_and_decode(request, &url).await?; - Ok(response.connector_ids) + let response: RemoteWorkspaceConnectorDirectoryResponse = + send_and_decode(request, &url).await?; + Ok(response + .apps + .into_iter() + .filter(|app| { + app.template_id.as_deref() == Some(template_id) + && !matches!(app.visibility.as_deref(), Some("HIDDEN")) + }) + .map(|app| app.id) + .collect()) } fn remote_plugin_skill_detail_url( @@ -1481,9 +1498,8 @@ fn remote_plugin_skill_detail_url( Ok(url.to_string()) } -fn remote_template_connector_ids_url( +fn remote_workspace_connector_directory_url( config: &RemotePluginServiceConfig, - template_id: &str, ) -> Result { let mut url = Url::parse(config.chatgpt_base_url.trim_end_matches('/')) .map_err(RemotePluginCatalogError::InvalidBaseUrl)?; @@ -1492,11 +1508,11 @@ fn remote_template_connector_ids_url( .path_segments_mut() .map_err(|()| RemotePluginCatalogError::InvalidBaseUrlPath)?; segments.pop_if_empty(); - segments.push("ps"); segments.push("connectors"); - segments.push("by_template_id"); - segments.push(template_id); + segments.push("directory"); + segments.push("list_workspace"); } + url.set_query(Some("external_logos=true")); Ok(url.to_string()) } diff --git a/codex-rs/core-plugins/src/remote/tests.rs b/codex-rs/core-plugins/src/remote/tests.rs index 516d615d6a..9ad9828e86 100644 --- a/codex-rs/core-plugins/src/remote/tests.rs +++ b/codex-rs/core-plugins/src/remote/tests.rs @@ -26,13 +26,16 @@ fn app(id: &str) -> AppConnectorId { async fn resolve_remote_plugin_app_ids_expands_templates_and_dedupes_stably() { let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/backend-api/ps/connectors/by_template_id/templated_apps_GitHubEnterprise", - )) + .and(path("/backend-api/connectors/directory/list_workspace")) .and(header("authorization", "Bearer Access Token")) .and(header("chatgpt-account-id", "account_id")) .respond_with(ResponseTemplate::new(200).set_body_string( - r#"{"connector_ids":["connector_ghe","asdk_app_ghe","connector_ghe"]}"#, + r#"{"apps":[ + {"id":"connector_ghe","template_id":"templated_apps_GitHubEnterprise"}, + {"id":"asdk_app_ghe","template_id":"templated_apps_GitHubEnterprise"}, + {"id":"asdk_app_other","template_id":"templated_apps_Other"}, + {"id":"asdk_app_hidden","template_id":"templated_apps_GitHubEnterprise","visibility":"HIDDEN"} + ]}"#, )) .mount(&server) .await; @@ -63,12 +66,12 @@ async fn resolve_remote_plugin_app_ids_expands_templates_and_dedupes_stably() { async fn resolve_remote_plugin_app_ids_drops_missing_template_mappings() { let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/backend-api/ps/connectors/by_template_id/templated_apps_GitHubEnterprise", - )) + .and(path("/backend-api/connectors/directory/list_workspace")) .and(header("authorization", "Bearer Access Token")) .and(header("chatgpt-account-id", "account_id")) - .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"connector_ids":[]}"#)) + .respond_with(ResponseTemplate::new(200).set_body_string( + r#"{"apps":[{"id":"asdk_app_other","template_id":"templated_apps_Other"}]}"#, + )) .mount(&server) .await; @@ -86,9 +89,7 @@ async fn resolve_remote_plugin_app_ids_drops_missing_template_mappings() { async fn resolve_remote_plugin_app_ids_drops_templates_when_lookup_fails() { let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/backend-api/ps/connectors/by_template_id/templated_apps_GitHubEnterprise", - )) + .and(path("/backend-api/connectors/directory/list_workspace")) .and(header("authorization", "Bearer Access Token")) .and(header("chatgpt-account-id", "account_id")) .respond_with(ResponseTemplate::new(500).set_body_string("lookup failed")) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index cfadc99ed7..6b64755584 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -29,10 +29,10 @@ use codex_config::types::AppToolApproval; use codex_config::types::AppsConfigToml; use codex_config::types::ToolSuggestDiscoverableType; use codex_core_plugins::PluginsManager; -use codex_core_plugins::remote::RemotePluginServiceConfig; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; +use codex_login::default_client::build_reqwest_client; use codex_login::default_client::originator; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::McpConnectionManager; @@ -44,8 +44,10 @@ use codex_mcp::compute_auth_statuses; use codex_mcp::host_owned_codex_apps_enabled; use codex_mcp::with_codex_apps_mcp; use codex_plugin::AppConnectorId; +use url::Url; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); +const TEMPLATE_APP_ID_PREFIX: &str = "templated_apps_"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct AppToolPolicy { @@ -117,14 +119,24 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( accessible_connectors: &[AppInfo], loaded_plugin_app_connector_ids: &[String], ) -> anyhow::Result> { - let connector_ids = tool_suggest_connector_ids(config, auth).await; + let connector_selection = tool_suggest_connector_selection(config).await; + let mut connector_ids = connector_selection.connector_ids.clone(); + let mut directory_connectors = + cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await; + let template_directory_connectors = workspace_template_directory_connectors( + config, + auth, + &connector_selection.template_ids, + &connector_selection.disabled_template_ids, + &connector_selection.disabled_connector_ids, + ) + .await; + for connector in template_directory_connectors { + connector_ids.insert(connector.id.clone()); + directory_connectors.push(connector); + } let directory_connectors = codex_connectors::merge::merge_plugin_connectors( - resolve_template_directory_connectors( - config, - auth, - cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await, - ) - .await, + directory_connectors, connector_ids.iter().cloned(), ); let discoverable_connectors = @@ -413,7 +425,15 @@ fn write_cached_accessible_connectors( }); } -async fn tool_suggest_connector_ids(config: &Config, auth: Option<&CodexAuth>) -> HashSet { +#[derive(Debug, Default, PartialEq, Eq)] +struct ToolSuggestConnectorSelection { + connector_ids: HashSet, + template_ids: HashSet, + disabled_connector_ids: HashSet, + disabled_template_ids: HashSet, +} + +async fn tool_suggest_connector_selection(config: &Config) -> ToolSuggestConnectorSelection { let plugins_input = config.plugins_config_input(); let connector_ids = PluginsManager::new(config.codex_home.to_path_buf()) .plugins_for_config(&plugins_input) @@ -431,18 +451,6 @@ async fn tool_suggest_connector_ids(config: &Config, auth: Option<&CodexAuth>) - .map(|discoverable| AppConnectorId(discoverable.id.clone())), ) .collect::>(); - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; - let mut connector_ids = codex_core_plugins::remote::resolve_remote_plugin_app_ids( - &remote_plugin_service_config, - auth, - &connector_ids, - ) - .await - .into_iter() - .map(|connector_id| connector_id.0) - .collect::>(); let disabled_connector_ids = config .tool_suggest @@ -451,68 +459,134 @@ async fn tool_suggest_connector_ids(config: &Config, auth: Option<&CodexAuth>) - .filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Connector) .map(|disabled_tool| AppConnectorId(disabled_tool.id.clone())) .collect::>(); - let mut disabled_connector_ids = codex_core_plugins::remote::resolve_remote_plugin_app_ids( - &remote_plugin_service_config, - auth, - &disabled_connector_ids, - ) - .await - .into_iter() - .map(|connector_id| connector_id.0) - .collect::>(); - disabled_connector_ids.extend( - config - .tool_suggest - .disabled_tools - .iter() - .filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Connector) - .map(|disabled_tool| disabled_tool.id.clone()), - ); - connector_ids.retain(|connector_id| !disabled_connector_ids.contains(connector_id)); - connector_ids + + let mut selection = ToolSuggestConnectorSelection::default(); + for connector_id in connector_ids { + selection.insert_connector_id(connector_id.0); + } + for connector_id in disabled_connector_ids { + selection.insert_disabled_connector_id(connector_id.0); + } + selection + .connector_ids + .retain(|connector_id| !selection.disabled_connector_ids.contains(connector_id)); + selection } -async fn resolve_template_directory_connectors( +impl ToolSuggestConnectorSelection { + fn insert_connector_id(&mut self, connector_id: String) { + insert_connector_or_template_id( + connector_id, + &mut self.connector_ids, + &mut self.template_ids, + ); + } + + fn insert_disabled_connector_id(&mut self, connector_id: String) { + insert_connector_or_template_id( + connector_id, + &mut self.disabled_connector_ids, + &mut self.disabled_template_ids, + ); + } +} + +fn insert_connector_or_template_id( + connector_id: String, + connector_ids: &mut HashSet, + template_ids: &mut HashSet, +) { + let connector_id = connector_id.trim(); + if connector_id.is_empty() { + return; + } + if is_template_app_id(connector_id) { + template_ids.insert(connector_id.to_string()); + } else { + connector_ids.insert(connector_id.to_string()); + } +} + +fn is_template_app_id(connector_id: &str) -> bool { + connector_id.starts_with(TEMPLATE_APP_ID_PREFIX) +} + +async fn workspace_template_directory_connectors( config: &Config, auth: Option<&CodexAuth>, - connectors: Vec, + template_ids: &HashSet, + disabled_template_ids: &HashSet, + disabled_connector_ids: &HashSet, ) -> Vec { - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), + if template_ids.is_empty() { + return Vec::new(); + } + let active_template_ids = template_ids + .difference(disabled_template_ids) + .cloned() + .collect::>(); + if active_template_ids.is_empty() { + return Vec::new(); + } + + let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else { + return Vec::new(); }; - let mut resolved_connectors = Vec::new(); - let mut seen_connector_ids = HashSet::new(); - for connector in connectors { - let resolved_connector_ids = codex_core_plugins::remote::resolve_remote_plugin_app_ids( - &remote_plugin_service_config, - auth, - &[AppConnectorId(connector.id.clone())], - ) - .await; - if resolved_connector_ids.is_empty() { - continue; + let client = build_reqwest_client(); + let base_url = config.chatgpt_base_url.clone(); + let auth_headers = codex_model_provider::auth_provider_from_auth(auth).to_auth_headers(); + match codex_connectors::list_workspace_template_connectors(&active_template_ids, move |path| { + let client = client.clone(); + let base_url = base_url.clone(); + let auth_headers = auth_headers.clone(); + async move { + let url = chatgpt_backend_path_url(&base_url, &path)?; + let response = client + .get(&url) + .timeout(Duration::from_secs(30)) + .headers(auth_headers) + .send() + .await?; + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("connector directory request failed with status {status}: {body}"); + } + Ok(serde_json::from_str(&body)?) } - - for connector_id in resolved_connector_ids { - if !seen_connector_ids.insert(connector_id.clone()) { - continue; - } - if connector_id.0 == connector.id { - resolved_connectors.push(connector.clone()); - continue; - } - - let mut resolved_connector = connector.clone(); - resolved_connector.id = connector_id.0; - resolved_connector.install_url = - Some(codex_connectors::metadata::connector_install_url( - &resolved_connector.name, - &resolved_connector.id, - )); - resolved_connectors.push(resolved_connector); + }) + .await + { + Ok(connectors) => connectors + .into_iter() + .filter(|connector| !disabled_connector_ids.contains(&connector.id)) + .collect(), + Err(err) => { + warn!("failed to load workspace connector directory for template resolution: {err:#}"); + Vec::new() } } - resolved_connectors +} + +fn chatgpt_backend_path_url(base_url: &str, path: &str) -> anyhow::Result { + let mut url = Url::parse(base_url.trim_end_matches('/'))?; + let (path, query) = path + .trim_start_matches('/') + .split_once('?') + .map_or((path.trim_start_matches('/'), None), |(path, query)| { + (path, Some(query)) + }); + { + let mut segments = url + .path_segments_mut() + .map_err(|()| anyhow::anyhow!("invalid ChatGPT base URL path"))?; + segments.pop_if_empty(); + for segment in path.split('/').filter(|segment| !segment.is_empty()) { + segments.push(segment); + } + } + url.set_query(query); + Ok(url.to_string()) } async fn cached_directory_connectors_for_tool_suggest_with_auth( diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index ed103aa8df..f83896bb49 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -1165,7 +1165,7 @@ fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { } #[tokio::test] -async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() { +async fn tool_suggest_connector_selection_includes_configured_tool_suggest_discoverables() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), @@ -1186,13 +1186,18 @@ discoverables = [ .expect("config should load"); assert_eq!( - tool_suggest_connector_ids(&config, None).await, - HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()]) + tool_suggest_connector_selection(&config).await, + ToolSuggestConnectorSelection { + connector_ids: HashSet::from( + ["connector_2128aebfecb84f64a069897515042a44".to_string()] + ), + ..ToolSuggestConnectorSelection::default() + } ); } #[tokio::test] -async fn tool_suggest_connector_ids_exclude_disabled_tool_suggestions() { +async fn tool_suggest_connector_selection_excludes_disabled_tool_suggestions() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), @@ -1215,27 +1220,17 @@ disabled_tools = [ .expect("config should load"); assert_eq!( - tool_suggest_connector_ids(&config, None).await, - HashSet::from(["connector_gmail".to_string()]) + tool_suggest_connector_selection(&config).await, + ToolSuggestConnectorSelection { + connector_ids: HashSet::from(["connector_gmail".to_string()]), + disabled_connector_ids: HashSet::from(["connector_calendar".to_string()]), + ..ToolSuggestConnectorSelection::default() + } ); } #[tokio::test] -async fn tool_suggest_connector_ids_resolve_template_connectors() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path( - "/backend-api/ps/connectors/by_template_id/templated_apps_Databricks", - )) - .and(header("authorization", "Bearer Access Token")) - .and(header("chatgpt-account-id", "account_id")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(r#"{"connector_ids":["asdk_app_databricks_workspace"]}"#), - ) - .mount(&server) - .await; - +async fn tool_suggest_connector_selection_tracks_template_connectors_separately() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), @@ -1247,17 +1242,18 @@ discoverables = [ "#, ) .expect("write config"); - let mut config = ConfigBuilder::default() + let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await .expect("config should load"); - config.chatgpt_base_url = format!("{}/backend-api", server.uri()); - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); assert_eq!( - tool_suggest_connector_ids(&config, Some(&auth)).await, - HashSet::from(["asdk_app_databricks_workspace".to_string()]) + tool_suggest_connector_selection(&config).await, + ToolSuggestConnectorSelection { + template_ids: HashSet::from(["templated_apps_Databricks".to_string()]), + ..ToolSuggestConnectorSelection::default() + } ); } @@ -1265,15 +1261,24 @@ discoverables = [ async fn tool_suggest_resolves_template_connectors_before_returning_install_entries() { let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/backend-api/ps/connectors/by_template_id/templated_apps_Databricks", - )) + .and(path("/backend-api/connectors/directory/list_workspace")) .and(header("authorization", "Bearer Access Token")) .and(header("chatgpt-account-id", "account_id")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(r#"{"connector_ids":["asdk_app_databricks_workspace"]}"#), - ) + .respond_with(ResponseTemplate::new(200).set_body_string( + r#"{"apps":[ + { + "id":"asdk_app_databricks_workspace", + "name":"Databricks Workspace", + "description":"Query Databricks", + "template_id":"templated_apps_Databricks" + }, + { + "id":"asdk_app_other", + "name":"Other", + "template_id":"templated_apps_Other" + } + ]}"#, + )) .mount(&server) .await; @@ -1306,29 +1311,57 @@ discoverables = [ assert_eq!( discoverable_tools, - vec![DiscoverableTool::from(plugin_connector_to_app_info( - "asdk_app_databricks_workspace".to_string(), - ))] + vec![DiscoverableTool::from(AppInfo { + id: "asdk_app_databricks_workspace".to_string(), + name: "Databricks Workspace".to_string(), + description: Some("Query Databricks".to_string()), + install_url: Some(connector_install_url( + "Databricks Workspace", + "asdk_app_databricks_workspace", + )), + ..app("asdk_app_databricks_workspace") + })] ); } #[tokio::test] -async fn template_directory_connector_metadata_is_carried_to_resolved_connector() { +async fn tool_suggest_returns_all_resolved_connectors_for_template() { let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/backend-api/ps/connectors/by_template_id/templated_apps_Databricks", - )) + .and(path("/backend-api/connectors/directory/list_workspace")) .and(header("authorization", "Bearer Access Token")) .and(header("chatgpt-account-id", "account_id")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(r#"{"connector_ids":["asdk_app_databricks_workspace"]}"#), - ) + .respond_with(ResponseTemplate::new(200).set_body_string( + r#"{"apps":[ + { + "id":"asdk_app_databricks_a", + "name":"Databricks A", + "template_id":"templated_apps_Databricks" + }, + { + "id":"asdk_app_databricks_b", + "name":"Databricks B", + "template_id":"templated_apps_Databricks" + } + ]}"#, + )) .mount(&server) .await; let codex_home = tempdir().expect("tempdir should succeed"); + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +[features] +apps = true + +[tool_suggest] +discoverables = [ + { type = "connector", id = "templated_apps_Databricks" } +] +"#, + ) + .expect("write config"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() @@ -1337,34 +1370,33 @@ async fn template_directory_connector_metadata_is_carried_to_resolved_connector( config.chatgpt_base_url = format!("{}/backend-api", server.uri()); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let resolved = resolve_template_directory_connectors( - &config, - Some(&auth), - vec![AppInfo { - id: "templated_apps_Databricks".to_string(), - name: "Databricks".to_string(), - description: Some("Query Databricks".to_string()), - install_url: Some(connector_install_url( - "Databricks", - "templated_apps_Databricks", - )), - ..app("templated_apps_Databricks") - }], - ) - .await; + let discoverable_tools = + list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[], &[]) + .await + .expect("discoverable tools should load"); assert_eq!( - resolved, - vec![AppInfo { - id: "asdk_app_databricks_workspace".to_string(), - name: "Databricks".to_string(), - description: Some("Query Databricks".to_string()), - install_url: Some(connector_install_url( - "Databricks", - "asdk_app_databricks_workspace", - )), - ..app("asdk_app_databricks_workspace") - }] + discoverable_tools, + vec![ + DiscoverableTool::from(AppInfo { + id: "asdk_app_databricks_a".to_string(), + name: "Databricks A".to_string(), + install_url: Some(connector_install_url( + "Databricks A", + "asdk_app_databricks_a", + )), + ..app("asdk_app_databricks_a") + }), + DiscoverableTool::from(AppInfo { + id: "asdk_app_databricks_b".to_string(), + name: "Databricks B".to_string(), + install_url: Some(connector_install_url( + "Databricks B", + "asdk_app_databricks_b", + )), + ..app("asdk_app_databricks_b") + }), + ] ); }