Skip to content

Commit b44ee7c

Browse files
committed
Extract MCP OAuth aggregate store lock
1 parent de84bf2 commit b44ee7c

2 files changed

Lines changed: 117 additions & 96 deletions

File tree

codex-rs/rmcp-client/src/oauth.rs

Lines changed: 3 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
mod persistor;
2020
mod refresh_lock;
2121
mod resolved_store;
22+
mod store_lock;
2223

2324
use anyhow::Context;
2425
use anyhow::Error;
@@ -45,17 +46,16 @@ use sha2::Digest;
4546
use sha2::Sha256;
4647
use std::collections::BTreeMap;
4748
use std::fs;
48-
use std::fs::File;
49-
use std::fs::OpenOptions;
5049
use std::io::ErrorKind;
5150
use std::path::PathBuf;
5251
use std::sync::Arc;
5352
use std::time::Duration;
54-
use std::time::Instant;
5553
use std::time::SystemTime;
5654
use std::time::UNIX_EPOCH;
5755
use tracing::warn;
5856

57+
use self::store_lock::OAuthStore;
58+
use self::store_lock::OAuthStoreLock;
5959
use codex_keyring_store::DefaultKeyringStore;
6060
use codex_keyring_store::KeyringStore;
6161
use codex_utils_home_dir::find_codex_home;
@@ -78,9 +78,6 @@ use rmcp::transport::auth::AuthorizationManager;
7878
const KEYRING_SERVICE: &str = "Codex MCP Credentials";
7979
const MCP_OAUTH_SECRET_PREFIX: &str = "MCP_OAUTH";
8080
const REFRESH_SKEW_MILLIS: u64 = 30_000;
81-
const OAUTH_STORE_LOCK_DIR: &str = "mcp-oauth-refresh-locks";
82-
const STORE_LOCK_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60);
83-
const STORE_LOCK_RETRY_SLEEP: Duration = Duration::from_millis(50);
8481

8582
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8683
pub struct StoredOAuthTokens {
@@ -554,89 +551,6 @@ fn delete_oauth_tokens_from_secrets_keyring<K: KeyringStore + Clone + 'static>(
554551
Ok(secrets_removed)
555552
}
556553

557-
#[derive(Clone, Copy)]
558-
enum OAuthStore {
559-
File,
560-
Secrets,
561-
}
562-
563-
impl OAuthStore {
564-
fn lock_filename(self) -> &'static str {
565-
match self {
566-
Self::File => "file-store.lock",
567-
Self::Secrets => "secrets-store.lock",
568-
}
569-
}
570-
571-
fn description(self) -> &'static str {
572-
match self {
573-
Self::File => "fallback file",
574-
Self::Secrets => "encrypted secrets",
575-
}
576-
}
577-
}
578-
579-
/// Serializes access to stores that aggregate credentials for multiple MCP servers.
580-
///
581-
/// A per-credential transaction lock may be acquired before this lock. Store operations must not
582-
/// acquire a credential lock, and cross-store cleanup must happen after releasing the first store
583-
/// lock. This ordering prevents deadlocks while keeping each aggregate read-modify-write atomic.
584-
struct OAuthStoreLock {
585-
_file: File,
586-
}
587-
588-
impl OAuthStoreLock {
589-
fn acquire(store: OAuthStore) -> Result<Self> {
590-
Self::acquire_with_timeout(store, STORE_LOCK_ACQUIRE_TIMEOUT)
591-
}
592-
593-
fn acquire_with_timeout(store: OAuthStore, acquire_timeout: Duration) -> Result<Self> {
594-
let path = oauth_store_lock_path(store)?;
595-
if let Some(parent) = path.parent() {
596-
fs::create_dir_all(parent)?;
597-
}
598-
599-
let file = OpenOptions::new()
600-
.read(true)
601-
.write(true)
602-
.create(true)
603-
.truncate(false)
604-
.open(&path)
605-
.with_context(|| {
606-
format!(
607-
"failed to open MCP OAuth {} store lock {}",
608-
store.description(),
609-
path.display()
610-
)
611-
})?;
612-
let started = Instant::now();
613-
614-
loop {
615-
match file.try_lock() {
616-
Ok(()) => return Ok(Self { _file: file }),
617-
Err(std::fs::TryLockError::WouldBlock) if started.elapsed() >= acquire_timeout => {
618-
anyhow::bail!(
619-
"timed out after {acquire_timeout:?} waiting for MCP OAuth {} store lock {}",
620-
store.description(),
621-
path.display()
622-
);
623-
}
624-
Err(std::fs::TryLockError::WouldBlock) => {
625-
std::thread::sleep(STORE_LOCK_RETRY_SLEEP.min(acquire_timeout));
626-
}
627-
Err(error) => {
628-
return Err(std::io::Error::from(error)).with_context(|| {
629-
format!(
630-
"failed to lock MCP OAuth {} store lock {}",
631-
store.description(),
632-
path.display()
633-
)
634-
});
635-
}
636-
}
637-
}
638-
}
639-
}
640554
const FALLBACK_FILENAME: &str = ".credentials.json";
641555
const MCP_SERVER_TYPE: &str = "http";
642556

