From 88fac6fe108237a105d3203e3508b0d531054312 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 21 Jul 2026 01:05:03 +0000 Subject: [PATCH] Honor managed permission profiles in network proxy resolution (#34436) ## Why Permission profiles defined by `requirements.toml` can be selected as active, but their network configuration was not included when resolving the network proxy specification for that selection. ## What changed Merge managed permission profiles from `requirements.toml` with configured profiles before looking up the active profile's network proxy settings. ## Testing - Verify config loading retains the proxy settings of an active managed profile. - Verify `turn/start` accepts a managed profile with network settings. GitOrigin-RevId: 3374f82a8fb0ce8f338fa2cea77c28ea2eb89636 --- .../app-server/tests/suite/v2/turn_start.rs | 90 +++++++++++++++++++ .../core/src/config/config_loader_tests.rs | 29 ++++-- codex-rs/core/src/config/mod.rs | 6 +- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 15994e7e22c3..e508ae62fe58 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1433,6 +1433,96 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() Ok(()) } +#[tokio::test] +async fn turn_start_accepts_managed_network_profile_from_requirements() -> Result<()> { + let responses = vec![create_final_assistant_message_sse_response("Done")?]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::from([(Feature::NetworkProxy, true)]), + )?; + std::fs::write( + codex_home.path().join("requirements.toml"), + r#" +default_permissions = "managed-network" + +[allowed_permission_profiles] +managed-network = true +":read-only" = true + +[permissions.managed-network] +extends = ":read-only" + +[permissions.managed-network.network] +enabled = true +allow_local_binding = false + +[permissions.managed-network.network.domains] +"packages.example" = "allow" +"#, + )?; + + let mut app_server = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build() + .await?; + timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??; + + let thread_req = app_server + .send_thread_start_request_with_auto_env(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { + thread, + active_permission_profile, + .. + } = to_response::(thread_resp)?; + let active_permission_profile = + active_permission_profile.context("expected active permission profile")?; + assert_eq!(active_permission_profile.id, "managed-network"); + + let turn_req = app_server + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + client_user_message_id: None, + input: vec![V2UserInput::Text { + text: "Use the managed network profile".to_string(), + text_elements: Vec::new(), + }], + permissions: Some("managed-network".to_string()), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + assert!( + !turn.id.is_empty(), + "turn/start should resolve the managed profile's network configuration" + ); + timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + Ok(()) +} + #[tokio::test] async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 9857e08f16cb..ed9416c12bb5 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1364,6 +1364,9 @@ async fn system_requirements_define_managed_permission_profiles() -> anyhow::Res codex_home.join(CONFIG_TOML_FILE), r#" default_permissions = "managed-standard" + +[features] +network_proxy = true "#, ) .await?; @@ -1378,6 +1381,11 @@ managed-standard = true [permissions.managed-standard] extends = ":workspace" + +[permissions.managed-standard.network] +enabled = true +proxy_url = "http://127.0.0.1:43128" +enable_socks5 = false "#, ) .await?; @@ -1399,13 +1407,20 @@ extends = ":workspace" .allowed_permission_profiles, Some(BTreeMap::from([("managed-standard".to_string(), true)])) ); - assert_eq!( - config - .permissions - .active_permission_profile() - .map(|profile| profile.id), - Some("managed-standard".to_string()) - ); + let active_permission_profile = config + .permissions + .active_permission_profile() + .expect("managed profile should be active"); + assert_eq!(active_permission_profile.id, "managed-standard"); + + let network = config + .network_proxy_spec_for_active_permission_profile( + &active_permission_profile, + config.permissions.permission_profile(), + )? + .expect("managed profile should retain its network proxy configuration"); + assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128"); + assert!(!network.socks_enabled()); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 48ca61c0c9d0..1fe1b8055849 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -4204,8 +4204,12 @@ impl Config { ), ) })?; - let mut configured_network_proxy_config = network_proxy_config_for_profile_selection( + let permissions = merge_managed_permission_profiles( cfg.permissions.as_ref(), + self.config_layer_stack.requirements_toml(), + )?; + let mut configured_network_proxy_config = network_proxy_config_for_profile_selection( + permissions.as_ref(), active_permission_profile.id.as_str(), )?; if self.features.enabled(Feature::NetworkProxy)