diff --git a/codex-rs/app-server/src/mcp_refresh.rs b/codex-rs/app-server/src/mcp_refresh.rs index 5ff35b92e8..45969c9dab 100644 --- a/codex-rs/app-server/src/mcp_refresh.rs +++ b/codex-rs/app-server/src/mcp_refresh.rs @@ -227,6 +227,7 @@ mod tests { &good_config, auth_manager.clone(), codex_core::build_models_manager(&good_config, auth_manager.clone()), + codex_core::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::clone(&environment_manager), thread_extensions( diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index dc97ea0970..4d1e7b1ced 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -263,6 +263,7 @@ impl MessageProcessor { config.as_ref(), auth_manager.clone(), codex_core::build_models_manager(config.as_ref(), auth_manager.clone()), + codex_core::CodexAppsToolsCache::default(), session_source, environment_manager, thread_extensions( diff --git a/codex-rs/connectors/src/connector_runtime/mod.rs b/codex-rs/connectors/src/connector_runtime/mod.rs index acddc82015..75916aa44d 100644 --- a/codex-rs/connectors/src/connector_runtime/mod.rs +++ b/codex-rs/connectors/src/connector_runtime/mod.rs @@ -117,12 +117,14 @@ impl ConnectorRuntimeSnapshot { /// remain independently available for clients that already hold their context. pub struct ConnectorRuntimeManager { entries: Arc>>>>, + disk_cache: ConnectorRuntimeDiskCache, } impl Clone for ConnectorRuntimeManager { fn clone(&self) -> Self { Self { entries: Arc::clone(&self.entries), + disk_cache: self.disk_cache, } } } @@ -131,11 +133,20 @@ impl Default for ConnectorRuntimeManager { fn default() -> Self { Self { entries: Arc::new(Mutex::new(HashMap::new())), + disk_cache: ConnectorRuntimeDiskCache::Enabled, } } } impl ConnectorRuntimeManager { + /// 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 ConnectorRuntimeManager { 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 ConnectorRuntimeContext { } pub fn cached_server_info(&self) -> Option { - 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 ConnectorRuntimeContext { server_info: &McpServerInfo, tools: Vec, ) -> Arc> { - 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 { identity: ConnectorRuntimeIdentity, + disk_cache: ConnectorRuntimeDiskCache, current_snapshot: ArcSwapOption>, next_fetch_generation: AtomicU64, last_accepted_generation: Mutex, } impl ConnectorRuntimeEntry { - 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 ConnectorRuntimeEntry { } } +#[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 diff --git a/codex-rs/connectors/src/connector_runtime/tests.rs b/codex-rs/connectors/src/connector_runtime/tests.rs index fb88695718..acad39bfb0 100644 --- a/codex-rs/connectors/src/connector_runtime/tests.rs +++ b/codex-rs/connectors/src/connector_runtime/tests.rs @@ -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::::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::::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() { diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index 5412049cbc..4f8de06651 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -25,6 +25,7 @@ pub use codex_config::types::TuiKeymap; pub use codex_config::types::TuiNotificationSettings; pub use codex_config::types::TuiPetAnchor; pub use codex_config::types::UriBasedFileOpener; +pub use codex_core::CodexAppsToolsCache; pub use codex_core::CodexThread; pub use codex_core::ForkSnapshot; pub use codex_core::LoadedAgentsMd; diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 3b14595f7c..d46ffdcb64 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -2338,6 +2338,7 @@ async fn resume_thread_subagent_restores_stored_metadata() { &config, auth_manager.clone(), crate::thread_manager::build_models_manager(&config, auth_manager), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index ed9788a2dd..d11edd786f 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -65,6 +65,7 @@ pub use network_proxy_loader::MtimeConfigReloader; pub use network_proxy_loader::build_network_proxy_state; pub use network_proxy_loader::build_network_proxy_state_and_reloader; mod original_image_detail; +pub use codex_mcp::CodexAppsToolsCache; pub use codex_mcp::SandboxState; mod mcp_openai_file; mod mcp_tool_call; diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index e51aa36385..0930d49175 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -58,6 +58,7 @@ impl McpManager { Self::new_with_extensions( plugins_manager, codex_extension_api::empty_extension_registry(), + ConnectorRuntimeManager::default(), ) } @@ -65,11 +66,12 @@ impl McpManager { pub fn new_with_extensions( plugins_manager: Arc, extensions: Arc>, + codex_apps_tools_cache: ConnectorRuntimeManager, ) -> Self { Self { plugins_manager, extensions, - codex_apps_tools_cache: ConnectorRuntimeManager::default(), + codex_apps_tools_cache, } } diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index 2962539e95..bf424cc941 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -45,6 +45,7 @@ pub async fn build_prompt_input( &config, Arc::clone(&auth_manager), crate::thread_manager::build_models_manager(&config, Arc::clone(&auth_manager)), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new( EnvironmentManager::from_codex_home( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index c82a2f98d5..53314ea9ae 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -7734,6 +7734,7 @@ async fn plugin_availability_change_reuses_the_mcp_manager() { session.services.mcp_manager = Arc::new(McpManager::new_with_extensions( Arc::clone(&session.services.plugins_manager), registry, + crate::CodexAppsToolsCache::default(), )); let session = Arc::new(session); session diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index f5f133e869..f5ea09d606 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -1,3 +1,4 @@ +use crate::CodexAppsToolsCache; use crate::SkillsService; use crate::agent::AgentControl; use crate::attestation::AttestationProvider; @@ -306,6 +307,7 @@ impl ThreadManager { config: &Config, auth_manager: Arc, models_manager: SharedModelsManager, + codex_apps_tools_cache: CodexAppsToolsCache, session_source: SessionSource, environment_manager: Arc, extensions: Arc>, @@ -328,6 +330,7 @@ impl ThreadManager { let mcp_manager = Arc::new(McpManager::new_with_extensions( Arc::clone(&plugins_manager), Arc::clone(&extensions), + codex_apps_tools_cache, )); let skills_service = Arc::new(SkillsService::new_with_restriction_product( codex_home, diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index e40425ede5..9d722a94ad 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -602,6 +602,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() &config, auth_manager.clone(), build_models_manager(&config, auth_manager), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), Arc::new(extensions.build()), @@ -822,6 +823,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -950,6 +952,7 @@ async fn explicit_installation_id_skips_codex_home_file() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -992,6 +995,7 @@ async fn resume_active_thread_from_rollout_returns_running_thread() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1053,6 +1057,7 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1121,6 +1126,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1214,6 +1220,7 @@ async fn subtree_listing_uses_injected_graph_store_without_state_db() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1260,6 +1267,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1366,6 +1374,7 @@ async fn new_uses_active_provider_for_model_refresh() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1412,6 +1421,7 @@ async fn injected_models_manager_controls_refresh_policy() { &config, auth_manager, models_manager, + crate::CodexAppsToolsCache::default(), SessionSource::Custom("test-embedder".to_string()), Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1661,6 +1671,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1772,6 +1783,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), @@ -1874,6 +1886,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_ &config, auth_manager.clone(), build_models_manager(&config, auth_manager.clone()), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index a7e6989a2e..6c963063f4 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -4281,6 +4281,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr &config, auth_manager.clone(), crate::thread_manager::build_models_manager(&config, auth_manager), + crate::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 6c543d075d..14b55f6a35 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -629,6 +629,7 @@ impl TestCodexBuilder { &config, auth_manager.clone(), codex_core::build_models_manager(&config, auth_manager), + codex_core::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::clone(&environment_manager), Arc::clone(&self.extensions), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 9afad5a027..e9c4059948 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1581,6 +1581,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { &config, auth_manager.clone(), codex_core::build_models_manager(&config, auth_manager), + codex_core::CodexAppsToolsCache::default(), SessionSource::Exec, Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), empty_extension_registry(), diff --git a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs index 3836234b84..66229713ce 100644 --- a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs +++ b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs @@ -120,6 +120,7 @@ async fn later_extension_can_remove_same_name_registration() -> TestResult { let manager = McpManager::new_with_extensions( Arc::new(PluginsManager::new(config.codex_home.to_path_buf())), Arc::new(builder.build()), + codex_core::CodexAppsToolsCache::default(), ); let servers = manager.effective_servers(&config, Some(&auth)).await; @@ -180,6 +181,7 @@ fn installed_manager(config: &Config) -> McpManager { McpManager::new_with_extensions( Arc::new(PluginsManager::new(config.codex_home.to_path_buf())), Arc::new(builder.build()), + codex_core::CodexAppsToolsCache::default(), ) } diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 57ca01e885..cf01df6814 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -76,6 +76,7 @@ impl MessageProcessor { config.as_ref(), Arc::clone(&auth_manager), codex_core::build_models_manager(config.as_ref(), auth_manager), + codex_core::CodexAppsToolsCache::default(), SessionSource::Mcp, environment_manager, Arc::new(extensions.build()), diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index da3cfb2cd7..fbf3ba9c53 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -16,6 +16,7 @@ use codex_core_api::AskForApproval; use codex_core_api::AuthCredentialsStoreMode; use codex_core_api::AuthManager; use codex_core_api::AutoCompactTokenLimitScope; +use codex_core_api::CodexAppsToolsCache; use codex_core_api::CodexHomeUserInstructionsProvider; use codex_core_api::CodexThread; use codex_core_api::Config; @@ -135,6 +136,7 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { &config, Arc::clone(&auth_manager), build_models_manager(&config, auth_manager), + CodexAppsToolsCache::default(), SessionSource::Exec, environment_manager, Arc::new(extensions.build()),