Files
codex/codex-rs/features/src/feature_configs.rs
T
viyatb-oai c7b55cdc46 feat: add network proxy feature flag (#20147)
## 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`
2026-05-11 14:12:00 -07:00

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,
}