@@ -33,6 +33,7 @@ use codex_protocol::config_types::ReasoningEffort;
33
33
use codex_protocol:: config_types:: ReasoningSummary ;
34
34
use codex_protocol:: config_types:: SandboxMode ;
35
35
use codex_protocol:: config_types:: Verbosity ;
36
+ use codex_rmcp_client:: OAuthCredentialsStoreMode ;
36
37
use dirs:: home_dir;
37
38
use serde:: Deserialize ;
38
39
use std:: collections:: BTreeMap ;
@@ -142,6 +143,15 @@ pub struct Config {
142
143
/// Definition for MCP servers that Codex can reach out to for tool calls.
143
144
pub mcp_servers : HashMap < String , McpServerConfig > ,
144
145
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
+
145
155
/// Combined provider map (defaults merged with user-defined overrides).
146
156
pub model_providers : HashMap < String , ModelProviderInfo > ,
147
157
@@ -694,6 +704,14 @@ pub struct ConfigToml {
694
704
#[ serde( default ) ]
695
705
pub mcp_servers : HashMap < String , McpServerConfig > ,
696
706
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
+
697
715
/// User-defined provider entries that extend/override the built-in list.
698
716
#[ serde( default ) ]
699
717
pub model_providers : HashMap < String , ModelProviderInfo > ,
@@ -1074,6 +1092,9 @@ impl Config {
1074
1092
user_instructions,
1075
1093
base_instructions,
1076
1094
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 ( ) ,
1077
1098
model_providers,
1078
1099
project_doc_max_bytes : cfg. project_doc_max_bytes . unwrap_or ( PROJECT_DOC_MAX_BYTES ) ,
1079
1100
project_doc_fallback_filenames : cfg
@@ -1364,6 +1385,85 @@ exclude_slash_tmp = true
1364
1385
) ;
1365
1386
}
1366
1387
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
+
1367
1467
#[ tokio:: test]
1368
1468
async fn load_global_mcp_servers_returns_empty_if_missing ( ) -> anyhow:: Result < ( ) > {
1369
1469
let codex_home = TempDir :: new ( ) ?;
@@ -1896,6 +1996,7 @@ model_verbosity = "high"
1896
1996
notify: None ,
1897
1997
cwd: fixture. cwd( ) ,
1898
1998
mcp_servers: HashMap :: new( ) ,
1999
+ mcp_oauth_credentials_store_mode: Default :: default ( ) ,
1899
2000
model_providers: fixture. model_provider_map. clone( ) ,
1900
2001
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES ,
1901
2002
project_doc_fallback_filenames: Vec :: new( ) ,
@@ -1958,6 +2059,7 @@ model_verbosity = "high"
1958
2059
notify : None ,
1959
2060
cwd : fixture. cwd ( ) ,
1960
2061
mcp_servers : HashMap :: new ( ) ,
2062
+ mcp_oauth_credentials_store_mode : Default :: default ( ) ,
1961
2063
model_providers : fixture. model_provider_map . clone ( ) ,
1962
2064
project_doc_max_bytes : PROJECT_DOC_MAX_BYTES ,
1963
2065
project_doc_fallback_filenames : Vec :: new ( ) ,
@@ -2035,6 +2137,7 @@ model_verbosity = "high"
2035
2137
notify : None ,
2036
2138
cwd : fixture. cwd ( ) ,
2037
2139
mcp_servers : HashMap :: new ( ) ,
2140
+ mcp_oauth_credentials_store_mode : Default :: default ( ) ,
2038
2141
model_providers : fixture. model_provider_map . clone ( ) ,
2039
2142
project_doc_max_bytes : PROJECT_DOC_MAX_BYTES ,
2040
2143
project_doc_fallback_filenames : Vec :: new ( ) ,
@@ -2098,6 +2201,7 @@ model_verbosity = "high"
2098
2201
notify : None ,
2099
2202
cwd : fixture. cwd ( ) ,
2100
2203
mcp_servers : HashMap :: new ( ) ,
2204
+ mcp_oauth_credentials_store_mode : Default :: default ( ) ,
2101
2205
model_providers : fixture. model_provider_map . clone ( ) ,
2102
2206
project_doc_max_bytes : PROJECT_DOC_MAX_BYTES ,
2103
2207
project_doc_fallback_filenames : Vec :: new ( ) ,
0 commit comments