diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index ea445e87894..f5b630cd3b6 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -43,7 +43,12 @@ impl ToolsConfig { let shell_type = if !features.enabled(Feature::ShellTool) { ConfigShellToolType::Disabled } else if features.enabled(Feature::UnifiedExec) { - ConfigShellToolType::UnifiedExec + // If ConPTY not supported (for old Windows versions), fallback on ShellCommand. + if codex_utils_pty::conpty_supported() { + ConfigShellToolType::UnifiedExec + } else { + ConfigShellToolType::ShellCommand + } } else { model_family.shell_type }; diff --git a/codex-rs/utils/pty/src/lib.rs b/codex-rs/utils/pty/src/lib.rs index 38e86bd2a6a..9e05416ed81 100644 --- a/codex-rs/utils/pty/src/lib.rs +++ b/codex-rs/utils/pty/src/lib.rs @@ -133,6 +133,15 @@ pub struct SpawnedPty { pub exit_rx: oneshot::Receiver, } +#[allow(unreachable_code)] +pub fn conpty_supported() -> bool { + // Annotation required because `win` can't be compiled on other OS. + #[cfg(windows)] + return win::conpty_supported(); + + true +} + #[cfg(windows)] fn platform_native_pty_system() -> Box { Box::new(win::ConPtySystem::default()) diff --git a/codex-rs/utils/pty/src/win/mod.rs b/codex-rs/utils/pty/src/win/mod.rs index 8206c9b890d..27057a36428 100644 --- a/codex-rs/utils/pty/src/win/mod.rs +++ b/codex-rs/utils/pty/src/win/mod.rs @@ -41,6 +41,7 @@ mod procthreadattr; mod psuedocon; pub use conpty::ConPtySystem; +pub use psuedocon::conpty_supported; #[derive(Debug)] pub struct WinChild { diff --git a/codex-rs/utils/pty/src/win/psuedocon.rs b/codex-rs/utils/pty/src/win/psuedocon.rs index a8db98eefe2..4365180c738 100644 --- a/codex-rs/utils/pty/src/win/psuedocon.rs +++ b/codex-rs/utils/pty/src/win/psuedocon.rs @@ -42,6 +42,8 @@ use std::path::Path; use std::ptr; use std::sync::Mutex; use winapi::shared::minwindef::DWORD; +use winapi::shared::ntdef::NTSTATUS; +use winapi::shared::ntstatus::STATUS_SUCCESS; use winapi::shared::winerror::HRESULT; use winapi::shared::winerror::S_OK; use winapi::um::handleapi::*; @@ -52,6 +54,7 @@ use winapi::um::winbase::STARTF_USESTDHANDLES; use winapi::um::winbase::STARTUPINFOEXW; use winapi::um::wincon::COORD; use winapi::um::winnt::HANDLE; +use winapi::um::winnt::OSVERSIONINFOW; pub type HPCON = HANDLE; @@ -59,6 +62,10 @@ pub const PSEUDOCONSOLE_RESIZE_QUIRK: DWORD = 0x2; #[allow(dead_code)] pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8; +// https://learn.microsoft.com/en-gb/windows/console/createpseudoconsole +// https://learn.microsoft.com/en-gb/windows/release-health/release-information +const MIN_CONPTY_BUILD: u32 = 17_763; + shared_library!(ConPtyFuncs, pub fn CreatePseudoConsole( size: COORD, @@ -71,6 +78,12 @@ shared_library!(ConPtyFuncs, pub fn ClosePseudoConsole(hpc: HPCON), ); +shared_library!(Ntdll, + pub fn RtlGetVersion( + version_info: *mut OSVERSIONINFOW + ) -> NTSTATUS, +); + fn load_conpty() -> ConPtyFuncs { let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( "this system does not support conpty. Windows 10 October 2018 or newer is required", @@ -87,6 +100,22 @@ lazy_static! { static ref CONPTY: ConPtyFuncs = load_conpty(); } +pub fn conpty_supported() -> bool { + windows_build_number().is_some_and(|build| build >= MIN_CONPTY_BUILD) +} + +fn windows_build_number() -> Option { + let ntdll = Ntdll::open(Path::new("ntdll.dll")).ok()?; + let mut info: OSVERSIONINFOW = unsafe { mem::zeroed() }; + info.dwOSVersionInfoSize = mem::size_of::() as u32; + let status = unsafe { (ntdll.RtlGetVersion)(&mut info) }; + if status == STATUS_SUCCESS { + Some(info.dwBuildNumber) + } else { + None + } +} + pub struct PsuedoCon { con: HPCON, } @@ -320,3 +349,17 @@ fn append_quoted(arg: &OsStr, cmdline: &mut Vec) { } cmdline.push('"' as u16); } + +#[cfg(test)] +mod tests { + use super::windows_build_number; + use super::MIN_CONPTY_BUILD; + + #[test] + fn windows_build_number_returns_value() { + // We can't stably check the version of the GH workers, but we can + // at least check that this. + let version = windows_build_number().unwrap(); + assert!(version > MIN_CONPTY_BUILD); + } +}