Add network policy metadata to environment configuration (#39055)

## What changed

- Add `EnvironmentNetworkPolicy` as a serializable view of portable domain,
  Unix-socket, upstream-proxy, and local-binding restrictions without exposing
  controller-owned proxy runtime settings.
- Add an optional `network_policy` field to `EnvironmentConfig` and re-export
  the policy and related permission types through the core APIs.
- Reject owner-provided network policies during environment preview and
  readiness until runtime enforcement is implemented, while preserving the
  existing controller policy when the field is absent.

## Testing

- Cover rejection through both environment preview and readiness, including
  preservation of the existing environment selection.

GitOrigin-RevId: 80ad4cf4d4a45632daa7a823e6cf568eb0e8bb80
This commit is contained in:
sayan-oai
2026-08-17 19:22:45 +00:00
committed by copyberry
parent fe5889928c
commit 4a7b51c560
21 changed files with 144 additions and 0 deletions
+7
View File
@@ -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;
+1
View File
@@ -346,6 +346,7 @@ fn resolved_local_environments<const N: usize>(
),
shell_environment_policy: Default::default(),
exec_policy: None,
network_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -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,
@@ -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();
+6
View File
@@ -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;
+1
View File
@@ -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(),
}),
},
+7
View File
@@ -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"
+1
View File
@@ -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(),
}
}
@@ -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(),
}),
},
@@ -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(),
}),
},
@@ -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(),
}),
},
@@ -544,6 +544,7 @@ mod tests {
),
shell_environment_policy: Default::default(),
exec_policy: None,
network_policy: None,
selected_capability_roots: Vec::new(),
}),
},
@@ -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(),
},
),
+3
View File
@@ -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(),
},
)
@@ -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
@@ -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 {
+58
View File
@@ -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],
}),
}]),
@@ -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(),
},
)
@@ -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<NetworkDomainPermissions>,
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
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,
}
}
}
+2
View File
@@ -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;
+5
View File
@@ -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<RequirementsExecPolicy>,
/// Owner-provided traffic restrictions. `None` keeps the existing controller policy.
/// Core rejects `Some` until attachment-owned network enforcement is implemented.
pub network_policy: Option<EnvironmentNetworkPolicy>,
/// Capability roots selected for this thread's environment attachment.
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
}
@@ -40,6 +44,7 @@ impl std::fmt::Debug for EnvironmentConfig {
.field("permission_profile", &self.permission_profile)
.field("shell_environment_policy", &"<redacted>")
.field("exec_policy", &self.exec_policy)
.field("network_policy", &self.network_policy)
.field("selected_capability_roots", &self.selected_capability_roots)
.finish()
}