@@ -822,13 +736,6 @@ fn fallback_file_path() -> Result<PathBuf> {
822736
Ok(find_codex_home()?.join(FALLBACK_FILENAME).to_path_buf())
823737
}
824738

825-
fn oauth_store_lock_path(store: OAuthStore) -> Result<PathBuf> {
826-
Ok(find_codex_home()?
827-
.join(OAUTH_STORE_LOCK_DIR)
828-
.join(store.lock_filename())
829-
.to_path_buf())
830-
}
831-
832739
fn read_fallback_file_unlocked() -> Result<Option<FallbackFile>> {
833740
let path = fallback_file_path()?;
834741
let contents = match fs::read_to_string(&path) {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! Cross-process serialization for MCP OAuth stores shared by multiple credentials.
2+
//!
3+
//! The File and Secrets backends each store a map containing entries for multiple MCP servers.
4+
//! Their lock therefore protects the complete read-modify-write operation, independently of the
5+
//! per-credential refresh transaction lock in `refresh_lock`.
6+
7+
use std::fs;
8+
use std::fs::File;
9+
use std::fs::OpenOptions;
10+
use std::path::PathBuf;
11+
use std::time::Duration;
12+
use std::time::Instant;
13+
14+
use anyhow::Context;
15+
use anyhow::Result;
16+
use codex_utils_home_dir::find_codex_home;
17+
18+
const OAUTH_STORE_LOCK_DIR: &str = "mcp-oauth-refresh-locks";
19+
const STORE_LOCK_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60);
20+
const STORE_LOCK_RETRY_SLEEP: Duration = Duration::from_millis(50);
21+
22+
#[derive(Clone, Copy)]
23+
pub(super) enum OAuthStore {
24+
File,
25+
Secrets,
26+
}
27+
28+
impl OAuthStore {
29+
fn lock_filename(self) -> &'static str {
30+
match self {
31+
Self::File => "file-store.lock",
32+
Self::Secrets => "secrets-store.lock",
33+
}
34+
}
35+
36+
fn description(self) -> &'static str {
37+
match self {
38+
Self::File => "fallback file",
39+
Self::Secrets => "encrypted secrets",
40+
}
41+
}
42+
}
43+
44+
/// Serializes access to stores that aggregate credentials for multiple MCP servers.
45+
///
46+
/// A per-credential transaction lock may be acquired before this lock. Store operations must not
47+
/// acquire a credential lock, and cross-store cleanup must happen after releasing the first store
48+
/// lock. This ordering prevents deadlocks while keeping each aggregate read-modify-write atomic.
49+
pub(super) struct OAuthStoreLock {
50+
_file: File,
51+
}
52+
53+
impl OAuthStoreLock {
54+
pub(super) fn acquire(store: OAuthStore) -> Result<Self> {
55+
Self::acquire_with_timeout(store, STORE_LOCK_ACQUIRE_TIMEOUT)
56+
}
57+
58+
pub(super) fn acquire_with_timeout(
59+
store: OAuthStore,
60+
acquire_timeout: Duration,
61+
) -> Result<Self> {
62+
let path = oauth_store_lock_path(store)?;
63+
if let Some(parent) = path.parent() {
64+
fs::create_dir_all(parent)?;
65+
}
66+
67+
let file = OpenOptions::new()
68+
.read(true)
69+
.write(true)
70+
.create(true)
71+
.truncate(false)
72+
.open(&path)
73+
.with_context(|| {
74+
format!(
75+
"failed to open MCP OAuth {} store lock {}",
76+
store.description(),
77+
path.display()
78+
)
79+
})?;
80+
let started = Instant::now();
81+
82+
loop {
83+
match file.try_lock() {
84+
Ok(()) => return Ok(Self { _file: file }),
85+
Err(std::fs::TryLockError::WouldBlock) if started.elapsed() >= acquire_timeout => {
86+
anyhow::bail!(
87+
"timed out after {acquire_timeout:?} waiting for MCP OAuth {} store lock {}",
88+
store.description(),
89+
path.display()
90+
);
91+
}
92+
Err(std::fs::TryLockError::WouldBlock) => {
93+
std::thread::sleep(STORE_LOCK_RETRY_SLEEP.min(acquire_timeout));
94+
}
95+
Err(error) => {
96+
return Err(std::io::Error::from(error)).with_context(|| {
97+
format!(
98+
"failed to lock MCP OAuth {} store lock {}",
99+
store.description(),
100+
path.display()
101+
)
102+
});
103+
}
104+
}
105+
}
106+
}
107+
}
108+
109+
fn oauth_store_lock_path(store: OAuthStore) -> Result<PathBuf> {
110+
Ok(find_codex_home()?
111+
.join(OAUTH_STORE_LOCK_DIR)
112+
.join(store.lock_filename())
113+
.to_path_buf())
114+
}

0 commit comments

Comments
 (0)