feat: include [experimental_network] in <environment_context>

This commit is contained in:
Michael Bolin
2026-02-07 13:47:53 -08:00
parent e6662d6387
commit c620d63c61
3 changed files with 105 additions and 27 deletions

View File

@@ -2184,9 +2184,9 @@ impl Session {
.into(),
);
}
items.push(ResponseItem::from(EnvironmentContext::new(
Some(turn_context.cwd.clone()),
shell.as_ref().clone(),
items.push(ResponseItem::from(EnvironmentContext::from_turn_context(
turn_context,
shell.as_ref(),
)));
items
}

View File

@@ -4,6 +4,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::fmt;
@@ -141,7 +142,7 @@ pub struct NetworkRequirementsToml {
}
/// Normalized network constraints derived from requirements TOML.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkConstraints {
pub enabled: Option<bool>,
pub http_port: Option<u16>,

View File

@@ -1,4 +1,5 @@
use crate::codex::TurnContext;
use crate::config_loader::NetworkConstraints;
use crate::shell::Shell;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
@@ -13,38 +14,57 @@ use std::path::PathBuf;
pub(crate) struct EnvironmentContext {
pub cwd: Option<PathBuf>,
pub shell: Shell,
pub network: Option<NetworkConstraints>,
}
impl EnvironmentContext {
pub fn new(cwd: Option<PathBuf>, shell: Shell) -> Self {
Self { cwd, shell }
pub fn new(cwd: Option<PathBuf>, shell: Shell, network: Option<NetworkConstraints>) -> Self {
Self {
cwd,
shell,
network,
}
}
/// Compares two environment contexts, ignoring the shell. Useful when
/// comparing turn to turn, since the initial environment_context will
/// include the shell, and then it is not configurable from turn to turn.
pub fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
let EnvironmentContext {
cwd,
// should compare all fields except shell
shell: _,
..
} = other;
self.cwd == *cwd
self.cwd == other.cwd && self.network == other.network
}
pub fn diff(before: &TurnContext, after: &TurnContext, shell: &Shell) -> Self {
let before_network = Self::network_from_turn_context(before);
let after_network = Self::network_from_turn_context(after);
let cwd = if before.cwd != after.cwd {
Some(after.cwd.clone())
} else {
None
};
EnvironmentContext::new(cwd, shell.clone())
let network = if before_network != after_network {
after_network
} else {
None
};
EnvironmentContext::new(cwd, shell.clone(), network)
}
pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
Self::new(Some(turn_context.cwd.clone()), shell.clone())
Self::new(
Some(turn_context.cwd.clone()),
shell.clone(),
Self::network_from_turn_context(turn_context),
)
}
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkConstraints> {
turn_context
.config
.config_layer_stack
.requirements()
.network
.as_ref()
.map(|network| network.value.clone())
}
}
@@ -67,6 +87,25 @@ impl EnvironmentContext {
let shell_name = self.shell.name();
lines.push(format!(" <shell>{shell_name}</shell>"));
if let Some(network) = self.network {
lines.push(" <network>".to_string());
if let Some(enabled) = network.enabled {
lines.push(format!(" <enabled>{enabled}</enabled>"));
}
if let Some(allowed_domains) = network.allowed_domains {
lines.push(format!(
" <allowed_domains>{}</allowed_domains>",
allowed_domains.join(", ")
));
}
if let Some(denied_domains) = network.denied_domains {
lines.push(format!(
" <denied_domains>{}</denied_domains>",
denied_domains.join(", ")
));
}
lines.push(" </network>".to_string());
}
lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string());
lines.join("\n")
}
@@ -105,7 +144,7 @@ mod tests {
#[test]
fn serialize_workspace_write_environment_context() {
let cwd = test_path_buf("/repo");
let context = EnvironmentContext::new(Some(cwd.clone()), fake_shell());
let context = EnvironmentContext::new(Some(cwd.clone()), fake_shell(), None);
let expected = format!(
r#"<environment_context>
@@ -118,9 +157,45 @@ mod tests {
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_environment_context_with_network() {
let network = NetworkConstraints {
enabled: Some(true),
allowed_domains: Some(vec![
"api.example.com".to_string(),
"*.openai.com".to_string(),
]),
denied_domains: Some(vec!["blocked.example.com".to_string()]),
allow_upstream_proxy: None,
allow_unix_sockets: None,
allow_local_binding: None,
dangerously_allow_non_loopback_admin: None,
dangerously_allow_non_loopback_proxy: None,
http_port: None,
socks_port: None,
};
let context =
EnvironmentContext::new(Some(test_path_buf("/repo")), fake_shell(), Some(network));
let expected = format!(
r#"<environment_context>
<cwd>{}</cwd>
<shell>bash</shell>
<network>
<enabled>true</enabled>
<allowed_domains>api.example.com, *.openai.com</allowed_domains>
<denied_domains>blocked.example.com</denied_domains>
</network>
</environment_context>"#,
test_path_buf("/repo").display()
);
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_read_only_environment_context() {
let context = EnvironmentContext::new(None, fake_shell());
let context = EnvironmentContext::new(None, fake_shell(), None);
let expected = r#"<environment_context>
<shell>bash</shell>
@@ -131,7 +206,7 @@ mod tests {
#[test]
fn serialize_external_sandbox_environment_context() {
let context = EnvironmentContext::new(None, fake_shell());
let context = EnvironmentContext::new(None, fake_shell(), None);
let expected = r#"<environment_context>
<shell>bash</shell>
@@ -142,7 +217,7 @@ mod tests {
#[test]
fn serialize_external_sandbox_with_restricted_network_environment_context() {
let context = EnvironmentContext::new(None, fake_shell());
let context = EnvironmentContext::new(None, fake_shell(), None);
let expected = r#"<environment_context>
<shell>bash</shell>
@@ -153,7 +228,7 @@ mod tests {
#[test]
fn serialize_full_access_environment_context() {
let context = EnvironmentContext::new(None, fake_shell());
let context = EnvironmentContext::new(None, fake_shell(), None);
let expected = r#"<environment_context>
<shell>bash</shell>
@@ -164,23 +239,23 @@ mod tests {
#[test]
fn equals_except_shell_compares_cwd() {
let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell());
let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell());
let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None);
let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_ignores_sandbox_policy() {
let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell());
let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell());
let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None);
let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_compares_cwd_differences() {
let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo1")), fake_shell());
let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo2")), fake_shell());
let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo1")), fake_shell(), None);
let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo2")), fake_shell(), None);
assert!(!context1.equals_except_shell(&context2));
}
@@ -194,6 +269,7 @@ mod tests {
shell_path: "/bin/bash".into(),
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
},
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
@@ -202,6 +278,7 @@ mod tests {
shell_path: "/bin/zsh".into(),
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
},
None,
);
assert!(context1.equals_except_shell(&context2));