mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
## Why The permissions migration is making `permissions.<profile>.network.enabled` the canonical sandbox network bit, while proxy startup is a separate concern. Enabling network access should not implicitly start the proxy, and users who are still on legacy sandbox modes need a separate place to opt into proxy startup and provide proxy-specific settings. This follow-up to #19900 gives the network proxy its own feature surface instead of overloading permission-profile network semantics. ## What changed - Add an experimental `network_proxy` feature with a configurable `[features.network_proxy]` table. - Overlay `features.network_proxy` settings onto the configured proxy state after permission-profile selection, so the proxy only starts when the active `NetworkSandboxPolicy` already allows network access. - Preserve `[experimental_network]` startup behavior independently of the new feature flag. ## Behavior and examples There are now three related knobs: - `permissions.<profile>.network.enabled` controls whether the active permission profile has network access at all. - `features.network_proxy` enables proxy restrictions for an already-network-enabled profile. - Legacy `sandbox_mode` plus `[sandbox_workspace_write].network_access` still control whether legacy `workspace-write` has network access at all. The rule is: - network off + proxy flag on -> network stays off, proxy is a no-op - network on + proxy flag off -> unrestricted direct network - network on + proxy flag on -> network stays on, with proxy restrictions applied For permission profiles, the feature toggle adds proxy restrictions only when network access is already enabled: ```toml default_permissions = "workspace" [permissions.workspace.filesystem] ":minimal" = "read" [permissions.workspace.network] enabled = true [features] network_proxy = true ``` If `network.enabled = false`, the same feature flag is a no-op: network remains off and the proxy does not start. For legacy sandbox config, `network_access` remains the master switch: ```toml sandbox_mode = "workspace-write" [sandbox_workspace_write] network_access = true [features] network_proxy = true ``` That keeps legacy `workspace-write` network access on, but routes it through the proxy policy. If `network_access = false`, the proxy feature is a no-op and legacy `workspace-write` remains offline. The same proxy opt-in can be supplied from the CLI: ```bash codex -c 'features.network_proxy=true' ``` Additional proxy settings can be supplied when a table is needed: ```bash codex \ -c 'features.network_proxy.enabled=true' \ -c 'features.network_proxy.enable_socks5=false' ``` The intended behavior matrix is: | Config surface | Network setting | `features.network_proxy` | Direct sandbox network | Proxy | | --- | --- | --- | --- | --- | | Permission profile | `network.enabled = false` | off | restricted | off | | Permission profile | `network.enabled = false` | on | restricted | off | | Permission profile | `network.enabled = true` | off | enabled | off | | Permission profile | `network.enabled = true` | on | enabled | on | | Legacy `workspace-write` | `network_access = false` | off | restricted | off | | Legacy `workspace-write` | `network_access = false` | on | restricted | off | | Legacy `workspace-write` | `network_access = true` | off | enabled | off | | Legacy `workspace-write` | `network_access = true` | on | enabled | on | `[experimental_network]` requirements remain separate from the user feature toggle and still start the proxy on their own. Relevant code: - [`features/src/feature_configs.rs`](https://github.com/openai/codex/blob/43785aff47/codex-rs/features/src/feature_configs.rs#L58-L117) defines the feature-specific proxy config. - [`core/src/config/mod.rs`](https://github.com/openai/codex/blob/43785aff47/codex-rs/core/src/config/mod.rs#L1959-L1964) reads the feature table, and [later applies it only when network access is already enabled](https://github.com/openai/codex/blob/43785aff47/codex-rs/core/src/config/mod.rs#L2448-L2458). ## Verification Added focused coverage for: - keeping the proxy off when `features.network_proxy` is enabled but sandbox network access is disabled - the full permission-profile and legacy `workspace-write` matrix above - preserving `[experimental_network]` startup without the feature - reusing profile-supplied proxy settings when the feature is enabled Ran: - `cargo test -p codex-features` - `cargo test -p codex-core network_proxy_feature` - `cargo test -p codex-core experimental_network_requirements_enable_proxy_without_feature`
118 lines
4.0 KiB
Rust
118 lines
4.0 KiB
Rust
use crate::FeatureConfig;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MultiAgentV2ConfigToml {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enabled: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[schemars(range(min = 1))]
|
|
pub max_concurrent_threads_per_session: Option<usize>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[schemars(range(min = 1, max = 3600000))]
|
|
pub min_wait_timeout_ms: Option<i64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub usage_hint_enabled: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub usage_hint_text: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub root_agent_usage_hint_text: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub subagent_usage_hint_text: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub hide_spawn_agent_metadata: Option<bool>,
|
|
}
|
|
|
|
impl FeatureConfig for MultiAgentV2ConfigToml {
|
|
fn enabled(&self) -> Option<bool> {
|
|
self.enabled
|
|
}
|
|
|
|
fn set_enabled(&mut self, enabled: bool) {
|
|
self.enabled = Some(enabled);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct AppsMcpPathOverrideConfigToml {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enabled: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub path: Option<String>,
|
|
}
|
|
|
|
impl FeatureConfig for AppsMcpPathOverrideConfigToml {
|
|
fn enabled(&self) -> Option<bool> {
|
|
self.enabled.or(self.path.as_ref().map(|_| true))
|
|
}
|
|
|
|
fn set_enabled(&mut self, enabled: bool) {
|
|
self.enabled = Some(enabled);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct NetworkProxyConfigToml {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enabled: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub proxy_url: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enable_socks5: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub socks_url: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub enable_socks5_udp: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub allow_upstream_proxy: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub mode: Option<NetworkProxyModeToml>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub domains: Option<BTreeMap<String, NetworkProxyDomainPermissionToml>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub unix_sockets: Option<BTreeMap<String, NetworkProxyUnixSocketPermissionToml>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub allow_local_binding: Option<bool>,
|
|
}
|
|
|
|
impl FeatureConfig for NetworkProxyConfigToml {
|
|
fn enabled(&self) -> Option<bool> {
|
|
self.enabled
|
|
}
|
|
|
|
fn set_enabled(&mut self, enabled: bool) {
|
|
self.enabled = Some(enabled);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum NetworkProxyModeToml {
|
|
Limited,
|
|
Full,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum NetworkProxyDomainPermissionToml {
|
|
Allow,
|
|
Deny,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum NetworkProxyUnixSocketPermissionToml {
|
|
Allow,
|
|
None,
|
|
}
|