From 3ba4bf354577401aaeaffdb25567f54b1682ff82 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 7 Feb 2026 13:47:53 -0800 Subject: [PATCH] feat: include [experimental_network] in --- codex-rs/core/src/codex.rs | 6 +- .../src/config_loader/config_requirements.rs | 3 +- codex-rs/core/src/environment_context.rs | 163 +++++++++++++++--- 3 files changed, 145 insertions(+), 27 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9fbbcd312d..aceca2d3b9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -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 } diff --git a/codex-rs/core/src/config_loader/config_requirements.rs b/codex-rs/core/src/config_loader/config_requirements.rs index 21dbdb3d7f..c0810ff304 100644 --- a/codex-rs/core/src/config_loader/config_requirements.rs +++ b/codex-rs/core/src/config_loader/config_requirements.rs @@ -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, pub http_port: Option, diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index 9f5455a69f..96f1a73f0f 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -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, pub shell: Shell, + pub network: Option, } impl EnvironmentContext { - pub fn new(cwd: Option, shell: Shell) -> Self { - Self { cwd, shell } + pub fn new(cwd: Option, shell: Shell, network: Option) -> 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 { + turn_context + .config + .config_layer_stack + .requirements() + .network + .as_ref() + .map(|network| network.value.clone()) } } @@ -67,6 +87,61 @@ impl EnvironmentContext { let shell_name = self.shell.name(); lines.push(format!(" {shell_name}")); + if let Some(network) = self.network { + lines.push(" ".to_string()); + if let Some(enabled) = network.enabled { + lines.push(format!(" {enabled}")); + } + if let Some(http_port) = network.http_port { + lines.push(format!(" {http_port}")); + } + if let Some(socks_port) = network.socks_port { + lines.push(format!(" {socks_port}")); + } + if let Some(allow_upstream_proxy) = network.allow_upstream_proxy { + lines.push(format!( + " {allow_upstream_proxy}" + )); + } + if let Some(dangerously_allow_non_loopback_proxy) = + network.dangerously_allow_non_loopback_proxy + { + lines.push(format!( + " {dangerously_allow_non_loopback_proxy}" + )); + } + if let Some(dangerously_allow_non_loopback_admin) = + network.dangerously_allow_non_loopback_admin + { + lines.push(format!( + " {dangerously_allow_non_loopback_admin}" + )); + } + if let Some(allowed_domains) = network.allowed_domains { + lines.push(format!( + " {}", + allowed_domains.join(", ") + )); + } + if let Some(denied_domains) = network.denied_domains { + lines.push(format!( + " {}", + denied_domains.join(", ") + )); + } + if let Some(allow_unix_sockets) = network.allow_unix_sockets { + lines.push(format!( + " {}", + allow_unix_sockets.join(", ") + )); + } + if let Some(allow_local_binding) = network.allow_local_binding { + lines.push(format!( + " {allow_local_binding}" + )); + } + lines.push(" ".to_string()); + } lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string()); lines.join("\n") } @@ -105,7 +180,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#" @@ -118,9 +193,49 @@ mod tests { assert_eq!(context.serialize_to_xml(), expected); } + #[test] + fn serialize_environment_context_with_network() { + let network = NetworkConstraints { + enabled: Some(true), + http_port: Some(3128), + socks_port: Some(1080), + allow_upstream_proxy: Some(false), + dangerously_allow_non_loopback_proxy: Some(false), + dangerously_allow_non_loopback_admin: 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_unix_sockets: Some(vec!["/tmp/example.sock".to_string()]), + allow_local_binding: Some(true), + }; + let context = + EnvironmentContext::new(Some(test_path_buf("/repo")), fake_shell(), Some(network)); + + let expected = r#" + /repo + bash + + true + 3128 + 1080 + false + false + true + api.example.com, *.openai.com + blocked.example.com + /tmp/example.sock + true + +"#; + + 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#" bash @@ -131,7 +246,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#" bash @@ -142,7 +257,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#" bash @@ -153,7 +268,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#" bash @@ -164,23 +279,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 +309,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 +318,7 @@ mod tests { shell_path: "/bin/zsh".into(), shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), }, + None, ); assert!(context1.equals_except_shell(&context2));