From b84b970653d8c7f52371c64b5f6aaa423d6c955b Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 23 Mar 2026 14:10:54 -0700 Subject: [PATCH] core: attribute network approvals to owning tool calls Thread a stable network owner id through managed proxy credentials and use it to resolve the exact active tool call for blocked network requests. This makes concurrent network approvals attribute deterministically and preserves parent tool item ids downstream. Co-authored-by: Codex --- .../app-server/src/codex_message_processor.rs | 1 + codex-rs/app-server/src/command_exec.rs | 2 + codex-rs/core/src/codex_tests.rs | 2 + codex-rs/core/src/codex_tests_guardian.rs | 1 + codex-rs/core/src/exec.rs | 13 +++- codex-rs/core/src/exec_tests.rs | 5 ++ .../core/src/network_policy_decision_tests.rs | 2 + codex-rs/core/src/sandboxing/mod.rs | 4 ++ codex-rs/core/src/sandboxing/mod_tests.rs | 3 + codex-rs/core/src/tasks/user_shell.rs | 1 + .../core/src/tools/handlers/apply_patch.rs | 2 + codex-rs/core/src/tools/handlers/shell.rs | 3 + codex-rs/core/src/tools/js_repl/mod.rs | 1 + codex-rs/core/src/tools/network_approval.rs | 49 +++++++++++--- .../core/src/tools/network_approval_tests.rs | 67 +++++++++++++++++++ codex-rs/core/src/tools/orchestrator.rs | 6 ++ .../core/src/tools/runtimes/apply_patch.rs | 2 +- codex-rs/core/src/tools/runtimes/shell.rs | 6 +- .../tools/runtimes/shell/unix_escalation.rs | 17 ++++- .../runtimes/shell/unix_escalation_tests.rs | 3 + .../core/src/tools/runtimes/unified_exec.rs | 14 +++- codex-rs/core/src/tools/sandboxing.rs | 3 + .../core/src/unified_exec/process_manager.rs | 1 + codex-rs/core/tests/suite/exec.rs | 1 + .../linux-sandbox/tests/suite/landlock.rs | 2 + codex-rs/network-proxy/src/http_proxy.rs | 21 ++++++ codex-rs/network-proxy/src/lib.rs | 1 + codex-rs/network-proxy/src/mitm.rs | 2 + codex-rs/network-proxy/src/network_policy.rs | 9 +++ codex-rs/network-proxy/src/owner_identity.rs | 38 +++++++++++ codex-rs/network-proxy/src/proxy.rs | 47 ++++++++++++- codex-rs/network-proxy/src/runtime.rs | 8 +++ codex-rs/network-proxy/src/socks5.rs | 18 ++++- 33 files changed, 331 insertions(+), 24 deletions(-) create mode 100644 codex-rs/network-proxy/src/owner_identity.rs diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 37ff0ca4f2..41946890f6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1693,6 +1693,7 @@ impl CodexMessageProcessor { network: started_network_proxy .as_ref() .map(codex_core::config::StartedNetworkProxy::proxy), + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level, windows_sandbox_private_desktop: self diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index e1a9cb3def..1e2bcc7ef4 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -732,6 +732,7 @@ mod tests { cwd: PathBuf::from("."), env: HashMap::new(), network: None, + network_owner_id: None, expiration: ExecExpiration::DefaultTimeout, capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool, sandbox: SandboxType::WindowsRestrictedToken, @@ -845,6 +846,7 @@ mod tests { cwd: PathBuf::from("."), env: HashMap::new(), network: None, + network_owner_id: None, expiration: ExecExpiration::Cancellation(CancellationToken::new()), capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool, sandbox: SandboxType::None, diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 5d6769e0ea..21c6aec33e 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -5033,6 +5033,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + network_owner_id: None, sandbox_permissions, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -5051,6 +5052,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + network_owner_id: None, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context .config diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index af0fccc9ac..8eea077dcd 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -128,6 +128,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 9be0518fa0..ce53baf98f 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -81,6 +81,7 @@ pub struct ExecParams { pub capture_policy: ExecCapturePolicy, pub env: HashMap, pub network: Option, + pub network_owner_id: Option, pub sandbox_permissions: SandboxPermissions, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, @@ -263,6 +264,7 @@ pub fn build_exec_request( expiration, capture_policy, network, + network_owner_id, sandbox_permissions, windows_sandbox_level, windows_sandbox_private_desktop, @@ -270,7 +272,7 @@ pub fn build_exec_request( arg0: _, } = params; if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref()); } let (program, args) = command.split_first().ok_or_else(|| { CodexErr::Io(io::Error::new( @@ -301,6 +303,7 @@ pub fn build_exec_request( sandbox: sandbox_type, enforce_managed_network, network: network.as_ref(), + network_owner_id: network_owner_id.as_deref(), sandbox_policy_cwd: sandbox_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, @@ -324,6 +327,7 @@ pub(crate) async fn execute_exec_request( cwd, env, network, + network_owner_id, expiration, capture_policy, sandbox, @@ -345,6 +349,7 @@ pub(crate) async fn execute_exec_request( capture_policy, env, network: network.clone(), + network_owner_id, sandbox_permissions, windows_sandbox_level, windows_sandbox_private_desktop, @@ -448,6 +453,7 @@ async fn exec_windows_sandbox( cwd, mut env, network, + network_owner_id, expiration, capture_policy, windows_sandbox_level, @@ -455,7 +461,7 @@ async fn exec_windows_sandbox( .. } = params; if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref()); } // TODO(iceweasel-oai): run_windows_sandbox_capture should support all @@ -838,6 +844,7 @@ async fn exec( cwd, mut env, network, + network_owner_id, arg0, expiration, capture_policy, @@ -845,7 +852,7 @@ async fn exec( .. } = params; if let Some(network) = network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref()); } let (program, args) = command.split_first().ok_or_else(|| { diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index fc312ec88e..ca0d0c5ef9 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -259,6 +259,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> { capture_policy: ExecCapturePolicy::FullBuffer, env, network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -298,6 +299,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -348,6 +350,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result capture_policy: ExecCapturePolicy::FullBuffer, env: std::env::vars().collect(), network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -588,6 +591,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> capture_policy: ExecCapturePolicy::ShellTool, env, network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -646,6 +650,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { capture_policy: ExecCapturePolicy::ShellTool, env, network: None, + network_owner_id: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/network_policy_decision_tests.rs b/codex-rs/core/src/network_policy_decision_tests.rs index ebb17f724f..404dca6cbd 100644 --- a/codex-rs/core/src/network_policy_decision_tests.rs +++ b/codex-rs/core/src/network_policy_decision_tests.rs @@ -156,6 +156,7 @@ fn denied_network_policy_message_requires_deny_decision() { let blocked = BlockedRequest { host: "example.com".to_string(), reason: "not_allowed".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, @@ -173,6 +174,7 @@ fn denied_network_policy_message_for_denylist_block_is_explicit() { let blocked = BlockedRequest { host: "example.com".to_string(), reason: "denied".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 277ff2b241..81246c0bc6 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -68,6 +68,7 @@ pub struct ExecRequest { pub cwd: PathBuf, pub env: HashMap, pub network: Option, + pub network_owner_id: Option, pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, pub sandbox: SandboxType, @@ -94,6 +95,7 @@ pub(crate) struct SandboxTransformRequest<'a> { // TODO(viyatb): Evaluate switching this to Option> // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, + pub network_owner_id: Option<&'a str>, pub sandbox_policy_cwd: &'a Path, #[cfg(target_os = "macos")] pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, @@ -592,6 +594,7 @@ impl SandboxManager { sandbox, enforce_managed_network, network, + network_owner_id, sandbox_policy_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, @@ -709,6 +712,7 @@ impl SandboxManager { cwd: spec.cwd, env, network: network.cloned(), + network_owner_id: network_owner_id.map(ToString::to_string), expiration: spec.expiration, capture_policy: spec.capture_policy, sandbox, diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 9a7a34e49d..f93108abc9 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -171,6 +171,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() sandbox: SandboxType::None, enforce_managed_network: false, network: None, + network_owner_id: None, sandbox_policy_cwd: cwd.as_path(), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, @@ -541,6 +542,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { sandbox: SandboxType::None, enforce_managed_network: false, network: None, + network_owner_id: None, sandbox_policy_cwd: cwd.as_path(), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, @@ -615,6 +617,7 @@ fn transform_additional_permissions_preserves_denied_entries() { sandbox: SandboxType::None, enforce_managed_network: false, network: None, + network_owner_id: None, sandbox_policy_cwd: cwd.as_path(), #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 6b42be3cef..f049f062ed 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -163,6 +163,7 @@ pub(crate) async fn execute_user_shell_command( Some(session.conversation_id), ), network: turn_context.network.clone(), + network_owner_id: None, // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we // should use that instead of an "arbitrarily large" timeout here. expiration: USER_SHELL_TIMEOUT_MS.into(), diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 1d799da7e7..35ebcea37b 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -215,6 +215,7 @@ impl ToolHandler for ApplyPatchHandler { session: session.clone(), turn: turn.clone(), call_id: call_id.clone(), + network_approval_owner_id: None, tool_name: tool_name.to_string(), }; let out = orchestrator @@ -319,6 +320,7 @@ pub(crate) async fn intercept_apply_patch( session: session.clone(), turn: turn.clone(), call_id: call_id.to_string(), + network_approval_owner_id: None, tool_name: tool_name.to_string(), }; let out = orchestrator diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 446ca100b7..14bf8717a6 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -74,6 +74,7 @@ impl ShellHandler { capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), + network_owner_id: None, sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -129,6 +130,7 @@ impl ShellCommandHandler { capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), + network_owner_id: None, sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -474,6 +476,7 @@ impl ShellHandler { session: session.clone(), turn: turn.clone(), call_id: call_id.clone(), + network_approval_owner_id: None, tool_name, }; let out = orchestrator diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 4f7c3d7436..7c15a87f18 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1067,6 +1067,7 @@ impl JsReplManager { sandbox: sandbox_type, enforce_managed_network: has_managed_network_requirements, network: None, + network_owner_id: None, sandbox_policy_cwd: &turn.cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 9e92a6b00f..a42652a560 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -64,6 +64,10 @@ impl ActiveNetworkApproval { self.mode } + pub(crate) fn owner_id(&self) -> Option<&str> { + self.registration_id.as_deref() + } + pub(crate) fn into_deferred(self) -> Option { match (self.mode, self.registration_id) { (NetworkApprovalMode::Deferred, Some(registration_id)) => { @@ -213,6 +217,18 @@ impl NetworkApprovalService { call_outcomes.remove(registration_id); } + async fn resolve_owner_call( + &self, + network_owner_id: Option<&str>, + ) -> Option> { + if let Some(network_owner_id) = network_owner_id { + let active_calls = self.active_calls.lock().await; + return active_calls.get(network_owner_id).cloned(); + } + + self.resolve_single_active_call().await + } + async fn resolve_single_active_call(&self) -> Option> { let active_calls = self.active_calls.lock().await; if active_calls.len() == 1 { @@ -236,8 +252,12 @@ impl NetworkApprovalService { (created, true) } - async fn record_outcome_for_single_active_call(&self, outcome: NetworkApprovalOutcome) { - let Some(owner_call) = self.resolve_single_active_call().await else { + async fn record_outcome_for_owner_call( + &self, + network_owner_id: Option<&str>, + outcome: NetworkApprovalOutcome, + ) { + let Some(owner_call) = self.resolve_owner_call(network_owner_id).await else { return; }; self.record_call_outcome(&owner_call.registration_id, outcome) @@ -265,8 +285,11 @@ impl NetworkApprovalService { return; }; - self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(message)) - .await; + self.record_outcome_for_owner_call( + blocked.network_owner_id.as_deref(), + NetworkApprovalOutcome::DeniedByPolicy(message), + ) + .await; } async fn active_turn_context(session: &Session) -> Option> { @@ -328,9 +351,10 @@ impl NetworkApprovalService { pending.set_decision(PendingApprovalDecision::Deny).await; let mut pending_approvals = self.pending_host_approvals.lock().await; pending_approvals.remove(&key); - self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( - policy_denial_message, - )) + self.record_outcome_for_owner_call( + request.network_owner_id.as_deref(), + NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message), + ) .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); }; @@ -338,9 +362,10 @@ impl NetworkApprovalService { pending.set_decision(PendingApprovalDecision::Deny).await; let mut pending_approvals = self.pending_host_approvals.lock().await; pending_approvals.remove(&key); - self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( - policy_denial_message, - )) + self.record_outcome_for_owner_call( + request.network_owner_id.as_deref(), + NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message), + ) .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); } @@ -349,7 +374,9 @@ impl NetworkApprovalService { host: request.host.clone(), protocol, }; - let owner_call = self.resolve_single_active_call().await; + let owner_call = self + .resolve_owner_call(request.network_owner_id.as_deref()) + .await; let approval_decision = if routes_approval_to_guardian(&turn_context) { // TODO(ccunningham): Attach guardian network reviews to the reviewed tool item // lifecycle instead of this temporary standalone network approval id. diff --git a/codex-rs/core/src/tools/network_approval_tests.rs b/codex-rs/core/src/tools/network_approval_tests.rs index ad01a45bbd..6f9100f470 100644 --- a/codex-rs/core/src/tools/network_approval_tests.rs +++ b/codex-rs/core/src/tools/network_approval_tests.rs @@ -180,9 +180,14 @@ fn only_never_policy_disables_network_approval_flow() { } fn denied_blocked_request(host: &str) -> BlockedRequest { + denied_blocked_request_with_owner(host, None) +} + +fn denied_blocked_request_with_owner(host: &str, network_owner_id: Option<&str>) -> BlockedRequest { BlockedRequest::new(BlockedRequestArgs { host: host.to_string(), reason: "not_allowed".to_string(), + network_owner_id: network_owner_id.map(ToString::to_string), client: None, method: None, mode: None, @@ -249,3 +254,65 @@ async fn record_blocked_request_ignores_ambiguous_unattributed_blocked_requests( assert_eq!(service.take_call_outcome("registration-1").await, None); assert_eq!(service.take_call_outcome("registration-2").await, None); } + +#[tokio::test] +async fn resolve_owner_call_uses_network_owner_id_when_multiple_calls_are_active() { + let service = NetworkApprovalService::default(); + service + .register_call( + "registration-1".to_string(), + "turn-1".to_string(), + "command-1".to_string(), + ) + .await; + service + .register_call( + "registration-2".to_string(), + "turn-2".to_string(), + "command-2".to_string(), + ) + .await; + + let owner_call = service + .resolve_owner_call(Some("registration-2")) + .await + .expect("owner call should resolve"); + + assert_eq!(owner_call.registration_id, "registration-2"); + assert_eq!(owner_call.turn_id, "turn-2"); + assert_eq!(owner_call.parent_tool_item_id, "command-2"); +} + +#[tokio::test] +async fn record_blocked_request_uses_network_owner_id_when_multiple_calls_are_active() { + let service = NetworkApprovalService::default(); + service + .register_call( + "registration-1".to_string(), + "turn-1".to_string(), + "command-1".to_string(), + ) + .await; + service + .register_call( + "registration-2".to_string(), + "turn-2".to_string(), + "command-2".to_string(), + ) + .await; + + service + .record_blocked_request(denied_blocked_request_with_owner( + "example.com", + Some("registration-2"), + )) + .await; + + assert_eq!(service.take_call_outcome("registration-1").await, None); + assert_eq!( + service.take_call_outcome("registration-2").await, + Some(NetworkApprovalOutcome::DeniedByPolicy( + "Network access to \"example.com\" was blocked: domain is not on the allowlist for the current sandbox mode.".to_string() + )) + ); +} diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 4b53ac156f..b4250afd87 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -13,6 +13,7 @@ use crate::guardian::GUARDIAN_REJECTION_MESSAGE; use crate::guardian::routes_approval_to_guardian; use crate::network_policy_decision::network_approval_context_from_payload; use crate::sandboxing::SandboxManager; +use crate::tools::network_approval::ActiveNetworkApproval; use crate::tools::network_approval::DeferredNetworkApproval; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::begin_network_approval; @@ -64,11 +65,16 @@ impl ToolOrchestrator { tool.network_approval_spec(req, tool_ctx), ) .await; + let network_approval_owner_id = network_approval + .as_ref() + .and_then(ActiveNetworkApproval::owner_id) + .map(ToString::to_string); let attempt_tool_ctx = ToolCtx { session: tool_ctx.session.clone(), turn: tool_ctx.turn.clone(), call_id: tool_ctx.call_id.clone(), + network_approval_owner_id, tool_name: tool_ctx.tool_name.clone(), }; let run_result = tool.run(req, attempt, &attempt_tool_ctx).await; diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index f1e9912bc5..151b5d06a3 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -208,7 +208,7 @@ impl ToolRuntime for ApplyPatchRuntime { ) -> Result { let spec = Self::build_command_spec(req, &ctx.turn.config.codex_home)?; let env = attempt - .env_for(spec, /*network*/ None) + .env_for(spec, /*network*/ None, /*network_owner_id*/ None) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 6ff8349b53..01f6493a60 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -253,7 +253,11 @@ impl ToolRuntime for ShellRuntime { req.justification.clone(), )?; let env = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 948018dae6..4e8da53a0c 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -117,13 +117,18 @@ pub(super) async fn try_run_zsh_fork( req.justification.clone(), )?; let sandbox_exec_request = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; let crate::sandboxing::ExecRequest { command, cwd: sandbox_cwd, env: sandbox_env, network: sandbox_network, + network_owner_id, expiration: _sandbox_expiration, capture_policy: _capture_policy, sandbox, @@ -153,6 +158,7 @@ pub(super) async fn try_run_zsh_fork( sandbox, env: sandbox_env, network: sandbox_network, + network_owner_id, windows_sandbox_level, sandbox_permissions, justification, @@ -259,6 +265,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( sandbox: exec_request.sandbox, env: exec_request.env.clone(), network: exec_request.network.clone(), + network_owner_id: exec_request.network_owner_id.clone(), windows_sandbox_level: exec_request.windows_sandbox_level, sandbox_permissions: exec_request.sandbox_permissions, justification: exec_request.justification.clone(), @@ -856,6 +863,7 @@ struct CoreShellCommandExecutor { sandbox: SandboxType, env: HashMap, network: Option, + network_owner_id: Option, windows_sandbox_level: WindowsSandboxLevel, sandbox_permissions: SandboxPermissions, justification: Option, @@ -904,6 +912,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { cwd: self.cwd.clone(), env: exec_env, network: self.network.clone(), + network_owner_id: self.network_owner_id.clone(), expiration: ExecExpiration::Cancellation(cancel_rx), capture_policy: ExecCapturePolicy::ShellTool, sandbox: self.sandbox, @@ -1060,6 +1069,7 @@ impl CoreShellCommandExecutor { sandbox, enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), + network_owner_id: self.network_owner_id.as_deref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, @@ -1069,7 +1079,10 @@ impl CoreShellCommandExecutor { windows_sandbox_private_desktop: false, })?; if let Some(network) = exec_request.network.as_ref() { - network.apply_to_env(&mut exec_request.env); + network.apply_to_env_for_owner( + &mut exec_request.env, + exec_request.network_owner_id.as_deref(), + ); } Ok(PreparedExec { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 37c1b53a13..ec26724af4 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -655,6 +655,7 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, + network_owner_id: None, sandbox: SandboxType::None, sandbox_policy: SandboxPolicy::new_read_only_policy(), file_system_sandbox_policy: read_only_file_system_sandbox_policy(), @@ -707,6 +708,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, + network_owner_id: None, sandbox: SandboxType::None, sandbox_policy: SandboxPolicy::DangerFullAccess, file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), @@ -782,6 +784,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac cwd: cwd.to_path_buf(), env: HashMap::new(), network: None, + network_owner_id: None, sandbox: SandboxType::None, sandbox_policy: sandbox_policy.clone(), file_system_sandbox_policy: read_only_file_system_sandbox_policy(), diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 49ef0e6385..b17f9c7dde 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -207,7 +207,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt let mut env = req.env.clone(); if let Some(network) = req.network.as_ref() { - network.apply_to_env(&mut env); + network.apply_to_env_for_owner(&mut env, ctx.network_approval_owner_id.as_deref()); } if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { let spec = build_command_spec( @@ -221,7 +221,11 @@ impl<'a> ToolRuntime for UnifiedExecRunt ) .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; let exec_env = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; match zsh_fork_backend::maybe_prepare_unified_exec( req, @@ -269,7 +273,11 @@ impl<'a> ToolRuntime for UnifiedExecRunt ) .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; let exec_env = attempt - .env_for(spec, req.network.as_ref()) + .env_for( + spec, + req.network.as_ref(), + ctx.network_approval_owner_id.as_deref(), + ) .map_err(|err| ToolError::Codex(err.into()))?; self.manager .open_session_with_exec_env(&exec_env, req.tty, Box::new(NoopSpawnLifecycle)) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index bf386a12d0..3251f83302 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -300,6 +300,7 @@ pub(crate) struct ToolCtx { pub session: Arc, pub turn: Arc, pub call_id: String, + pub network_approval_owner_id: Option, pub tool_name: String, } @@ -341,6 +342,7 @@ impl<'a> SandboxAttempt<'a> { &self, spec: CommandSpec, network: Option<&NetworkProxy>, + network_owner_id: Option<&str>, ) -> Result { self.manager .transform(crate::sandboxing::SandboxTransformRequest { @@ -351,6 +353,7 @@ impl<'a> SandboxAttempt<'a> { sandbox: self.sandbox, enforce_managed_network: self.enforce_managed_network, network, + network_owner_id, sandbox_policy_cwd: self.sandbox_cwd, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 52d668c000..0ea2e653e2 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -627,6 +627,7 @@ impl UnifiedExecProcessManager { session: context.session.clone(), turn: context.turn.clone(), call_id: context.call_id.clone(), + network_approval_owner_id: None, tool_name: "exec_command".to_string(), }; orchestrator diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 069e824ee5..22b19c71b7 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result(req: &Request) -> Option { + req.headers() + .typed_get::>() + .map(|header| header.0.username().to_string()) +} + async fn http_connect_accept( policy_decider: Option>, mut req: Request, @@ -173,6 +181,7 @@ async fn http_connect_accept( } let client = client_addr(&req); + let network_owner_id = proxy_authorization_network_owner_id(&req); let enabled = app_state .enabled() .await @@ -185,6 +194,7 @@ async fn http_connect_accept( host, authority.port, client_addr(&req), + network_owner_id, Some("CONNECT".to_string()), NetworkProtocol::HttpsConnect, /*audit_endpoint_override*/ None, @@ -196,6 +206,7 @@ async fn http_connect_accept( protocol: NetworkProtocol::HttpsConnect, host: host.clone(), port: authority.port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: Some("CONNECT".to_string()), command: None, @@ -220,6 +231,7 @@ async fn http_connect_accept( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id, client: client.clone(), method: Some("CONNECT".to_string()), mode: None, @@ -283,6 +295,7 @@ async fn http_connect_accept( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_MITM_REQUIRED.to_string(), + network_owner_id: None, client: client.clone(), method: Some("CONNECT".to_string()), mode: Some(NetworkMode::Limited), @@ -432,6 +445,7 @@ async fn http_plain_proxy( } }; let client = client_addr(&req); + let network_owner_id = proxy_authorization_network_owner_id(&req); let method_allowed = match app_state .method_allowed(req.method().as_str()) .await @@ -471,6 +485,7 @@ async fn http_plain_proxy( socket_path, /*port*/ 0, client_addr(&req), + network_owner_id.clone(), Some(req.method().as_str().to_string()), NetworkProtocol::Http, Some(("unix-socket", 0)), @@ -616,6 +631,7 @@ async fn http_plain_proxy( host, port, client_addr(&req), + network_owner_id.clone(), Some(req.method().as_str().to_string()), NetworkProtocol::Http, /*audit_endpoint_override*/ None, @@ -627,6 +643,7 @@ async fn http_plain_proxy( protocol: NetworkProtocol::Http, host: host.clone(), port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: Some(req.method().as_str().to_string()), command: None, @@ -651,6 +668,7 @@ async fn http_plain_proxy( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: Some(req.method().as_str().to_string()), mode: None, @@ -696,6 +714,7 @@ async fn http_plain_proxy( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id, client: client.clone(), method: Some(req.method().as_str().to_string()), mode: Some(NetworkMode::Limited), @@ -889,6 +908,7 @@ async fn proxy_disabled_response( host: String, port: u16, client: Option, + network_owner_id: Option, method: Option, protocol: NetworkProtocol, audit_endpoint_override: Option<(&str, u16)>, @@ -913,6 +933,7 @@ async fn proxy_disabled_response( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: blocked_host, reason: REASON_PROXY_DISABLED.to_string(), + network_owner_id, client, method, mode: None, diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 1093a14aa4..f896bf72e3 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -5,6 +5,7 @@ mod config; mod http_proxy; mod mitm; mod network_policy; +mod owner_identity; mod policy; mod proxy; mod reasons; diff --git a/codex-rs/network-proxy/src/mitm.rs b/codex-rs/network-proxy/src/mitm.rs index 8f20afaa3d..5b37e1207c 100644 --- a/codex-rs/network-proxy/src/mitm.rs +++ b/codex-rs/network-proxy/src/mitm.rs @@ -289,6 +289,7 @@ async fn mitm_blocking_response( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: policy.target_host.clone(), reason: reason.to_string(), + network_owner_id: None, client: client.clone(), method: Some(method.clone()), mode: Some(policy.mode), @@ -311,6 +312,7 @@ async fn mitm_blocking_response( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: policy.target_host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id: None, client: client.clone(), method: Some(method.clone()), mode: Some(policy.mode), diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index e759e75c0d..0aadc10bfd 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -79,6 +79,7 @@ pub struct NetworkPolicyRequest { pub protocol: NetworkProtocol, pub host: String, pub port: u16, + pub network_owner_id: Option, pub client_addr: Option, pub method: Option, pub command: Option, @@ -89,6 +90,7 @@ pub struct NetworkPolicyRequestArgs { pub protocol: NetworkProtocol, pub host: String, pub port: u16, + pub network_owner_id: Option, pub client_addr: Option, pub method: Option, pub command: Option, @@ -101,6 +103,7 @@ impl NetworkPolicyRequest { protocol, host, port, + network_owner_id, client_addr, method, command, @@ -110,6 +113,7 @@ impl NetworkPolicyRequest { protocol, host, port, + network_owner_id, client_addr, method, command, @@ -625,6 +629,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "example.com".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: None, command: None, @@ -685,6 +690,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "blocked.com".to_string(), port: 80, + network_owner_id: None, client_addr: Some("127.0.0.1:1234".to_string()), method: Some("GET".to_string()), command: None, @@ -726,6 +732,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "example.com".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: Some("GET".to_string()), command: None, @@ -776,6 +783,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "example.com".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: Some("GET".to_string()), command: None, @@ -859,6 +867,7 @@ mod tests { protocol: NetworkProtocol::Http, host: "127.0.0.1".to_string(), port: 80, + network_owner_id: None, client_addr: None, method: Some("GET".to_string()), command: None, diff --git a/codex-rs/network-proxy/src/owner_identity.rs b/codex-rs/network-proxy/src/owner_identity.rs new file mode 100644 index 0000000000..3c898ec1d2 --- /dev/null +++ b/codex-rs/network-proxy/src/owner_identity.rs @@ -0,0 +1,38 @@ +use rama_core::extensions::Extensions; +use rama_core::extensions::ExtensionsRef; +use rama_http::headers::authorization::AuthoritySync; +use rama_net::user::Basic; +use rama_net::user::UserId; +use rama_net::user::authority::AuthorizeResult; +use rama_net::user::authority::Authorizer; + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct ProxyOwnerAuthorizer; + +impl AuthoritySync for ProxyOwnerAuthorizer { + fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool { + ext.insert(UserId::Username(credentials.username().to_string())); + true + } +} + +impl Authorizer for ProxyOwnerAuthorizer { + type Error = std::convert::Infallible; + + async fn authorize(&self, credentials: Basic) -> AuthorizeResult { + let mut extensions = Extensions::new(); + extensions.insert(UserId::Username(credentials.username().to_string())); + AuthorizeResult { + credentials, + result: Ok(Some(extensions)), + } + } +} + +pub(crate) fn network_owner_id(input: &T) -> Option { + match input.extensions().get::() { + Some(UserId::Username(username)) => Some(username.clone()), + Some(UserId::Token(token)) => String::from_utf8(token.clone()).ok(), + Some(UserId::Anonymous) | None => None, + } +} diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 8f596f6842..8cf281d27f 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -305,15 +305,23 @@ fn set_env_keys(env: &mut HashMap, keys: &[&str], value: &str) { } } +fn proxy_url(scheme: &str, addr: SocketAddr, network_owner_id: Option<&str>) -> String { + match network_owner_id { + Some(network_owner_id) => format!("{scheme}://{network_owner_id}@{addr}"), + None => format!("{scheme}://{addr}"), + } +} + fn apply_proxy_env_overrides( env: &mut HashMap, http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, allow_local_binding: bool, + network_owner_id: Option<&str>, ) { - let http_proxy_url = format!("http://{http_addr}"); - let socks_proxy_url = format!("socks5h://{socks_addr}"); + let http_proxy_url = proxy_url("http", http_addr, network_owner_id); + let socks_proxy_url = proxy_url("socks5h", socks_addr, network_owner_id); env.insert( ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), if allow_local_binding { @@ -414,6 +422,14 @@ impl NetworkProxy { } pub fn apply_to_env(&self, env: &mut HashMap) { + self.apply_to_env_for_owner(env, /*network_owner_id*/ None); + } + + pub fn apply_to_env_for_owner( + &self, + env: &mut HashMap, + network_owner_id: Option<&str>, + ) { // Enforce proxying for child processes. We intentionally override existing values so // command-level environment cannot bypass the managed proxy endpoint. apply_proxy_env_overrides( @@ -422,6 +438,7 @@ impl NetworkProxy { self.socks_addr, self.socks_enabled, self.allow_local_binding, + network_owner_id, ); } @@ -696,6 +713,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, false, + /*network_owner_id*/ None, ); assert_eq!( @@ -746,6 +764,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), false, true, + /*network_owner_id*/ None, ); assert_eq!( @@ -764,6 +783,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, false, + /*network_owner_id*/ None, ); assert_eq!( @@ -809,6 +829,7 @@ mod tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), true, false, + /*network_owner_id*/ None, ); assert_eq!( @@ -816,4 +837,26 @@ mod tests { Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string()) ); } + + #[test] + fn apply_proxy_env_overrides_includes_owner_credentials() { + let mut env = HashMap::new(); + apply_proxy_env_overrides( + &mut env, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), + true, + false, + Some("owner-1"), + ); + + assert_eq!( + env.get("HTTP_PROXY"), + Some(&"http://owner-1@127.0.0.1:3128".to_string()) + ); + assert_eq!( + env.get("ALL_PROXY"), + Some(&"socks5h://owner-1@127.0.0.1:8081".to_string()) + ); + } } diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index b634a8630e..d51729aed8 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -84,6 +84,8 @@ pub enum HostBlockDecision { pub struct BlockedRequest { pub host: String, pub reason: String, + #[serde(skip_serializing)] + pub network_owner_id: Option, pub client: Option, pub method: Option, pub mode: Option, @@ -100,6 +102,7 @@ pub struct BlockedRequest { pub struct BlockedRequestArgs { pub host: String, pub reason: String, + pub network_owner_id: Option, pub client: Option, pub method: Option, pub mode: Option, @@ -114,6 +117,7 @@ impl BlockedRequest { let BlockedRequestArgs { host, reason, + network_owner_id, client, method, mode, @@ -125,6 +129,7 @@ impl BlockedRequest { Self { host, reason, + network_owner_id, client, method, mode, @@ -994,6 +999,7 @@ mod tests { .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: "google.com".to_string(), reason: "not_allowed".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, @@ -1034,6 +1040,7 @@ mod tests { .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: format!("example{idx}.com"), reason: "not_allowed".to_string(), + network_owner_id: None, client: None, method: Some("GET".to_string()), mode: None, @@ -1056,6 +1063,7 @@ mod tests { let entry = BlockedRequest { host: "google.com".to_string(), reason: "not_allowed".to_string(), + network_owner_id: None, client: Some("127.0.0.1".to_string()), method: Some("GET".to_string()), mode: Some(NetworkMode::Full), diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index 30ec4d2903..16b4f653ae 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -9,6 +9,8 @@ use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; use crate::network_policy::emit_block_decision_audit_event; use crate::network_policy::evaluate_host_policy; +use crate::owner_identity::ProxyOwnerAuthorizer; +use crate::owner_identity::network_owner_id as extract_network_owner_id; use crate::policy::normalize_host; use crate::reasons::REASON_METHOD_NOT_ALLOWED; use crate::reasons::REASON_PROXY_DISABLED; @@ -105,7 +107,11 @@ async fn run_socks5_with_listener( }); let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector); - let base = Socks5Acceptor::new().with_connector(socks_connector); + let mut base = Socks5Acceptor::new(); + base.set_auth_optional(true); + let base = base + .with_connector(socks_connector) + .with_authorizer(ProxyOwnerAuthorizer); if enable_socks5_udp { let udp_state = state.clone(); @@ -150,6 +156,7 @@ async fn handle_socks5_tcp( .extensions() .get::() .map(|info| info.peer_addr().to_string()); + let network_owner_id = extract_network_owner_id(&req); match app_state.enabled().await { Ok(true) => {} @@ -175,6 +182,7 @@ async fn handle_socks5_tcp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_PROXY_DISABLED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: None, @@ -217,6 +225,7 @@ async fn handle_socks5_tcp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: Some(NetworkMode::Limited), @@ -243,6 +252,7 @@ async fn handle_socks5_tcp( protocol: NetworkProtocol::Socks5Tcp, host: host.clone(), port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: None, command: None, @@ -267,6 +277,7 @@ async fn handle_socks5_tcp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id, client: client.clone(), method: None, mode: None, @@ -314,6 +325,7 @@ async fn inspect_socks5_udp( let client = extensions .get::() .map(|info| info.peer_addr().to_string()); + let network_owner_id = extract_network_owner_id(&extensions); match state.enabled().await { Ok(true) => {} @@ -339,6 +351,7 @@ async fn inspect_socks5_udp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_PROXY_DISABLED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: None, @@ -381,6 +394,7 @@ async fn inspect_socks5_udp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: REASON_METHOD_NOT_ALLOWED.to_string(), + network_owner_id: network_owner_id.clone(), client: client.clone(), method: None, mode: Some(NetworkMode::Limited), @@ -403,6 +417,7 @@ async fn inspect_socks5_udp( protocol: NetworkProtocol::Socks5Udp, host: host.clone(), port, + network_owner_id: network_owner_id.clone(), client_addr: client.clone(), method: None, command: None, @@ -427,6 +442,7 @@ async fn inspect_socks5_udp( .record_blocked(BlockedRequest::new(BlockedRequestArgs { host: host.clone(), reason: reason.clone(), + network_owner_id, client: client.clone(), method: None, mode: None,