mirror of
https://github.com/openai/codex.git
synced 2026-05-18 10:12:59 +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`
128 lines
4.3 KiB
Rust
128 lines
4.3 KiB
Rust
use crate::config_toml::ConfigToml;
|
|
use crate::types::RawMcpServerConfig;
|
|
use codex_features::FEATURES;
|
|
use codex_features::legacy_feature_keys;
|
|
use schemars::r#gen::SchemaGenerator;
|
|
use schemars::r#gen::SchemaSettings;
|
|
use schemars::schema::InstanceType;
|
|
use schemars::schema::ObjectValidation;
|
|
use schemars::schema::RootSchema;
|
|
use schemars::schema::Schema;
|
|
use schemars::schema::SchemaObject;
|
|
use serde_json::Map;
|
|
use serde_json::Value;
|
|
use std::path::Path;
|
|
|
|
/// Schema for the `[features]` map with known + legacy keys only.
|
|
pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
|
let mut object = SchemaObject {
|
|
instance_type: Some(InstanceType::Object.into()),
|
|
..Default::default()
|
|
};
|
|
|
|
let mut validation = ObjectValidation::default();
|
|
for feature in FEATURES {
|
|
if feature.id == codex_features::Feature::Artifact {
|
|
continue;
|
|
}
|
|
if feature.id == codex_features::Feature::MultiAgentV2 {
|
|
validation.properties.insert(
|
|
feature.key.to_string(),
|
|
schema_gen.subschema_for::<codex_features::FeatureToml<
|
|
codex_features::MultiAgentV2ConfigToml,
|
|
>>(),
|
|
);
|
|
continue;
|
|
}
|
|
if feature.id == codex_features::Feature::AppsMcpPathOverride {
|
|
validation.properties.insert(
|
|
feature.key.to_string(),
|
|
schema_gen.subschema_for::<codex_features::FeatureToml<
|
|
codex_features::AppsMcpPathOverrideConfigToml,
|
|
>>(),
|
|
);
|
|
continue;
|
|
}
|
|
if feature.id == codex_features::Feature::NetworkProxy {
|
|
validation.properties.insert(
|
|
feature.key.to_string(),
|
|
schema_gen.subschema_for::<codex_features::FeatureToml<
|
|
codex_features::NetworkProxyConfigToml,
|
|
>>(),
|
|
);
|
|
continue;
|
|
}
|
|
validation
|
|
.properties
|
|
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());
|
|
}
|
|
for legacy_key in legacy_feature_keys() {
|
|
validation
|
|
.properties
|
|
.insert(legacy_key.to_string(), schema_gen.subschema_for::<bool>());
|
|
}
|
|
validation.additional_properties = Some(Box::new(Schema::Bool(false)));
|
|
object.object = Some(Box::new(validation));
|
|
|
|
Schema::Object(object)
|
|
}
|
|
|
|
/// Schema for the `[mcp_servers]` map using the raw input shape.
|
|
pub fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
|
let mut object = SchemaObject {
|
|
instance_type: Some(InstanceType::Object.into()),
|
|
..Default::default()
|
|
};
|
|
|
|
let validation = ObjectValidation {
|
|
additional_properties: Some(Box::new(schema_gen.subschema_for::<RawMcpServerConfig>())),
|
|
..Default::default()
|
|
};
|
|
object.object = Some(Box::new(validation));
|
|
|
|
Schema::Object(object)
|
|
}
|
|
|
|
/// Build the config schema for `config.toml`.
|
|
pub fn config_schema() -> RootSchema {
|
|
SchemaSettings::draft07()
|
|
.with(|settings| {
|
|
settings.option_add_null_type = false;
|
|
})
|
|
.into_generator()
|
|
.into_root_schema_for::<ConfigToml>()
|
|
}
|
|
|
|
/// Canonicalize a JSON value by sorting its keys.
|
|
pub fn canonicalize(value: &Value) -> Value {
|
|
match value {
|
|
Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()),
|
|
Value::Object(map) => {
|
|
let mut entries: Vec<_> = map.iter().collect();
|
|
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
|
|
let mut sorted = Map::with_capacity(map.len());
|
|
for (key, child) in entries {
|
|
sorted.insert(key.clone(), canonicalize(child));
|
|
}
|
|
Value::Object(sorted)
|
|
}
|
|
_ => value.clone(),
|
|
}
|
|
}
|
|
|
|
/// Render the config schema as pretty-printed JSON.
|
|
pub fn config_schema_json() -> anyhow::Result<Vec<u8>> {
|
|
let schema = config_schema();
|
|
let value = serde_json::to_value(schema)?;
|
|
let value = canonicalize(&value);
|
|
let json = serde_json::to_vec_pretty(&value)?;
|
|
Ok(json)
|
|
}
|
|
|
|
/// Write the config schema fixture to disk.
|
|
pub fn write_config_schema(out_path: &Path) -> anyhow::Result<()> {
|
|
let json = config_schema_json()?;
|
|
std::fs::write(out_path, json)?;
|
|
Ok(())
|
|
}
|