mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat(core, mcp): cache codex_apps tools in memory (#29003)
## Description This makes Codex Apps tool reads use a shared in-memory snapshot instead of rereading the disk cache every time `list_all_tools()` runs. Disk still seeds the cache on startup and gets updated after successful fetches, but it is no longer the live read path. The core change is that `McpManager` now owns a process-scoped `CodexAppsToolsCache`. Codex threads in the same app-server process now share this Codex Apps in-memory tools snapshot. The snapshot is keyed by the Codex home plus the Codex Apps identity: the active Codex auth user/workspace and the effective Codex Apps MCP source config. There's already code to hard-refresh the cache, so we respect it in this PR. ## Local benchmark I ran a local steady-state microbenchmark of the exact repeated Codex Apps cached-tools read this PR removes, using the same real local cache payload in both trees: `3,678,138` bytes and `381` tools. The cache file was already warm in the OS page cache, so this measures same-process reread/deserialization work rather than cold-disk latency or full turn latency. Each run is 25 iterations (mimicking a turn that makes 25 inference calls). | Version | Run 1 | Run 2 | Avg | |---|---:|---:|---:| | `origin/main` disk read + JSON deserialize + `filter_tools` | `50.755 ms` | `52.894 ms` | `51.825 ms` | | This branch in-memory `current_tools` + `filter_tools` | `0.740 ms` | `0.778 ms` | `0.759 ms` | That removes about `51 ms` from each repeated Codex Apps cached-tools read on this machine, roughly `68x` faster for that subpath. It is useful evidence for the hot path this PR changes, but not a claim that every production turn gets `51 ms` faster; end-to-end impact also depends on the rest of `list_all_tools()` and tool-payload construction. This is on my M2 Max macbook, so with a slower disk this would be much worse (and indeed we did see this really blew up turn runtime with a slow disk).
This commit is contained in:
1 parent
62c7f506d9
commit
703793c22e
15 files changed
+1109
-621
No files matched your search
@@ -1,11 +1,6 @@
|
||||
use super::*;
|
||||
use crate::codex_apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_server_info;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::read_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_codex_apps_tools_cache;
|
||||
use crate::codex_apps_cache::CodexAppsToolsCache;
|
||||
use crate::codex_apps_cache::CodexAppsToolsCacheContext;
|
||||
use crate::declared_openai_file_input_param_names;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::elicitation_is_rejected_by_policy;
|
||||
@@ -31,7 +26,10 @@ use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_rmcp_client::InProcessTransportFactory;
|
||||
use codex_rmcp_client::RmcpClient;
|
||||
use futures::FutureExt;
|
||||
use futures::future::BoxFuture;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::ElicitationAction;
|
||||
@@ -41,8 +39,10 @@ use rmcp::model::Meta;
|
||||
use rmcp::model::NumberOrString;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
use tokio::io::DuplexStream;
|
||||
|
||||
fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
ToolInfo {
|
||||
@@ -63,31 +63,19 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_tool_with_connector(
|
||||
server_name: &str,
|
||||
tool_name: &str,
|
||||
connector_id: &str,
|
||||
connector_name: Option<&str>,
|
||||
) -> ToolInfo {
|
||||
let mut tool = create_test_tool(server_name, tool_name);
|
||||
tool.connector_id = Some(connector_id.to_string());
|
||||
tool.connector_name = connector_name.map(ToOwned::to_owned);
|
||||
tool
|
||||
}
|
||||
|
||||
fn create_codex_apps_tools_cache_context(
|
||||
codex_home: PathBuf,
|
||||
account_id: Option<&str>,
|
||||
chatgpt_user_id: Option<&str>,
|
||||
) -> CodexAppsToolsCacheContext {
|
||||
CodexAppsToolsCacheContext {
|
||||
CodexAppsToolsCache::default().context(
|
||||
codex_home,
|
||||
user_key: CodexAppsToolsCacheKey {
|
||||
CodexAppsToolsCacheKey {
|
||||
account_id: account_id.map(ToOwned::to_owned),
|
||||
chatgpt_user_id: chatgpt_user_id.map(ToOwned::to_owned),
|
||||
is_workspace_account: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn create_test_server_info(title: &str) -> McpServerInfo {
|
||||
@@ -101,6 +89,51 @@ fn create_test_server_info(title: &str) -> McpServerInfo {
|
||||
}
|
||||
}
|
||||
|
||||
struct TestInProcessTransportFactory;
|
||||
|
||||
impl InProcessTransportFactory for TestInProcessTransportFactory {
|
||||
fn open(&self) -> BoxFuture<'static, io::Result<DuplexStream>> {
|
||||
async {
|
||||
let (client_stream, _server_stream) = tokio::io::duplex(1);
|
||||
Ok(client_stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_ready_async_managed_client(tools: Vec<ToolInfo>) -> AsyncManagedClient {
|
||||
let tool_filter = ToolFilter::default();
|
||||
let managed_client = ManagedClient {
|
||||
client: Arc::new(
|
||||
RmcpClient::new_in_process_client(Arc::new(TestInProcessTransportFactory))
|
||||
.await
|
||||
.expect("create in-process RMCP client"),
|
||||
),
|
||||
server_info: create_test_server_info("Ready"),
|
||||
tools,
|
||||
tool_filter: tool_filter.clone(),
|
||||
tool_timeout: None,
|
||||
server_instructions: None,
|
||||
server_supports_sandbox_state_meta_capability: false,
|
||||
codex_apps_tools_cache_context: None,
|
||||
};
|
||||
|
||||
AsyncManagedClient {
|
||||
client: futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Ok(
|
||||
managed_client,
|
||||
))
|
||||
.boxed()
|
||||
.shared(),
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_server_info: None,
|
||||
codex_apps_tools_cache_context: None,
|
||||
tool_filter,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
|
||||
tools
|
||||
.iter()
|
||||
@@ -554,235 +587,25 @@ fn filter_tools_applies_per_server_filters() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tools_cache_is_overwritten_by_last_write() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let tools_gateway_1 = vec![create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "one")];
|
||||
let tools_gateway_2 = vec![create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "two")];
|
||||
|
||||
write_cached_codex_apps_tools(&cache_context, &tools_gateway_1);
|
||||
let cached_gateway_1 =
|
||||
read_cached_codex_apps_tools(&cache_context).expect("cache entry exists for first write");
|
||||
assert_eq!(cached_gateway_1[0].callable_name, "one");
|
||||
|
||||
write_cached_codex_apps_tools(&cache_context, &tools_gateway_2);
|
||||
let cached_gateway_2 =
|
||||
read_cached_codex_apps_tools(&cache_context).expect("cache entry exists for second write");
|
||||
assert_eq!(cached_gateway_2[0].callable_name, "two");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tools_cache_is_scoped_per_user() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context_user_1 = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_context_user_2 = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-two"),
|
||||
Some("user-two"),
|
||||
);
|
||||
let tools_user_1 = vec![create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "one")];
|
||||
let tools_user_2 = vec![create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "two")];
|
||||
|
||||
write_cached_codex_apps_tools(&cache_context_user_1, &tools_user_1);
|
||||
write_cached_codex_apps_tools(&cache_context_user_2, &tools_user_2);
|
||||
|
||||
let read_user_1 =
|
||||
read_cached_codex_apps_tools(&cache_context_user_1).expect("cache entry for user one");
|
||||
let read_user_2 =
|
||||
read_cached_codex_apps_tools(&cache_context_user_2).expect("cache entry for user two");
|
||||
|
||||
assert_eq!(read_user_1[0].callable_name, "one");
|
||||
assert_eq!(read_user_2[0].callable_name, "two");
|
||||
assert_ne!(
|
||||
cache_context_user_1.tools_cache_path(),
|
||||
cache_context_user_2.tools_cache_path(),
|
||||
"each user should get an isolated cache file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tools_cache_preserves_formerly_disallowed_connectors() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let tools = vec![
|
||||
create_test_tool_with_connector(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"formerly_blocked_tool",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
Some("Formerly Blocked"),
|
||||
),
|
||||
create_test_tool_with_connector(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_tool",
|
||||
"calendar",
|
||||
Some("Calendar"),
|
||||
),
|
||||
];
|
||||
|
||||
write_cached_codex_apps_tools(&cache_context, &tools);
|
||||
let cached = read_cached_codex_apps_tools(&cache_context).expect("cache entry exists for user");
|
||||
|
||||
assert_eq!(
|
||||
cached
|
||||
.iter()
|
||||
.map(|tool| (tool.callable_name.as_str(), tool.connector_id.as_deref()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(
|
||||
"formerly_blocked_tool",
|
||||
Some("connector_2b0a9009c9c64bf9933a3dae3f2b1254")
|
||||
),
|
||||
("calendar_tool", Some("calendar")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tools_cache_is_ignored_when_schema_version_mismatches() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION + 1,
|
||||
"tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "one")],
|
||||
}))
|
||||
.expect("serialize");
|
||||
std::fs::write(cache_path, bytes).expect("write");
|
||||
|
||||
assert!(read_cached_codex_apps_tools(&cache_context).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tools_cache_is_ignored_when_json_is_invalid() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
std::fs::write(cache_path, b"{not json").expect("write");
|
||||
|
||||
assert!(read_cached_codex_apps_tools(&cache_context).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_cached_codex_apps_tools_loads_from_disk_cache() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cached_tools = vec![create_test_tool(
|
||||
fn codex_apps_env_bearer_token_bypasses_shared_tools_cache() {
|
||||
assert!(!should_share_codex_apps_tools_cache(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_search",
|
||||
)];
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
write_codex_apps_tools_cache(Some(&cache_context), &server_info, &cached_tools);
|
||||
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(Some(&cache_context))
|
||||
.expect("expected startup snapshot to load from cache");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(Some(&cache_context));
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
assert_eq!(cached_server_info, Some(server_info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_cached_codex_apps_tools_loads_without_server_info_cache() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION,
|
||||
"tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "calendar_search")],
|
||||
}))
|
||||
.expect("serialize");
|
||||
std::fs::write(cache_path, bytes).expect("write");
|
||||
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(Some(&cache_context))
|
||||
.expect("legacy startup snapshot should remain available");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(Some(&cache_context));
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
assert_eq!(cached_server_info, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_info_cache_survives_legacy_tools_cache_write() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
write_codex_apps_tools_cache(
|
||||
Some(&cache_context),
|
||||
&server_info,
|
||||
&[create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_search",
|
||||
)],
|
||||
);
|
||||
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION - 1,
|
||||
"tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "calendar_search")],
|
||||
}))
|
||||
.expect("serialize");
|
||||
std::fs::write(cache_path, bytes).expect("write legacy tools cache");
|
||||
|
||||
assert_eq!(
|
||||
load_startup_cached_codex_apps_server_info(Some(&cache_context)),
|
||||
Some(server_info)
|
||||
);
|
||||
assert!(load_startup_cached_codex_apps_tools_snapshot(Some(&cache_context)).is_none());
|
||||
/*uses_env_bearer_token*/ true,
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending() {
|
||||
let startup_tools = vec![create_test_tool(
|
||||
async fn list_all_tools_uses_shared_codex_apps_cache_while_client_is_pending() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
cache_context.store_current_tools_for_test(vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
)];
|
||||
)]);
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
@@ -798,8 +621,9 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending()
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
codex_apps_tools_cache_context: Some(cache_context),
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -813,7 +637,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending()
|
||||
tool.canonical_tool_name()
|
||||
== ToolName::namespaced("mcp__codex_apps", "calendar_create_event")
|
||||
})
|
||||
.expect("tool from startup cache");
|
||||
.expect("tool from shared cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
}
|
||||
@@ -836,8 +660,9 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(Vec::new()),
|
||||
cached_server_info: Some(server_info.clone()),
|
||||
codex_apps_tools_cache_context: None,
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -858,10 +683,8 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
let startup_tools = vec![create_test_tool("rmcp", "echo")];
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let managed_client =
|
||||
create_ready_async_managed_client(vec![create_test_tool("rmcp", "echo")]).await;
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
@@ -869,18 +692,7 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ false,
|
||||
);
|
||||
manager.clients.insert(
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
},
|
||||
);
|
||||
manager.clients.insert("rmcp".to_string(), managed_client);
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
let tool = tools
|
||||
@@ -902,10 +714,8 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
let startup_tools = vec![create_test_tool("rmcp", "echo")];
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let managed_client =
|
||||
create_ready_async_managed_client(vec![create_test_tool("rmcp", "echo")]).await;
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
@@ -913,18 +723,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
manager.clients.insert(
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
},
|
||||
);
|
||||
manager.clients.insert("rmcp".to_string(), managed_client);
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
let tool = tools
|
||||
@@ -945,7 +744,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_snapshot() {
|
||||
async fn list_all_tools_blocks_while_client_is_pending_without_cached_tools() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
@@ -961,8 +760,9 @@ async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
codex_apps_tools_cache_context: None,
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -998,8 +798,9 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
codex_apps_tools_cache_context: None,
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token,
|
||||
@@ -1043,8 +844,9 @@ async fn shutdown_continues_after_caller_is_aborted() {
|
||||
AsyncManagedClient {
|
||||
client: blocking_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
codex_apps_tools_cache_context: None,
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -1071,7 +873,14 @@ async fn shutdown_continues_after_caller_is_aborted() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty() {
|
||||
async fn list_all_tools_does_not_block_when_shared_codex_apps_cache_is_empty() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
cache_context.store_current_tools_for_test(Vec::new());
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
@@ -1087,8 +896,9 @@ async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty()
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(Vec::new()),
|
||||
cached_server_info: None,
|
||||
codex_apps_tools_cache_context: Some(cache_context),
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -1097,16 +907,22 @@ async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty()
|
||||
|
||||
let timeout_result =
|
||||
tokio::time::timeout(Duration::from_millis(10), manager.list_all_tools()).await;
|
||||
let tools = timeout_result.expect("cache-hit startup snapshot should not block");
|
||||
let tools = timeout_result.expect("shared empty cache should not block");
|
||||
assert!(tools.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails() {
|
||||
let startup_tools = vec![create_test_tool(
|
||||
async fn list_all_tools_uses_shared_codex_apps_cache_when_client_startup_fails() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
cache_context.store_current_tools_for_test(vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
)];
|
||||
)]);
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
let failed_client = futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Err(
|
||||
StartupOutcomeError::Failed {
|
||||
@@ -1128,8 +944,9 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails
|
||||
AsyncManagedClient {
|
||||
client: failed_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: Some(server_info.clone()),
|
||||
codex_apps_tools_cache_context: Some(cache_context),
|
||||
tool_filter: ToolFilter::default(),
|
||||
startup_complete,
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -1143,7 +960,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails
|
||||
tool.canonical_tool_name()
|
||||
== ToolName::namespaced("mcp__codex_apps", "calendar_create_event")
|
||||
})
|
||||
.expect("tool from startup cache");
|
||||
.expect("tool from shared cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
assert_eq!(
|
||||
@@ -1156,12 +973,10 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
async fn list_all_tools_adds_server_metadata_to_tools() {
|
||||
let server_name = "docs";
|
||||
let startup_tools = vec![create_test_tool(server_name, "search")];
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let managed_client =
|
||||
create_ready_async_managed_client(vec![create_test_tool(server_name, "search")]).await;
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
@@ -1182,18 +997,9 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
tool_approval_modes: HashMap::new(),
|
||||
},
|
||||
);
|
||||
manager.clients.insert(
|
||||
server_name.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
},
|
||||
);
|
||||
manager
|
||||
.clients
|
||||
.insert(server_name.to_string(), managed_client);
|
||||
|
||||
let tools = manager.list_all_tools().await;
|
||||
assert_eq!(tools.len(), 1);
|
||||
@@ -1340,6 +1146,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
PathBuf::from("/tmp"),
|
||||
),
|
||||
codex_home.path().to_path_buf(),
|
||||
CodexAppsToolsCache::default(),
|
||||
CodexAppsToolsCacheKey {
|
||||
account_id: None,
|
||||
chatgpt_user_id: None,
|
||||
|
||||
Reference in new issue
Block a user