@@ -55,6 +55,8 @@ use crate::windows_sandbox::WindowsSandboxLevelExt;
5555use crate :: windows_sandbox:: resolve_windows_sandbox_mode;
5656use codex_app_server_protocol:: Tools ;
5757use codex_app_server_protocol:: UserSavedConfig ;
58+ use codex_network_proxy:: NetworkMode ;
59+ use codex_network_proxy:: NetworkProxyConfig ;
5860use codex_protocol:: config_types:: AltScreenMode ;
5961use codex_protocol:: config_types:: ForcedLoginMethod ;
6062use 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) ]
11341220pub struct ProjectConfig {
@@ -1218,6 +1304,16 @@ pub struct GhostSnapshotToml {
12181304}
12191305
12201306impl 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#"
0 commit comments