@@ -33,6 +33,7 @@ use codex_protocol::config_types::ReasoningEffort;
3333use codex_protocol:: config_types:: ReasoningSummary ;
3434use codex_protocol:: config_types:: SandboxMode ;
3535use codex_protocol:: config_types:: Verbosity ;
36+ use codex_rmcp_client:: OAuthCredentialsStoreMode ;
3637use dirs:: home_dir;
3738use serde:: Deserialize ;
3839use std:: collections:: BTreeMap ;
@@ -142,6 +143,15 @@ pub struct Config {
142143 /// Definition for MCP servers that Codex can reach out to for tool calls.
143144 pub mcp_servers : HashMap < String , McpServerConfig > ,
144145
146+ /// Preferred store for MCP OAuth credentials.
147+ /// keyring: Use an OS-specific keyring service.
148+ /// Credentials stored in the keyring will only be readable by Codex unless the user explicitly grants access via OS-level keyring access.
149+ /// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2
150+ /// file: CODEX_HOME/.credentials.json
151+ /// This file will be readable to Codex and other applications running as the same user.
152+ /// auto (default): keyring if available, otherwise file.
153+ pub mcp_oauth_credentials_store_mode : OAuthCredentialsStoreMode ,
154+
145155 /// Combined provider map (defaults merged with user-defined overrides).
146156 pub model_providers : HashMap < String , ModelProviderInfo > ,
147157
@@ -694,6 +704,14 @@ pub struct ConfigToml {
694704 #[ serde( default ) ]
695705 pub mcp_servers : HashMap < String , McpServerConfig > ,
696706
707+ /// Preferred backend for storing MCP OAuth credentials.
708+ /// keyring: Use an OS-specific keyring service.
709+ /// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2
710+ /// file: Use a file in the Codex home directory.
711+ /// auto (default): Use the OS-specific keyring service if available, otherwise use a file.
712+ #[ serde( default ) ]
713+ pub mcp_oauth_credentials_store : Option < OAuthCredentialsStoreMode > ,
714+
697715 /// User-defined provider entries that extend/override the built-in list.
698716 #[ serde( default ) ]
699717 pub model_providers : HashMap < String , ModelProviderInfo > ,
@@ -1074,6 +1092,9 @@ impl Config {
10741092 user_instructions,
10751093 base_instructions,
10761094 mcp_servers : cfg. mcp_servers ,
1095+ // The config.toml omits "_mode" because it's a config file. However, "_mode"
1096+ // is important in code to differentiate the mode from the store implementation.
1097+ mcp_oauth_credentials_store_mode : cfg. mcp_oauth_credentials_store . unwrap_or_default ( ) ,
10771098 model_providers,
10781099 project_doc_max_bytes : cfg. project_doc_max_bytes . unwrap_or ( PROJECT_DOC_MAX_BYTES ) ,
10791100 project_doc_fallback_filenames : cfg
@@ -1364,6 +1385,85 @@ exclude_slash_tmp = true
13641385 ) ;
13651386 }
13661387
1388+ #[ test]
1389+ fn config_defaults_to_auto_oauth_store_mode ( ) -> std:: io:: Result < ( ) > {
1390+ let codex_home = TempDir :: new ( ) ?;
1391+ let cfg = ConfigToml :: default ( ) ;
1392+
1393+ let config = Config :: load_from_base_config_with_overrides (
1394+ cfg,
1395+ ConfigOverrides :: default ( ) ,
1396+ codex_home. path ( ) . to_path_buf ( ) ,
1397+ ) ?;
1398+
1399+ assert_eq ! (
1400+ config. mcp_oauth_credentials_store_mode,
1401+ OAuthCredentialsStoreMode :: Auto ,
1402+ ) ;
1403+
1404+ Ok ( ( ) )
1405+ }
1406+
1407+ #[ test]
1408+ fn config_honors_explicit_file_oauth_store_mode ( ) -> std:: io:: Result < ( ) > {
1409+ let codex_home = TempDir :: new ( ) ?;
1410+ let cfg = ConfigToml {
1411+ mcp_oauth_credentials_store : Some ( OAuthCredentialsStoreMode :: File ) ,
1412+ ..Default :: default ( )
1413+ } ;
1414+
1415+ let config = Config :: load_from_base_config_with_overrides (
1416+ cfg,
1417+ ConfigOverrides :: default ( ) ,
1418+ codex_home. path ( ) . to_path_buf ( ) ,
1419+ ) ?;
1420+
1421+ assert_eq ! (
1422+ config. mcp_oauth_credentials_store_mode,
1423+ OAuthCredentialsStoreMode :: File ,
1424+ ) ;
1425+
1426+ Ok ( ( ) )
1427+ }
1428+
1429+ #[ tokio:: test]
1430+ async fn managed_config_overrides_oauth_store_mode ( ) -> anyhow:: Result < ( ) > {
1431+ let codex_home = TempDir :: new ( ) ?;
1432+ let managed_path = codex_home. path ( ) . join ( "managed_config.toml" ) ;
1433+ let config_path = codex_home. path ( ) . join ( CONFIG_TOML_FILE ) ;
1434+
1435+ std:: fs:: write ( & config_path, "mcp_oauth_credentials_store = \" file\" \n " ) ?;
1436+ std:: fs:: write ( & managed_path, "mcp_oauth_credentials_store = \" keyring\" \n " ) ?;
1437+
1438+ let overrides = crate :: config_loader:: LoaderOverrides {
1439+ managed_config_path : Some ( managed_path. clone ( ) ) ,
1440+ #[ cfg( target_os = "macos" ) ]
1441+ managed_preferences_base64 : None ,
1442+ } ;
1443+
1444+ let root_value = load_resolved_config ( codex_home. path ( ) , Vec :: new ( ) , overrides) . await ?;
1445+ let cfg: ConfigToml = root_value. try_into ( ) . map_err ( |e| {
1446+ tracing:: error!( "Failed to deserialize overridden config: {e}" ) ;
1447+ std:: io:: Error :: new ( std:: io:: ErrorKind :: InvalidData , e)
1448+ } ) ?;
1449+ assert_eq ! (
1450+ cfg. mcp_oauth_credentials_store,
1451+ Some ( OAuthCredentialsStoreMode :: Keyring ) ,
1452+ ) ;
1453+
1454+ let final_config = Config :: load_from_base_config_with_overrides (
1455+ cfg,
1456+ ConfigOverrides :: default ( ) ,
1457+ codex_home. path ( ) . to_path_buf ( ) ,
1458+ ) ?;
1459+ assert_eq ! (
1460+ final_config. mcp_oauth_credentials_store_mode,
1461+ OAuthCredentialsStoreMode :: Keyring ,
1462+ ) ;
1463+
1464+ Ok ( ( ) )
1465+ }
1466+
13671467 #[ tokio:: test]
13681468 async fn load_global_mcp_servers_returns_empty_if_missing ( ) -> anyhow:: Result < ( ) > {
13691469 let codex_home = TempDir :: new ( ) ?;
@@ -1896,6 +1996,7 @@ model_verbosity = "high"
18961996 notify: None ,
18971997 cwd: fixture. cwd( ) ,
18981998 mcp_servers: HashMap :: new( ) ,
1999+ mcp_oauth_credentials_store_mode: Default :: default ( ) ,
18992000 model_providers: fixture. model_provider_map. clone( ) ,
19002001 project_doc_max_bytes: PROJECT_DOC_MAX_BYTES ,
19012002 project_doc_fallback_filenames: Vec :: new( ) ,
@@ -1958,6 +2059,7 @@ model_verbosity = "high"
19582059 notify : None ,
19592060 cwd : fixture. cwd ( ) ,
19602061 mcp_servers : HashMap :: new ( ) ,
2062+ mcp_oauth_credentials_store_mode : Default :: default ( ) ,
19612063 model_providers : fixture. model_provider_map . clone ( ) ,
19622064 project_doc_max_bytes : PROJECT_DOC_MAX_BYTES ,
19632065 project_doc_fallback_filenames : Vec :: new ( ) ,
@@ -2035,6 +2137,7 @@ model_verbosity = "high"
20352137 notify : None ,
20362138 cwd : fixture. cwd ( ) ,
20372139 mcp_servers : HashMap :: new ( ) ,
2140+ mcp_oauth_credentials_store_mode : Default :: default ( ) ,
20382141 model_providers : fixture. model_provider_map . clone ( ) ,
20392142 project_doc_max_bytes : PROJECT_DOC_MAX_BYTES ,
20402143 project_doc_fallback_filenames : Vec :: new ( ) ,
@@ -2098,6 +2201,7 @@ model_verbosity = "high"
20982201 notify : None ,
20992202 cwd : fixture. cwd ( ) ,
21002203 mcp_servers : HashMap :: new ( ) ,
2204+ mcp_oauth_credentials_store_mode : Default :: default ( ) ,
21012205 model_providers : fixture. model_provider_map . clone ( ) ,
21022206 project_doc_max_bytes : PROJECT_DOC_MAX_BYTES ,
21032207 project_doc_fallback_filenames : Vec :: new ( ) ,
0 commit comments