mirror of
https://github.com/openai/codex.git
synced 2026-04-22 05:34:49 +00:00
Compare commits
8 Commits
codex-debu
...
cc/network
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949ad9ddfb | ||
|
|
3cee35bfef | ||
|
|
b45bbe8278 | ||
|
|
13489789bf | ||
|
|
28014e514c | ||
|
|
1a0c7660b7 | ||
|
|
f891b16475 | ||
|
|
b84b970653 |
@@ -1693,6 +1693,7 @@ impl CodexMessageProcessor {
|
||||
network: started_network_proxy
|
||||
.as_ref()
|
||||
.map(codex_core::config::StartedNetworkProxy::proxy),
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self
|
||||
|
||||
@@ -732,6 +732,7 @@ mod tests {
|
||||
cwd: PathBuf::from("."),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_id: None,
|
||||
expiration: ExecExpiration::Cancellation(CancellationToken::new()),
|
||||
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::None,
|
||||
|
||||
@@ -5033,6 +5033,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_id: None,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
.config
|
||||
|
||||
@@ -128,6 +128,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
|
||||
@@ -81,6 +81,9 @@ pub struct ExecParams {
|
||||
pub capture_policy: ExecCapturePolicy,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
/// Parent tool item id to propagate through managed proxy settings so
|
||||
/// blocked requests can be attributed back to the originating tool call.
|
||||
pub parent_tool_item_id: Option<String>,
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
@@ -263,6 +266,7 @@ pub fn build_exec_request(
|
||||
expiration,
|
||||
capture_policy,
|
||||
network,
|
||||
parent_tool_item_id,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
@@ -270,7 +274,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_parent_tool_item(&mut env, parent_tool_item_id.as_deref());
|
||||
}
|
||||
let (program, args) = command.split_first().ok_or_else(|| {
|
||||
CodexErr::Io(io::Error::new(
|
||||
@@ -301,6 +305,7 @@ pub fn build_exec_request(
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
parent_tool_item_id: parent_tool_item_id.as_deref(),
|
||||
sandbox_policy_cwd: sandbox_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -324,6 +329,7 @@ pub(crate) async fn execute_exec_request(
|
||||
cwd,
|
||||
env,
|
||||
network,
|
||||
parent_tool_item_id,
|
||||
expiration,
|
||||
capture_policy,
|
||||
sandbox,
|
||||
@@ -345,6 +351,7 @@ pub(crate) async fn execute_exec_request(
|
||||
capture_policy,
|
||||
env,
|
||||
network: network.clone(),
|
||||
parent_tool_item_id,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
@@ -448,6 +455,7 @@ async fn exec_windows_sandbox(
|
||||
cwd,
|
||||
mut env,
|
||||
network,
|
||||
parent_tool_item_id,
|
||||
expiration,
|
||||
capture_policy,
|
||||
windows_sandbox_level,
|
||||
@@ -455,7 +463,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_parent_tool_item(&mut env, parent_tool_item_id.as_deref());
|
||||
}
|
||||
|
||||
// TODO(iceweasel-oai): run_windows_sandbox_capture should support all
|
||||
@@ -838,6 +846,7 @@ async fn exec(
|
||||
cwd,
|
||||
mut env,
|
||||
network,
|
||||
parent_tool_item_id,
|
||||
arg0,
|
||||
expiration,
|
||||
capture_policy,
|
||||
@@ -845,7 +854,7 @@ async fn exec(
|
||||
..
|
||||
} = params;
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
network.apply_to_env_for_parent_tool_item(&mut env, parent_tool_item_id.as_deref());
|
||||
}
|
||||
|
||||
let (program, args) = command.split_first().ok_or_else(|| {
|
||||
|
||||
@@ -259,6 +259,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> {
|
||||
capture_policy: ExecCapturePolicy::FullBuffer,
|
||||
env,
|
||||
network: None,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -47,6 +47,7 @@ pub(crate) enum GuardianApprovalRequest {
|
||||
NetworkAccess {
|
||||
id: String,
|
||||
turn_id: String,
|
||||
parent_tool_item_id: Option<String>,
|
||||
target: String,
|
||||
host: String,
|
||||
protocol: NetworkApprovalProtocol,
|
||||
@@ -252,6 +253,7 @@ pub(crate) fn guardian_approval_request_to_json(
|
||||
host,
|
||||
protocol,
|
||||
port,
|
||||
..
|
||||
} => Ok(serde_json::json!({
|
||||
"tool": "network_access",
|
||||
"target": target,
|
||||
@@ -324,6 +326,7 @@ pub(crate) fn guardian_assessment_action_value(action: &GuardianApprovalRequest)
|
||||
host,
|
||||
protocol,
|
||||
port,
|
||||
..
|
||||
} => serde_json::json!({
|
||||
"tool": "network_access",
|
||||
"target": target,
|
||||
|
||||
@@ -348,6 +348,7 @@ fn guardian_request_turn_id_prefers_network_access_owner_turn() {
|
||||
let network_access = GuardianApprovalRequest::NetworkAccess {
|
||||
id: "network-1".to_string(),
|
||||
turn_id: "owner-turn".to_string(),
|
||||
parent_tool_item_id: Some("command-1".to_string()),
|
||||
target: "https://example.com:443".to_string(),
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
|
||||
@@ -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(),
|
||||
parent_tool_item_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(),
|
||||
parent_tool_item_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
|
||||
@@ -68,6 +68,9 @@ pub struct ExecRequest {
|
||||
pub cwd: PathBuf,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
/// Parent tool item id propagated into child process proxy settings for
|
||||
/// blocked-request attribution.
|
||||
pub parent_tool_item_id: Option<String>,
|
||||
pub expiration: ExecExpiration,
|
||||
pub capture_policy: ExecCapturePolicy,
|
||||
pub sandbox: SandboxType,
|
||||
@@ -94,6 +97,10 @@ pub(crate) struct SandboxTransformRequest<'a> {
|
||||
// TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
|
||||
// to make shared ownership explicit across runtime/sandbox plumbing.
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
/// Parent tool item id for the current tool call. This is threaded into
|
||||
/// managed proxy credentials so the proxy can report blocked requests
|
||||
/// against the exact originating call.
|
||||
pub parent_tool_item_id: Option<&'a str>,
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
||||
@@ -592,6 +599,7 @@ impl SandboxManager {
|
||||
sandbox,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
parent_tool_item_id,
|
||||
sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
@@ -709,6 +717,7 @@ impl SandboxManager {
|
||||
cwd: spec.cwd,
|
||||
env,
|
||||
network: network.cloned(),
|
||||
parent_tool_item_id: parent_tool_item_id.map(ToString::to_string),
|
||||
expiration: spec.expiration,
|
||||
capture_policy: spec.capture_policy,
|
||||
sandbox,
|
||||
|
||||
@@ -171,6 +171,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
||||
@@ -163,6 +163,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
Some(session.conversation_id),
|
||||
),
|
||||
network: turn_context.network.clone(),
|
||||
parent_tool_item_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(),
|
||||
|
||||
@@ -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(),
|
||||
parent_tool_item_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(),
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
|
||||
@@ -1067,6 +1067,7 @@ impl JsReplManager {
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
network: None,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_policy_cwd: &turn.cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
||||
@@ -28,7 +28,6 @@ use tokio::sync::Mutex;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum NetworkApprovalMode {
|
||||
@@ -44,18 +43,18 @@ pub(crate) struct NetworkApprovalSpec {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DeferredNetworkApproval {
|
||||
registration_id: String,
|
||||
parent_tool_item_id: String,
|
||||
}
|
||||
|
||||
impl DeferredNetworkApproval {
|
||||
pub(crate) fn registration_id(&self) -> &str {
|
||||
&self.registration_id
|
||||
pub(crate) fn parent_tool_item_id(&self) -> &str {
|
||||
&self.parent_tool_item_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ActiveNetworkApproval {
|
||||
registration_id: Option<String>,
|
||||
parent_tool_item_id: Option<String>,
|
||||
mode: NetworkApprovalMode,
|
||||
}
|
||||
|
||||
@@ -65,9 +64,11 @@ impl ActiveNetworkApproval {
|
||||
}
|
||||
|
||||
pub(crate) fn into_deferred(self) -> Option<DeferredNetworkApproval> {
|
||||
match (self.mode, self.registration_id) {
|
||||
(NetworkApprovalMode::Deferred, Some(registration_id)) => {
|
||||
Some(DeferredNetworkApproval { registration_id })
|
||||
match (self.mode, self.parent_tool_item_id) {
|
||||
(NetworkApprovalMode::Deferred, Some(parent_tool_item_id)) => {
|
||||
Some(DeferredNetworkApproval {
|
||||
parent_tool_item_id,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -160,8 +161,8 @@ impl PendingHostApproval {
|
||||
}
|
||||
|
||||
struct ActiveNetworkApprovalCall {
|
||||
registration_id: String,
|
||||
turn_id: String,
|
||||
parent_tool_item_id: String,
|
||||
}
|
||||
|
||||
pub(crate) struct NetworkApprovalService {
|
||||
@@ -194,23 +195,35 @@ impl NetworkApprovalService {
|
||||
other_approved_hosts.extend(approved_hosts.iter().cloned());
|
||||
}
|
||||
|
||||
async fn register_call(&self, registration_id: String, turn_id: String) {
|
||||
async fn register_call(&self, turn_id: String, parent_tool_item_id: String) {
|
||||
let mut active_calls = self.active_calls.lock().await;
|
||||
let key = registration_id.clone();
|
||||
let key = parent_tool_item_id.clone();
|
||||
active_calls.insert(
|
||||
key,
|
||||
Arc::new(ActiveNetworkApprovalCall {
|
||||
registration_id,
|
||||
turn_id,
|
||||
parent_tool_item_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn unregister_call(&self, registration_id: &str) {
|
||||
pub(crate) async fn unregister_call(&self, parent_tool_item_id: &str) {
|
||||
let mut active_calls = self.active_calls.lock().await;
|
||||
active_calls.shift_remove(registration_id);
|
||||
active_calls.shift_remove(parent_tool_item_id);
|
||||
let mut call_outcomes = self.call_outcomes.lock().await;
|
||||
call_outcomes.remove(registration_id);
|
||||
call_outcomes.remove(parent_tool_item_id);
|
||||
}
|
||||
|
||||
async fn resolve_active_call(
|
||||
&self,
|
||||
parent_tool_item_id: Option<&str>,
|
||||
) -> Option<Arc<ActiveNetworkApprovalCall>> {
|
||||
if let Some(parent_tool_item_id) = parent_tool_item_id {
|
||||
let active_calls = self.active_calls.lock().await;
|
||||
return active_calls.get(parent_tool_item_id).cloned();
|
||||
}
|
||||
|
||||
self.resolve_single_active_call().await
|
||||
}
|
||||
|
||||
async fn resolve_single_active_call(&self) -> Option<Arc<ActiveNetworkApprovalCall>> {
|
||||
@@ -236,28 +249,36 @@ 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_active_call(
|
||||
&self,
|
||||
parent_tool_item_id: Option<&str>,
|
||||
outcome: NetworkApprovalOutcome,
|
||||
) {
|
||||
let Some(active_call) = self.resolve_active_call(parent_tool_item_id).await else {
|
||||
return;
|
||||
};
|
||||
self.record_call_outcome(&owner_call.registration_id, outcome)
|
||||
self.record_call_outcome(&active_call.parent_tool_item_id, outcome)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn take_call_outcome(&self, registration_id: &str) -> Option<NetworkApprovalOutcome> {
|
||||
async fn take_call_outcome(&self, parent_tool_item_id: &str) -> Option<NetworkApprovalOutcome> {
|
||||
let mut call_outcomes = self.call_outcomes.lock().await;
|
||||
call_outcomes.remove(registration_id)
|
||||
call_outcomes.remove(parent_tool_item_id)
|
||||
}
|
||||
|
||||
async fn record_call_outcome(&self, registration_id: &str, outcome: NetworkApprovalOutcome) {
|
||||
async fn record_call_outcome(
|
||||
&self,
|
||||
parent_tool_item_id: &str,
|
||||
outcome: NetworkApprovalOutcome,
|
||||
) {
|
||||
let mut call_outcomes = self.call_outcomes.lock().await;
|
||||
if matches!(
|
||||
call_outcomes.get(registration_id),
|
||||
call_outcomes.get(parent_tool_item_id),
|
||||
Some(NetworkApprovalOutcome::DeniedByUser)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
call_outcomes.insert(registration_id.to_string(), outcome);
|
||||
call_outcomes.insert(parent_tool_item_id.to_string(), outcome);
|
||||
}
|
||||
|
||||
pub(crate) async fn record_blocked_request(&self, blocked: BlockedRequest) {
|
||||
@@ -265,8 +286,11 @@ impl NetworkApprovalService {
|
||||
return;
|
||||
};
|
||||
|
||||
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(message))
|
||||
.await;
|
||||
self.record_outcome_for_active_call(
|
||||
blocked.parent_tool_item_id.as_deref(),
|
||||
NetworkApprovalOutcome::DeniedByPolicy(message),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn active_turn_context(session: &Session) -> Option<Arc<crate::codex::TurnContext>> {
|
||||
@@ -328,9 +352,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_active_call(
|
||||
request.parent_tool_item_id.as_deref(),
|
||||
NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message),
|
||||
)
|
||||
.await;
|
||||
return NetworkDecision::deny(REASON_NOT_ALLOWED);
|
||||
};
|
||||
@@ -338,9 +363,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_active_call(
|
||||
request.parent_tool_item_id.as_deref(),
|
||||
NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message),
|
||||
)
|
||||
.await;
|
||||
return NetworkDecision::deny(REASON_NOT_ALLOWED);
|
||||
}
|
||||
@@ -349,18 +375,23 @@ impl NetworkApprovalService {
|
||||
host: request.host.clone(),
|
||||
protocol,
|
||||
};
|
||||
let owner_call = self.resolve_single_active_call().await;
|
||||
let active_call = self
|
||||
.resolve_active_call(request.parent_tool_item_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.
|
||||
// lifecycle instead of this temporary standalone network review id.
|
||||
review_approval_request(
|
||||
&session,
|
||||
&turn_context,
|
||||
GuardianApprovalRequest::NetworkAccess {
|
||||
id: Self::approval_id_for_key(&key),
|
||||
turn_id: owner_call
|
||||
turn_id: active_call
|
||||
.as_ref()
|
||||
.map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()),
|
||||
parent_tool_item_id: active_call
|
||||
.as_ref()
|
||||
.map(|call| call.parent_tool_item_id.clone()),
|
||||
target,
|
||||
host: request.host,
|
||||
protocol,
|
||||
@@ -457,9 +488,9 @@ impl NetworkApprovalService {
|
||||
.await;
|
||||
}
|
||||
}
|
||||
if let Some(owner_call) = owner_call.as_ref() {
|
||||
if let Some(active_call) = active_call.as_ref() {
|
||||
self.record_call_outcome(
|
||||
&owner_call.registration_id,
|
||||
&active_call.parent_tool_item_id,
|
||||
NetworkApprovalOutcome::DeniedByUser,
|
||||
)
|
||||
.await;
|
||||
@@ -470,18 +501,18 @@ impl NetworkApprovalService {
|
||||
},
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
if routes_approval_to_guardian(&turn_context) {
|
||||
if let Some(owner_call) = owner_call.as_ref() {
|
||||
if let Some(active_call) = active_call.as_ref() {
|
||||
self.record_call_outcome(
|
||||
&owner_call.registration_id,
|
||||
&active_call.parent_tool_item_id,
|
||||
NetworkApprovalOutcome::DeniedByPolicy(
|
||||
GUARDIAN_REJECTION_MESSAGE.to_string(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if let Some(owner_call) = owner_call.as_ref() {
|
||||
} else if let Some(active_call) = active_call.as_ref() {
|
||||
self.record_call_outcome(
|
||||
&owner_call.registration_id,
|
||||
&active_call.parent_tool_item_id,
|
||||
NetworkApprovalOutcome::DeniedByUser,
|
||||
)
|
||||
.await;
|
||||
@@ -548,6 +579,7 @@ pub(crate) fn build_network_policy_decider(
|
||||
pub(crate) async fn begin_network_approval(
|
||||
session: &Session,
|
||||
turn_id: &str,
|
||||
parent_tool_item_id: &str,
|
||||
has_managed_network_requirements: bool,
|
||||
spec: Option<NetworkApprovalSpec>,
|
||||
) -> Option<ActiveNetworkApproval> {
|
||||
@@ -556,15 +588,14 @@ pub(crate) async fn begin_network_approval(
|
||||
return None;
|
||||
}
|
||||
|
||||
let registration_id = Uuid::new_v4().to_string();
|
||||
session
|
||||
.services
|
||||
.network_approval
|
||||
.register_call(registration_id.clone(), turn_id.to_string())
|
||||
.register_call(turn_id.to_string(), parent_tool_item_id.to_string())
|
||||
.await;
|
||||
|
||||
Some(ActiveNetworkApproval {
|
||||
registration_id: Some(registration_id),
|
||||
parent_tool_item_id: Some(parent_tool_item_id.to_string()),
|
||||
mode: spec.mode,
|
||||
})
|
||||
}
|
||||
@@ -573,20 +604,20 @@ pub(crate) async fn finish_immediate_network_approval(
|
||||
session: &Session,
|
||||
active: ActiveNetworkApproval,
|
||||
) -> Result<(), ToolError> {
|
||||
let Some(registration_id) = active.registration_id.as_deref() else {
|
||||
let Some(parent_tool_item_id) = active.parent_tool_item_id.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let approval_outcome = session
|
||||
.services
|
||||
.network_approval
|
||||
.take_call_outcome(registration_id)
|
||||
.take_call_outcome(parent_tool_item_id)
|
||||
.await;
|
||||
|
||||
session
|
||||
.services
|
||||
.network_approval
|
||||
.unregister_call(registration_id)
|
||||
.unregister_call(parent_tool_item_id)
|
||||
.await;
|
||||
|
||||
match approval_outcome {
|
||||
@@ -608,7 +639,7 @@ pub(crate) async fn finish_deferred_network_approval(
|
||||
session
|
||||
.services
|
||||
.network_approval
|
||||
.unregister_call(deferred.registration_id())
|
||||
.unregister_call(deferred.parent_tool_item_id())
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
@@ -180,9 +180,17 @@ fn only_never_policy_disables_network_approval_flow() {
|
||||
}
|
||||
|
||||
fn denied_blocked_request(host: &str) -> BlockedRequest {
|
||||
denied_blocked_request_with_parent_tool_item(host, None)
|
||||
}
|
||||
|
||||
fn denied_blocked_request_with_parent_tool_item(
|
||||
host: &str,
|
||||
parent_tool_item_id: Option<&str>,
|
||||
) -> BlockedRequest {
|
||||
BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
parent_tool_item_id: parent_tool_item_id.map(ToString::to_string),
|
||||
client: None,
|
||||
method: None,
|
||||
mode: None,
|
||||
@@ -194,10 +202,10 @@ fn denied_blocked_request(host: &str) -> BlockedRequest {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_blocked_request_sets_policy_outcome_for_owner_call() {
|
||||
async fn record_blocked_request_sets_policy_outcome_for_active_call() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call("registration-1".to_string(), "turn-1".to_string())
|
||||
.register_call("turn-1".to_string(), "command-1".to_string())
|
||||
.await;
|
||||
|
||||
service
|
||||
@@ -205,7 +213,7 @@ async fn record_blocked_request_sets_policy_outcome_for_owner_call() {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
service.take_call_outcome("registration-1").await,
|
||||
service.take_call_outcome("command-1").await,
|
||||
Some(NetworkApprovalOutcome::DeniedByPolicy(
|
||||
"Network access to \"example.com\" was blocked: domain is not on the allowlist for the current sandbox mode.".to_string()
|
||||
))
|
||||
@@ -216,18 +224,18 @@ async fn record_blocked_request_sets_policy_outcome_for_owner_call() {
|
||||
async fn blocked_request_policy_does_not_override_user_denial_outcome() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call("registration-1".to_string(), "turn-1".to_string())
|
||||
.register_call("turn-1".to_string(), "command-1".to_string())
|
||||
.await;
|
||||
|
||||
service
|
||||
.record_call_outcome("registration-1", NetworkApprovalOutcome::DeniedByUser)
|
||||
.record_call_outcome("command-1", NetworkApprovalOutcome::DeniedByUser)
|
||||
.await;
|
||||
service
|
||||
.record_blocked_request(denied_blocked_request("example.com"))
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
service.take_call_outcome("registration-1").await,
|
||||
service.take_call_outcome("command-1").await,
|
||||
Some(NetworkApprovalOutcome::DeniedByUser)
|
||||
);
|
||||
}
|
||||
@@ -236,16 +244,61 @@ async fn blocked_request_policy_does_not_override_user_denial_outcome() {
|
||||
async fn record_blocked_request_ignores_ambiguous_unattributed_blocked_requests() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call("registration-1".to_string(), "turn-1".to_string())
|
||||
.register_call("turn-1".to_string(), "command-1".to_string())
|
||||
.await;
|
||||
service
|
||||
.register_call("registration-2".to_string(), "turn-1".to_string())
|
||||
.register_call("turn-1".to_string(), "command-2".to_string())
|
||||
.await;
|
||||
|
||||
service
|
||||
.record_blocked_request(denied_blocked_request("example.com"))
|
||||
.await;
|
||||
|
||||
assert_eq!(service.take_call_outcome("registration-1").await, None);
|
||||
assert_eq!(service.take_call_outcome("registration-2").await, None);
|
||||
assert_eq!(service.take_call_outcome("command-1").await, None);
|
||||
assert_eq!(service.take_call_outcome("command-2").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_active_call_uses_parent_tool_item_id_when_multiple_calls_are_active() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call("turn-1".to_string(), "command-1".to_string())
|
||||
.await;
|
||||
service
|
||||
.register_call("turn-2".to_string(), "command-2".to_string())
|
||||
.await;
|
||||
|
||||
let active_call = service
|
||||
.resolve_active_call(Some("command-2"))
|
||||
.await
|
||||
.expect("active call should resolve");
|
||||
|
||||
assert_eq!(active_call.turn_id, "turn-2");
|
||||
assert_eq!(active_call.parent_tool_item_id, "command-2");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_blocked_request_uses_parent_tool_item_id_when_multiple_calls_are_active() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call("turn-1".to_string(), "command-1".to_string())
|
||||
.await;
|
||||
service
|
||||
.register_call("turn-2".to_string(), "command-2".to_string())
|
||||
.await;
|
||||
|
||||
service
|
||||
.record_blocked_request(denied_blocked_request_with_parent_tool_item(
|
||||
"example.com",
|
||||
Some("command-2"),
|
||||
))
|
||||
.await;
|
||||
|
||||
assert_eq!(service.take_call_outcome("command-1").await, None);
|
||||
assert_eq!(
|
||||
service.take_call_outcome("command-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()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,18 +60,12 @@ impl ToolOrchestrator {
|
||||
let network_approval = begin_network_approval(
|
||||
&tool_ctx.session,
|
||||
&tool_ctx.turn.sub_id,
|
||||
&tool_ctx.call_id,
|
||||
has_managed_network_requirements,
|
||||
tool.network_approval_spec(req, tool_ctx),
|
||||
)
|
||||
.await;
|
||||
|
||||
let attempt_tool_ctx = ToolCtx {
|
||||
session: tool_ctx.session.clone(),
|
||||
turn: tool_ctx.turn.clone(),
|
||||
call_id: tool_ctx.call_id.clone(),
|
||||
tool_name: tool_ctx.tool_name.clone(),
|
||||
};
|
||||
let run_result = tool.run(req, attempt, &attempt_tool_ctx).await;
|
||||
let run_result = tool.run(req, attempt, tool_ctx).await;
|
||||
|
||||
let Some(network_approval) = network_approval else {
|
||||
return (run_result, None);
|
||||
|
||||
@@ -208,7 +208,9 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
|
||||
) -> Result<ExecToolCallOutput, ToolError> {
|
||||
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, /*parent_tool_item_id*/ None,
|
||||
)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let out = execute_env(env, Self::stdout_stream(ctx))
|
||||
.await
|
||||
|
||||
@@ -253,7 +253,7 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||||
req.justification.clone(),
|
||||
)?;
|
||||
let env = attempt
|
||||
.env_for(spec, req.network.as_ref())
|
||||
.env_for(spec, req.network.as_ref(), Some(ctx.call_id.as_str()))
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let out = execute_env(env, Self::stdout_stream(ctx))
|
||||
.await
|
||||
|
||||
@@ -117,13 +117,14 @@ 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(), Some(ctx.call_id.as_str()))
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let crate::sandboxing::ExecRequest {
|
||||
command,
|
||||
cwd: sandbox_cwd,
|
||||
env: sandbox_env,
|
||||
network: sandbox_network,
|
||||
parent_tool_item_id,
|
||||
expiration: _sandbox_expiration,
|
||||
capture_policy: _capture_policy,
|
||||
sandbox,
|
||||
@@ -153,6 +154,7 @@ pub(super) async fn try_run_zsh_fork(
|
||||
sandbox,
|
||||
env: sandbox_env,
|
||||
network: sandbox_network,
|
||||
parent_tool_item_id,
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions,
|
||||
justification,
|
||||
@@ -259,6 +261,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
sandbox: exec_request.sandbox,
|
||||
env: exec_request.env.clone(),
|
||||
network: exec_request.network.clone(),
|
||||
parent_tool_item_id: exec_request.parent_tool_item_id.clone(),
|
||||
windows_sandbox_level: exec_request.windows_sandbox_level,
|
||||
sandbox_permissions: exec_request.sandbox_permissions,
|
||||
justification: exec_request.justification.clone(),
|
||||
@@ -856,6 +859,7 @@ struct CoreShellCommandExecutor {
|
||||
sandbox: SandboxType,
|
||||
env: HashMap<String, String>,
|
||||
network: Option<codex_network_proxy::NetworkProxy>,
|
||||
parent_tool_item_id: Option<String>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
justification: Option<String>,
|
||||
@@ -904,6 +908,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
cwd: self.cwd.clone(),
|
||||
env: exec_env,
|
||||
network: self.network.clone(),
|
||||
parent_tool_item_id: self.parent_tool_item_id.clone(),
|
||||
expiration: ExecExpiration::Cancellation(cancel_rx),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
sandbox: self.sandbox,
|
||||
@@ -1060,6 +1065,7 @@ impl CoreShellCommandExecutor {
|
||||
sandbox,
|
||||
enforce_managed_network: self.network.is_some(),
|
||||
network: self.network.as_ref(),
|
||||
parent_tool_item_id: self.parent_tool_item_id.as_deref(),
|
||||
sandbox_policy_cwd: &self.sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
@@ -1069,7 +1075,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_parent_tool_item(
|
||||
&mut exec_request.env,
|
||||
exec_request.parent_tool_item_id.as_deref(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(PreparedExec {
|
||||
|
||||
@@ -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,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_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,
|
||||
parent_tool_item_id: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
|
||||
@@ -207,7 +207,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> 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_parent_tool_item(&mut env, Some(ctx.call_id.as_str()));
|
||||
}
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let spec = build_command_spec(
|
||||
@@ -221,7 +221,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> 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(), Some(ctx.call_id.as_str()))
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
match zsh_fork_backend::maybe_prepare_unified_exec(
|
||||
req,
|
||||
@@ -269,7 +269,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> 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(), Some(ctx.call_id.as_str()))
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
self.manager
|
||||
.open_session_with_exec_env(&exec_env, req.tty, Box::new(NoopSpawnLifecycle))
|
||||
|
||||
@@ -341,6 +341,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
&self,
|
||||
spec: CommandSpec,
|
||||
network: Option<&NetworkProxy>,
|
||||
parent_tool_item_id: Option<&str>,
|
||||
) -> Result<crate::sandboxing::ExecRequest, SandboxTransformError> {
|
||||
self.manager
|
||||
.transform(crate::sandboxing::SandboxTransformRequest {
|
||||
@@ -351,6 +352,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
sandbox: self.sandbox,
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
network,
|
||||
parent_tool_item_id,
|
||||
sandbox_policy_cwd: self.sandbox_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
||||
@@ -198,7 +198,7 @@ impl UnifiedExecProcessManager {
|
||||
if process_started_alive {
|
||||
let network_approval_id = deferred_network_approval
|
||||
.as_ref()
|
||||
.map(|deferred| deferred.registration_id().to_string());
|
||||
.map(|deferred| deferred.parent_tool_item_id().to_string());
|
||||
self.store_process(
|
||||
Arc::clone(&process),
|
||||
context,
|
||||
|
||||
@@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -120,6 +120,7 @@ async fn run_cmd_result_with_policies(
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env_from_core_vars(),
|
||||
network: None,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -380,6 +381,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env_from_core_vars(),
|
||||
network: None,
|
||||
parent_tool_item_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ use rama_http::StatusCode;
|
||||
use rama_http::header;
|
||||
use rama_http::headers::HeaderMapExt;
|
||||
use rama_http::headers::Host;
|
||||
use rama_http::headers::ProxyAuthorization;
|
||||
use rama_http::layer::remove_header::RemoveResponseHeaderLayer;
|
||||
use rama_http::matcher::MethodMatcher;
|
||||
use rama_http_backend::client::proxy::layer::HttpProxyConnector;
|
||||
@@ -65,6 +66,7 @@ use rama_net::proxy::ProxyRequest;
|
||||
use rama_net::proxy::ProxyTarget;
|
||||
use rama_net::proxy::StreamForwardService;
|
||||
use rama_net::stream::SocketInfo;
|
||||
use rama_net::user::Basic;
|
||||
use rama_tcp::client::Request as TcpRequest;
|
||||
use rama_tcp::client::service::TcpConnector;
|
||||
use rama_tcp::server::TcpListener;
|
||||
@@ -149,6 +151,12 @@ async fn run_http_proxy_with_listener(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn proxy_authorization_parent_tool_item_id<T>(req: &Request<T>) -> Option<String> {
|
||||
req.headers()
|
||||
.typed_get::<ProxyAuthorization<Basic>>()
|
||||
.map(|header| header.0.username().to_string())
|
||||
}
|
||||
|
||||
async fn http_connect_accept(
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
mut req: Request,
|
||||
@@ -173,6 +181,7 @@ async fn http_connect_accept(
|
||||
}
|
||||
|
||||
let client = client_addr(&req);
|
||||
let parent_tool_item_id = proxy_authorization_parent_tool_item_id(&req);
|
||||
let enabled = app_state
|
||||
.enabled()
|
||||
.await
|
||||
@@ -182,12 +191,15 @@ async fn http_connect_accept(
|
||||
warn!("CONNECT blocked; proxy disabled (client={client}, host={host})");
|
||||
return Err(proxy_disabled_response(
|
||||
&app_state,
|
||||
host,
|
||||
authority.port,
|
||||
client_addr(&req),
|
||||
Some("CONNECT".to_string()),
|
||||
NetworkProtocol::HttpsConnect,
|
||||
/*audit_endpoint_override*/ None,
|
||||
ProxyDisabledResponseArgs {
|
||||
host,
|
||||
port: authority.port,
|
||||
client: client_addr(&req),
|
||||
parent_tool_item_id,
|
||||
method: Some("CONNECT".to_string()),
|
||||
protocol: NetworkProtocol::HttpsConnect,
|
||||
audit_endpoint_override: None,
|
||||
},
|
||||
)
|
||||
.await);
|
||||
}
|
||||
@@ -196,6 +208,7 @@ async fn http_connect_accept(
|
||||
protocol: NetworkProtocol::HttpsConnect,
|
||||
host: host.clone(),
|
||||
port: authority.port,
|
||||
parent_tool_item_id: parent_tool_item_id.clone(),
|
||||
client_addr: client.clone(),
|
||||
method: Some("CONNECT".to_string()),
|
||||
command: None,
|
||||
@@ -220,6 +233,7 @@ async fn http_connect_accept(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: reason.clone(),
|
||||
parent_tool_item_id,
|
||||
client: client.clone(),
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: None,
|
||||
@@ -283,6 +297,7 @@ async fn http_connect_accept(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_MITM_REQUIRED.to_string(),
|
||||
parent_tool_item_id,
|
||||
client: client.clone(),
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
@@ -432,6 +447,7 @@ async fn http_plain_proxy(
|
||||
}
|
||||
};
|
||||
let client = client_addr(&req);
|
||||
let parent_tool_item_id = proxy_authorization_parent_tool_item_id(&req);
|
||||
let method_allowed = match app_state
|
||||
.method_allowed(req.method().as_str())
|
||||
.await
|
||||
@@ -468,12 +484,15 @@ async fn http_plain_proxy(
|
||||
warn!("unix socket blocked; proxy disabled (client={client}, path={socket_path})");
|
||||
return Ok(proxy_disabled_response(
|
||||
&app_state,
|
||||
socket_path,
|
||||
/*port*/ 0,
|
||||
client_addr(&req),
|
||||
Some(req.method().as_str().to_string()),
|
||||
NetworkProtocol::Http,
|
||||
Some(("unix-socket", 0)),
|
||||
ProxyDisabledResponseArgs {
|
||||
host: socket_path,
|
||||
port: 0,
|
||||
client: client_addr(&req),
|
||||
parent_tool_item_id: parent_tool_item_id.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
protocol: NetworkProtocol::Http,
|
||||
audit_endpoint_override: Some(("unix-socket", 0)),
|
||||
},
|
||||
)
|
||||
.await);
|
||||
}
|
||||
@@ -613,12 +632,15 @@ async fn http_plain_proxy(
|
||||
warn!("request blocked; proxy disabled (client={client}, host={host}, method={method})");
|
||||
return Ok(proxy_disabled_response(
|
||||
&app_state,
|
||||
host,
|
||||
port,
|
||||
client_addr(&req),
|
||||
Some(req.method().as_str().to_string()),
|
||||
NetworkProtocol::Http,
|
||||
/*audit_endpoint_override*/ None,
|
||||
ProxyDisabledResponseArgs {
|
||||
host,
|
||||
port,
|
||||
client: client_addr(&req),
|
||||
parent_tool_item_id: parent_tool_item_id.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
protocol: NetworkProtocol::Http,
|
||||
audit_endpoint_override: None,
|
||||
},
|
||||
)
|
||||
.await);
|
||||
}
|
||||
@@ -627,6 +649,7 @@ async fn http_plain_proxy(
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: host.clone(),
|
||||
port,
|
||||
parent_tool_item_id: parent_tool_item_id.clone(),
|
||||
client_addr: client.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
command: None,
|
||||
@@ -651,6 +674,7 @@ async fn http_plain_proxy(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: reason.clone(),
|
||||
parent_tool_item_id: parent_tool_item_id.clone(),
|
||||
client: client.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
mode: None,
|
||||
@@ -696,6 +720,7 @@ async fn http_plain_proxy(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
parent_tool_item_id,
|
||||
client: client.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
@@ -884,15 +909,29 @@ fn blocked_text_with_details(reason: &str, details: &PolicyDecisionDetails<'_>)
|
||||
blocked_text_response_with_policy(reason, details)
|
||||
}
|
||||
|
||||
async fn proxy_disabled_response(
|
||||
app_state: &NetworkProxyState,
|
||||
struct ProxyDisabledResponseArgs {
|
||||
host: String,
|
||||
port: u16,
|
||||
client: Option<String>,
|
||||
parent_tool_item_id: Option<String>,
|
||||
method: Option<String>,
|
||||
protocol: NetworkProtocol,
|
||||
audit_endpoint_override: Option<(&str, u16)>,
|
||||
audit_endpoint_override: Option<(&'static str, u16)>,
|
||||
}
|
||||
|
||||
async fn proxy_disabled_response(
|
||||
app_state: &NetworkProxyState,
|
||||
args: ProxyDisabledResponseArgs,
|
||||
) -> Response {
|
||||
let ProxyDisabledResponseArgs {
|
||||
host,
|
||||
port,
|
||||
client,
|
||||
parent_tool_item_id,
|
||||
method,
|
||||
protocol,
|
||||
audit_endpoint_override,
|
||||
} = args;
|
||||
let (audit_server_address, audit_server_port) =
|
||||
audit_endpoint_override.unwrap_or((host.as_str(), port));
|
||||
emit_http_block_decision_audit_event(
|
||||
@@ -913,6 +952,7 @@ async fn proxy_disabled_response(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: blocked_host,
|
||||
reason: REASON_PROXY_DISABLED.to_string(),
|
||||
parent_tool_item_id,
|
||||
client,
|
||||
method,
|
||||
mode: None,
|
||||
@@ -1013,9 +1053,10 @@ mod tests {
|
||||
.method(Method::CONNECT)
|
||||
.uri("https://example.com:443")
|
||||
.header("host", "example.com:443")
|
||||
.header(header::PROXY_AUTHORIZATION, "Basic Y29tbWFuZC0xOg==")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
req.extensions_mut().insert(state);
|
||||
req.extensions_mut().insert(Arc::clone(&state));
|
||||
|
||||
let response = http_connect_accept(None, req).await.unwrap_err();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
@@ -1023,6 +1064,26 @@ mod tests {
|
||||
response.headers().get("x-proxy-error").unwrap(),
|
||||
"blocked-by-mitm-required"
|
||||
);
|
||||
let blocked = state
|
||||
.blocked_snapshot()
|
||||
.await
|
||||
.expect("blocked snapshot should succeed");
|
||||
assert_eq!(
|
||||
blocked,
|
||||
vec![BlockedRequest {
|
||||
host: "example.com".to_string(),
|
||||
reason: REASON_MITM_REQUIRED.to_string(),
|
||||
parent_tool_item_id: Some("command-1".to_string()),
|
||||
client: None,
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
protocol: "http-connect".to_string(),
|
||||
decision: Some("deny".to_string()),
|
||||
source: Some("mode_guard".to_string()),
|
||||
port: Some(443),
|
||||
timestamp: blocked[0].timestamp,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -5,6 +5,7 @@ mod config;
|
||||
mod http_proxy;
|
||||
mod mitm;
|
||||
mod network_policy;
|
||||
mod owner_identity;
|
||||
mod policy;
|
||||
mod proxy;
|
||||
mod reasons;
|
||||
|
||||
@@ -289,6 +289,7 @@ async fn mitm_blocking_response(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: policy.target_host.clone(),
|
||||
reason: reason.to_string(),
|
||||
parent_tool_item_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(),
|
||||
parent_tool_item_id: None,
|
||||
client: client.clone(),
|
||||
method: Some(method.clone()),
|
||||
mode: Some(policy.mode),
|
||||
|
||||
@@ -79,6 +79,10 @@ pub struct NetworkPolicyRequest {
|
||||
pub protocol: NetworkProtocol,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
/// Parent tool item id forwarded from the originating tool call. Core uses
|
||||
/// this to resolve blocked requests back to the exact active call instead
|
||||
/// of guessing by session state.
|
||||
pub parent_tool_item_id: Option<String>,
|
||||
pub client_addr: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub command: Option<String>,
|
||||
@@ -89,6 +93,9 @@ pub struct NetworkPolicyRequestArgs {
|
||||
pub protocol: NetworkProtocol,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
/// Parent tool item id forwarded from the originating tool call for later
|
||||
/// attribution.
|
||||
pub parent_tool_item_id: Option<String>,
|
||||
pub client_addr: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub command: Option<String>,
|
||||
@@ -101,6 +108,7 @@ impl NetworkPolicyRequest {
|
||||
protocol,
|
||||
host,
|
||||
port,
|
||||
parent_tool_item_id,
|
||||
client_addr,
|
||||
method,
|
||||
command,
|
||||
@@ -110,6 +118,7 @@ impl NetworkPolicyRequest {
|
||||
protocol,
|
||||
host,
|
||||
port,
|
||||
parent_tool_item_id,
|
||||
client_addr,
|
||||
method,
|
||||
command,
|
||||
@@ -625,6 +634,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
parent_tool_item_id: None,
|
||||
client_addr: None,
|
||||
method: None,
|
||||
command: None,
|
||||
@@ -685,6 +695,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "blocked.com".to_string(),
|
||||
port: 80,
|
||||
parent_tool_item_id: None,
|
||||
client_addr: Some("127.0.0.1:1234".to_string()),
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
@@ -726,6 +737,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
parent_tool_item_id: None,
|
||||
client_addr: None,
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
@@ -776,6 +788,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
parent_tool_item_id: None,
|
||||
client_addr: None,
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
@@ -859,6 +872,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 80,
|
||||
parent_tool_item_id: None,
|
||||
client_addr: None,
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
|
||||
41
codex-rs/network-proxy/src/owner_identity.rs
Normal file
41
codex-rs/network-proxy/src/owner_identity.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
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 ProxyParentToolItemAuthorizer;
|
||||
|
||||
impl AuthoritySync<Basic, ()> for ProxyParentToolItemAuthorizer {
|
||||
fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool {
|
||||
ext.insert(UserId::Username(credentials.username().to_string()));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Authorizer<Basic> for ProxyParentToolItemAuthorizer {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
async fn authorize(&self, credentials: Basic) -> AuthorizeResult<Basic, Self::Error> {
|
||||
let mut extensions = Extensions::new();
|
||||
extensions.insert(UserId::Username(credentials.username().to_string()));
|
||||
AuthorizeResult {
|
||||
credentials,
|
||||
result: Ok(Some(extensions)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the parent tool item id from proxy auth/user extensions. Managed
|
||||
/// proxy callbacks carry the parent tool item id as the proxy auth username so
|
||||
/// core can attribute blocked requests back to the originating tool call.
|
||||
pub(crate) fn extract_parent_tool_item_id<T: ExtensionsRef>(input: &T) -> Option<String> {
|
||||
match input.extensions().get::<UserId>() {
|
||||
Some(UserId::Username(username)) => Some(username.clone()),
|
||||
Some(UserId::Token(token)) => String::from_utf8(token.clone()).ok(),
|
||||
Some(UserId::Anonymous) | None => None,
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")]
|
||||
@@ -305,15 +306,34 @@ fn set_env_keys(env: &mut HashMap<String, String>, keys: &[&str], value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_url(scheme: &str, addr: SocketAddr, parent_tool_item_id: Option<&str>) -> String {
|
||||
let base = format!("{scheme}://{addr}");
|
||||
let mut url = match Url::parse(&base) {
|
||||
Ok(url) => url,
|
||||
Err(err) => panic!("failed to build proxy URL for {base}: {err}"),
|
||||
};
|
||||
if let Some(parent_tool_item_id) = parent_tool_item_id
|
||||
&& let Err(()) = url.set_username(parent_tool_item_id)
|
||||
{
|
||||
panic!("failed to encode parent tool item id in proxy URL");
|
||||
}
|
||||
let mut proxy_url = url.to_string();
|
||||
if proxy_url.ends_with('/') {
|
||||
proxy_url.pop();
|
||||
}
|
||||
proxy_url
|
||||
}
|
||||
|
||||
fn apply_proxy_env_overrides(
|
||||
env: &mut HashMap<String, String>,
|
||||
http_addr: SocketAddr,
|
||||
socks_addr: SocketAddr,
|
||||
socks_enabled: bool,
|
||||
allow_local_binding: bool,
|
||||
parent_tool_item_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, parent_tool_item_id);
|
||||
let socks_proxy_url = proxy_url("socks5h", socks_addr, parent_tool_item_id);
|
||||
env.insert(
|
||||
ALLOW_LOCAL_BINDING_ENV_KEY.to_string(),
|
||||
if allow_local_binding {
|
||||
@@ -414,6 +434,16 @@ impl NetworkProxy {
|
||||
}
|
||||
|
||||
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
|
||||
self.apply_to_env_for_parent_tool_item(env, /*parent_tool_item_id*/ None);
|
||||
}
|
||||
|
||||
/// Apply managed proxy environment variables, optionally tagging them with
|
||||
/// the originating parent tool item id for blocked-request attribution.
|
||||
pub fn apply_to_env_for_parent_tool_item(
|
||||
&self,
|
||||
env: &mut HashMap<String, String>,
|
||||
parent_tool_item_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 +452,7 @@ impl NetworkProxy {
|
||||
self.socks_addr,
|
||||
self.socks_enabled,
|
||||
self.allow_local_binding,
|
||||
parent_tool_item_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -696,6 +727,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
/*parent_tool_item_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -746,6 +778,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
false,
|
||||
true,
|
||||
/*parent_tool_item_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -764,6 +797,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
/*parent_tool_item_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -809,6 +843,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
/*parent_tool_item_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -816,4 +851,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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,10 +80,15 @@ pub enum HostBlockDecision {
|
||||
Blocked(HostBlockReason),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct BlockedRequest {
|
||||
pub host: String,
|
||||
pub reason: String,
|
||||
/// Parent tool item id carried alongside the blocked request for
|
||||
/// attribution in core. This is internal-only and is intentionally omitted
|
||||
/// from serialized logs/snapshots.
|
||||
#[serde(skip_serializing)]
|
||||
pub parent_tool_item_id: Option<String>,
|
||||
pub client: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub mode: Option<NetworkMode>,
|
||||
@@ -100,6 +105,9 @@ pub struct BlockedRequest {
|
||||
pub struct BlockedRequestArgs {
|
||||
pub host: String,
|
||||
pub reason: String,
|
||||
/// Parent tool item id carried alongside the blocked request for
|
||||
/// attribution in core.
|
||||
pub parent_tool_item_id: Option<String>,
|
||||
pub client: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub mode: Option<NetworkMode>,
|
||||
@@ -114,6 +122,7 @@ impl BlockedRequest {
|
||||
let BlockedRequestArgs {
|
||||
host,
|
||||
reason,
|
||||
parent_tool_item_id,
|
||||
client,
|
||||
method,
|
||||
mode,
|
||||
@@ -125,6 +134,7 @@ impl BlockedRequest {
|
||||
Self {
|
||||
host,
|
||||
reason,
|
||||
parent_tool_item_id,
|
||||
client,
|
||||
method,
|
||||
mode,
|
||||
@@ -994,6 +1004,7 @@ mod tests {
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: "google.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
parent_tool_item_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
@@ -1034,6 +1045,7 @@ mod tests {
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: format!("example{idx}.com"),
|
||||
reason: "not_allowed".to_string(),
|
||||
parent_tool_item_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
@@ -1056,6 +1068,7 @@ mod tests {
|
||||
let entry = BlockedRequest {
|
||||
host: "google.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
parent_tool_item_id: None,
|
||||
client: Some("127.0.0.1".to_string()),
|
||||
method: Some("GET".to_string()),
|
||||
mode: Some(NetworkMode::Full),
|
||||
|
||||
@@ -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::ProxyParentToolItemAuthorizer;
|
||||
use crate::owner_identity::extract_parent_tool_item_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(ProxyParentToolItemAuthorizer);
|
||||
|
||||
if enable_socks5_udp {
|
||||
let udp_state = state.clone();
|
||||
@@ -150,6 +156,7 @@ async fn handle_socks5_tcp(
|
||||
.extensions()
|
||||
.get::<SocketInfo>()
|
||||
.map(|info| info.peer_addr().to_string());
|
||||
let parent_tool_item_id = extract_parent_tool_item_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(),
|
||||
parent_tool_item_id: parent_tool_item_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(),
|
||||
parent_tool_item_id: parent_tool_item_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,
|
||||
parent_tool_item_id: parent_tool_item_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(),
|
||||
parent_tool_item_id,
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: None,
|
||||
@@ -314,6 +325,7 @@ async fn inspect_socks5_udp(
|
||||
let client = extensions
|
||||
.get::<SocketInfo>()
|
||||
.map(|info| info.peer_addr().to_string());
|
||||
let parent_tool_item_id = extract_parent_tool_item_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(),
|
||||
parent_tool_item_id: parent_tool_item_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(),
|
||||
parent_tool_item_id: parent_tool_item_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,
|
||||
parent_tool_item_id: parent_tool_item_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(),
|
||||
parent_tool_item_id,
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: None,
|
||||
|
||||
Reference in New Issue
Block a user