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,
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)]
#[serde(rename = "environment_context", rename_all = "snake_case")]
pub(crate) struct EnvironmentContext {
@@ -29,6 +37,7 @@ pub(crate) struct EnvironmentContext {
pub network_access: Option<NetworkAccess>,
pub writable_roots: Option<Vec<PathBuf>>,
pub shell: Option<Shell>,
pub operating_system: Option<OperatingSystemInfo>,
}
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 {
@@ -110,7 +122,12 @@ impl EnvironmentContext {
} else {
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>
/// </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()];
if let Some(cwd) = self.cwd {
lines.push(format!(" <cwd>{}</cwd>", cwd.to_string_lossy()));
if let Some(cwd) = self.cwd.as_ref() {
let cwd = cwd.to_string_lossy();
lines.push(format!(" <cwd>{cwd}</cwd>"));
}
if let Some(approval_policy) = self.approval_policy {
lines.push(format!(
@@ -154,29 +172,44 @@ impl EnvironmentContext {
if let Some(sandbox_mode) = self.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!(
" <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());
for writable_root in writable_roots {
lines.push(format!(
" <root>{}</root>",
writable_root.to_string_lossy()
));
let writable_root = writable_root.to_string_lossy();
lines.push(format!(" <root>{writable_root}</root>"));
}
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()
{
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.join("\n")
}
fn operating_system_info() -> Option<OperatingSystemInfo> {
operating_system_info_impl()
}
}
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)]
mod tests {
use crate::shell::BashShell;
@@ -198,6 +272,58 @@ mod tests {
use super::*;
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 {
SandboxPolicy::WorkspaceWrite {
@@ -217,16 +343,16 @@ mod tests {
None,
);
let expected = r#"<environment_context>
<cwd>/repo</cwd>
<approval_policy>on-request</approval_policy>
<sandbox_mode>workspace-write</sandbox_mode>
<network_access>restricted</network_access>
<writable_roots>
<root>/repo</root>
<root>/tmp</root>
</writable_roots>
</environment_context>"#;
let expected = expected_environment_context(vec![
" <cwd>/repo</cwd>".to_string(),
" <approval_policy>on-request</approval_policy>".to_string(),
" <sandbox_mode>workspace-write</sandbox_mode>".to_string(),
" <network_access>restricted</network_access>".to_string(),
" <writable_roots>".to_string(),
" <root>/repo</root>".to_string(),
" <root>/tmp</root>".to_string(),
" </writable_roots>".to_string(),
]);
assert_eq!(context.serialize_to_xml(), expected);
}
@@ -240,11 +366,11 @@ mod tests {
None,
);
let expected = r#"<environment_context>
<approval_policy>never</approval_policy>
<sandbox_mode>read-only</sandbox_mode>
<network_access>restricted</network_access>
</environment_context>"#;
let expected = expected_environment_context(vec![
" <approval_policy>never</approval_policy>".to_string(),
" <sandbox_mode>read-only</sandbox_mode>".to_string(),
" <network_access>restricted</network_access>".to_string(),
]);
assert_eq!(context.serialize_to_xml(), expected);
}
@@ -258,11 +384,11 @@ mod tests {
None,
);
let expected = r#"<environment_context>
<approval_policy>on-failure</approval_policy>
<sandbox_mode>danger-full-access</sandbox_mode>
<network_access>enabled</network_access>
</environment_context>"#;
let expected = expected_environment_context(vec![
" <approval_policy>on-failure</approval_policy>".to_string(),
" <sandbox_mode>danger-full-access</sandbox_mode>".to_string(),
" <network_access>enabled</network_access>".to_string(),
]);
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 {
let shell_line = match shell.name() {
Some(name) => format!(" <shell>{name}</shell>\n"),
None => String::new(),
};
let os_block = operating_system_context_block();
format!(
r#"<environment_context>
<cwd>{}</cwd>
<cwd>{cwd}</cwd>
<approval_policy>on-request</approval_policy>
<sandbox_mode>read-only</sandbox_mode>
<network_access>restricted</network_access>
{}</environment_context>"#,
cwd,
match shell.name() {
Some(name) => format!(" <shell>{name}</shell>\n"),
None => String::new(),
}
{shell_line}{os_block}</environment_context>"#
)
}
@@ -341,22 +375,10 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
let shell = default_user_shell().await;
let expected_env_text = format!(
r#"<environment_context>
<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 cwd_str = cwd.path().to_string_lossy().into_owned();
let expected_env_text = default_env_context_str(&cwd_str, &shell);
let expected_ui_text = format!(
"# AGENTS.md instructions for {}\n\n<INSTRUCTIONS>\nbe consistent and helpful\n</INSTRUCTIONS>",
cwd.path().to_string_lossy()
"# AGENTS.md instructions for {cwd_str}\n\n<INSTRUCTIONS>\nbe consistent and helpful\n</INSTRUCTIONS>"
);
let expected_env_msg = serde_json::json!({