diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9775e4991..9ebd24d91 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2204,9 +2204,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 99f789603..bed810e25 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,11 @@ 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 +668,11 @@ 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 +724,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 +738,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, @@ -730,7 +751,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 +777,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 +791,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, @@ -770,7 +804,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 +833,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 +847,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 +860,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 21dbdb3d7..c0810ff30 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 9f5455a69..e6995b7a9 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -13,11 +13,22 @@ 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 @@ -26,25 +37,49 @@ impl EnvironmentContext { pub fn equals_except_shell(&self, other: &EnvironmentContext) -> bool { let EnvironmentContext { cwd, + network, // should compare all fields except shell shell: _, - .. } = other; - - self.cwd == *cwd + self.cwd == *cwd && self.network == *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 network = turn_context + .config + .config_layer_stack + .requirements() + .network + .as_ref()?; + + Some(NetworkContext { + allowed_domains: network.allowed_domains.clone().unwrap_or_default(), + denied_domains: network.denied_domains.clone().unwrap_or_default(), + }) } } @@ -67,6 +102,22 @@ impl EnvironmentContext { let shell_name = self.shell.name(); lines.push(format!(" {shell_name}")); + match self.network { + Some(ref network) => { + 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()); + } + None => { + // TODO(mbolin): Include this line if it helps the model. + // lines.push(" ".to_string()); + } + } lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string()); lines.join("\n") } @@ -105,7 +156,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 +169,34 @@ 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 @@ -131,7 +207,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 +218,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 +229,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 +240,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 +270,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 +279,7 @@ mod tests { shell_path: "/bin/zsh".into(), shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), }, + None, ); assert!(context1.equals_except_shell(&context2));