mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Resolve templated connectors from workspace directory
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user