mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Move more connector logic into connectors crate (#18158)
Reduce the size of core
This commit is contained in:
committed by
GitHub
Unverified
parent
ab97c9aaad
commit
206dd13c32
Generated
+2
@@ -1649,6 +1649,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-connectors",
|
||||
"codex-core",
|
||||
@@ -2936,6 +2937,7 @@ dependencies = [
|
||||
"codex-cli",
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
"codex-connectors",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
|
||||
@@ -10,6 +10,7 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-connectors = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::token_data::TokenData;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -9,21 +5,24 @@ use crate::chatgpt_client::chatgpt_get_request_with_timeout;
|
||||
use crate::chatgpt_token::get_chatgpt_token_data;
|
||||
use crate::chatgpt_token::init_chatgpt_token_from_auth;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_connectors::AllConnectorsCacheKey;
|
||||
use codex_connectors::DirectoryListResponse;
|
||||
|
||||
pub use codex_core::connectors::AppInfo;
|
||||
pub use codex_core::connectors::connector_display_label;
|
||||
use codex_core::connectors::filter_disallowed_connectors;
|
||||
use codex_connectors::filter::filter_disallowed_connectors;
|
||||
use codex_connectors::merge::merge_connectors;
|
||||
use codex_connectors::merge::merge_plugin_connectors;
|
||||
use codex_core::config::Config;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options;
|
||||
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
|
||||
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
|
||||
use codex_core::connectors::merge_connectors;
|
||||
use codex_core::connectors::merge_plugin_apps;
|
||||
pub use codex_core::connectors::with_app_enabled_state;
|
||||
use codex_core::plugins::AppConnectorId;
|
||||
use codex_core::plugins::PluginsManager;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_login::token_data::TokenData;
|
||||
|
||||
const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
@@ -74,8 +73,17 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>>
|
||||
let token_data = get_chatgpt_token_data()?;
|
||||
let cache_key = all_connectors_cache_key(config, &token_data);
|
||||
let connectors = codex_connectors::cached_all_connectors(&cache_key)?;
|
||||
let connectors = merge_plugin_apps(connectors, plugin_apps_for_config(config).await);
|
||||
Some(filter_disallowed_connectors(connectors))
|
||||
let connectors = merge_plugin_connectors(
|
||||
connectors,
|
||||
plugin_apps_for_config(config)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|connector_id| connector_id.0),
|
||||
);
|
||||
Some(filter_disallowed_connectors(
|
||||
connectors,
|
||||
originator().value.as_str(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_all_connectors_with_options(
|
||||
@@ -105,8 +113,17 @@ pub async fn list_all_connectors_with_options(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let connectors = merge_plugin_apps(connectors, plugin_apps_for_config(config).await);
|
||||
Ok(filter_disallowed_connectors(connectors))
|
||||
let connectors = merge_plugin_connectors(
|
||||
connectors,
|
||||
plugin_apps_for_config(config)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|connector_id| connector_id.0),
|
||||
);
|
||||
Ok(filter_disallowed_connectors(
|
||||
connectors,
|
||||
originator().value.as_str(),
|
||||
))
|
||||
}
|
||||
|
||||
fn all_connectors_cache_key(config: &Config, token_data: &TokenData) -> AllConnectorsCacheKey {
|
||||
@@ -134,7 +151,13 @@ pub fn connectors_for_plugin_apps(
|
||||
.map(|connector_id| connector_id.0.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
filter_disallowed_connectors(merge_plugin_apps(connectors, plugin_apps.to_vec()))
|
||||
let connectors = merge_plugin_connectors(
|
||||
connectors,
|
||||
plugin_apps
|
||||
.iter()
|
||||
.map(|connector_id| connector_id.0.clone()),
|
||||
);
|
||||
filter_disallowed_connectors(connectors, originator().value.as_str())
|
||||
.into_iter()
|
||||
.filter(|connector| plugin_app_ids.contains(connector.id.as_str()))
|
||||
.collect()
|
||||
@@ -158,13 +181,13 @@ pub fn merge_connectors_with_accessible(
|
||||
accessible_connectors
|
||||
};
|
||||
let merged = merge_connectors(connectors, accessible_connectors);
|
||||
filter_disallowed_connectors(merged)
|
||||
filter_disallowed_connectors(merged, originator().value.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_core::connectors::connector_install_url;
|
||||
use codex_connectors::metadata::connector_install_url;
|
||||
use codex_core::plugins::AppConnectorId;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -186,46 +209,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_asdk_connectors() {
|
||||
let filtered = filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
assert_eq!(filtered, vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_whitelisted_asdk_connectors() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("asdk_app_69781557cc1481919cf5e9824fa2e792"),
|
||||
app("beta"),
|
||||
]);
|
||||
assert_eq!(
|
||||
filtered,
|
||||
vec![
|
||||
app("asdk_app_69781557cc1481919cf5e9824fa2e792"),
|
||||
app("beta")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_openai_prefixed_connectors() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("connector_openai_foo"),
|
||||
app("connector_openai_bar"),
|
||||
app("gamma"),
|
||||
]);
|
||||
assert_eq!(filtered, vec![app("gamma")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_disallowed_connector_ids() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
app("delta"),
|
||||
]);
|
||||
assert_eq!(filtered, vec![app("delta")]);
|
||||
}
|
||||
|
||||
fn merged_app(id: &str, is_accessible: bool) -> AppInfo {
|
||||
AppInfo {
|
||||
id: id.to_string(),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::metadata::connector_install_url;
|
||||
use crate::normalize_connector_value;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
|
||||
pub struct AccessibleConnectorTool {
|
||||
pub connector_id: String,
|
||||
pub connector_name: Option<String>,
|
||||
pub connector_description: Option<String>,
|
||||
pub plugin_display_names: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn collect_accessible_connectors<I>(tools: I) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = AccessibleConnectorTool>,
|
||||
{
|
||||
let mut connectors: HashMap<String, (AppInfo, BTreeSet<String>)> = HashMap::new();
|
||||
for tool in tools {
|
||||
let connector_id = tool.connector_id;
|
||||
let connector_name = normalize_connector_value(tool.connector_name.as_deref())
|
||||
.unwrap_or_else(|| connector_id.clone());
|
||||
let connector_description =
|
||||
normalize_connector_value(tool.connector_description.as_deref());
|
||||
if let Some((existing, existing_plugin_display_names)) = connectors.get_mut(&connector_id) {
|
||||
if existing.name == connector_id && connector_name != connector_id {
|
||||
existing.name = connector_name;
|
||||
}
|
||||
if existing.description.is_none() && connector_description.is_some() {
|
||||
existing.description = connector_description;
|
||||
}
|
||||
existing_plugin_display_names.extend(tool.plugin_display_names);
|
||||
} else {
|
||||
connectors.insert(
|
||||
connector_id.clone(),
|
||||
(
|
||||
AppInfo {
|
||||
id: connector_id.clone(),
|
||||
name: connector_name,
|
||||
description: connector_description,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
tool.plugin_display_names
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<String>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut accessible: Vec<AppInfo> = connectors
|
||||
.into_values()
|
||||
.map(|(mut connector, plugin_display_names)| {
|
||||
connector.plugin_display_names = plugin_display_names.into_iter().collect();
|
||||
connector.install_url = Some(connector_install_url(&connector.name, &connector.id));
|
||||
connector
|
||||
})
|
||||
.collect();
|
||||
accessible.sort_by(|left, right| {
|
||||
right
|
||||
.is_accessible
|
||||
.cmp(&left.is_accessible)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
accessible
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
|
||||
pub fn filter_tool_suggest_discoverable_connectors(
|
||||
directory_connectors: Vec<AppInfo>,
|
||||
accessible_connectors: &[AppInfo],
|
||||
discoverable_connector_ids: &HashSet<String>,
|
||||
originator_value: &str,
|
||||
) -> Vec<AppInfo> {
|
||||
let accessible_connector_ids: HashSet<&str> = accessible_connectors
|
||||
.iter()
|
||||
.filter(|connector| connector.is_accessible)
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
|
||||
let mut connectors = filter_disallowed_connectors(directory_connectors, originator_value)
|
||||
.into_iter()
|
||||
.filter(|connector| !accessible_connector_ids.contains(connector.id.as_str()))
|
||||
.filter(|connector| discoverable_connector_ids.contains(connector.id.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
connectors.sort_by(|left, right| {
|
||||
left.name
|
||||
.cmp(&right.name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
connectors
|
||||
}
|
||||
|
||||
const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
|
||||
"asdk_app_6938a94a61d881918ef32cb999ff937c",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
"connector_3f8d1a79f27c4c7ba1a897ab13bf37dc",
|
||||
"connector_68de829bf7648191acd70a907364c67c",
|
||||
"connector_68e004f14af881919eb50893d3d9f523",
|
||||
"connector_69272cb413a081919685ec3c88d1744e",
|
||||
];
|
||||
const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] =
|
||||
&["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"];
|
||||
const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_";
|
||||
|
||||
pub fn filter_disallowed_connectors(
|
||||
connectors: Vec<AppInfo>,
|
||||
originator_value: &str,
|
||||
) -> Vec<AppInfo> {
|
||||
let first_party_chat_originator = is_first_party_chat_originator(originator_value);
|
||||
connectors
|
||||
.into_iter()
|
||||
.filter(|connector| {
|
||||
is_connector_id_allowed(connector.id.as_str(), first_party_chat_originator)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_first_party_chat_originator(originator_value: &str) -> bool {
|
||||
originator_value == "codex_atlas" || originator_value == "codex_chatgpt_desktop"
|
||||
}
|
||||
|
||||
fn is_connector_id_allowed(connector_id: &str, first_party_chat_originator: bool) -> bool {
|
||||
let disallowed_connector_ids = if first_party_chat_originator {
|
||||
FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS
|
||||
} else {
|
||||
DISALLOWED_CONNECTOR_IDS
|
||||
};
|
||||
|
||||
!connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX)
|
||||
&& !disallowed_connector_ids.contains(&connector_id)
|
||||
}
|
||||
@@ -10,6 +10,11 @@ use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppMetadata;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod accessible;
|
||||
pub mod filter;
|
||||
pub mod merge;
|
||||
pub mod metadata;
|
||||
|
||||
pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::metadata::connector_install_url;
|
||||
use crate::metadata::sort_connectors_by_accessibility_and_name;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
|
||||
pub fn merge_connectors(
|
||||
connectors: Vec<AppInfo>,
|
||||
accessible_connectors: Vec<AppInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
let mut merged: HashMap<String, AppInfo> = connectors
|
||||
.into_iter()
|
||||
.map(|mut connector| {
|
||||
connector.is_accessible = false;
|
||||
(connector.id.clone(), connector)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for mut connector in accessible_connectors {
|
||||
connector.is_accessible = true;
|
||||
let connector_id = connector.id.clone();
|
||||
if let Some(existing) = merged.get_mut(&connector_id) {
|
||||
existing.is_accessible = true;
|
||||
if existing.name == existing.id && connector.name != connector.id {
|
||||
existing.name = connector.name;
|
||||
}
|
||||
if existing.description.is_none() && connector.description.is_some() {
|
||||
existing.description = connector.description;
|
||||
}
|
||||
if existing.logo_url.is_none() && connector.logo_url.is_some() {
|
||||
existing.logo_url = connector.logo_url;
|
||||
}
|
||||
if existing.logo_url_dark.is_none() && connector.logo_url_dark.is_some() {
|
||||
existing.logo_url_dark = connector.logo_url_dark;
|
||||
}
|
||||
if existing.distribution_channel.is_none() && connector.distribution_channel.is_some() {
|
||||
existing.distribution_channel = connector.distribution_channel;
|
||||
}
|
||||
existing
|
||||
.plugin_display_names
|
||||
.extend(connector.plugin_display_names);
|
||||
} else {
|
||||
merged.insert(connector_id, connector);
|
||||
}
|
||||
}
|
||||
|
||||
let mut merged = merged.into_values().collect::<Vec<_>>();
|
||||
for connector in &mut merged {
|
||||
if connector.install_url.is_none() {
|
||||
connector.install_url = Some(connector_install_url(&connector.name, &connector.id));
|
||||
}
|
||||
connector.plugin_display_names.sort_unstable();
|
||||
connector.plugin_display_names.dedup();
|
||||
}
|
||||
sort_connectors_by_accessibility_and_name(&mut merged);
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn merge_plugin_connectors<I>(connectors: Vec<AppInfo>, plugin_app_ids: I) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut merged = connectors;
|
||||
let mut connector_ids = merged
|
||||
.iter()
|
||||
.map(|connector| connector.id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for connector_id in plugin_app_ids {
|
||||
if connector_ids.insert(connector_id.clone()) {
|
||||
merged.push(plugin_connector_to_app_info(connector_id));
|
||||
}
|
||||
}
|
||||
|
||||
sort_connectors_by_accessibility_and_name(&mut merged);
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn merge_plugin_connectors_with_accessible<I>(
|
||||
plugin_app_ids: I,
|
||||
accessible_connectors: Vec<AppInfo>,
|
||||
) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let accessible_connector_ids: HashSet<&str> = accessible_connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
let plugin_connectors = plugin_app_ids
|
||||
.into_iter()
|
||||
.filter(|connector_id| accessible_connector_ids.contains(connector_id.as_str()))
|
||||
.map(plugin_connector_to_app_info)
|
||||
.collect::<Vec<_>>();
|
||||
merge_connectors(plugin_connectors, accessible_connectors)
|
||||
}
|
||||
|
||||
pub fn plugin_connector_to_app_info(connector_id: String) -> AppInfo {
|
||||
// Leave the placeholder name as the connector id so merge_connectors() can
|
||||
// replace it with canonical app metadata from directory fetches or
|
||||
// connector_name values from codex_apps tool discovery.
|
||||
let name = connector_id.clone();
|
||||
AppInfo {
|
||||
id: connector_id.clone(),
|
||||
name: name.clone(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(connector_install_url(&name, &connector_id)),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
|
||||
pub fn connector_display_label(connector: &AppInfo) -> String {
|
||||
connector.name.clone()
|
||||
}
|
||||
|
||||
pub fn connector_mention_slug(connector: &AppInfo) -> String {
|
||||
crate::connector_name_slug(&connector_display_label(connector))
|
||||
}
|
||||
|
||||
pub fn connector_install_url(name: &str, connector_id: &str) -> String {
|
||||
crate::connector_install_url(name, connector_id)
|
||||
}
|
||||
|
||||
pub fn sanitize_name(name: &str) -> String {
|
||||
crate::connector_name_slug(name).replace("-", "_")
|
||||
}
|
||||
|
||||
pub(crate) fn sort_connectors_by_accessibility_and_name(connectors: &mut [AppInfo]) {
|
||||
connectors.sort_by(|left, right| {
|
||||
right
|
||||
.is_accessible
|
||||
.cmp(&left.is_accessible)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
}
|
||||
@@ -6267,8 +6267,11 @@ pub(crate) async fn run_turn(
|
||||
HashMap::new()
|
||||
};
|
||||
let available_connectors = if turn_context.apps_enabled() {
|
||||
let connectors = connectors::merge_plugin_apps_with_accessible(
|
||||
loaded_plugins.effective_apps(),
|
||||
let connectors = codex_connectors::merge::merge_plugin_connectors_with_accessible(
|
||||
loaded_plugins
|
||||
.effective_apps()
|
||||
.into_iter()
|
||||
.map(|connector_id| connector_id.0),
|
||||
connectors::accessible_connectors_from_mcp_tools(&mcp_tools),
|
||||
);
|
||||
connectors::with_app_enabled_state(connectors, &turn_context.config)
|
||||
@@ -6914,7 +6917,7 @@ fn collect_explicit_app_ids_from_skill_items(
|
||||
|
||||
let connector_slug_counts = build_connector_slug_counts(connectors);
|
||||
for connector in connectors {
|
||||
let slug = connectors::connector_mention_slug(connector);
|
||||
let slug = codex_connectors::metadata::connector_mention_slug(connector);
|
||||
let connector_count = connector_slug_counts.get(&slug).copied().unwrap_or(0);
|
||||
let skill_count = skill_name_counts_lower.get(&slug).copied().unwrap_or(0);
|
||||
if connector_count == 1 && skill_count == 0 && mention_names_lower.contains(&slug) {
|
||||
@@ -6989,7 +6992,7 @@ fn connector_inserted_in_messages(
|
||||
return true;
|
||||
}
|
||||
|
||||
let mention_slug = connectors::connector_mention_slug(connector);
|
||||
let mention_slug = codex_connectors::metadata::connector_mention_slug(connector);
|
||||
let connector_count = connector_slug_counts
|
||||
.get(&mention_slug)
|
||||
.copied()
|
||||
@@ -7217,8 +7220,11 @@ pub(crate) async fn built_tools(
|
||||
connectors::with_app_enabled_state(connectors.clone(), &turn_context.config)
|
||||
});
|
||||
let connectors = if apps_enabled {
|
||||
let connectors = connectors::merge_plugin_apps_with_accessible(
|
||||
loaded_plugins.effective_apps(),
|
||||
let connectors = codex_connectors::merge::merge_plugin_connectors_with_accessible(
|
||||
loaded_plugins
|
||||
.effective_apps()
|
||||
.into_iter()
|
||||
.map(|connector_id| connector_id.0),
|
||||
accessible_connectors.clone().unwrap_or_default(),
|
||||
);
|
||||
Some(connectors::with_app_enabled_state(
|
||||
|
||||
@@ -413,7 +413,7 @@ fn make_mcp_tool(
|
||||
) -> ToolInfo {
|
||||
let tool_namespace = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connector_name
|
||||
.map(crate::connectors::sanitize_name)
|
||||
.map(codex_connectors::metadata::sanitize_name)
|
||||
.map(|connector_name| format!("mcp__{server_name}__{connector_name}"))
|
||||
.unwrap_or_else(|| server_name.to_string())
|
||||
} else {
|
||||
|
||||
+34
-314
@@ -1,4 +1,3 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
@@ -26,7 +25,6 @@ use crate::codex::INITIAL_SUBMIT_ID;
|
||||
use crate::config::Config;
|
||||
use crate::config_loader::AppsRequirementsToml;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::plugins::AppConnectorId;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::plugins::list_tool_suggest_discoverable_plugins;
|
||||
use codex_config::types::AppToolApproval;
|
||||
@@ -36,7 +34,6 @@ use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::create_client;
|
||||
use codex_login::default_client::is_first_party_chat_originator;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::McpConnectionManager;
|
||||
@@ -46,7 +43,6 @@ use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::with_codex_apps_mcp;
|
||||
|
||||
pub use codex_connectors::CONNECTORS_CACHE_TTL;
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
@@ -122,13 +118,15 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
|
||||
let directory_connectors =
|
||||
list_directory_connectors_for_tool_suggest_with_auth(config, auth).await?;
|
||||
let connector_ids = tool_suggest_connector_ids(config).await;
|
||||
let discoverable_connectors = filter_tool_suggest_discoverable_connectors(
|
||||
directory_connectors,
|
||||
accessible_connectors,
|
||||
&connector_ids,
|
||||
)
|
||||
.into_iter()
|
||||
.map(DiscoverableTool::from);
|
||||
let discoverable_connectors =
|
||||
codex_connectors::filter::filter_tool_suggest_discoverable_connectors(
|
||||
directory_connectors,
|
||||
accessible_connectors,
|
||||
&connector_ids,
|
||||
originator().value.as_str(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(DiscoverableTool::from);
|
||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(config)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -151,7 +149,12 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
|
||||
return Some(Vec::new());
|
||||
}
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
read_cached_accessible_connectors(&cache_key).map(filter_disallowed_connectors)
|
||||
read_cached_accessible_connectors(&cache_key).map(|connectors| {
|
||||
codex_connectors::filter::filter_disallowed_connectors(
|
||||
connectors,
|
||||
originator().value.as_str(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
|
||||
@@ -164,8 +167,10 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
|
||||
}
|
||||
|
||||
let cache_key = accessible_connectors_cache_key(config, auth);
|
||||
let accessible_connectors =
|
||||
filter_disallowed_connectors(accessible_connectors_from_mcp_tools(mcp_tools));
|
||||
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
|
||||
accessible_connectors_from_mcp_tools(mcp_tools),
|
||||
originator().value.as_str(),
|
||||
);
|
||||
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
||||
}
|
||||
|
||||
@@ -202,7 +207,10 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await;
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
||||
{
|
||||
let cached_connectors = filter_disallowed_connectors(cached_connectors);
|
||||
let cached_connectors = codex_connectors::filter::filter_disallowed_connectors(
|
||||
cached_connectors,
|
||||
originator().value.as_str(),
|
||||
);
|
||||
let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance);
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: cached_connectors,
|
||||
@@ -293,8 +301,10 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
let accessible_connectors =
|
||||
filter_disallowed_connectors(accessible_connectors_from_mcp_tools(&tools));
|
||||
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
|
||||
accessible_connectors_from_mcp_tools(&tools),
|
||||
originator().value.as_str(),
|
||||
);
|
||||
if codex_apps_ready || !accessible_connectors.is_empty() {
|
||||
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
||||
}
|
||||
@@ -357,35 +367,11 @@ fn write_cached_accessible_connectors(
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
*cache_guard = Some(CachedAccessibleConnectors {
|
||||
key: cache_key,
|
||||
expires_at: Instant::now() + CONNECTORS_CACHE_TTL,
|
||||
expires_at: Instant::now() + codex_connectors::CONNECTORS_CACHE_TTL,
|
||||
connectors: connectors.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
fn filter_tool_suggest_discoverable_connectors(
|
||||
directory_connectors: Vec<AppInfo>,
|
||||
accessible_connectors: &[AppInfo],
|
||||
discoverable_connector_ids: &HashSet<String>,
|
||||
) -> Vec<AppInfo> {
|
||||
let accessible_connector_ids: HashSet<&str> = accessible_connectors
|
||||
.iter()
|
||||
.filter(|connector| connector.is_accessible)
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
|
||||
let mut connectors = filter_disallowed_connectors(directory_connectors)
|
||||
.into_iter()
|
||||
.filter(|connector| !accessible_connector_ids.contains(connector.id.as_str()))
|
||||
.filter(|connector| discoverable_connector_ids.contains(connector.id.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
connectors.sort_by(|left, right| {
|
||||
left.name
|
||||
.cmp(&right.name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
connectors
|
||||
}
|
||||
|
||||
async fn tool_suggest_connector_ids(config: &Config) -> HashSet<String> {
|
||||
let mut connector_ids = PluginsManager::new(config.codex_home.to_path_buf())
|
||||
.plugins_for_config(config)
|
||||
@@ -493,14 +479,6 @@ async fn chatgpt_get_request_with_token<T: DeserializeOwned>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connector_display_label(connector: &AppInfo) -> String {
|
||||
format_connector_label(&connector.name, &connector.id)
|
||||
}
|
||||
|
||||
pub fn connector_mention_slug(connector: &AppInfo) -> String {
|
||||
sanitize_slug(&connector_display_label(connector))
|
||||
}
|
||||
|
||||
pub(crate) fn accessible_connectors_from_mcp_tools(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
@@ -511,114 +489,14 @@ pub(crate) fn accessible_connectors_from_mcp_tools(
|
||||
return None;
|
||||
}
|
||||
let connector_id = tool.connector_id.as_deref()?;
|
||||
Some((
|
||||
connector_id.to_string(),
|
||||
normalize_connector_value(tool.connector_name.as_deref()),
|
||||
normalize_connector_value(tool.connector_description.as_deref()),
|
||||
tool.plugin_display_names.clone(),
|
||||
))
|
||||
});
|
||||
collect_accessible_connectors(tools)
|
||||
}
|
||||
|
||||
pub fn merge_connectors(
|
||||
connectors: Vec<AppInfo>,
|
||||
accessible_connectors: Vec<AppInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
let mut merged: HashMap<String, AppInfo> = connectors
|
||||
.into_iter()
|
||||
.map(|mut connector| {
|
||||
connector.is_accessible = false;
|
||||
(connector.id.clone(), connector)
|
||||
Some(codex_connectors::accessible::AccessibleConnectorTool {
|
||||
connector_id: connector_id.to_string(),
|
||||
connector_name: tool.connector_name.clone(),
|
||||
connector_description: tool.connector_description.clone(),
|
||||
plugin_display_names: tool.plugin_display_names.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
for mut connector in accessible_connectors {
|
||||
connector.is_accessible = true;
|
||||
let connector_id = connector.id.clone();
|
||||
if let Some(existing) = merged.get_mut(&connector_id) {
|
||||
existing.is_accessible = true;
|
||||
if existing.name == existing.id && connector.name != connector.id {
|
||||
existing.name = connector.name;
|
||||
}
|
||||
if existing.description.is_none() && connector.description.is_some() {
|
||||
existing.description = connector.description;
|
||||
}
|
||||
if existing.logo_url.is_none() && connector.logo_url.is_some() {
|
||||
existing.logo_url = connector.logo_url;
|
||||
}
|
||||
if existing.logo_url_dark.is_none() && connector.logo_url_dark.is_some() {
|
||||
existing.logo_url_dark = connector.logo_url_dark;
|
||||
}
|
||||
if existing.distribution_channel.is_none() && connector.distribution_channel.is_some() {
|
||||
existing.distribution_channel = connector.distribution_channel;
|
||||
}
|
||||
existing
|
||||
.plugin_display_names
|
||||
.extend(connector.plugin_display_names);
|
||||
} else {
|
||||
merged.insert(connector_id, connector);
|
||||
}
|
||||
}
|
||||
|
||||
let mut merged = merged.into_values().collect::<Vec<_>>();
|
||||
for connector in &mut merged {
|
||||
if connector.install_url.is_none() {
|
||||
connector.install_url = Some(connector_install_url(&connector.name, &connector.id));
|
||||
}
|
||||
connector.plugin_display_names.sort_unstable();
|
||||
connector.plugin_display_names.dedup();
|
||||
}
|
||||
merged.sort_by(|left, right| {
|
||||
right
|
||||
.is_accessible
|
||||
.cmp(&left.is_accessible)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn merge_plugin_apps(
|
||||
connectors: Vec<AppInfo>,
|
||||
plugin_apps: Vec<AppConnectorId>,
|
||||
) -> Vec<AppInfo> {
|
||||
let mut merged = connectors;
|
||||
let mut connector_ids = merged
|
||||
.iter()
|
||||
.map(|connector| connector.id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for connector_id in plugin_apps {
|
||||
if connector_ids.insert(connector_id.0.clone()) {
|
||||
merged.push(plugin_app_to_app_info(connector_id));
|
||||
}
|
||||
}
|
||||
|
||||
merged.sort_by(|left, right| {
|
||||
right
|
||||
.is_accessible
|
||||
.cmp(&left.is_accessible)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn merge_plugin_apps_with_accessible(
|
||||
plugin_apps: Vec<AppConnectorId>,
|
||||
accessible_connectors: Vec<AppInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
let accessible_connector_ids: HashSet<&str> = accessible_connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
let plugin_connectors = plugin_apps
|
||||
.into_iter()
|
||||
.filter(|connector_id| accessible_connector_ids.contains(connector_id.0.as_str()))
|
||||
.map(plugin_app_to_app_info)
|
||||
.collect::<Vec<_>>();
|
||||
merge_connectors(plugin_connectors, accessible_connectors)
|
||||
codex_connectors::accessible::collect_accessible_connectors(tools)
|
||||
}
|
||||
|
||||
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
|
||||
@@ -691,45 +569,6 @@ pub(crate) fn codex_app_tool_is_enabled(config: &Config, tool_info: &ToolInfo) -
|
||||
.enabled
|
||||
}
|
||||
|
||||
const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
|
||||
"asdk_app_6938a94a61d881918ef32cb999ff937c",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
"connector_3f8d1a79f27c4c7ba1a897ab13bf37dc",
|
||||
"connector_68de829bf7648191acd70a907364c67c",
|
||||
"connector_68e004f14af881919eb50893d3d9f523",
|
||||
"connector_69272cb413a081919685ec3c88d1744e",
|
||||
];
|
||||
const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] =
|
||||
&["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"];
|
||||
const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_";
|
||||
|
||||
pub fn filter_disallowed_connectors(connectors: Vec<AppInfo>) -> Vec<AppInfo> {
|
||||
filter_disallowed_connectors_for_originator(connectors, originator().value.as_str())
|
||||
}
|
||||
|
||||
fn filter_disallowed_connectors_for_originator(
|
||||
connectors: Vec<AppInfo>,
|
||||
originator_value: &str,
|
||||
) -> Vec<AppInfo> {
|
||||
connectors
|
||||
.into_iter()
|
||||
.filter(|connector| {
|
||||
is_connector_id_allowed_for_originator(connector.id.as_str(), originator_value)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_connector_id_allowed_for_originator(connector_id: &str, originator_value: &str) -> bool {
|
||||
let disallowed_connector_ids = if is_first_party_chat_originator(originator_value) {
|
||||
FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS
|
||||
} else {
|
||||
DISALLOWED_CONNECTOR_IDS
|
||||
};
|
||||
|
||||
!connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX)
|
||||
&& !disallowed_connector_ids.contains(&connector_id)
|
||||
}
|
||||
|
||||
fn read_apps_config(config: &Config) -> Option<AppsConfigToml> {
|
||||
let apps_config = read_user_apps_config(config);
|
||||
let had_apps_config = apps_config.is_some();
|
||||
@@ -851,125 +690,6 @@ fn app_tool_policy_from_apps_config(
|
||||
AppToolPolicy { enabled, approval }
|
||||
}
|
||||
|
||||
fn collect_accessible_connectors<I>(tools: I) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = (String, Option<String>, Option<String>, Vec<String>)>,
|
||||
{
|
||||
let mut connectors: HashMap<String, (AppInfo, BTreeSet<String>)> = HashMap::new();
|
||||
for (connector_id, connector_name, connector_description, plugin_display_names) in tools {
|
||||
let connector_name = connector_name.unwrap_or_else(|| connector_id.clone());
|
||||
if let Some((existing, existing_plugin_display_names)) = connectors.get_mut(&connector_id) {
|
||||
if existing.name == connector_id && connector_name != connector_id {
|
||||
existing.name = connector_name;
|
||||
}
|
||||
if existing.description.is_none() && connector_description.is_some() {
|
||||
existing.description = connector_description;
|
||||
}
|
||||
existing_plugin_display_names.extend(plugin_display_names);
|
||||
} else {
|
||||
connectors.insert(
|
||||
connector_id.clone(),
|
||||
(
|
||||
AppInfo {
|
||||
id: connector_id.clone(),
|
||||
name: connector_name,
|
||||
description: connector_description,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
plugin_display_names
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<String>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut accessible: Vec<AppInfo> = connectors
|
||||
.into_values()
|
||||
.map(|(mut connector, plugin_display_names)| {
|
||||
connector.plugin_display_names = plugin_display_names.into_iter().collect();
|
||||
connector.install_url = Some(connector_install_url(&connector.name, &connector.id));
|
||||
connector
|
||||
})
|
||||
.collect();
|
||||
accessible.sort_by(|left, right| {
|
||||
right
|
||||
.is_accessible
|
||||
.cmp(&left.is_accessible)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
accessible
|
||||
}
|
||||
|
||||
fn plugin_app_to_app_info(connector_id: AppConnectorId) -> AppInfo {
|
||||
// Leave the placeholder name as the connector id so merge_connectors() can
|
||||
// replace it with canonical app metadata from directory fetches or
|
||||
// connector_name values from codex_apps tool discovery.
|
||||
let connector_id = connector_id.0;
|
||||
let name = connector_id.clone();
|
||||
AppInfo {
|
||||
id: connector_id.clone(),
|
||||
name: name.clone(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(connector_install_url(&name, &connector_id)),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_connector_value(value: Option<&str>) -> Option<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub fn connector_install_url(name: &str, connector_id: &str) -> String {
|
||||
let slug = sanitize_slug(name);
|
||||
format!("https://chatgpt.com/apps/{slug}/{connector_id}")
|
||||
}
|
||||
|
||||
pub fn sanitize_name(name: &str) -> String {
|
||||
sanitize_slug(name).replace("-", "_")
|
||||
}
|
||||
|
||||
fn sanitize_slug(name: &str) -> String {
|
||||
let mut normalized = String::with_capacity(name.len());
|
||||
for character in name.chars() {
|
||||
if character.is_ascii_alphanumeric() {
|
||||
normalized.push(character.to_ascii_lowercase());
|
||||
} else {
|
||||
normalized.push('-');
|
||||
}
|
||||
}
|
||||
let normalized = normalized.trim_matches('-');
|
||||
if normalized.is_empty() {
|
||||
"app".to_string()
|
||||
} else {
|
||||
normalized.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_connector_label(name: &str, _id: &str) -> String {
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "connectors_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -11,6 +11,13 @@ use codex_config::types::AppConfig;
|
||||
use codex_config::types::AppToolConfig;
|
||||
use codex_config::types::AppToolsConfig;
|
||||
use codex_config::types::AppsDefaultConfig;
|
||||
use codex_connectors::filter::filter_disallowed_connectors;
|
||||
use codex_connectors::filter::filter_tool_suggest_discoverable_connectors;
|
||||
use codex_connectors::merge::merge_connectors;
|
||||
use codex_connectors::merge::plugin_connector_to_app_info;
|
||||
use codex_connectors::metadata::connector_install_url;
|
||||
use codex_connectors::metadata::connector_mention_slug;
|
||||
use codex_connectors::metadata::sanitize_name;
|
||||
use codex_features::Feature;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
@@ -138,7 +145,7 @@ fn with_accessible_connectors_cache_cleared<R>(f: impl FnOnce() -> R) -> R {
|
||||
|
||||
#[test]
|
||||
fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
|
||||
let plugin = plugin_app_to_app_info(AppConnectorId("calendar".to_string()));
|
||||
let plugin = plugin_connector_to_app_info("calendar".to_string());
|
||||
let accessible = google_calendar_accessible_connector(&[]);
|
||||
|
||||
let merged = merge_connectors(vec![plugin], vec![accessible]);
|
||||
@@ -281,7 +288,7 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
|
||||
#[test]
|
||||
fn merge_connectors_unions_and_dedupes_plugin_display_names() {
|
||||
let mut plugin = plugin_app_to_app_info(AppConnectorId("calendar".to_string()));
|
||||
let mut plugin = plugin_connector_to_app_info("calendar".to_string());
|
||||
plugin.plugin_display_names = plugin_names(&["sample", "alpha", "sample"]);
|
||||
|
||||
let accessible = google_calendar_accessible_connector(&["beta", "alpha"]);
|
||||
@@ -975,33 +982,40 @@ fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() {
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_allows_non_disallowed_connectors() {
|
||||
let filtered = filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
let filtered =
|
||||
filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")], "codex_cli");
|
||||
assert_eq!(filtered, vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_filters_openai_prefix() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("connector_openai_foo"),
|
||||
app("connector_openai_bar"),
|
||||
app("gamma"),
|
||||
]);
|
||||
let filtered = filter_disallowed_connectors(
|
||||
vec![
|
||||
app("connector_openai_foo"),
|
||||
app("connector_openai_bar"),
|
||||
app("gamma"),
|
||||
],
|
||||
"codex_cli",
|
||||
);
|
||||
assert_eq!(filtered, vec![app("gamma")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_filters_disallowed_connector_ids() {
|
||||
let filtered = filter_disallowed_connectors(vec![
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
app("connector_3f8d1a79f27c4c7ba1a897ab13bf37dc"),
|
||||
app("delta"),
|
||||
]);
|
||||
let filtered = filter_disallowed_connectors(
|
||||
vec![
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
app("connector_3f8d1a79f27c4c7ba1a897ab13bf37dc"),
|
||||
app("delta"),
|
||||
],
|
||||
"codex_cli",
|
||||
);
|
||||
assert_eq!(filtered, vec![app("delta")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() {
|
||||
let filtered = filter_disallowed_connectors_for_originator(
|
||||
let filtered = filter_disallowed_connectors(
|
||||
vec![
|
||||
app("connector_openai_foo"),
|
||||
app("asdk_app_6938a94a61d881918ef32cb999ff937c"),
|
||||
@@ -1064,6 +1078,7 @@ fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstal
|
||||
"connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
"connector_68df038e0ba48191908c8434991bbac2".to_string(),
|
||||
]),
|
||||
"codex_cli",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1103,6 +1118,7 @@ fn filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_whe
|
||||
"connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
"connector_68df038e0ba48191908c8434991bbac2".to_string(),
|
||||
]),
|
||||
"codex_cli",
|
||||
);
|
||||
|
||||
assert_eq!(filtered, Vec::<AppInfo>::new());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_connectors::metadata::connector_display_label;
|
||||
use codex_protocol::models::DeveloperInstructions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
@@ -46,7 +47,7 @@ pub(crate) fn build_plugin_injections(
|
||||
.iter()
|
||||
.any(|plugin_name| plugin_name == &plugin.display_name)
|
||||
})
|
||||
.map(connectors::connector_display_label)
|
||||
.map(connector_display_label)
|
||||
.collect::<BTreeSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_connectors::metadata::connector_mention_slug;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
use crate::connectors;
|
||||
@@ -108,7 +109,7 @@ pub(crate) fn build_connector_slug_counts(
|
||||
) -> HashMap<String, usize> {
|
||||
let mut counts: HashMap<String, usize> = HashMap::new();
|
||||
for connector in connectors {
|
||||
let slug = connectors::connector_mention_slug(connector);
|
||||
let slug = connector_mention_slug(connector);
|
||||
*counts.entry(slug).or_insert(0) += 1;
|
||||
}
|
||||
counts
|
||||
|
||||
@@ -33,6 +33,7 @@ codex-install-context = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-connectors = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
use codex_chatgpt::connectors::AppInfo;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
|
||||
@@ -209,8 +209,7 @@ use crate::legacy_core::plugins::PluginCapabilitySummary;
|
||||
use crate::legacy_core::skills::model::SkillMetadata;
|
||||
use crate::tui::FrameRequester;
|
||||
use crate::ui_consts::LIVE_PREFIX_COLS;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_chatgpt::connectors::AppInfo;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_file_search::FileMatch;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -3417,9 +3416,9 @@ impl ChatComposer {
|
||||
if !connector.is_accessible || !connector.is_enabled {
|
||||
continue;
|
||||
}
|
||||
let display_name = connectors::connector_display_label(connector);
|
||||
let display_name = codex_connectors::metadata::connector_display_label(connector);
|
||||
let description = Some(Self::connector_brief_description(connector));
|
||||
let slug = crate::legacy_core::connectors::connector_mention_slug(connector);
|
||||
let slug = codex_connectors::metadata::connector_mention_slug(connector);
|
||||
let search_terms = vec![display_name.clone(), connector.id.clone(), slug.clone()];
|
||||
let connector_id = connector.id.as_str();
|
||||
mentions.push(MentionItem {
|
||||
|
||||
@@ -79,6 +79,7 @@ use crate::terminal_title::clear_terminal_title;
|
||||
use crate::terminal_title::set_terminal_title;
|
||||
use crate::text_formatting::proper_join;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CollabAgentState as AppServerCollabAgentState;
|
||||
@@ -5525,7 +5526,7 @@ impl ChatWidget {
|
||||
|
||||
let app_mentions = find_app_mentions(&mentions, apps, &skill_names_lower);
|
||||
for app in app_mentions {
|
||||
let slug = crate::legacy_core::connectors::connector_mention_slug(&app);
|
||||
let slug = codex_connectors::metadata::connector_mention_slug(&app);
|
||||
if bound_names.contains(&slug) || !selected_app_ids.insert(app.id.clone()) {
|
||||
continue;
|
||||
}
|
||||
@@ -9701,7 +9702,7 @@ impl ChatWidget {
|
||||
self.config.features.enabled(Feature::Apps) && self.has_chatgpt_account
|
||||
}
|
||||
|
||||
fn connectors_for_mentions(&self) -> Option<&[connectors::AppInfo]> {
|
||||
fn connectors_for_mentions(&self) -> Option<&[AppInfo]> {
|
||||
if !self.connectors_enabled() {
|
||||
return None;
|
||||
}
|
||||
@@ -9888,7 +9889,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_connectors_popup(&mut self, connectors: &[connectors::AppInfo]) {
|
||||
fn open_connectors_popup(&mut self, connectors: &[AppInfo]) {
|
||||
self.bottom_pane.show_selection_view(
|
||||
self.connectors_popup_params(connectors, /*selected_connector_id*/ None),
|
||||
);
|
||||
@@ -9914,7 +9915,7 @@ impl ChatWidget {
|
||||
|
||||
fn connectors_popup_params(
|
||||
&self,
|
||||
connectors: &[connectors::AppInfo],
|
||||
connectors: &[AppInfo],
|
||||
selected_connector_id: Option<&str>,
|
||||
) -> SelectionViewParams {
|
||||
let total = connectors.len();
|
||||
@@ -9937,7 +9938,7 @@ impl ChatWidget {
|
||||
});
|
||||
let mut items: Vec<SelectionItem> = Vec::with_capacity(connectors.len());
|
||||
for connector in connectors {
|
||||
let connector_label = connectors::connector_display_label(connector);
|
||||
let connector_label = codex_connectors::metadata::connector_display_label(connector);
|
||||
let connector_title = connector_label.clone();
|
||||
let link_description = Self::connector_description(connector);
|
||||
let description = Self::connector_brief_description(connector);
|
||||
@@ -10011,7 +10012,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_connectors_popup_if_open(&mut self, connectors: &[connectors::AppInfo]) {
|
||||
fn refresh_connectors_popup_if_open(&mut self, connectors: &[AppInfo]) {
|
||||
let selected_connector_id =
|
||||
if let (Some(selected_index), ConnectorsCacheState::Ready(snapshot)) = (
|
||||
self.bottom_pane
|
||||
@@ -10039,7 +10040,7 @@ impl ChatWidget {
|
||||
])
|
||||
}
|
||||
|
||||
fn connector_brief_description(connector: &connectors::AppInfo) -> String {
|
||||
fn connector_brief_description(connector: &AppInfo) -> String {
|
||||
let status_label = Self::connector_status_label(connector);
|
||||
match Self::connector_description(connector) {
|
||||
Some(description) => format!("{status_label} · {description}"),
|
||||
@@ -10047,7 +10048,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn connector_status_label(connector: &connectors::AppInfo) -> &'static str {
|
||||
fn connector_status_label(connector: &AppInfo) -> &'static str {
|
||||
if connector.is_accessible {
|
||||
if connector.is_enabled {
|
||||
"Installed"
|
||||
@@ -10059,7 +10060,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn connector_description(connector: &connectors::AppInfo) -> Option<String> {
|
||||
fn connector_description(connector: &AppInfo) -> Option<String> {
|
||||
connector
|
||||
.description
|
||||
.as_deref()
|
||||
|
||||
@@ -9,14 +9,13 @@ use crate::bottom_pane::SkillsToggleItem;
|
||||
use crate::bottom_pane::SkillsToggleView;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::legacy_core::TOOL_MENTION_SIGIL;
|
||||
use crate::legacy_core::connectors::connector_mention_slug;
|
||||
use crate::legacy_core::skills::model::SkillDependencies;
|
||||
use crate::legacy_core::skills::model::SkillInterface;
|
||||
use crate::legacy_core::skills::model::SkillMetadata;
|
||||
use crate::legacy_core::skills::model::SkillToolDependency;
|
||||
use crate::skills_helpers::skill_description;
|
||||
use crate::skills_helpers::skill_display_name;
|
||||
use codex_chatgpt::connectors::AppInfo;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::ListSkillsResponseEvent;
|
||||
use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata;
|
||||
@@ -296,12 +295,12 @@ pub(crate) fn find_app_mentions(
|
||||
|
||||
let mut slug_counts: HashMap<String, usize> = HashMap::new();
|
||||
for app in apps.iter().filter(|app| app.is_enabled) {
|
||||
let slug = connector_mention_slug(app);
|
||||
let slug = codex_connectors::metadata::connector_mention_slug(app);
|
||||
*slug_counts.entry(slug).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
for app in apps.iter().filter(|app| app.is_enabled) {
|
||||
let slug = connector_mention_slug(app);
|
||||
let slug = codex_connectors::metadata::connector_mention_slug(app);
|
||||
let slug_count = slug_counts.get(&slug).copied().unwrap_or(0);
|
||||
if mentions.names.contains(&slug)
|
||||
&& !explicit_names.contains(&slug)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -511,7 +512,7 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: notion_id.to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -545,7 +546,7 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: notion_id.to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -560,7 +561,7 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: linear_id.to_string(),
|
||||
name: "Linear".to_string(),
|
||||
description: Some("Project tracking".to_string()),
|
||||
@@ -604,7 +605,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
let linear_id = "unit_test_apps_refresh_failure_connector_2";
|
||||
|
||||
let full_connectors = vec![
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: notion_id.to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -619,7 +620,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: linear_id.to_string(),
|
||||
name: "Linear".to_string(),
|
||||
description: Some("Project tracking".to_string()),
|
||||
@@ -644,7 +645,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: notion_id.to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -693,7 +694,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "notion".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -708,7 +709,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "slack".to_string(),
|
||||
name: "Slack".to_string(),
|
||||
description: Some("Team chat".to_string()),
|
||||
@@ -739,7 +740,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "airtable".to_string(),
|
||||
name: "Airtable".to_string(),
|
||||
description: Some("Spreadsheets".to_string()),
|
||||
@@ -754,7 +755,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "notion".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -769,7 +770,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "slack".to_string(),
|
||||
name: "Slack".to_string(),
|
||||
description: Some("Team chat".to_string()),
|
||||
@@ -812,7 +813,7 @@ async fn apps_refresh_failure_with_cached_snapshot_triggers_pending_force_refetc
|
||||
chat.connectors_prefetch_in_flight = true;
|
||||
chat.connectors_force_refetch_pending = true;
|
||||
|
||||
let full_connectors = vec![codex_chatgpt::connectors::AppInfo {
|
||||
let full_connectors = vec![AppInfo {
|
||||
id: "unit_test_apps_refresh_failure_pending_connector".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -855,7 +856,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
|
||||
chat.bottom_pane.set_connectors_enabled(/*enabled*/ true);
|
||||
|
||||
let full_connectors = vec![
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "unit_test_connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -870,7 +871,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "unit_test_connector_2".to_string(),
|
||||
name: "Linear".to_string(),
|
||||
description: Some("Project tracking".to_string()),
|
||||
@@ -897,7 +898,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "unit_test_connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -912,7 +913,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
AppInfo {
|
||||
id: "connector_openai_hidden".to_string(),
|
||||
name: "Hidden OpenAI".to_string(),
|
||||
description: Some("Should be filtered".to_string()),
|
||||
@@ -960,7 +961,7 @@ async fn apps_refresh_failure_without_full_snapshot_falls_back_to_installed_apps
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "unit_test_apps_refresh_failure_fallback_connector".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1019,7 +1020,7 @@ async fn apps_popup_shows_disabled_status_for_installed_but_disabled_apps() {
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1073,7 +1074,7 @@ async fn apps_initial_load_applies_enabled_state_from_config() {
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1139,7 +1140,7 @@ async fn apps_initial_load_applies_enabled_state_from_requirements_with_user_ove
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1203,7 +1204,7 @@ async fn apps_initial_load_applies_enabled_state_from_requirements_without_user_
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1252,7 +1253,7 @@ async fn apps_refresh_preserves_toggled_enabled_state() {
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1274,7 +1275,7 @@ async fn apps_refresh_preserves_toggled_enabled_state() {
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
@@ -1323,7 +1324,7 @@ async fn apps_popup_for_not_installed_app_uses_install_only_selected_description
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
connectors: vec![AppInfo {
|
||||
id: "connector_2".to_string(),
|
||||
name: "Linear".to_string(),
|
||||
description: Some("Project tracking".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user