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/compact.rs b/codex-rs/core/src/compact.rs
index 99f7896031..7cab3d32e0 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -489,11 +489,15 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "# AGENTS.md instructions for project\n\n\ndo things\n"
+ text: r#"# AGENTS.md instructions for project
+
+
+do things
+"#
.to_string(),
}],
end_turn: None,
- phase: None,
+ phase: None,
},
ResponseItem::Message {
id: None,
@@ -502,7 +506,7 @@ mod tests {
text: "cwd=/tmp".to_string(),
}],
end_turn: None,
- phase: None,
+ phase: None,
},
ResponseItem::Message {
id: None,
@@ -511,7 +515,7 @@ mod tests {
text: "real user message".to_string(),
}],
end_turn: None,
- phase: None,
+ phase: None,
},
];
@@ -629,7 +633,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "cwd=/tmp".to_string(),
+ text: r#"
+ /tmp
+ zsh
+
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -660,7 +669,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "cwd=/tmp".to_string(),
+ text: r#"
+ /tmp
+ zsh
+
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -712,7 +726,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "# AGENTS.md instructions for /repo\n\n\nkeep me updated\n".to_string(),
+ text: r#"# AGENTS.md instructions for /repo
+
+
+keep me updated
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -721,7 +740,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "\n /repo\n zsh\n".to_string(),
+ text: r#"
+ /repo
+ zsh
+
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -730,7 +754,11 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "\n turn-1\n interrupted\n".to_string(),
+ text: r#"
+ turn-1
+ interrupted
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -752,7 +780,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "# AGENTS.md instructions for /repo\n\n\nkeep me updated\n".to_string(),
+ text: r#"# AGENTS.md instructions for /repo
+
+
+keep me updated
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -761,7 +794,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "\n /repo\n zsh\n".to_string(),
+ text: r#"
+ /repo
+ zsh
+
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -770,7 +808,10 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "\n turn-1\n interrupted\n"
+ text: r#"
+ turn-1
+ interrupted
+"#
.to_string(),
}],
end_turn: None,
@@ -796,7 +837,12 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "# AGENTS.md instructions for /repo\n\n\nkeep me updated\n".to_string(),
+ text: r#"# AGENTS.md instructions for /repo
+
+
+keep me updated
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -805,7 +851,11 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "\n /repo\n zsh\n".to_string(),
+ text: r#"
+ /repo
+ zsh
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
@@ -814,7 +864,11 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
- text: "\n turn-1\n interrupted\n".to_string(),
+ text: r#"
+ turn-1
+ interrupted
+"#
+ .to_string(),
}],
end_turn: None,
phase: None,
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..8db0d3fb3c 100644
--- a/codex-rs/core/src/environment_context.rs
+++ b/codex-rs/core/src/environment_context.rs
@@ -13,38 +13,70 @@ use std::path::PathBuf;
pub(crate) struct EnvironmentContext {
pub cwd: Option,
pub shell: Shell,
+ pub network: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub(crate) struct NetworkContext {
+ allowed_domains: Vec,
+ denied_domains: Vec,
}
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 {
+ before_network
+ };
+ 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 {
+ let maybe_network = turn_context
+ .config
+ .config_layer_stack
+ .requirements()
+ .network
+ .as_ref();
+ let Some(network) = maybe_network else {
+ return None;
+ };
+
+ Some(NetworkContext {
+ allowed_domains: network.allowed_domains.clone().unwrap_or_default(),
+ denied_domains: network.denied_domains.clone().unwrap_or_default(),
+ })
}
}
@@ -67,6 +99,23 @@ impl EnvironmentContext {
let shell_name = self.shell.name();
lines.push(format!(" {shell_name}"));
+ match self.network {
+ Some(ref network)
+ if !network.allowed_domains.is_empty() || !network.denied_domains.is_empty() =>
+ {
+ lines.push(" ".to_string());
+ for allowed in &network.allowed_domains {
+ lines.push(format!(" {allowed}"));
+ }
+ for denied in &network.denied_domains {
+ lines.push(format!(" {denied}"));
+ }
+ lines.push(" ".to_string());
+ }
+ _ => {
+ lines.push(" ".to_string());
+ }
+ }
lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string());
lines.join("\n")
}
@@ -105,12 +154,13 @@ 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#"
{cwd}
bash
+
"#,
cwd = cwd.display(),
);
@@ -118,12 +168,38 @@ mod tests {
assert_eq!(context.serialize_to_xml(), expected);
}
+ #[test]
+ fn serialize_environment_context_with_network() {
+ let network = NetworkContext {
+ allowed_domains: vec!["api.example.com".to_string(), "*.openai.com".to_string()],
+ denied_domains: vec!["blocked.example.com".to_string()],
+ };
+ let context =
+ EnvironmentContext::new(Some(test_path_buf("/repo")), fake_shell(), Some(network));
+
+ let expected = format!(
+ r#"
+ {}
+ bash
+
+ api.example.com
+ *.openai.com
+ blocked.example.com
+
+"#,
+ 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#"
bash
+
"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -131,10 +207,11 @@ 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
+
"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -142,10 +219,11 @@ 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
+
"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -153,10 +231,11 @@ 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
+
"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -164,23 +243,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 +273,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 +282,7 @@ mod tests {
shell_path: "/bin/zsh".into(),
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
},
+ None,
);
assert!(context1.equals_except_shell(&context2));
diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs
index 242f931406..71d534ba20 100644
--- a/codex-rs/core/tests/suite/prompt_caching.rs
+++ b/codex-rs/core/tests/suite/prompt_caching.rs
@@ -44,6 +44,7 @@ fn default_env_context_str(cwd: &str, shell: &Shell) -> String {
r#"
{cwd}
{shell_name}
+
"#
)
}