Allow injecting the Codex Apps tools cache (#33113)

## What changed

- Accept a caller-provided `CodexAppsToolsCache` when constructing a `ThreadManager` and pass it through to the MCP manager.
- Add `ConnectorRuntimeManager::new_without_cache()` for an in-memory runtime that neither loads nor persists connector state on disk.
- Re-export `CodexAppsToolsCache` from `codex-core` and `codex-core-api` for embedders.

## Testing

- Verify that a cache-disabled connector runtime ignores existing disk state and publishes live tools without creating cache files.

GitOrigin-RevId: f52f4f27c425a8d84ad0d0987b30a6fc3c14d702
This commit is contained in:
Bryan Ashley
2026-07-14 17:03:16 +00:00
committed by copyberry
parent 69acc71cb1
commit 8604689ec5
18 changed files with 127 additions and 11 deletions

View File

@@ -117,12 +117,14 @@ impl<T> ConnectorRuntimeSnapshot<T> {
/// remain independently available for clients that already hold their context.
pub struct ConnectorRuntimeManager<T: ConnectorRuntimePayload> {
entries: Arc<Mutex<HashMap<ConnectorRuntimeIdentity, Arc<ConnectorRuntimeEntry<T>>>>>,
disk_cache: ConnectorRuntimeDiskCache,
}
impl<T: ConnectorRuntimePayload> Clone for ConnectorRuntimeManager<T> {
fn clone(&self) -> Self {
Self {
entries: Arc::clone(&self.entries),
disk_cache: self.disk_cache,
}
}
}
@@ -131,11 +133,20 @@ impl<T: ConnectorRuntimePayload> Default for ConnectorRuntimeManager<T> {
fn default() -> Self {
Self {
entries: Arc::new(Mutex::new(HashMap::new())),
disk_cache: ConnectorRuntimeDiskCache::Enabled,
}
}
}
impl<T: ConnectorRuntimePayload> ConnectorRuntimeManager<T> {
/// Constructs a process-local connector runtime that never reads or writes the disk cache.
pub fn new_without_cache() -> Self {
Self {
entries: Arc::new(Mutex::new(HashMap::new())),
disk_cache: ConnectorRuntimeDiskCache::Disabled,
}
}
pub fn current_snapshot(
&self,
codex_home: PathBuf,
@@ -153,7 +164,7 @@ impl<T: ConnectorRuntimePayload> ConnectorRuntimeManager<T> {
let mut entries = lock_unpoisoned(&self.entries);
let entry = entries
.entry(identity.clone())
.or_insert_with(|| Arc::new(ConnectorRuntimeEntry::new(identity)))
.or_insert_with(|| Arc::new(ConnectorRuntimeEntry::new(identity, self.disk_cache)))
.clone();
ConnectorRuntimeContext { entry }
}
@@ -193,7 +204,10 @@ impl<T: ConnectorRuntimePayload> ConnectorRuntimeContext<T> {
}
pub fn cached_server_info(&self) -> Option<McpServerInfo> {
load_cached_codex_apps_server_info(self)
match self.entry.disk_cache {
ConnectorRuntimeDiskCache::Enabled => load_cached_codex_apps_server_info(self),
ConnectorRuntimeDiskCache::Disabled => None,
}
}
fn tools_cache_path(&self) -> PathBuf {
@@ -215,12 +229,20 @@ impl<T: ConnectorRuntimePayload> ConnectorRuntimeContext<T> {
server_info: &McpServerInfo,
tools: Vec<T>,
) -> Arc<ConnectorRuntimeSnapshot<T>> {
self.publish_runtime_if_newest_accepted_with(
ticket,
server_info,
tools,
persist_codex_apps_cache,
)
match self.entry.disk_cache {
ConnectorRuntimeDiskCache::Enabled => self.publish_runtime_if_newest_accepted_with(
ticket,
server_info,
tools,
persist_codex_apps_cache,
),
ConnectorRuntimeDiskCache::Disabled => self.publish_runtime_if_newest_accepted_with(
ticket,
server_info,
tools,
|_, _, _| {},
),
}
}
fn publish_runtime_if_newest_accepted_with(
@@ -300,16 +322,23 @@ pub struct ConnectorRuntimeFetchTicket {
/// All live state owned by one connector identity.
struct ConnectorRuntimeEntry<T: ConnectorRuntimePayload> {
identity: ConnectorRuntimeIdentity,
disk_cache: ConnectorRuntimeDiskCache,
current_snapshot: ArcSwapOption<ConnectorRuntimeSnapshot<T>>,
next_fetch_generation: AtomicU64,
last_accepted_generation: Mutex<u64>,
}
impl<T: ConnectorRuntimePayload> ConnectorRuntimeEntry<T> {
fn new(identity: ConnectorRuntimeIdentity) -> Self {
let current_snapshot = load_cached_connector_runtime_for_identity(&identity).map(Arc::new);
fn new(identity: ConnectorRuntimeIdentity, disk_cache: ConnectorRuntimeDiskCache) -> Self {
let current_snapshot = match disk_cache {
ConnectorRuntimeDiskCache::Enabled => {
load_cached_connector_runtime_for_identity(&identity).map(Arc::new)
}
ConnectorRuntimeDiskCache::Disabled => None,
};
Self {
identity,
disk_cache,
current_snapshot: ArcSwapOption::from(current_snapshot),
next_fetch_generation: AtomicU64::new(0),
last_accepted_generation: Mutex::new(0),
@@ -317,6 +346,12 @@ impl<T: ConnectorRuntimePayload> ConnectorRuntimeEntry<T> {
}
}
#[derive(Clone, Copy)]
enum ConnectorRuntimeDiskCache {
Enabled,
Disabled,
}
/// Everything that decides whether two connector runtime clients can share a snapshot.
///
/// The auth key says whose runtime catalog we are reading. `codex_home` keeps

View File

@@ -413,6 +413,54 @@ fn codex_apps_tools_cache_keeps_live_publish_when_disk_persistence_fails() {
assert_eq!(cache_context.current_tools(), Some(tools));
}
#[test]
fn connector_runtime_without_cache_ignores_disk_state() {
let codex_home = tempdir().expect("tempdir");
let writer = create_codex_apps_tools_cache_context(
codex_home.path().to_path_buf(),
Some("account-one"),
Some("user-one"),
);
let tools = vec![create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "cached")];
let server_info = create_test_server_info("Codex Apps");
write_cached_codex_apps_tools_for_test(&writer, &server_info, &tools);
let context = ConnectorRuntimeManager::<TestTool>::new_without_cache().context(
codex_home.path().to_path_buf(),
ConnectorRuntimeContextKey {
account_id: Some("account-one".to_string()),
chatgpt_user_id: Some("user-one".to_string()),
is_workspace_account: false,
},
);
assert_eq!(context.current_tools(), None);
assert_eq!(context.cached_server_info(), None);
}
#[test]
fn connector_runtime_without_cache_publishes_without_writing() {
let temp_dir = tempdir().expect("tempdir");
let codex_home = temp_dir.path().join("codex-home");
let context = ConnectorRuntimeManager::<TestTool>::new_without_cache().context(
codex_home.clone(),
ConnectorRuntimeContextKey {
account_id: Some("account-one".to_string()),
chatgpt_user_id: Some("user-one".to_string()),
is_workspace_account: false,
},
);
let tools = vec![create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "live")];
let published_tools = context.publish_if_newest_accepted(
context.begin_fetch(ConnectorRuntimeFetchSource::HardRefresh),
&create_test_server_info("Codex Apps"),
tools.clone(),
);
assert_eq!(published_tools, tools);
assert_eq!(context.current_tools(), Some(tools));
assert!(!codex_home.exists());
}
#[cfg(unix)]
#[test]
fn codex_apps_tools_cache_scopes_non_utf8_home_disk_paths() {