Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions codex-rs/codex-mcp/src/codex_apps_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl CodexAppsToolsCacheContext {
pub(crate) fn server_info_cache_path(&self) -> PathBuf {
self.entry
.identity
.cache_path_in(CODEX_APPS_SERVER_INFO_CACHE_DIR)
.auth_cache_path_in(CODEX_APPS_SERVER_INFO_CACHE_DIR)
}

pub(crate) fn current_tools(&self) -> Option<Vec<ToolInfo>> {
Expand Down Expand Up @@ -156,11 +156,12 @@ impl CodexAppsToolsCache {
let mut expected_connector_ids = expected_connector_ids.into_iter().collect::<Vec<_>>();
expected_connector_ids.sort_unstable();
expected_connector_ids.dedup();
let expected_connector_ids = Arc::new(expected_connector_ids);
let identity = CodexAppsToolsCacheIdentity {
codex_home,
auth_key,
expected_connector_ids: expected_connector_ids.clone(),
};
let expected_connector_ids = Arc::new(expected_connector_ids);
let mut entries = lock_unpoisoned(&self.entries);
let entry = entries
.entry(identity.clone())
Expand Down Expand Up @@ -214,19 +215,30 @@ impl CodexAppsToolsCacheEntry {

/// Everything that decides whether two Codex Apps clients can share tools.
///
/// The auth key says whose catalog we are reading. `codex_home` keeps the
/// The auth key says whose catalog we are reading. Expected connector IDs keep
/// plugin contexts from sharing a raw snapshot. `codex_home` keeps the
/// persisted cache under the right home directory.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CodexAppsToolsCacheIdentity {
codex_home: PathBuf,
auth_key: CodexAppsToolsCacheKey,
expected_connector_ids: Vec<String>,
}

impl CodexAppsToolsCacheIdentity {
fn cache_path_in(&self, cache_dir: &str) -> PathBuf {
// `codex_home` is already the parent directory. Keep it out of the
// filename hash so non-UTF-8 Unix paths cannot collapse distinct auth
// keys onto the same disk cache file.
let identity_json = serde_json::to_string(&(&self.auth_key, &self.expected_connector_ids))
.unwrap_or_default();
let identity_hash = sha1_hex(&identity_json);
self.codex_home
.join(cache_dir)
.join(format!("{identity_hash}.json"))
}

fn auth_cache_path_in(&self, cache_dir: &str) -> PathBuf {
let identity_json = serde_json::to_string(&self.auth_key).unwrap_or_default();
let identity_hash = sha1_hex(&identity_json);
self.codex_home
Expand Down
37 changes: 37 additions & 0 deletions codex-rs/codex-mcp/src/codex_apps_cache_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,43 @@ fn codex_apps_tools_cache_publishes_newest_shared_snapshot() {
);
}

#[test]
fn codex_apps_tools_cache_scopes_plugin_connector_expectations() {
let codex_home = tempdir().expect("tempdir");
let cache = CodexAppsToolsCache::default();
let auth_key = CodexAppsToolsCacheKey {
account_id: Some("account-one".to_string()),
chatgpt_user_id: Some("user-one".to_string()),
is_workspace_account: false,
};
let connector_a = cache.context(
codex_home.path().to_path_buf(),
auth_key.clone(),
["connector_a".to_string()],
);
let connector_b = cache.context(
codex_home.path().to_path_buf(),
auth_key,
["connector_b".to_string()],
);

connector_a.store_current_tools_for_test(vec![create_test_tool(
CODEX_APPS_MCP_SERVER_NAME,
"connector_a_search",
)]);

assert!(connector_a.current_tools().is_some());
assert!(connector_b.current_tools().is_none());
assert_ne!(
connector_a.tools_cache_path(),
connector_b.tools_cache_path()
);
assert_eq!(
connector_a.server_info_cache_path(),
connector_b.server_info_cache_path()
);
}

#[test]
fn codex_apps_tools_cache_keeps_live_publish_when_disk_persistence_fails() {
let codex_home = tempdir().expect("tempdir");
Expand Down
Loading