mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Route selected apps over MCP fallbacks
This commit is contained in:
@@ -11,6 +11,8 @@ use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppsListParams;
|
||||
use codex_app_server_protocol::AppsListResponse;
|
||||
use codex_app_server_protocol::CapabilityRootLocation;
|
||||
use codex_app_server_protocol::ListMcpServerStatusParams;
|
||||
use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
@@ -29,6 +31,10 @@ use super::app_list::connector_tool;
|
||||
use super::app_list::start_apps_server_with_delays_and_access_token;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const APP_CONFIG: &[u8] = br#"{"apps":{"calendar_app":{"id":"calendar"}}}"#;
|
||||
const APP_NAME: &str = "calendar_app";
|
||||
const CONFLICTING_MCP_CONFIG: &[u8] =
|
||||
br#"{"mcpServers":{"calendar_app":{"command":"must-not-start","startup_timeout_sec":1}}}"#;
|
||||
const CONNECTOR_ID: &str = "calendar";
|
||||
const CONNECTOR_NAME: &str = "Calendar";
|
||||
const PLUGIN_DISPLAY_NAME: &str = "Executor Calendar";
|
||||
@@ -92,13 +98,8 @@ async fn selected_executor_connector_is_frozen_thread_scoped_and_hosted() -> Res
|
||||
.await?;
|
||||
let app_config_path = plugin_root.join(".app.json")?;
|
||||
executor_file_system
|
||||
.write_file(
|
||||
&app_config_path,
|
||||
br#"{"apps":{"calendar":{"id":"calendar"}}}"#.to_vec(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.write_file(&app_config_path, APP_CONFIG.to_vec(), /*sandbox*/ None)
|
||||
.await?;
|
||||
|
||||
let mut app_server = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??;
|
||||
let environment_id = app_server.auto_env_params()?.environment_id;
|
||||
@@ -177,10 +178,13 @@ async fn selected_executor_connector_is_frozen_thread_scoped_and_hosted() -> Res
|
||||
}
|
||||
);
|
||||
|
||||
executor_file_system
|
||||
.write_file(&app_config_path, APP_CONFIG.to_vec(), /*sandbox*/ None)
|
||||
.await?;
|
||||
executor_file_system
|
||||
.write_file(
|
||||
&app_config_path,
|
||||
br#"{"apps":{"calendar":{"id":"calendar"}}}"#.to_vec(),
|
||||
&plugin_root.join(".mcp.json")?,
|
||||
CONFLICTING_MCP_CONFIG.to_vec(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
@@ -195,6 +199,22 @@ async fn selected_executor_connector_is_frozen_thread_scoped_and_hosted() -> Res
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let request_id = app_server
|
||||
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
detail: None,
|
||||
thread_id: Some(model_selected_thread.clone()),
|
||||
})
|
||||
.await?;
|
||||
let response = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: ListMcpServerStatusResponse = to_response(response)?;
|
||||
assert!(response.data.iter().all(|server| server.name != APP_NAME));
|
||||
|
||||
let selected_description =
|
||||
model_tool_description(&mut app_server, &responses_server, &model_selected_thread).await?;
|
||||
let unselected_description =
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_plugin::PluginCapabilitySummary;
|
||||
pub struct PluginConnectorSource {
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
app_names: HashSet<String>,
|
||||
connector_ids: Vec<AppConnectorId>,
|
||||
}
|
||||
|
||||
@@ -20,13 +21,21 @@ impl PluginConnectorSource {
|
||||
plugin_display_name: impl Into<String>,
|
||||
declarations: impl IntoIterator<Item = AppDeclaration>,
|
||||
) -> Self {
|
||||
Self::from_connector_ids(
|
||||
let declarations = declarations.into_iter().collect::<Vec<_>>();
|
||||
let app_names = declarations
|
||||
.iter()
|
||||
.filter(|declaration| !declaration.connector_id.0.trim().is_empty())
|
||||
.map(|declaration| declaration.name.clone())
|
||||
.collect();
|
||||
let mut source = Self::from_connector_ids(
|
||||
plugin_id,
|
||||
plugin_display_name,
|
||||
declarations
|
||||
.into_iter()
|
||||
.map(|declaration| declaration.connector_id),
|
||||
)
|
||||
);
|
||||
source.app_names = app_names;
|
||||
source
|
||||
}
|
||||
|
||||
/// Creates one plugin source from connector IDs that were already parsed.
|
||||
@@ -44,6 +53,7 @@ impl PluginConnectorSource {
|
||||
Self {
|
||||
plugin_id: plugin_id.into(),
|
||||
plugin_display_name: plugin_display_name.into(),
|
||||
app_names: HashSet::new(),
|
||||
connector_ids,
|
||||
}
|
||||
}
|
||||
@@ -125,6 +135,13 @@ impl ConnectorSnapshot {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns whether one package declares an app with the given logical name.
|
||||
pub fn plugin_declares_app(&self, plugin_id: &str, app_name: &str) -> bool {
|
||||
self.sources
|
||||
.iter()
|
||||
.any(|source| source.plugin_id == plugin_id && source.app_names.contains(app_name))
|
||||
}
|
||||
|
||||
/// Combines two snapshots while preserving source order and provenance.
|
||||
pub fn merged_with(&self, other: &Self) -> Self {
|
||||
Self::from_plugin_sources(self.sources.iter().chain(&other.sources).cloned())
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::config::Config;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_connectors::ConnectorSnapshot;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core_plugins::apps_route_available;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
use codex_extension_api::McpServerContribution;
|
||||
@@ -96,6 +97,8 @@ impl McpManager {
|
||||
Some(thread_init) => McpServerContributionContext::for_thread(config, thread_init),
|
||||
None => McpServerContributionContext::global(config),
|
||||
};
|
||||
let selected_connector_snapshot =
|
||||
thread_init.and_then(ExtensionDataInit::get::<ConnectorSnapshot>);
|
||||
let mut selected_plugin_registrations = Vec::new();
|
||||
let mut overlays = Vec::new();
|
||||
// A contributor can emit multiple ordered actions, so order each action globally rather
|
||||
@@ -118,14 +121,25 @@ impl McpManager {
|
||||
plugin_display_name,
|
||||
selection_order,
|
||||
config,
|
||||
} => selected_plugin_registrations.push(
|
||||
McpServerRegistration::from_selected_plugin(
|
||||
name,
|
||||
McpPluginAttribution::new(plugin_id, plugin_display_name),
|
||||
selection_order,
|
||||
*config,
|
||||
),
|
||||
),
|
||||
} => {
|
||||
let routed_through_app =
|
||||
apps_route_available(self.plugins_manager.auth_mode())
|
||||
&& selected_connector_snapshot
|
||||
.as_ref()
|
||||
.is_some_and(|snapshot| {
|
||||
snapshot.plugin_declares_app(&plugin_id, &name)
|
||||
});
|
||||
if !routed_through_app {
|
||||
selected_plugin_registrations.push(
|
||||
McpServerRegistration::from_selected_plugin(
|
||||
name,
|
||||
McpPluginAttribution::new(plugin_id, plugin_display_name),
|
||||
selection_order,
|
||||
*config,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
McpServerContribution::Remove { name } => {
|
||||
overlays.push(OrderedMcpOverlay::Remove {
|
||||
contributor_id: contributor.id(),
|
||||
|
||||
Reference in New Issue
Block a user