Resolve templated connectors from workspace directory

This commit is contained in:
Noah MacCallum
2026-05-29 15:04:17 -07:00
parent 25d081d10b
commit 4cd3ab4a3d
5 changed files with 343 additions and 175 deletions

View File

@@ -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<String>,
#[serde(alias = "distributionChannel")]
distribution_channel: Option<String>,
#[serde(alias = "templateId")]
template_id: Option<String>,
visibility: Option<String>,
}
@@ -152,16 +155,7 @@ where
.into_iter()
.map(directory_app_to_app_info)
.collect::<Vec<_>>();
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<F, Fut>(
template_ids: &HashSet<String>,
mut fetch_page: F,
) -> anyhow::Result<Vec<AppInfo>>
where
F: FnMut(String) -> Fut,
Fut: Future<Output = anyhow::Result<DirectoryListResponse>>,
{
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::<Vec<_>>();
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<DirectoryApp>) -> Vec<DirectoryApp> {
let mut merged: HashMap<String, DirectoryApp> = 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,
}
}

View File

@@ -489,9 +489,17 @@ struct RemotePluginMutationResponse {
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct RemoteTemplateConnectorIdsResponse {
struct RemoteWorkspaceConnectorDirectoryResponse {
#[serde(default)]
connector_ids: Vec<String>,
apps: Vec<RemoteWorkspaceConnectorDirectoryApp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct RemoteWorkspaceConnectorDirectoryApp {
id: String,
#[serde(alias = "templateId")]
template_id: Option<String>,
visibility: Option<String>,
}
pub async fn fetch_remote_marketplaces(
@@ -1453,11 +1461,20 @@ async fn fetch_template_connector_ids(
auth: &CodexAuth,
template_id: &str,
) -> Result<Vec<String>, 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<String, RemotePluginCatalogError> {
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())
}

View File

@@ -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"))

View File

@@ -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<Vec<DiscoverableTool>> {
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<String> {
#[derive(Debug, Default, PartialEq, Eq)]
struct ToolSuggestConnectorSelection {
connector_ids: HashSet<String>,
template_ids: HashSet<String>,
disabled_connector_ids: HashSet<String>,
disabled_template_ids: HashSet<String>,
}
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::<Vec<_>>();
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::<HashSet<_>>();
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::<Vec<_>>();
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::<HashSet<_>>();
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<String>,
template_ids: &mut HashSet<String>,
) {
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<AppInfo>,
template_ids: &HashSet<String>,
disabled_template_ids: &HashSet<String>,
disabled_connector_ids: &HashSet<String>,
) -> Vec<AppInfo> {
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::<HashSet<_>>();
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<String> {
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(

View File

@@ -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")
}),
]
);
}