mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Prepare MCP connections for coordinated OAuth refresh (#42128)
## What changed - Add the under-development `mcp_oauth_refresh_coordination` feature and pin its selected refresh mode to each MCP connection. - Treat the refresh mode as part of OAuth connection identity so runtime and MCP configuration refreshes reconnect when the mode changes. - Keep legacy refresh behavior in builds where coordination is unavailable and emit a warning when coordinated mode is selected. ## Testing - Cover connection replacement when the feature is toggled through both runtime and MCP configuration refresh paths. GitOrigin-RevId: 89596150f0338e6ede0e712badecea4450831766
This commit is contained in:
@@ -231,6 +231,7 @@ impl McpConnectionSet {
|
||||
} = input;
|
||||
let store_mode = config.mcp_oauth_credentials_store_mode;
|
||||
let keyring_backend_kind = config.auth_keyring_backend_kind;
|
||||
let oauth_refresh_mode = config.oauth_refresh_mode;
|
||||
let codex_home = config.codex_home.clone();
|
||||
let prefix_mcp_tool_names = config.prefix_mcp_tool_names;
|
||||
let non_prefixed_mcp_tool_servers = config.non_prefixed_mcp_tool_servers.clone();
|
||||
@@ -362,6 +363,7 @@ impl McpConnectionSet {
|
||||
server: server.clone(),
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
runtime_context: runtime_context.clone(),
|
||||
resolved_environment: resolved_environment.clone(),
|
||||
auth_provider: runtime_auth_provider.clone(),
|
||||
@@ -377,6 +379,7 @@ impl McpConnectionSet {
|
||||
host_plugin_root,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
&resolved_environment,
|
||||
&runtime_context,
|
||||
runtime_auth_provider.as_ref(),
|
||||
@@ -539,6 +542,7 @@ impl McpConnectionSet {
|
||||
server,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
cancel_token.clone(),
|
||||
tx_event.clone(),
|
||||
elicitation_requests.clone(),
|
||||
|
||||
@@ -57,6 +57,7 @@ use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rmcp_client::InProcessTransportFactory;
|
||||
use codex_rmcp_client::McpAuthState;
|
||||
use codex_rmcp_client::McpLoginRequirement;
|
||||
use codex_rmcp_client::McpOAuthRefreshMode;
|
||||
use codex_rmcp_client::RmcpClient;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use futures::FutureExt;
|
||||
@@ -4153,6 +4154,7 @@ async fn executor_owned_chatgpt_mcp_accepts_only_safe_explicit_authorization() -
|
||||
/*host_plugin_root*/ None,
|
||||
OAuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind,
|
||||
McpOAuthRefreshMode::Legacy,
|
||||
&resolved_environment,
|
||||
&runtime_context,
|
||||
/*runtime_auth_provider*/ None,
|
||||
@@ -4675,6 +4677,7 @@ fn reusable_server_identity(
|
||||
/*host_plugin_root*/ None,
|
||||
OAuthCredentialsStoreMode::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
McpOAuthRefreshMode::Legacy,
|
||||
&resolved_environment,
|
||||
runtime_context,
|
||||
/*runtime_auth_provider*/ None,
|
||||
@@ -5054,6 +5057,7 @@ fn connection_identity_uses_effective_authorization_headers() {
|
||||
(Some("invalid\nheader"), None, false),
|
||||
(None, Some("PATH"), true),
|
||||
(None, Some(missing_env_var.as_str()), false),
|
||||
(None, None, false),
|
||||
] {
|
||||
let mut config = reusable_server_config("http://127.0.0.1:1");
|
||||
config.transport = McpServerTransportConfig::StreamableHttp {
|
||||
@@ -5066,13 +5070,14 @@ fn connection_identity_uses_effective_authorization_headers() {
|
||||
http_headers_helper: None,
|
||||
};
|
||||
let server = EffectiveMcpServer::configured(config);
|
||||
let identity = |keyring_backend_kind| {
|
||||
let identity = |keyring_backend_kind, oauth_refresh_mode| {
|
||||
McpServerConnectionIdentity::new(
|
||||
"docs",
|
||||
&server,
|
||||
/*host_plugin_root*/ None,
|
||||
OAuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
&Ok(None),
|
||||
&runtime_context,
|
||||
/*runtime_auth_provider*/ None,
|
||||
@@ -5085,8 +5090,19 @@ fn connection_identity_uses_effective_authorization_headers() {
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
identity(AuthKeyringBackendKind::Direct)
|
||||
.has_same_connection_config(&identity(AuthKeyringBackendKind::Secrets)),
|
||||
identity(AuthKeyringBackendKind::Direct, McpOAuthRefreshMode::Legacy)
|
||||
.has_same_connection_config(&identity(
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
McpOAuthRefreshMode::Legacy,
|
||||
)),
|
||||
has_authorization,
|
||||
);
|
||||
assert_eq!(
|
||||
identity(AuthKeyringBackendKind::Direct, McpOAuthRefreshMode::Legacy)
|
||||
.has_same_connection_config(&identity(
|
||||
AuthKeyringBackendKind::Direct,
|
||||
McpOAuthRefreshMode::Coordinated,
|
||||
)),
|
||||
has_authorization,
|
||||
);
|
||||
}
|
||||
@@ -5472,6 +5488,7 @@ async fn reconciliation_reconnects_when_host_plugin_root_changes() {
|
||||
Some(&original_root),
|
||||
OAuthCredentialsStoreMode::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
McpOAuthRefreshMode::Legacy,
|
||||
&resolved_environment,
|
||||
&runtime_context,
|
||||
/*runtime_auth_provider*/ None,
|
||||
@@ -5550,6 +5567,7 @@ async fn connection_identity_distinguishes_accounts_with_the_same_token() -> any
|
||||
/*host_plugin_root*/ None,
|
||||
OAuthCredentialsStoreMode::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
McpOAuthRefreshMode::Legacy,
|
||||
&Ok(None),
|
||||
&runtime_context,
|
||||
Some(&provider),
|
||||
@@ -5603,6 +5621,7 @@ async fn connection_identity_distinguishes_agent_account_runtime_and_task() -> a
|
||||
/*host_plugin_root*/ None,
|
||||
OAuthCredentialsStoreMode::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
McpOAuthRefreshMode::Legacy,
|
||||
&Ok(None),
|
||||
&runtime_context,
|
||||
Some(&provider),
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rmcp_client::McpOAuthRefreshMode;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use serde_json::Map;
|
||||
@@ -31,6 +32,7 @@ pub(crate) struct EventStreamConnectionSettings {
|
||||
pub server: EffectiveMcpServer,
|
||||
pub store_mode: OAuthCredentialsStoreMode,
|
||||
pub keyring_backend_kind: AuthKeyringBackendKind,
|
||||
pub oauth_refresh_mode: McpOAuthRefreshMode,
|
||||
pub runtime_context: McpRuntimeContext,
|
||||
pub resolved_environment: std::result::Result<Option<Arc<Environment>>, String>,
|
||||
pub auth_provider: Option<SharedAuthProvider>,
|
||||
@@ -81,6 +83,7 @@ impl McpEventStreamOpener {
|
||||
connection.server.clone(),
|
||||
connection.store_mode,
|
||||
connection.keyring_backend_kind,
|
||||
connection.oauth_refresh_mode,
|
||||
connection.runtime_context.clone(),
|
||||
connection.resolved_environment.clone(),
|
||||
connection.auth_provider.clone(),
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_protocol::mcp::Tool;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_rmcp_client::McpOAuthRefreshMode;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
@@ -128,6 +129,8 @@ pub struct McpConfig {
|
||||
pub codex_home: PathBuf,
|
||||
/// Preferred credential store for MCP OAuth tokens.
|
||||
pub mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode,
|
||||
/// OAuth refresh ownership selected for new MCP connections.
|
||||
pub oauth_refresh_mode: McpOAuthRefreshMode,
|
||||
/// Backend used when MCP OAuth storage is configured for keyring-backed persistence.
|
||||
pub auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
/// Optional fixed localhost callback port for MCP OAuth login.
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
apps_mcp_product_sku: None,
|
||||
codex_home,
|
||||
mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode::default(),
|
||||
oauth_refresh_mode: McpOAuthRefreshMode::Legacy,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
mcp_oauth_callback_port: None,
|
||||
mcp_oauth_callback_url: None,
|
||||
|
||||
@@ -60,6 +60,7 @@ use codex_protocol::protocol::McpStartupStatus;
|
||||
use codex_protocol::protocol::McpStartupUpdateEvent;
|
||||
use codex_rmcp_client::ExecutorStdioServerLauncher;
|
||||
use codex_rmcp_client::LocalStdioServerLauncher;
|
||||
use codex_rmcp_client::McpOAuthRefreshMode;
|
||||
use codex_rmcp_client::McpProtocolMode;
|
||||
use codex_rmcp_client::RmcpClient;
|
||||
use codex_rmcp_client::StdioServerLauncher;
|
||||
@@ -283,6 +284,7 @@ struct ManagedClientStartup {
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
oauth_refresh_mode: McpOAuthRefreshMode,
|
||||
tx_event: Option<Sender<Event>>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
codex_apps_tools_cache_context: Option<ConnectorRuntimeContext<ToolInfo>>,
|
||||
@@ -305,6 +307,7 @@ impl ManagedClientStartup {
|
||||
server,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
tx_event,
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
@@ -342,6 +345,7 @@ impl ManagedClientStartup {
|
||||
server.clone(),
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
runtime_context,
|
||||
resolved_environment,
|
||||
runtime_auth_provider,
|
||||
@@ -423,6 +427,7 @@ impl AsyncManagedClient {
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
oauth_refresh_mode: McpOAuthRefreshMode,
|
||||
cancel_token: CancellationToken,
|
||||
tx_event: Option<Sender<Event>>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
@@ -452,6 +457,7 @@ impl AsyncManagedClient {
|
||||
server,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_refresh_mode,
|
||||
tx_event,
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context: codex_apps_tools_cache_context.clone(),
|
||||
@@ -1072,11 +1078,17 @@ pub(crate) async fn make_rmcp_client(
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
oauth_refresh_mode: McpOAuthRefreshMode,
|
||||
runtime_context: McpRuntimeContext,
|
||||
resolved_environment: std::result::Result<Option<Arc<Environment>>, String>,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
protocol_mode: McpProtocolMode,
|
||||
) -> Result<RmcpClient, StartupOutcomeError> {
|
||||
if oauth_refresh_mode == McpOAuthRefreshMode::Coordinated {
|
||||
warn!(
|
||||
"MCP OAuth refresh coordination is not available in this build; using legacy refresh"
|
||||
);
|
||||
}
|
||||
let config = server.config().clone();
|
||||
if matches!(config.auth, McpServerAuth::ChatGpt)
|
||||
&& !config.is_local_environment()
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_connectors::ConnectorRuntimeContextKey;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_rmcp_client::McpOAuthRefreshMode;
|
||||
use codex_rmcp_client::StoredOAuthCredentialSnapshot;
|
||||
use codex_rmcp_client::StoredOAuthTokens;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -99,6 +100,7 @@ pub(crate) struct McpServerConnectionIdentity {
|
||||
environment_id: String,
|
||||
host_plugin_root: Option<PathUri>,
|
||||
oauth_store: Option<(OAuthCredentialsStoreMode, AuthKeyringBackendKind)>,
|
||||
oauth_refresh_mode: Option<McpOAuthRefreshMode>,
|
||||
oauth_credentials: Result<Option<StoredOAuthCredentialSnapshot>, String>,
|
||||
pub(crate) oauth_store_was_contended: bool,
|
||||
resolved_environment: Result<Option<Arc<Environment>>, String>,
|
||||
@@ -120,6 +122,7 @@ impl McpServerConnectionIdentity {
|
||||
host_plugin_root: Option<&PathUri>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
oauth_refresh_mode: McpOAuthRefreshMode,
|
||||
resolved_environment: &Result<Option<Arc<Environment>>, String>,
|
||||
runtime_context: &McpRuntimeContext,
|
||||
runtime_auth_provider: Option<&SharedAuthProvider>,
|
||||
@@ -217,6 +220,7 @@ impl McpServerConnectionIdentity {
|
||||
oauth_store: stored_oauth_url
|
||||
.is_some()
|
||||
.then_some((store_mode, keyring_backend_kind)),
|
||||
oauth_refresh_mode: stored_oauth_url.is_some().then_some(oauth_refresh_mode),
|
||||
oauth_credentials,
|
||||
oauth_store_was_contended,
|
||||
resolved_environment: resolved_environment.clone(),
|
||||
@@ -250,6 +254,7 @@ impl McpServerConnectionIdentity {
|
||||
&& self.environment_id == other.environment_id
|
||||
&& self.host_plugin_root == other.host_plugin_root
|
||||
&& self.oauth_store == other.oauth_store
|
||||
&& self.oauth_refresh_mode == other.oauth_refresh_mode
|
||||
&& same_resolved_environment(&self.resolved_environment, &other.resolved_environment)
|
||||
&& self.local_stdio_fallback_cwd == other.local_stdio_fallback_cwd
|
||||
&& self.referenced_environment_variables == other.referenced_environment_variables
|
||||
|
||||
@@ -822,6 +822,9 @@
|
||||
"mcp_2026_07_28": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mcp_oauth_refresh_coordination": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"memories": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6111,6 +6114,9 @@
|
||||
"mcp_2026_07_28": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mcp_oauth_refresh_coordination": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"memories": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -124,6 +124,7 @@ use codex_protocol::permissions::ReadDenyMatcher;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_rmcp_client::McpOAuthRefreshMode;
|
||||
pub use codex_thread_store::ExtraConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
@@ -1739,6 +1740,11 @@ impl Config {
|
||||
apps_mcp_product_sku: self.apps_mcp_product_sku.clone(),
|
||||
codex_home: self.codex_home.to_path_buf(),
|
||||
mcp_oauth_credentials_store_mode: self.mcp_oauth_credentials_store_mode,
|
||||
oauth_refresh_mode: if self.features.enabled(Feature::McpOAuthRefreshCoordination) {
|
||||
McpOAuthRefreshMode::Coordinated
|
||||
} else {
|
||||
McpOAuthRefreshMode::Legacy
|
||||
},
|
||||
auth_keyring_backend_kind: self.auth_keyring_backend_kind(),
|
||||
mcp_oauth_callback_port: self.mcp_oauth_callback_port,
|
||||
mcp_oauth_callback_url: self.mcp_oauth_callback_url.clone(),
|
||||
|
||||
@@ -1883,6 +1883,14 @@ impl Session {
|
||||
) {
|
||||
warn!("failed to refresh MCP auth storage config: {err}");
|
||||
}
|
||||
if let Err(err) = config.features.set_enabled(
|
||||
Feature::McpOAuthRefreshCoordination,
|
||||
next_config
|
||||
.features
|
||||
.enabled(Feature::McpOAuthRefreshCoordination),
|
||||
) {
|
||||
warn!("failed to refresh MCP OAuth coordination config: {err}");
|
||||
}
|
||||
let config = Arc::new(config);
|
||||
state.session_configuration.original_config_do_not_use = Arc::clone(&config);
|
||||
self.mark_mcp_runtime_dirty();
|
||||
@@ -1931,6 +1939,14 @@ impl Session {
|
||||
) {
|
||||
warn!("failed to refresh MCP auth storage config: {err}");
|
||||
}
|
||||
if let Err(err) = config.features.set_enabled(
|
||||
Feature::McpOAuthRefreshCoordination,
|
||||
next_config
|
||||
.features
|
||||
.enabled(Feature::McpOAuthRefreshCoordination),
|
||||
) {
|
||||
warn!("failed to refresh MCP OAuth coordination config: {err}");
|
||||
}
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
self.mark_mcp_runtime_dirty();
|
||||
drop(state);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//! Covers OAuth connection replacement when a running thread refreshes its feature config.
|
||||
|
||||
use super::EnvVarGuard;
|
||||
use super::OAuthCredentialExpiry;
|
||||
use super::write_fallback_oauth_tokens;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_features::Feature;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use serial_test::serial;
|
||||
use test_case::test_case;
|
||||
use wiremock::Mock;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
enum ConfigRefreshPath {
|
||||
Runtime,
|
||||
Mcp,
|
||||
}
|
||||
|
||||
#[test_case(ConfigRefreshPath::Runtime; "runtime configuration refresh")]
|
||||
#[test_case(ConfigRefreshPath::Mcp; "MCP configuration refresh")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial(codex_home)]
|
||||
async fn oauth_mode_refresh_replaces_the_live_connection(
|
||||
refresh_path: ConfigRefreshPath,
|
||||
) -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let mcp_server = responses::start_mock_server().await;
|
||||
let (http_server, startup_control) =
|
||||
AppsTestServer::mount_with_startup_control(&mcp_server).await?;
|
||||
let server_name = "oauth_refresh";
|
||||
let server_url = format!("{}/api/codex/ps/mcp", http_server.chatgpt_base_url);
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"/.well-known/oauth-authorization-server/api/codex/ps/mcp",
|
||||
))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"issuer": server_url,
|
||||
"authorization_endpoint": format!("{}/oauth/authorize", mcp_server.uri()),
|
||||
"token_endpoint": format!("{}/oauth/token", mcp_server.uri()),
|
||||
})))
|
||||
.mount(&mcp_server)
|
||||
.await;
|
||||
let server_config: McpServerConfig = serde_json::from_value(json!({
|
||||
"url": server_url,
|
||||
"environment_id": DEFAULT_MCP_SERVER_ENVIRONMENT_ID,
|
||||
}))?;
|
||||
let home = Arc::new(tempfile::tempdir()?);
|
||||
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", home.path().as_os_str());
|
||||
write_fallback_oauth_tokens(
|
||||
server_config.oauth_credential_name(server_name).as_ref(),
|
||||
&server_url,
|
||||
"test-client-id",
|
||||
"synthetic-access-token",
|
||||
"synthetic-refresh-token",
|
||||
OAuthCredentialExpiry::Valid,
|
||||
)?;
|
||||
let fixture = test_codex()
|
||||
.with_home(home)
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::McpOAuthRefreshCoordination)
|
||||
.expect("test config should allow the OAuth coordination feature");
|
||||
config.mcp_oauth_credentials_store_mode = OAuthCredentialsStoreMode::File;
|
||||
config
|
||||
.mcp_servers
|
||||
.set([(server_name.to_string(), server_config)].into())
|
||||
.expect("test config should allow the OAuth server");
|
||||
})
|
||||
// The mock MCP endpoint is on the host, including when the executor is remote.
|
||||
.build_with_remote_and_local_env(&responses_server)
|
||||
.await?;
|
||||
|
||||
let initial_result = fixture
|
||||
.codex
|
||||
.call_mcp_tool(
|
||||
server_name,
|
||||
"calendar_list_events",
|
||||
/*arguments*/ None,
|
||||
/*meta*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(startup_control.initialize_attempts(), 1);
|
||||
|
||||
for (enabled, expected_initializations) in [(true, 2), (true, 2), (false, 3)] {
|
||||
let mut refreshed_config = fixture.config.clone();
|
||||
refreshed_config
|
||||
.features
|
||||
.set_enabled(Feature::McpOAuthRefreshCoordination, enabled)?;
|
||||
match refresh_path {
|
||||
ConfigRefreshPath::Runtime => {
|
||||
fixture.codex.refresh_runtime_config(refreshed_config).await;
|
||||
}
|
||||
ConfigRefreshPath::Mcp => {
|
||||
fixture.codex.refresh_mcp_config(refreshed_config).await;
|
||||
}
|
||||
}
|
||||
// A normal tool call reconciles the refreshed config without forcing a reconnect.
|
||||
let result = fixture
|
||||
.codex
|
||||
.call_mcp_tool(
|
||||
server_name,
|
||||
"calendar_list_events",
|
||||
/*arguments*/ None,
|
||||
/*meta*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(result, initial_result);
|
||||
assert_eq!(
|
||||
startup_control.initialize_attempts(),
|
||||
expected_initializations
|
||||
);
|
||||
}
|
||||
|
||||
fixture.codex.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -119,6 +119,9 @@ use tokio::time::Instant;
|
||||
use tokio::time::sleep;
|
||||
use wiremock::MockServer;
|
||||
|
||||
#[path = "mcp_oauth_refresh_tests.rs"]
|
||||
mod oauth_refresh_tests;
|
||||
|
||||
static OPENAI_PNG: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD0AAAA9CAYAAAAeYmHpAAAE6klEQVR4Aeyau44UVxCGx1fZsmRLlm3Zoe0XcGQ5cUiCCIgJeS9CHgAhMkISQnIuGQgJEkBcxLW+nqnZ6uqqc+nuWRC7q/P3qetf9e+MtOwyX25O4Nep6JPyop++0qev9HrfgZ+F6r2DuB/vHOrt/UIkqdDHYvujOW6fO7h/CNEI+a5jc+pBR8uy0jVFsziYu5HtfSUk+Io34q921hLNctFSX0gwww+S8wce8K1LfCU+cYW4888aov8NxqvQILUPPReLOrm6zyLxa4i+6VZuFbJo8d1MOHZm+7VUtB/aIvhPWc/3SWg49JcwFLlHxuXKjtyloo+YNhuW3VS+WPBuUEMvCFKjEDVgFBQHXrnazpqiSxNZCkQ1kYiozsbm9Oz7l4i2Il7vGccGNWAc3XosDrZe/9P3ZnMmzHNEQw4smf8RQ87XEAMsC7Az0Au+dgXerfH4+sHvEc0SYGic8WBBUGqFH2gN7yDrazy7m2pbRTeRmU3+MjZmr1h6LJgPbGy23SI6GlYT0brQ71IY8Us4PNQCm+zepSbaD2BY9xCaAsD9IIj/IzFmKMSdHHonwdZATbTnYREf6/VZGER98N9yCWIvXQwXDoDdhZJoT8jwLnJXDB9w4Sb3e6nK5ndzlkTLnP3JBu4LKkbrYrU69gCVceV0JvpyuW1xlsUVngzhwMetn/XamtTORF9IO5YnWNiyeF9zCAfqR3fUW+vZZKLtgP+ts8BmQRBREAdRDhH3o8QuRh/YucNFz2BEjxbRN6LGzphfKmvP6v6QhqIQyZ8XNJ0W0X83MR1PEcJBNO2KC2Z1TW/v244scp9FwRViZxIOBF0Lctk7ZVSavdLvRlV1hz/ysUi9sr8CIcB3nvWBwA93ykTz18eAYxQ6N/K2DkPA1lv3iXCwmDUT7YkjIby9siXueIJj9H+pzSqJ9oIuJWTUgSSt4WO7o/9GGg0viR4VinNRUDoIj34xoCd6pxD3aK3zfdbnx5v1J3ZNNEJsE0sBG7N27ReDrJc4sFxz7dI/ZAbOmmiKvHBitQXpAdR6+F7v+/ol/tOouUV01EeMZQF2BoQDn6dP4XNr+j9GZEtEK1/L8pFw7bd3a53tsTa7WD+054jOFmPg1XBKPQgnqFfmFcy32ZRvjmiIIQTYFvyDxQ8nH8WIwwGwlyDjDznnilYyFr6njrlZwsKkBpO59A7OwgdzPEWRm+G+oeb7IfyNuzjEEVLrOVxJsxvxwF8kmCM6I2QYmJunz4u4TrADpfl7mlbRTWQ7VmrBzh3+C9f6Grc3YoGN9dg/SXFthpRsT6vobfXRs2VBlgBHXVMLHjDNbIZv1sZ9+X3hB09cXdH1JKViyG0+W9bWZDa/r2f9zAFR71sTzGpMSWz2iI4YssWjWo3REy1MDGjdwe5e0dFSiAC1JakBvu4/CUS8Eh6dqHdU0Or0ioY3W5ClSqDXAy7/6SRfgw8vt4I+tbvvNtFT2kVDhY5+IGb1rCqYaXNF08vSALsXCPmt0kQNqJT1p5eI1mkIV/BxCY1z85lOzeFbPBQHURkkPTlwTYK9gTVE25l84IbFFN+YJDHjdpn0gq6mrHht0dkcjbM4UL9283O5p77GN+SPW/QwVB4IUYg7Or+Kp7naR6qktP98LNF2UxWo9yObPIT9KYg+hK4i56no4rfnM0qeyFf6AwAAAP//trwR3wAAAAZJREFUAwBZ0sR75itw5gAAAABJRU5ErkJggg==";
|
||||
|
||||
fn assert_wall_time_line(line: &str) {
|
||||
|
||||
@@ -204,6 +204,8 @@ pub enum Feature {
|
||||
EnableMcpApps,
|
||||
/// Enable MCP protocol version 2026-07-28 support.
|
||||
Mcp20260728,
|
||||
/// Let RMCP coordinate OAuth refresh through the configured credential store.
|
||||
McpOAuthRefreshCoordination,
|
||||
/// Removed compatibility flag for the legacy Apps MCP path override.
|
||||
AppsMcpPathOverride,
|
||||
/// Removed compatibility flag retained as a no-op now that tool_search is always enabled.
|
||||
@@ -1251,6 +1253,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::McpOAuthRefreshCoordination,
|
||||
key: "mcp_oauth_refresh_coordination",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::AppsMcpPathOverride,
|
||||
key: "apps_mcp_path_override",
|
||||
|
||||
@@ -18,6 +18,7 @@ mod oauth;
|
||||
mod oauth_callback;
|
||||
mod oauth_client_registration;
|
||||
mod oauth_http_client;
|
||||
mod oauth_refresh_mode;
|
||||
mod perform_oauth_login;
|
||||
mod program_resolver;
|
||||
mod protocol_mode;
|
||||
@@ -61,6 +62,7 @@ pub use oauth::stored_oauth_credentials;
|
||||
pub use oauth_callback::McpOAuthCallbackMode;
|
||||
pub use oauth_callback::resolve_mcp_oauth_callback_url;
|
||||
pub use oauth_client_registration::McpOAuthClientRegistration;
|
||||
pub use oauth_refresh_mode::McpOAuthRefreshMode;
|
||||
pub use perform_oauth_login::OAuthProviderError;
|
||||
pub use perform_oauth_login::OauthLoginHandle;
|
||||
pub use perform_oauth_login::perform_oauth_login;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Selects the owner of MCP OAuth refresh and credential persistence.
|
||||
|
||||
/// MCP OAuth policy pinned for the lifetime of a connection.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum McpOAuthRefreshMode {
|
||||
/// Keep Codex's existing refresh and persistence path.
|
||||
#[default]
|
||||
Legacy,
|
||||
/// Let RMCP coordinate refresh through Codex's credential store.
|
||||
Coordinated,
|
||||
}
|
||||
Reference in New Issue
Block a user