Compare commits

...

5 Commits

Author SHA1 Message Date
Dylan Hurd
66858e4b29 fix tests 2025-11-04 16:41:31 -08:00
Dylan Hurd
78aafa465b rebase fix 2025-11-03 22:08:52 -08:00
Dylan Hurd
c9c5d694c9 OnceLock 2025-11-03 21:04:18 -08:00
Dylan Hurd
86d636cf33 core: exclude OS from env_context diffs; update tests for Windows/WSL OS block 2025-11-03 21:04:17 -08:00
Dylan Hurd
9f7097160d feat: Better OS Detection, starting with windows 2025-11-03 20:56:27 -08:00
2 changed files with 201 additions and 53 deletions

View File

@@ -20,6 +20,14 @@ pub enum NetworkAccess {
Restricted, Restricted,
Enabled, Enabled,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OperatingSystemInfo {
pub name: String,
pub version: String,
pub is_likely_windows_subsystem_for_linux: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename = "environment_context", rename_all = "snake_case")] #[serde(rename = "environment_context", rename_all = "snake_case")]
pub(crate) struct EnvironmentContext { pub(crate) struct EnvironmentContext {
@@ -29,6 +37,7 @@ pub(crate) struct EnvironmentContext {
pub network_access: Option<NetworkAccess>, pub network_access: Option<NetworkAccess>,
pub writable_roots: Option<Vec<PathBuf>>, pub writable_roots: Option<Vec<PathBuf>>,
pub shell: Option<Shell>, pub shell: Option<Shell>,
pub operating_system: Option<OperatingSystemInfo>,
} }
impl EnvironmentContext { impl EnvironmentContext {
@@ -70,6 +79,7 @@ impl EnvironmentContext {
_ => None, _ => None,
}, },
shell, shell,
operating_system: Self::operating_system_info(),
} }
} }
@@ -83,6 +93,7 @@ impl EnvironmentContext {
sandbox_mode, sandbox_mode,
network_access, network_access,
writable_roots, writable_roots,
operating_system,
// should compare all fields except shell // should compare all fields except shell
shell: _, shell: _,
} = other; } = other;
@@ -92,6 +103,7 @@ impl EnvironmentContext {
&& self.sandbox_mode == *sandbox_mode && self.sandbox_mode == *sandbox_mode
&& self.network_access == *network_access && self.network_access == *network_access
&& self.writable_roots == *writable_roots && self.writable_roots == *writable_roots
&& self.operating_system == *operating_system
} }
pub fn diff(before: &TurnContext, after: &TurnContext) -> Self { pub fn diff(before: &TurnContext, after: &TurnContext) -> Self {
@@ -110,7 +122,12 @@ impl EnvironmentContext {
} else { } else {
None None
}; };
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, None) // Diff messages should only include fields that changed between turns.
// Operating system is a static property of the host and should not be
// emitted as part of a per-turn diff.
let mut ec = EnvironmentContext::new(cwd, approval_policy, sandbox_policy, None);
ec.operating_system = None;
ec
} }
} }
@@ -141,10 +158,11 @@ impl EnvironmentContext {
/// <shell>...</shell> /// <shell>...</shell>
/// </environment_context> /// </environment_context>
/// ``` /// ```
pub fn serialize_to_xml(self) -> String { pub fn serialize_to_xml(&self) -> String {
let mut lines = vec![ENVIRONMENT_CONTEXT_OPEN_TAG.to_string()]; let mut lines = vec![ENVIRONMENT_CONTEXT_OPEN_TAG.to_string()];
if let Some(cwd) = self.cwd { if let Some(cwd) = self.cwd.as_ref() {
lines.push(format!(" <cwd>{}</cwd>", cwd.to_string_lossy())); let cwd = cwd.to_string_lossy();
lines.push(format!(" <cwd>{cwd}</cwd>"));
} }
if let Some(approval_policy) = self.approval_policy { if let Some(approval_policy) = self.approval_policy {
lines.push(format!( lines.push(format!(
@@ -154,29 +172,44 @@ impl EnvironmentContext {
if let Some(sandbox_mode) = self.sandbox_mode { if let Some(sandbox_mode) = self.sandbox_mode {
lines.push(format!(" <sandbox_mode>{sandbox_mode}</sandbox_mode>")); lines.push(format!(" <sandbox_mode>{sandbox_mode}</sandbox_mode>"));
} }
if let Some(network_access) = self.network_access { if let Some(network_access) = self.network_access.as_ref() {
lines.push(format!( lines.push(format!(
" <network_access>{network_access}</network_access>" " <network_access>{network_access}</network_access>"
)); ));
} }
if let Some(writable_roots) = self.writable_roots { if let Some(writable_roots) = self.writable_roots.as_ref() {
lines.push(" <writable_roots>".to_string()); lines.push(" <writable_roots>".to_string());
for writable_root in writable_roots { for writable_root in writable_roots {
lines.push(format!( let writable_root = writable_root.to_string_lossy();
" <root>{}</root>", lines.push(format!(" <root>{writable_root}</root>"));
writable_root.to_string_lossy()
));
} }
lines.push(" </writable_roots>".to_string()); lines.push(" </writable_roots>".to_string());
} }
if let Some(shell) = self.shell if let Some(shell) = self.shell.as_ref()
&& let Some(shell_name) = shell.name() && let Some(shell_name) = shell.name()
{ {
lines.push(format!(" <shell>{shell_name}</shell>")); lines.push(format!(" <shell>{shell_name}</shell>"));
} }
if let Some(operating_system) = self.operating_system.as_ref() {
lines.push(" <operating_system>".to_string());
let name = operating_system.name.as_str();
lines.push(format!(" <name>{name}</name>"));
let version = operating_system.version.as_str();
lines.push(format!(" <version>{version}</version>"));
if let Some(is_wsl) = operating_system.is_likely_windows_subsystem_for_linux {
lines.push(format!(
" <is_likely_windows_subsystem_for_linux>{is_wsl}</is_likely_windows_subsystem_for_linux>"
));
}
lines.push(" </operating_system>".to_string());
}
lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string()); lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string());
lines.join("\n") lines.join("\n")
} }
fn operating_system_info() -> Option<OperatingSystemInfo> {
operating_system_info_impl()
}
} }
impl From<EnvironmentContext> for ResponseItem { impl From<EnvironmentContext> for ResponseItem {
@@ -191,6 +224,47 @@ impl From<EnvironmentContext> for ResponseItem {
} }
} }
// Restrict Operating System Info to Windows and Linux inside WSL for now
#[cfg(target_os = "windows")]
fn operating_system_info_impl() -> Option<OperatingSystemInfo> {
let info = os_info::get();
Some(OperatingSystemInfo {
name: info.os_type().to_string(),
version: 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<OperatingSystemInfo> {
let info = os_info::get();
match has_wsl_env_markers() {
true => Some(OperatingSystemInfo {
name: info.os_type().to_string(),
version: info.version().to_string(),
is_likely_windows_subsystem_for_linux: Some(true),
}),
false => None,
}
}
#[cfg(target_os = "macos")]
fn operating_system_info_impl() -> Option<OperatingSystemInfo> {
None
}
#[cfg(not(target_os = "macos"))]
fn has_wsl_env_markers() -> bool {
// Cache detection result since env vars are stable across process lifetime
// and this function may be called multiple times.
static CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*CACHE.get_or_init(|| {
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)] #[cfg(test)]
mod tests { mod tests {
use crate::shell::BashShell; use crate::shell::BashShell;
@@ -198,6 +272,58 @@ mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
fn expected_environment_context(mut body_lines: Vec<String>) -> String {
let mut lines = vec!["<environment_context>".to_string()];
lines.append(&mut body_lines);
if let Some(os) = EnvironmentContext::operating_system_info() {
lines.push(" <operating_system>".to_string());
lines.push(format!(" <name>{}</name>", os.name));
lines.push(format!(" <version>{}</version>", os.version));
if let Some(is_wsl) = os.is_likely_windows_subsystem_for_linux {
lines.push(format!(
" <is_likely_windows_subsystem_for_linux>{is_wsl}</is_likely_windows_subsystem_for_linux>"
));
}
lines.push(" </operating_system>".to_string());
}
lines.push("</environment_context>".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();
let os_details = os_info::get();
if has_wsl_env_markers() {
let info = info.expect("expected WSL operating system info");
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(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 { fn workspace_write_policy(writable_roots: Vec<&str>, network_access: bool) -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite { SandboxPolicy::WorkspaceWrite {
@@ -217,16 +343,16 @@ mod tests {
None, None,
); );
let expected = r#"<environment_context> let expected = expected_environment_context(vec![
<cwd>/repo</cwd> " <cwd>/repo</cwd>".to_string(),
<approval_policy>on-request</approval_policy> " <approval_policy>on-request</approval_policy>".to_string(),
<sandbox_mode>workspace-write</sandbox_mode> " <sandbox_mode>workspace-write</sandbox_mode>".to_string(),
<network_access>restricted</network_access> " <network_access>restricted</network_access>".to_string(),
<writable_roots> " <writable_roots>".to_string(),
<root>/repo</root> " <root>/repo</root>".to_string(),
<root>/tmp</root> " <root>/tmp</root>".to_string(),
</writable_roots> " </writable_roots>".to_string(),
</environment_context>"#; ]);
assert_eq!(context.serialize_to_xml(), expected); assert_eq!(context.serialize_to_xml(), expected);
} }
@@ -240,11 +366,11 @@ mod tests {
None, None,
); );
let expected = r#"<environment_context> let expected = expected_environment_context(vec![
<approval_policy>never</approval_policy> " <approval_policy>never</approval_policy>".to_string(),
<sandbox_mode>read-only</sandbox_mode> " <sandbox_mode>read-only</sandbox_mode>".to_string(),
<network_access>restricted</network_access> " <network_access>restricted</network_access>".to_string(),
</environment_context>"#; ]);
assert_eq!(context.serialize_to_xml(), expected); assert_eq!(context.serialize_to_xml(), expected);
} }
@@ -258,11 +384,11 @@ mod tests {
None, None,
); );
let expected = r#"<environment_context> let expected = expected_environment_context(vec![
<approval_policy>on-failure</approval_policy> " <approval_policy>on-failure</approval_policy>".to_string(),
<sandbox_mode>danger-full-access</sandbox_mode> " <sandbox_mode>danger-full-access</sandbox_mode>".to_string(),
<network_access>enabled</network_access> " <network_access>enabled</network_access>".to_string(),
</environment_context>"#; ]);
assert_eq!(context.serialize_to_xml(), expected); assert_eq!(context.serialize_to_xml(), expected);
} }

View File

@@ -36,19 +36,53 @@ fn text_user_input(text: String) -> serde_json::Value {
}) })
} }
#[allow(dead_code)]
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()
}
fn operating_system_context_block() -> String {
#[cfg(target_os = "windows")]
{
let info = os_info::get();
let name = info.os_type().to_string();
let version = info.version().to_string();
let is_wsl = has_wsl_env_markers();
format!(
" <operating_system>\n <name>{name}</name>\n <version>{version}</version>\n <is_likely_windows_subsystem_for_linux>{is_wsl}</is_likely_windows_subsystem_for_linux>\n </operating_system>\n"
)
}
#[cfg(all(unix, not(target_os = "macos")))]
{
if has_wsl_env_markers() {
" <operating_system>\n <name>{name}</name>\n <version></version>\n <is_likely_windows_subsystem_for_linux>true</is_likely_windows_subsystem_for_linux>\n </operating_system>\n".to_string()
} else {
String::new()
}
}
#[cfg(target_os = "macos")]
{
String::new()
}
}
fn default_env_context_str(cwd: &str, shell: &Shell) -> String { fn default_env_context_str(cwd: &str, shell: &Shell) -> String {
let shell_line = match shell.name() {
Some(name) => format!(" <shell>{name}</shell>\n"),
None => String::new(),
};
let os_block = operating_system_context_block();
format!( format!(
r#"<environment_context> r#"<environment_context>
<cwd>{}</cwd> <cwd>{cwd}</cwd>
<approval_policy>on-request</approval_policy> <approval_policy>on-request</approval_policy>
<sandbox_mode>read-only</sandbox_mode> <sandbox_mode>read-only</sandbox_mode>
<network_access>restricted</network_access> <network_access>restricted</network_access>
{}</environment_context>"#, {shell_line}{os_block}</environment_context>"#
cwd,
match shell.name() {
Some(name) => format!(" <shell>{name}</shell>\n"),
None => String::new(),
}
) )
} }
@@ -341,22 +375,10 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
let shell = default_user_shell().await; let shell = default_user_shell().await;
let expected_env_text = format!( let cwd_str = cwd.path().to_string_lossy().into_owned();
r#"<environment_context> let expected_env_text = default_env_context_str(&cwd_str, &shell);
<cwd>{}</cwd>
<approval_policy>on-request</approval_policy>
<sandbox_mode>read-only</sandbox_mode>
<network_access>restricted</network_access>
{}</environment_context>"#,
cwd.path().to_string_lossy(),
match shell.name() {
Some(name) => format!(" <shell>{name}</shell>\n"),
None => String::new(),
}
);
let expected_ui_text = format!( let expected_ui_text = format!(
"# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\nbe consistent and helpful\n</INSTRUCTIONS>", "# AGENTS.md instructions for {cwd_str}\n\n<INSTRUCTIONS>\nbe consistent and helpful\n</INSTRUCTIONS>"
cwd.path().to_string_lossy()
); );
let expected_env_msg = serde_json::json!({ let expected_env_msg = serde_json::json!({