diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index a97099f0b6..2793b341e0 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -28,9 +28,15 @@ pub use codex_config::types::UriBasedFileOpener; pub use codex_core::CodexAppsToolsCache; pub use codex_core::CodexThread; pub use codex_core::EnvironmentConfig; +pub use codex_core::EnvironmentNetworkPolicy; pub use codex_core::ForkSnapshot; pub use codex_core::LoadedAgentsMd; pub use codex_core::McpManager; +pub use codex_core::NetworkDomainPermission; +pub use codex_core::NetworkDomainPermissionEntry; +pub use codex_core::NetworkDomainPermissions; +pub use codex_core::NetworkUnixSocketPermission; +pub use codex_core::NetworkUnixSocketPermissions; pub use codex_core::NewThread; pub use codex_core::NotSubmittedReason; pub use codex_core::RecoverTurnRequest; @@ -51,6 +57,7 @@ pub use codex_core::config::Constrained; pub use codex_core::config::ExtraConfig; pub use codex_core::config::GhostSnapshotConfig; pub use codex_core::config::MultiAgentV2Config; +pub use codex_core::config::NetworkProxySpec; pub use codex_core::config::Permissions; pub use codex_core::config::TerminalResizeReflowConfig; pub use codex_core::config::ThreadStoreConfig; diff --git a/codex-rs/core/src/agents_md_tests.rs b/codex-rs/core/src/agents_md_tests.rs index ffcdc48a33..5f93f2fc38 100644 --- a/codex-rs/core/src/agents_md_tests.rs +++ b/codex-rs/core/src/agents_md_tests.rs @@ -346,6 +346,7 @@ fn resolved_local_environments( ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), }, diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index a751bf7ac5..191340d407 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -4,6 +4,7 @@ use codex_network_proxy::BlockedRequestObserver; use codex_network_proxy::ConfigReloader; use codex_network_proxy::ConfigReloaderFuture; use codex_network_proxy::ConfigState; +use codex_network_proxy::EnvironmentNetworkPolicy; use codex_network_proxy::NetworkDecision; use codex_network_proxy::NetworkPolicyDecider; use codex_network_proxy::NetworkProxy; @@ -173,6 +174,11 @@ impl NetworkProxySpec { ) } + /// Returns the effective traffic policy without exposing controller-owned proxy settings. + pub fn environment_policy(&self) -> EnvironmentNetworkPolicy { + EnvironmentNetworkPolicy::from_config(&self.config, self.hard_deny_allowlist_misses) + } + pub(crate) fn with_exec_policy_network_rules( &self, exec_policy: &Policy, diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 4a5c1332c5..7de8bff965 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -788,6 +788,7 @@ mod tests { permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), } } @@ -961,6 +962,7 @@ url = "ws://127.0.0.1:8765" ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }; let turn_environments = ThreadEnvironments::new( @@ -1164,6 +1166,7 @@ url = "ws://127.0.0.1:8765" ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }; let cwd = PathUri::from_abs_path(&cwd); @@ -1499,6 +1502,7 @@ url = "ws://127.0.0.1:8765" ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }; let environments = ThreadEnvironments::new( @@ -1544,6 +1548,7 @@ url = "ws://127.0.0.1:8765" permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![root("parent-root")], }; let mut owner_selection = selection.clone(); diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e8335a4486..43e3bc1e02 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -31,6 +31,12 @@ mod compact_remote; mod compact_remote_history; mod compact_remote_v2; mod compact_token_budget; +pub use codex_network_proxy::EnvironmentNetworkPolicy; +pub use codex_network_proxy::NetworkDomainPermission; +pub use codex_network_proxy::NetworkDomainPermissionEntry; +pub use codex_network_proxy::NetworkDomainPermissions; +pub use codex_network_proxy::NetworkUnixSocketPermission; +pub use codex_network_proxy::NetworkUnixSocketPermissions; pub use codex_protocol::protocol::EnvironmentConfig; pub use codex_thread::BackgroundTerminalInfo; pub use codex_thread::CodexThread; diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 5867e3aa92..4954cc9fba 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1122,6 +1122,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul .snapshot(), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), }, diff --git a/codex-rs/core/src/session/environment.rs b/codex-rs/core/src/session/environment.rs index c9365b47a5..07924c9c50 100644 --- a/codex-rs/core/src/session/environment.rs +++ b/codex-rs/core/src/session/environment.rs @@ -34,6 +34,13 @@ fn validate_environment_config( selection: &TurnEnvironmentSelection, config: &EnvironmentConfig, ) -> CodexResult<()> { + // The public type can be used by owners before runtime enforcement lands. Do not + // accept restrictions here until the managed proxy can actually enforce them. + if config.network_policy.is_some() { + return Err(CodexErr::InvalidRequest( + "attachment-owned network policy is not supported yet".to_string(), + )); + } if config.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS { return Err(CodexErr::InvalidRequest(format!( "environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots" diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 5bda5821bd..207dcc7c5f 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -146,6 +146,7 @@ impl SessionConfiguration { permission_profile: self.permission_profile_state.snapshot(), shell_environment_policy: self.shell_environment_policy.clone(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), } } diff --git a/codex-rs/core/src/tools/handlers/shell_tests.rs b/codex-rs/core/src/tools/handlers/shell_tests.rs index 3668ab3658..aab52b8e7c 100644 --- a/codex-rs/core/src/tools/handlers/shell_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_tests.rs @@ -140,6 +140,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() { ), shell_environment_policy: selected_shell_environment_policy.clone(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), }, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 5d18a0d5b0..db1d61cd67 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -32,6 +32,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context:: ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), }, diff --git a/codex-rs/core/src/tools/runtimes/shell_tests.rs b/codex-rs/core/src/tools/runtimes/shell_tests.rs index cead75495b..6a9d6d3c6e 100644 --- a/codex-rs/core/src/tools/runtimes/shell_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell_tests.rs @@ -29,6 +29,7 @@ async fn approval_key_uses_path_uri_and_includes_environment_id() { ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), }, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 836142a495..38d7fa6bcb 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -544,6 +544,7 @@ mod tests { ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), }, diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 62d784f49e..bb20d9711d 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -918,6 +918,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava .snapshot(), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }, ), diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index 1232dc0e17..042b96f48c 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -573,6 +573,7 @@ async fn environment_command_restrictions_override_saved_prefix_approvals() -> R permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled), shell_environment_policy: Default::default(), exec_policy: Some(RequirementsExecPolicy::new(invalid_policy)), + network_policy: None, selected_capability_roots: Vec::new(), }, ) @@ -594,6 +595,7 @@ async fn environment_command_restrictions_override_saved_prefix_approvals() -> R permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled), shell_environment_policy: Default::default(), exec_policy: Some(RequirementsExecPolicy::new(environment_policy)), + network_policy: None, selected_capability_roots: Vec::new(), }, ) @@ -696,6 +698,7 @@ async fn environment_command_policy_changes_invalidate_session_approvals() -> Re ), shell_environment_policy: Default::default(), exec_policy: Some(RequirementsExecPolicy::new(policy)), + network_policy: None, selected_capability_roots: Vec::new(), }, ) diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 2c24ff955b..d1c46a372b 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -599,6 +599,7 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }; parent_environments diff --git a/codex-rs/core/tests/suite/mcp_tool_exposure.rs b/codex-rs/core/tests/suite/mcp_tool_exposure.rs index eae4516af7..9f371df5c1 100644 --- a/codex-rs/core/tests/suite/mcp_tool_exposure.rs +++ b/codex-rs/core/tests/suite/mcp_tool_exposure.rs @@ -484,6 +484,7 @@ async fn root_reconciliation_reuses_pending_apps_startup() -> Result<()> { ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![SelectedCapabilityRoot { id: "calendar-root".to_string(), location: CapabilityRootLocation::Environment { diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 55b0bb5266..2cf7dbaf5f 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -6,6 +6,7 @@ use codex_api::AuthProvider; use codex_config::types::ApprovalsReviewer; use codex_core::CodexThreadSettingsOverrides; use codex_core::EnvironmentConfig; +use codex_core::EnvironmentNetworkPolicy; use codex_core::StartThreadOptions; use codex_core::TurnInputRequest; use codex_core::WaitForEnvironmentToolConfig; @@ -39,6 +40,7 @@ use codex_history::RolloutItem; use codex_history::RolloutLine; use codex_http_client::HttpClientFactory; use codex_http_client::OutboundProxyPolicy; +use codex_network_proxy::NetworkProxyConfig; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::config_types::CollaborationMode; @@ -552,6 +554,7 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()> ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }), ..selection @@ -1175,6 +1178,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( permission_profile: permission_profile.clone(), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![root("duplicate"), root("duplicate")], }), ] { @@ -1211,6 +1215,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( permission_profile: permission_profile.clone(), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![root("startup-root"), root("second-root")], }), ..selection.clone() @@ -1231,6 +1236,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( permission_profile: permission_profile.clone(), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![root("first-root")], }), ..selection.clone() @@ -1290,6 +1296,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( permission_profile: permission_profile.clone(), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![root("first-updated-root")], }), ..selection.clone() @@ -1344,6 +1351,55 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn owner_network_policy_is_rejected_until_runtime_enforcement_exists() -> Result<()> { + let server = start_mock_server().await; + let test = test_codex().build_with_auto_env(&server).await?; + let selections = test.codex.environment_selections().await; + let selection = selections + .first() + .context("thread should select its executor environment")?; + let owner_config = EnvironmentConfig { + allow_login_shell: test.config.permissions.allow_login_shell, + permission_profile: PermissionProfileSnapshot::legacy( + test.config.permissions.permission_profile().clone(), + ), + shell_environment_policy: test.config.permissions.shell_environment_policy.clone(), + exec_policy: None, + network_policy: Some(EnvironmentNetworkPolicy::from_config( + &NetworkProxyConfig::default(), + /*managed_allowed_domains_only*/ true, + )), + selected_capability_roots: Vec::new(), + }; + let preview_error = test + .codex + .preview_thread_settings_overrides(CodexThreadSettingsOverrides { + environments: Some(TurnEnvironmentSelections::new( + test.config.cwd.clone(), + vec![TurnEnvironmentSelection { + config: EnvironmentConfigState::Ready(owner_config.clone()), + ..selection.clone() + }], + )), + ..Default::default() + }) + .await + .err() + .context("preview must not accept an unenforced policy")?; + let ready_error = test + .codex + .environment_ready(selection, owner_config) + .await + .expect_err("readiness must not accept an unenforced policy"); + + for error in [preview_error.to_string(), ready_error.to_string()] { + assert!(error.contains("attachment-owned network policy is not supported yet")); + } + assert_eq!(test.codex.environment_selections().await, selections); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn pending_attachment_installs_configuration_before_waiting_turn_resumes() -> Result<()> { const WAIT_CALL_ID: &str = "wait-for-owner-configuration"; @@ -1387,6 +1443,7 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes() permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![root(id)], }; let start_pending_thread = || { @@ -1704,6 +1761,7 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte ), shell_environment_policy: Default::default(), exec_policy: None, + network_policy: None, selected_capability_roots: vec![ready_root], }), }]), diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index d57305a6ad..69201e8f64 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -356,6 +356,7 @@ async fn exec_command_uses_installed_environment_shell_policy_with_explicit_over ..Default::default() }, exec_policy: None, + network_policy: None, selected_capability_roots: Vec::new(), }, ) diff --git a/codex-rs/network-proxy/src/environment_policy.rs b/codex-rs/network-proxy/src/environment_policy.rs new file mode 100644 index 0000000000..2cd5f264a3 --- /dev/null +++ b/codex-rs/network-proxy/src/environment_policy.rs @@ -0,0 +1,34 @@ +use crate::NetworkDomainPermissions; +use crate::NetworkProxyConfig; +use crate::NetworkUnixSocketPermissions; +use serde::Deserialize; +use serde::Serialize; + +/// Traffic restrictions supplied by the owner of one execution environment. +/// +/// Proxy enablement, listeners, network mode, MITM, and credentials remain owned by the +/// controller's network-proxy runtime. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EnvironmentNetworkPolicy { + pub domains: Option, + pub unix_sockets: Option, + pub allow_upstream_proxy: bool, + pub dangerously_allow_all_unix_sockets: bool, + pub allow_local_binding: bool, + pub managed_allowed_domains_only: bool, +} + +impl EnvironmentNetworkPolicy { + /// Captures portable traffic restrictions without exposing controller runtime settings. + pub fn from_config(config: &NetworkProxyConfig, managed_allowed_domains_only: bool) -> Self { + Self { + domains: config.domains.clone(), + unix_sockets: config.unix_sockets.clone(), + allow_upstream_proxy: config.allow_upstream_proxy, + dangerously_allow_all_unix_sockets: config.dangerously_allow_all_unix_sockets, + allow_local_binding: config.allow_local_binding, + managed_allowed_domains_only, + } + } +} diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index baf771a7b7..99b8b7f172 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -6,6 +6,7 @@ mod certs; mod config; mod connect_policy; mod credential_broker; +mod environment_policy; mod http_proxy; mod mitm; mod mitm_hook; @@ -41,6 +42,7 @@ pub use config::managed_proxy_ports; pub use credential_broker::CREDENTIAL_BROKER_ACTIVE_ENV_KEY; pub use credential_broker::brokered_credential_dummy_env_keys; pub use credential_broker::brokered_credential_env_keys; +pub use environment_policy::EnvironmentNetworkPolicy; pub use mitm_hook::InjectedHeaderConfig; pub use mitm_hook::MitmHookActionsConfig; pub use mitm_hook::MitmHookBodyConfig; diff --git a/codex-rs/protocol/src/environment.rs b/codex-rs/protocol/src/environment.rs index d9f74c3055..83001bae39 100644 --- a/codex-rs/protocol/src/environment.rs +++ b/codex-rs/protocol/src/environment.rs @@ -2,6 +2,7 @@ use crate::capabilities::SelectedCapabilityRoot; use crate::config_types::ShellEnvironmentPolicy; use crate::models::PermissionProfileSnapshot; use codex_execpolicy::RequirementsExecPolicy; +use codex_network_proxy::EnvironmentNetworkPolicy; /// Configuration supplied for a thread's selected environment. #[allow(clippy::large_enum_variant)] @@ -28,6 +29,9 @@ pub struct EnvironmentConfig { pub shell_environment_policy: ShellEnvironmentPolicy, /// Additional managed command restrictions for this environment attachment. pub exec_policy: Option, + /// Owner-provided traffic restrictions. `None` keeps the existing controller policy. + /// Core rejects `Some` until attachment-owned network enforcement is implemented. + pub network_policy: Option, /// Capability roots selected for this thread's environment attachment. pub selected_capability_roots: Vec, } @@ -40,6 +44,7 @@ impl std::fmt::Debug for EnvironmentConfig { .field("permission_profile", &self.permission_profile) .field("shell_environment_policy", &"") .field("exec_policy", &self.exec_policy) + .field("network_policy", &self.network_policy) .field("selected_capability_roots", &self.selected_capability_roots) .finish() }