Skip to content

Commit 3a80cac

Browse files
committed
feat(config): add permissions.network proxy config wiring
1 parent f2ad519 commit 3a80cac

File tree

3 files changed

+314
-50
lines changed

3 files changed

+314
-50
lines changed

codex-rs/core/src/config/mod.rs

Lines changed: 204 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ use crate::windows_sandbox::WindowsSandboxLevelExt;
5555
use crate::windows_sandbox::resolve_windows_sandbox_mode;
5656
use codex_app_server_protocol::Tools;
5757
use codex_app_server_protocol::UserSavedConfig;
58+
use codex_network_proxy::NetworkMode;
59+
use codex_network_proxy::NetworkProxyConfig;
5860
use codex_protocol::config_types::AltScreenMode;
5961
use codex_protocol::config_types::ForcedLoginMethod;
6062
use codex_protocol::config_types::ModeKind;
@@ -904,6 +906,11 @@ pub struct ConfigToml {
904906
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
905907
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
906908

909+
/// Nested permissions settings.
910+
#[serde(default)]
911+
#[schemars(skip)]
912+
pub permissions: Option<PermissionsToml>,
913+
907914
/// Optional external command to spawn for end-user notifications.
908915
#[serde(default)]
909916
pub notify: Option<Vec<String>>,
@@ -1129,6 +1136,85 @@ impl From<ConfigToml> for UserSavedConfig {
11291136
}
11301137
}
11311138

