diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index e7b2e19ffb..4be06a3300 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -20,6 +20,14 @@ pub enum NetworkAccess { Restricted, Enabled, } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct OperatingSystemInfo { + pub name: String, + pub version: String, + pub is_likely_windows_subsystem_for_linux: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename = "environment_context", rename_all = "snake_case")] pub(crate) struct EnvironmentContext { @@ -29,6 +37,7 @@ pub(crate) struct EnvironmentContext { pub network_access: Option, pub writable_roots: Option>, pub shell: Option, + pub operating_system: Option, } impl EnvironmentContext { @@ -70,6 +79,7 @@ impl EnvironmentContext { _ => None, }, shell, + operating_system: Self::operating_system_info(), } } @@ -83,6 +93,7 @@ impl EnvironmentContext { sandbox_mode, network_access, writable_roots, + operating_system, // should compare all fields except shell shell: _, } = other; @@ -92,6 +103,7 @@ impl EnvironmentContext { && self.sandbox_mode == *sandbox_mode && self.network_access == *network_access && self.writable_roots == *writable_roots + && self.operating_system == *operating_system } pub fn diff(before: &TurnContext, after: &TurnContext) -> Self { @@ -174,9 +186,27 @@ impl EnvironmentContext { { lines.push(format!(" {shell_name}")); } + if let Some(operating_system) = self.operating_system { + lines.push(" ".to_string()); + lines.push(format!(" {}", operating_system.name)); + lines.push(format!( + " {}", + operating_system.version + )); + if let Some(is_wsl) = operating_system.is_likely_windows_subsystem_for_linux { + lines.push(format!( + " {is_wsl}" + )); + } + lines.push(" ".to_string()); + } lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string()); lines.join("\n") } + + fn operating_system_info() -> Option { + operating_system_info_impl() + } } impl From for ResponseItem { @@ -191,6 +221,41 @@ impl From for ResponseItem { } } +// Restrict Operating System Info to Windows and Linux inside WSL for now +#[cfg(target_os = "windows")] +fn operating_system_info_impl() -> Option { + let os_info = os_info::get(); + Some(OperatingSystemInfo { + name: os_info.os_type().to_string(), + version: os_info.version().to_string(), + is_likely_windows_subsystem_for_linux: Some(has_wsl_env_markers()), + }) +} + +#[cfg(all(unix, not(target_os = "macos")))] +fn operating_system_info_impl() -> Option { + match has_wsl_env_markers() { + true => Some(OperatingSystemInfo { + name: "Windows Subsystem for Linux".to_string(), + version: "".to_string(), + is_likely_windows_subsystem_for_linux: Some(true), + }), + false => None, + } +} + +#[cfg(target_os = "macos")] +fn operating_system_info_impl() -> Option { + None +} + +#[cfg(not(target_os = "macos"))] +fn has_wsl_env_markers() -> bool { + std::env::var_os("WSL_INTEROP").is_some() + || std::env::var_os("WSLENV").is_some() + || std::env::var_os("WSL_DISTRO_NAME").is_some() +} + #[cfg(test)] mod tests { use crate::shell::BashShell; @@ -198,6 +263,57 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + fn expected_environment_context(mut body_lines: Vec) -> String { + let mut lines = vec!["".to_string()]; + lines.append(&mut body_lines); + if let Some(os) = EnvironmentContext::operating_system_info() { + lines.push(" ".to_string()); + lines.push(format!(" {}", os.name)); + lines.push(format!(" {}", os.version)); + if let Some(is_wsl) = os.is_likely_windows_subsystem_for_linux { + lines.push(format!( + " {is_wsl}" + )); + } + lines.push(" ".to_string()); + } + lines.push("".to_string()); + lines.join("\n") + } + + #[cfg(target_os = "windows")] + #[test] + fn operating_system_info_on_windows_includes_os_details() { + let info = operating_system_info_impl().expect("expected Windows operating system info"); + let os_details = os_info::get(); + + assert_eq!(info.name, os_details.os_type().to_string()); + assert_eq!(info.version, os_details.version().to_string()); + assert_eq!( + info.is_likely_windows_subsystem_for_linux, + Some(has_wsl_env_markers()) + ); + } + + #[cfg(all(unix, not(target_os = "macos")))] + #[test] + fn operating_system_info_matches_wsl_detection_on_unix() { + let info = operating_system_info_impl(); + if has_wsl_env_markers() { + let info = info.expect("expected WSL operating system info"); + assert_eq!(info.name, "Windows Subsystem for Linux"); + assert_eq!(info.version, ""); + assert_eq!(info.is_likely_windows_subsystem_for_linux, Some(true)); + } else { + assert_eq!(info, None); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn operating_system_info_is_none_on_macos() { + assert_eq!(operating_system_info_impl(), None); + } fn workspace_write_policy(writable_roots: Vec<&str>, network_access: bool) -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { @@ -217,16 +333,16 @@ mod tests { None, ); - let expected = r#" - /repo - on-request - workspace-write - restricted - - /repo - /tmp - -"#; + let expected = expected_environment_context(vec![ + " /repo".to_string(), + " on-request".to_string(), + " workspace-write".to_string(), + " restricted".to_string(), + " ".to_string(), + " /repo".to_string(), + " /tmp".to_string(), + " ".to_string(), + ]); assert_eq!(context.serialize_to_xml(), expected); } @@ -240,11 +356,11 @@ mod tests { None, ); - let expected = r#" - never - read-only - restricted -"#; + let expected = expected_environment_context(vec![ + " never".to_string(), + " read-only".to_string(), + " restricted".to_string(), + ]); assert_eq!(context.serialize_to_xml(), expected); } @@ -258,11 +374,11 @@ mod tests { None, ); - let expected = r#" - on-failure - danger-full-access - enabled -"#; + let expected = expected_environment_context(vec![ + " on-failure".to_string(), + " danger-full-access".to_string(), + " enabled".to_string(), + ]); assert_eq!(context.serialize_to_xml(), expected); }