mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[connectors] Ignore synthetic links for app accessibility (#28770)
Summary - Stop treating Codex Apps MCP tools with `_meta._codex_apps.synthetic_link: true` as evidence that a connector is accessible in `app/list`. - Preserve synthetic tools in the agent-facing MCP connector set so they remain available for install/auth flows. - Keep the app-list accessibility cache limited to connectors backed by at least one non-synthetic tool. - Add focused regression coverage for both sides of the boundary. Validation - `just fmt` - `just test -p codex-core synthetic_links_are_exposed_to_the_agent_but_not_accessible_in_app_list` - `git diff --check` - A crate-wide `just test -p codex-core` run completed with 2,699 passing and 51 unrelated local sandbox/state failures, primarily state DB migration races (`UNIQUE constraint failed: _sqlx_migrations.version`).
This commit is contained in:
committed by
GitHub
Unverified
parent
ec848dde0e
commit
4af7762f01
@@ -32,6 +32,7 @@ use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
|
||||
use codex_mcp::McpConnectionManager;
|
||||
use codex_mcp::McpRuntimeContext;
|
||||
use codex_mcp::ToolInfo;
|
||||
@@ -154,7 +155,7 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
|
||||
}
|
||||
|
||||
let cache_key = accessible_connectors_cache_key(config, auth);
|
||||
let accessible_connectors = accessible_connectors_from_mcp_tools(mcp_tools);
|
||||
let accessible_connectors = accessible_connectors_for_app_list_from_mcp_tools(mcp_tools);
|
||||
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
||||
}
|
||||
|
||||
@@ -339,7 +340,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
let accessible_connectors = accessible_connectors_from_mcp_tools(&tools);
|
||||
let accessible_connectors = accessible_connectors_for_app_list_from_mcp_tools(&tools);
|
||||
if codex_apps_ready || !accessible_connectors.is_empty() {
|
||||
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
||||
}
|
||||
@@ -469,9 +470,15 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth(
|
||||
}
|
||||
|
||||
pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo> {
|
||||
collect_accessible_connectors_from_mcp_tools(mcp_tools.iter())
|
||||
}
|
||||
|
||||
fn collect_accessible_connectors_from_mcp_tools<'a>(
|
||||
mcp_tools: impl Iterator<Item = &'a ToolInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
// ToolInfo already carries plugin provenance, so app-level plugin sources
|
||||
// can be derived here instead of requiring a separate enrichment pass.
|
||||
let tools = mcp_tools.iter().filter_map(|tool| {
|
||||
let tools = mcp_tools.filter_map(|tool| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
@@ -486,6 +493,20 @@ pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Ve
|
||||
codex_connectors::accessible::collect_accessible_connectors(tools)
|
||||
}
|
||||
|
||||
fn accessible_connectors_for_app_list_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo> {
|
||||
let non_synthetic_tools = mcp_tools.iter().filter(|tool| {
|
||||
tool.tool
|
||||
.meta
|
||||
.as_deref()
|
||||
.and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|meta| meta.get("synthetic_link"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
!= Some(true)
|
||||
});
|
||||
collect_accessible_connectors_from_mcp_tools(non_synthetic_tools)
|
||||
}
|
||||
|
||||
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
|
||||
let user_apps_config = apps_config_from_layer_stack(&config.config_layer_stack);
|
||||
let requirements_apps_config = config.config_layer_stack.requirements_toml().apps.as_ref();
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -140,6 +141,69 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_links_are_exposed_to_the_agent_but_not_accessible_in_app_list() {
|
||||
let mut synthetic_tool = codex_app_tool("gmail_batch_read_email", "gmail", Some("Gmail"), &[]);
|
||||
synthetic_tool.tool.meta = Some(Meta(
|
||||
serde_json::json!({
|
||||
"resource_name": "gmail.batch_read_email",
|
||||
"_codex_apps": {
|
||||
"resource_uri": "/connector/gmail/batch_read_email",
|
||||
"contains_mcp_source": false,
|
||||
"synthetic_link": true
|
||||
}
|
||||
})
|
||||
.as_object()
|
||||
.expect("meta should be an object")
|
||||
.clone(),
|
||||
));
|
||||
let tools = vec![
|
||||
synthetic_tool,
|
||||
codex_app_tool("calendar_list_events", "calendar", Some("Calendar"), &[]),
|
||||
];
|
||||
|
||||
let calendar = AppInfo {
|
||||
id: "calendar".to_string(),
|
||||
name: "Calendar".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: Some(connector_install_url("Calendar", "calendar")),
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
accessible_connectors_for_app_list_from_mcp_tools(&tools),
|
||||
vec![calendar.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
accessible_connectors_from_mcp_tools(&tools),
|
||||
vec![
|
||||
calendar,
|
||||
AppInfo {
|
||||
id: "gmail".to_string(),
|
||||
name: "Gmail".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: Some(connector_install_url("Gmail", "gmail")),
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_installed_apps() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
|
||||
Reference in New Issue
Block a user