diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index b839b05c44..26626f7053 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -31,6 +31,7 @@ use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; use codex_sandboxing::policy_transforms::effective_permission_profile; +use codex_sandboxing::record_filesystem_sandbox_violation; use codex_utils_path_uri::PathUri; use futures::future::BoxFuture; use std::path::PathBuf; @@ -267,6 +268,7 @@ impl ToolRuntime for ApplyPatchRunti timed_out: false, }; if failed && is_likely_sandbox_denied(attempt.sandbox, &output) { + record_filesystem_sandbox_violation(attempt.sandbox, &output); return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(output), network_policy_decision: None, diff --git a/codex-rs/sandboxing/src/violation.rs b/codex-rs/sandboxing/src/violation.rs index f1622a8e0a..57d8a3a006 100644 --- a/codex-rs/sandboxing/src/violation.rs +++ b/codex-rs/sandboxing/src/violation.rs @@ -1,5 +1,6 @@ use crate::SandboxType; use codex_network_proxy::BlockedRequest; +use codex_network_proxy::NetworkMode; use codex_protocol::exec_output::ExecToolCallOutput; use tracing::warn; @@ -86,6 +87,7 @@ pub struct NetworkSandboxViolation { pub reason: String, pub client: Option, pub method: Option, + pub mode: Option, pub protocol: String, pub decision: Option, pub source: Option, @@ -100,6 +102,7 @@ impl NetworkSandboxViolation { reason: blocked.reason.clone(), client: blocked.client.clone(), method: blocked.method.clone(), + mode: blocked.mode, protocol: blocked.protocol.clone(), decision: blocked.decision.clone(), source: blocked.source.clone(), @@ -179,12 +182,13 @@ pub fn record_sandbox_violation(event: &SandboxViolationEvent) { } SandboxViolationEvent::Network(violation) => { warn!( - "recorded sandbox violation: resource=network protocol={} host={} port={:?} reason={} method={:?} client={:?} decision={:?} source={:?}", + "recorded sandbox violation: resource=network protocol={} host={} port={:?} reason={} method={:?} mode={:?} client={:?} decision={:?} source={:?}", violation.protocol, violation.host, violation.port, violation.reason, violation.method, + violation.mode, violation.client, violation.decision, violation.source @@ -228,9 +232,13 @@ fn extract_denied_path_from_text(text: &str) -> Option { ]; for line in text.lines() { - let lower = line.to_lowercase(); for marker in PATH_MARKERS { - let Some(marker_start) = lower.find(marker) else { + let Some(marker_start) = line.match_indices(':').find_map(|(marker_start, _)| { + line.get(marker_start..) + .and_then(|suffix| suffix.get(..marker.len())) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(marker)) + .then_some(marker_start) + }) else { continue; }; let candidate_prefix = &line[..marker_start]; diff --git a/codex-rs/sandboxing/src/violation_tests.rs b/codex-rs/sandboxing/src/violation_tests.rs index d7c2a89ba2..9c03af16bd 100644 --- a/codex-rs/sandboxing/src/violation_tests.rs +++ b/codex-rs/sandboxing/src/violation_tests.rs @@ -1,6 +1,7 @@ use super::*; use codex_network_proxy::BlockedRequest; use codex_network_proxy::BlockedRequestArgs; +use codex_network_proxy::NetworkMode; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; use pretty_assertions::assert_eq; @@ -97,6 +98,27 @@ fn classifies_filesystem_violation_with_path() { ); } +#[test] +fn classifies_filesystem_violation_with_unicode_before_marker() { + let output = make_exec_output( + /*exit_code*/ 1, + "", + "bash: /private/tmp/\u{130}-denied: Operation not permitted", + "", + ); + + assert_eq!( + classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output), + Some(FileSystemSandboxViolation { + sandbox_type: SandboxType::MacosSeatbelt, + reason: FileSystemSandboxViolationReason::OperationNotPermitted, + path: Some("/private/tmp/\u{130}-denied".to_string()), + output_snippet: "bash: /private/tmp/\u{130}-denied: Operation not permitted" + .to_string(), + }) + ); +} + #[test] fn classifies_filesystem_violation_from_aggregated_output() { let output = make_exec_output( @@ -152,7 +174,7 @@ fn converts_blocked_request_to_network_violation() { reason: "not_allowed".to_string(), client: Some("curl".to_string()), method: Some("CONNECT".to_string()), - mode: None, + mode: Some(NetworkMode::Limited), protocol: "https".to_string(), decision: Some("block".to_string()), source: Some("policy".to_string()), @@ -166,6 +188,7 @@ fn converts_blocked_request_to_network_violation() { reason: "not_allowed".to_string(), client: Some("curl".to_string()), method: Some("CONNECT".to_string()), + mode: Some(NetworkMode::Limited), protocol: "https".to_string(), decision: Some("block".to_string()), source: Some("policy".to_string()),