diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs index 6c2929e544..f2dbc6376c 100644 --- a/codex-rs/rmcp-client/src/oauth.rs +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -736,13 +736,10 @@ fn os_shared_temp_dir() -> Result { use windows_sys::Win32::System::SystemInformation::GetSystemWindowsDirectoryW; let mut buffer = vec![0_u16; 32_768]; + let buffer_len = + u32::try_from(buffer.len()).context("Windows path buffer length did not fit u32")?; // SAFETY: buffer is writable for the length passed to GetSystemWindowsDirectoryW. - let length = unsafe { - GetSystemWindowsDirectoryW( - buffer.as_mut_ptr(), - u32::try_from(buffer.len()).expect("Windows path buffer length fits in u32"), - ) - }; + let length = unsafe { GetSystemWindowsDirectoryW(buffer.as_mut_ptr(), buffer_len) }; if length == 0 { return Err(io::Error::last_os_error()) .context("failed to resolve the system Windows directory"); @@ -1321,6 +1318,7 @@ mod tests { } } + #[cfg(unix)] fn path(&self) -> &Path { self._dir.path() } diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index 62b7a28f31..46e32fec73 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -1119,57 +1119,98 @@ async fn create_oauth_transport_and_runtime( #[cfg(test)] mod tests { + #[cfg(unix)] use std::ffi::OsString; + #[cfg(unix)] use std::fs::OpenOptions; + #[cfg(unix)] use std::path::Path; + #[cfg(unix)] use std::path::PathBuf; + #[cfg(unix)] use std::sync::Arc; use std::time::Duration; + #[cfg(unix)] use std::time::SystemTime; + #[cfg(unix)] use std::time::UNIX_EPOCH; + #[cfg(unix)] use anyhow::Context; + #[cfg(unix)] use codex_exec_server::Environment; + #[cfg(unix)] use oauth2::AccessToken; + #[cfg(unix)] use oauth2::RefreshToken; + #[cfg(unix)] use oauth2::basic::BasicTokenType; use pretty_assertions::assert_eq; + #[cfg(unix)] use rmcp::model::ClientCapabilities; + #[cfg(unix)] use rmcp::model::Implementation; + #[cfg(unix)] use rmcp::model::ProtocolVersion; + #[cfg(unix)] use rmcp::transport::auth::CredentialStore; + #[cfg(unix)] use rmcp::transport::auth::OAuthTokenResponse; + #[cfg(unix)] use rmcp::transport::auth::StoredCredentials; + #[cfg(unix)] use rmcp::transport::auth::VendorExtraTokenFields; + #[cfg(unix)] use serde_json::Value; + #[cfg(unix)] use serde_json::json; + #[cfg(unix)] use sha2::Digest; + #[cfg(unix)] use sha2::Sha256; + #[cfg(unix)] use tempfile::TempDir; use tokio::time; + #[cfg(unix)] use wiremock::Mock; + #[cfg(unix)] use wiremock::MockServer; + #[cfg(unix)] use wiremock::Request; + #[cfg(unix)] use wiremock::ResponseTemplate; + #[cfg(unix)] use wiremock::matchers::method; + #[cfg(unix)] use wiremock::matchers::path; use super::*; + #[cfg(unix)] use crate::oauth::WrappedOAuthTokenResponse; + #[cfg(unix)] use crate::oauth::load_oauth_tokens; + #[cfg(unix)] use crate::oauth::save_oauth_tokens; + #[cfg(unix)] const OPERATION_TEST_SERVER_NAME: &str = "test-completed-operation-persistence"; + #[cfg(unix)] const OPERATION_TEST_CLIENT_ID: &str = "test-operation-client-id"; + #[cfg(unix)] const OPERATION_TEST_OLD_ACCESS_TOKEN: &str = "old-operation-access-token"; + #[cfg(unix)] const OPERATION_TEST_OLD_REFRESH_TOKEN: &str = "old-operation-refresh-token"; + #[cfg(unix)] const OPERATION_TEST_NEW_ACCESS_TOKEN: &str = "new-operation-access-token"; + #[cfg(unix)] const OPERATION_TEST_NEW_REFRESH_TOKEN: &str = "new-operation-refresh-token"; + #[cfg(unix)] struct CodexHomeGuard { original: Option, } + #[cfg(unix)] impl CodexHomeGuard { fn set(path: &Path) -> Self { let original = std::env::var_os("CODEX_HOME"); @@ -1181,6 +1222,7 @@ mod tests { } } + #[cfg(unix)] impl Drop for CodexHomeGuard { fn drop(&mut self) { if let Some(original) = &self.original { @@ -1346,7 +1388,7 @@ mod tests { .write(true) .create(true) .truncate(false) - .open(operation_test_fallback_lock_path(codex_home.path()))?; + .open(operation_test_fallback_lock_path(codex_home.path())?)?; fallback_lock.lock_shared()?; let new_response = operation_test_token_response( @@ -1445,6 +1487,7 @@ mod tests { Ok(()) } + #[cfg(unix)] fn operation_test_token_response( access_token: &str, refresh_token: &str, @@ -1460,18 +1503,20 @@ mod tests { } #[cfg(unix)] - fn operation_test_fallback_lock_path(codex_home: &Path) -> PathBuf { + fn operation_test_fallback_lock_path(codex_home: &Path) -> Result { + let canonical_home = codex_home.canonicalize()?; let mut hasher = Sha256::new(); - hasher.update(codex_home.as_os_str().to_string_lossy().as_bytes()); + hasher.update(canonical_home.as_os_str().to_string_lossy().as_bytes()); let digest = format!("{:x}", hasher.finalize()); // SAFETY: getuid has no preconditions and returns the real UID. let user_namespace = format!("uid-{}", unsafe { libc::getuid() }); - PathBuf::from("/tmp").join(format!( + Ok(PathBuf::from("/tmp").join(format!( "codex-mcp-oauth-fallback-{user_namespace}-{}.lock", &digest[..16] - )) + ))) } + #[cfg(unix)] fn unix_millis() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -1479,6 +1524,7 @@ mod tests { .as_millis() as u64 } + #[cfg(unix)] fn unix_seconds() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/codex-rs/rmcp-client/tests/streamable_http_oauth_refresh_race.rs b/codex-rs/rmcp-client/tests/streamable_http_oauth_refresh_race.rs index 0b4565a17d..c17e5bb191 100644 --- a/codex-rs/rmcp-client/tests/streamable_http_oauth_refresh_race.rs +++ b/codex-rs/rmcp-client/tests/streamable_http_oauth_refresh_race.rs @@ -1669,6 +1669,7 @@ fn spawn_client_child_held_open( Ok(command.spawn()?) } +#[cfg(unix)] fn spawn_client_child_with_attempt( codex_home: &TempDir, server_url: &str, @@ -1745,6 +1746,7 @@ fn spawn_operation_timeout_child_held_open( Ok(command.spawn()?) } +#[cfg(unix)] fn spawn_concurrent_waiter_child( codex_home: &TempDir, server_url: &str, @@ -1769,6 +1771,7 @@ fn spawn_concurrent_waiter_child( Ok(command.spawn()?) } +#[cfg(unix)] fn spawn_read_only_refresh_child( codex_home: &TempDir, server_url: &str, @@ -1861,6 +1864,7 @@ fn file_oauth_refresh_lock_path(codex_home: &TempDir, server_url: &str) -> anyho ))) } +#[cfg(unix)] fn legacy_file_oauth_refresh_lock_path( codex_home: &TempDir, server_url: &str, @@ -1885,8 +1889,10 @@ fn fallback_lock_path_and_user_namespace( codex_home: &TempDir, ) -> anyhow::Result<(PathBuf, String)> { let canonical_home = codex_home.path().canonicalize()?; - let codex_home_id = - sha_256_bytes_prefix(canonical_home.as_os_str().to_string_lossy().as_bytes()); + let canonical_home = canonical_home.as_os_str().to_string_lossy().into_owned(); + #[cfg(windows)] + let canonical_home = normalize_windows_device_path(canonical_home); + let codex_home_id = sha_256_bytes_prefix(canonical_home.as_bytes()); let prefix = format!("{FALLBACK_LOCK_PREFIX}-"); let suffix = format!("-{codex_home_id}.lock"); for entry in fs::read_dir(shared_lock_root()?)? { @@ -1917,13 +1923,10 @@ fn shared_lock_root() -> anyhow::Result { use windows_sys::Win32::System::SystemInformation::GetSystemWindowsDirectoryW; let mut buffer = vec![0_u16; 32_768]; + let buffer_len = + u32::try_from(buffer.len()).context("Windows path buffer length did not fit u32")?; // SAFETY: buffer is writable for the length passed to GetSystemWindowsDirectoryW. - let length = unsafe { - GetSystemWindowsDirectoryW( - buffer.as_mut_ptr(), - u32::try_from(buffer.len()).expect("Windows path buffer length fits in u32"), - ) - }; + let length = unsafe { GetSystemWindowsDirectoryW(buffer.as_mut_ptr(), buffer_len) }; if length == 0 { return Err(io::Error::last_os_error()) .context("failed to resolve the system Windows directory"); @@ -1936,6 +1939,23 @@ fn shared_lock_root() -> anyhow::Result { Ok(PathBuf::from(OsString::from_wide(&buffer[..length])).join("Temp")) } +#[cfg(windows)] +fn normalize_windows_device_path(path: String) -> String { + if let Some(unc) = path.strip_prefix(r"\\?\UNC\") { + return format!(r"\\{unc}"); + } + if let Some(unc) = path.strip_prefix(r"\\.\UNC\") { + return format!(r"\\{unc}"); + } + if let Some(path) = path + .strip_prefix(r"\\?\") + .or_else(|| path.strip_prefix(r"\\.\")) + { + return path.to_string(); + } + path +} + #[cfg(not(any(unix, windows)))] fn shared_lock_root() -> anyhow::Result { anyhow::bail!("OAuth refresh race tests do not support this platform")