1139+
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
1140+
#[schemars(deny_unknown_fields)]
1141+
pub struct PermissionsToml {
1142+
/// Network proxy settings used by managed network mode.
1143+
pub network: Option<NetworkToml>,
1144+
}
1145+
1146+
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
1147+
#[schemars(deny_unknown_fields)]
1148+
pub struct NetworkToml {
1149+
pub enabled: Option<bool>,
1150+
pub proxy_url: Option<String>,
1151+
pub admin_url: Option<String>,
1152+
pub enable_socks5: Option<bool>,
1153+
pub socks_url: Option<String>,
1154+
pub enable_socks5_udp: Option<bool>,
1155+
pub allow_upstream_proxy: Option<bool>,
1156+
pub dangerously_allow_non_loopback_proxy: Option<bool>,
1157+
pub dangerously_allow_non_loopback_admin: Option<bool>,
1158+
pub mode: Option<NetworkMode>,
1159+
pub allowed_domains: Option<Vec<String>>,
1160+
pub denied_domains: Option<Vec<String>>,
1161+
pub allow_unix_sockets: Option<Vec<String>>,
1162+
pub allow_local_binding: Option<bool>,
1163+
}
1164+
1165+
impl NetworkToml {
1166+
pub(crate) fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) {
1167+
if let Some(enabled) = self.enabled {
1168+
config.network.enabled = enabled;
1169+
}
1170+
if let Some(proxy_url) = self.proxy_url.as_ref() {
1171+
config.network.proxy_url = proxy_url.clone();
1172+
}
1173+
if let Some(admin_url) = self.admin_url.as_ref() {
1174+
config.network.admin_url = admin_url.clone();
1175+
}
1176+
if let Some(enable_socks5) = self.enable_socks5 {
1177+
config.network.enable_socks5 = enable_socks5;
1178+
}
1179+
if let Some(socks_url) = self.socks_url.as_ref() {
1180+
config.network.socks_url = socks_url.clone();
1181+
}
1182+
if let Some(enable_socks5_udp) = self.enable_socks5_udp {
1183+
config.network.enable_socks5_udp = enable_socks5_udp;
1184+
}
1185+
if let Some(allow_upstream_proxy) = self.allow_upstream_proxy {
1186+
config.network.allow_upstream_proxy = allow_upstream_proxy;
1187+
}
1188+
if let Some(dangerously_allow_non_loopback_proxy) =
1189+
self.dangerously_allow_non_loopback_proxy
1190+
{
1191+
config.network.dangerously_allow_non_loopback_proxy =
1192+
dangerously_allow_non_loopback_proxy;
1193+
}
1194+
if let Some(dangerously_allow_non_loopback_admin) =
1195+
self.dangerously_allow_non_loopback_admin
1196+
{
1197+
config.network.dangerously_allow_non_loopback_admin =
1198+
dangerously_allow_non_loopback_admin;
1199+
}
1200+
if let Some(mode) = self.mode {
1201+
config.network.mode = mode;
1202+
}
1203+
if let Some(allowed_domains) = self.allowed_domains.as_ref() {
1204+
config.network.allowed_domains = allowed_domains.clone();
1205+
}
1206+
if let Some(denied_domains) = self.denied_domains.as_ref() {
1207+
config.network.denied_domains = denied_domains.clone();
1208+
}
1209+
if let Some(allow_unix_sockets) = self.allow_unix_sockets.as_ref() {
1210+
config.network.allow_unix_sockets = allow_unix_sockets.clone();
1211+
}
1212+
if let Some(allow_local_binding) = self.allow_local_binding {
1213+
config.network.allow_local_binding = allow_local_binding;
1214+
}
1215+
}
1216+
}
1217+
11321218
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
11331219
#[schemars(deny_unknown_fields)]
11341220
pub struct ProjectConfig {
@@ -1218,6 +1304,16 @@ pub struct GhostSnapshotToml {
12181304
}
12191305

12201306
impl ConfigToml {
1307+
pub(crate) fn network_proxy_config(&self) -> NetworkProxyConfig {
1308+
let mut config = NetworkProxyConfig::default();
1309+
if let Some(permissions) = self.permissions.as_ref()
1310+
&& let Some(network) = permissions.network.as_ref()
1311+
{
1312+
network.apply_to_network_proxy_config(&mut config);
1313+
}
1314+
config
1315+
}
1316+
12211317
/// Derive the effective sandbox policy from the configuration.
12221318
fn derive_sandbox_policy(
12231319
&self,
@@ -1697,6 +1793,8 @@ impl Config {
16971793

16981794
let forced_login_method = cfg.forced_login_method;
16991795

1796+
let configured_network_proxy_config = cfg.network_proxy_config();
1797+
17001798
let model = model.or(config_profile.model).or(cfg.model);
17011799

17021800
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
@@ -1793,16 +1891,25 @@ impl Config {
17931891

17941892
let network = match network_requirements {
17951893
Some(Sourced { value, source }) => {
1796-
let network = NetworkProxySpec::from_constraints(&config_layer_stack, value)
1797-
.map_err(|err| {
1798-
std::io::Error::new(
1799-
err.kind(),
1800-
format!("failed to build managed network proxy from {source}: {err}"),
1801-
)
1802-
})?;
1894+
let network = NetworkProxySpec::from_config_and_constraints(
1895+
configured_network_proxy_config.clone(),
1896+
Some(value),
1897+
)
1898+
.map_err(|err| {
1899+
std::io::Error::new(
1900+
err.kind(),
1901+
format!("failed to build managed network proxy from {source}: {err}"),
1902+
)
1903+
})?;
18031904
Some(network)
18041905
}
1805-
None => None,
1906+
None => {
1907+
let network = NetworkProxySpec::from_config_and_constraints(
1908+
configured_network_proxy_config,
1909+
None,
1910+
)?;
1911+
network.enabled().then_some(network)
1912+
}
18061913
};
18071914

18081915
let config = Self {
@@ -2207,6 +2314,95 @@ phase_2_model = "gpt-5"
22072314
);
22082315
}
22092316

2317+
#[test]
2318+
fn config_toml_deserializes_permissions_network() {
2319+
let toml = r#"
2320+
[permissions.network]
2321+
enabled = true
2322+
proxy_url = "http://127.0.0.1:43128"
2323+
enable_socks5 = false
2324+
allow_upstream_proxy = false
2325+
allowed_domains = ["openai.com"]
2326+
"#;
2327+
let cfg: ConfigToml = toml::from_str(toml)
2328+
.expect("TOML deserialization should succeed for permissions.network");
2329+
2330+
assert_eq!(
2331+
cfg.permissions
2332+
.and_then(|permissions| permissions.network)
2333+
.expect("permissions.network should deserialize"),
2334+
NetworkToml {
2335+
enabled: Some(true),
2336+
proxy_url: Some("http://127.0.0.1:43128".to_string()),
2337+
admin_url: None,
2338+
enable_socks5: Some(false),
2339+
socks_url: None,
2340+
enable_socks5_udp: None,
2341+
allow_upstream_proxy: Some(false),
2342+
dangerously_allow_non_loopback_proxy: None,
2343+
dangerously_allow_non_loopback_admin: None,
2344+
mode: None,
2345+
allowed_domains: Some(vec!["openai.com".to_string()]),
2346+
denied_domains: None,
2347+
allow_unix_sockets: None,
2348+
allow_local_binding: None,
2349+
}
2350+
);
2351+
}
2352+
2353+
#[test]
2354+
fn permissions_network_enabled_populates_runtime_network_proxy_spec() -> std::io::Result<()> {
2355+
let codex_home = TempDir::new()?;
2356+
let cfg = ConfigToml {
2357+
permissions: Some(PermissionsToml {
2358+
network: Some(NetworkToml {
2359+
enabled: Some(true),
2360+
proxy_url: Some("http://127.0.0.1:43128".to_string()),
2361+
enable_socks5: Some(false),
2362+
..Default::default()
2363+
}),
2364+
}),
2365+
..Default::default()
2366+
};
2367+
2368+
let config = Config::load_from_base_config_with_overrides(
2369+
cfg,
2370+
ConfigOverrides::default(),
2371+
codex_home.path().to_path_buf(),
2372+
)?;
2373+
let network = config
2374+
.permissions
2375+
.network
2376+
.as_ref()
2377+
.expect("enabled permissions.network should produce a NetworkProxySpec");
2378+
2379+
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
2380+
assert!(!network.socks_enabled());
2381+
Ok(())
2382+
}
2383+
2384+
#[test]
2385+
fn permissions_network_disabled_by_default_does_not_start_proxy() -> std::io::Result<()> {
2386+
let codex_home = TempDir::new()?;
2387+
let cfg = ConfigToml {
2388+
permissions: Some(PermissionsToml {
2389+
network: Some(NetworkToml {
2390+
allowed_domains: Some(vec!["openai.com".to_string()]),
2391+
..Default::default()
2392+
}),
2393+
}),
2394+
..Default::default()
2395+
};
2396+
2397+
let config = Config::load_from_base_config_with_overrides(
2398+
cfg,
2399+
ConfigOverrides::default(),
2400+
codex_home.path().to_path_buf(),
2401+
)?;
2402+
assert!(config.permissions.network.is_none());
2403+
Ok(())
2404+
}
2405+
22102406
#[test]
22112407
fn tui_config_missing_notifications_field_defaults_to_enabled() {
22122408
let cfg = r#"

codex-rs/core/src/config/network_proxy_spec.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::config;
21
use crate::config_loader::NetworkConstraints;
32
use async_trait::async_trait;
43
use codex_network_proxy::BlockedRequestObserver;
@@ -68,6 +67,10 @@ impl ConfigReloader for StaticNetworkProxyReloader {
6867
}
6968

7069
impl NetworkProxySpec {
70+
pub(crate) fn enabled(&self) -> bool {
71+
self.config.network.enabled
72+
}
73+
7174
pub fn proxy_host_and_port(&self) -> String {
7275
host_and_port_from_network_addr(&self.config.network.proxy_url, 3128)
7376
}
@@ -76,14 +79,15 @@ impl NetworkProxySpec {
7679
self.config.network.enable_socks5
7780
}
7881

79-
pub(crate) fn from_constraints(
80-
_config_layer_stack: &config::ConfigLayerStack,
81-
requirements: NetworkConstraints,
82+
pub(crate) fn from_config_and_constraints(
83+
config: NetworkProxyConfig,
84+
requirements: Option<NetworkConstraints>,
8285
) -> std::io::Result<Self> {
83-
// TODO(mbolin): Use ConfigLayerStack once we are ready to start
84-
// honoring network configuration in config.toml.
85-
let config = NetworkProxyConfig::default();
86-
let (config, constraints) = Self::apply_requirements(config, &requirements);
86+
let (config, constraints) = if let Some(requirements) = requirements {
87+
Self::apply_requirements(config, &requirements)
88+
} else {
89+
(config, NetworkProxyConstraints::default())
90+
};
8791
validate_policy_against_constraints(&config, &constraints).map_err(|err| {
8892
std::io::Error::new(
8993
std::io::ErrorKind::InvalidInput,

0 commit comments

Comments
 (0)