1919mod persistor;
2020mod refresh_lock;
2121mod resolved_store;
22+ mod store_lock;
2223
2324use anyhow:: Context ;
2425use anyhow:: Error ;
@@ -45,17 +46,16 @@ use sha2::Digest;
4546use sha2:: Sha256 ;
4647use std:: collections:: BTreeMap ;
4748use std:: fs;
48- use std:: fs:: File ;
49- use std:: fs:: OpenOptions ;
5049use std:: io:: ErrorKind ;
5150use std:: path:: PathBuf ;
5251use std:: sync:: Arc ;
5352use std:: time:: Duration ;
54- use std:: time:: Instant ;
5553use std:: time:: SystemTime ;
5654use std:: time:: UNIX_EPOCH ;
5755use tracing:: warn;
5856
57+ use self :: store_lock:: OAuthStore ;
58+ use self :: store_lock:: OAuthStoreLock ;
5959use codex_keyring_store:: DefaultKeyringStore ;
6060use codex_keyring_store:: KeyringStore ;
6161use codex_utils_home_dir:: find_codex_home;
@@ -78,9 +78,6 @@ use rmcp::transport::auth::AuthorizationManager;
7878const KEYRING_SERVICE : & str = "Codex MCP Credentials" ;
7979const MCP_OAUTH_SECRET_PREFIX : & str = "MCP_OAUTH" ;
8080const 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 ) ]
8683pub 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- }
640554const FALLBACK_FILENAME : & str = ".credentials.json" ;
641555const 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-
832739fn read_fallback_file_unlocked ( ) -> Result < Option < FallbackFile > > {
833740 let path = fallback_file_path ( ) ?;
834741 let contents = match fs:: read_to_string ( & path) {
0 commit comments