mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user