Compare commits

...

2 Commits

Author SHA1 Message Date
Michael D'Angelo
a09254f227 Merge branch 'main' of https://github.com/openai/codex into fix/unified-exec-sandbox-denial-lifecycle 2026-05-12 17:13:46 -07:00
Michael D'Angelo
6ec9a8f7bf fix(core): emit unified exec sandbox denial lifecycle 2026-05-12 16:43:47 -07:00
2 changed files with 115 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ use crate::sandboxing::ExecServerEnvConfig;
use crate::tools::context::ExecCommandToolOutput;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
use crate::tools::events::ToolEventFailure;
use crate::tools::events::ToolEventStage;
use crate::tools::network_approval::DeferredNetworkApproval;
use crate::tools::network_approval::finish_deferred_network_approval;
@@ -52,6 +53,7 @@ use crate::unified_exec::process::UnifiedExecProcess;
use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::error::CodexErr;
use codex_protocol::error::SandboxErr;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::protocol::ExecCommandSource;
use codex_tools::ToolName;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -305,6 +307,34 @@ async fn emit_failed_initial_exec_end_if_unstored(
.await;
}
#[allow(clippy::too_many_arguments)]
async fn emit_failed_initial_exec_lifecycle_before_process_start(
context: &UnifiedExecContext,
request: &ExecCommandRequest,
cwd: AbsolutePathBuf,
output: ExecToolCallOutput,
) {
let event_ctx = ToolEventCtx::new(
context.session.as_ref(),
context.turn.as_ref(),
&context.call_id,
/*turn_diff_tracker*/ None,
);
let emitter = ToolEmitter::unified_exec(
&request.command,
cwd,
ExecCommandSource::UnifiedExecStartup,
Some(request.process_id.to_string()),
);
emitter.emit(event_ctx, ToolEventStage::Begin).await;
emitter
.emit(
event_ctx,
ToolEventStage::Failure(ToolEventFailure::Output(output)),
)
.await;
}
fn terminate_process_on_network_denial(
process: Arc<UnifiedExecProcess>,
session: std::sync::Weak<crate::session::session::Session>,
@@ -381,6 +411,15 @@ impl UnifiedExecProcessManager {
(Arc::new(process), deferred_network_approval)
}
Err(err) => {
if let UnifiedExecError::SandboxDenied { output, .. } = &err {
emit_failed_initial_exec_lifecycle_before_process_start(
context,
&request,
cwd.clone(),
output.clone(),
)
.await;
}
self.release_process_id(request.process_id).await;
return Err(err);
}

View File

@@ -228,6 +228,82 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
);
}
#[tokio::test]
async fn failed_pre_spawn_sandbox_denial_emits_begin_and_end() {
let (session, turn, rx_event) = crate::session::tests::make_session_and_context_with_rx().await;
let context = UnifiedExecContext::new(
Arc::clone(&session),
Arc::clone(&turn),
"call-unified-pre-spawn-denied".to_string(),
);
let request = ExecCommandRequest {
command: vec![
"sh".to_string(),
"-lc".to_string(),
"cat probe.txt".to_string(),
],
hook_command: "cat probe.txt".to_string(),
process_id: 456,
yield_time_ms: 1000,
max_output_tokens: None,
cwd: turn.cwd.clone(),
sandbox_cwd: turn.cwd.clone(),
environment: turn
.environments
.primary_environment()
.expect("primary environment"),
network: None,
tty: true,
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
additional_permissions: None,
additional_permissions_preapproved: false,
justification: None,
prefix_rule: None,
};
let denial = "sandbox-exec: sandbox_apply: Operation not permitted\n";
emit_failed_initial_exec_lifecycle_before_process_start(
&context,
&request,
turn.cwd.clone(),
ExecToolCallOutput {
exit_code: 71,
stdout: codex_protocol::exec_output::StreamOutput::new(String::new()),
stderr: codex_protocol::exec_output::StreamOutput::new(denial.to_string()),
aggregated_output: codex_protocol::exec_output::StreamOutput::new(denial.to_string()),
duration: Duration::from_millis(9),
timed_out: false,
},
)
.await;
let begin = tokio::time::timeout(Duration::from_secs(1), rx_event.recv())
.await
.expect("timed out waiting for failed exec begin event")
.expect("event channel closed");
let codex_protocol::protocol::EventMsg::ExecCommandBegin(begin_event) = begin.msg else {
panic!("expected ExecCommandBegin event");
};
assert_eq!(begin_event.call_id, "call-unified-pre-spawn-denied");
assert_eq!(begin_event.process_id.as_deref(), Some("456"));
let end = tokio::time::timeout(Duration::from_secs(1), rx_event.recv())
.await
.expect("timed out waiting for failed exec end event")
.expect("event channel closed");
let codex_protocol::protocol::EventMsg::ExecCommandEnd(end_event) = end.msg else {
panic!("expected ExecCommandEnd event");
};
assert_eq!(end_event.call_id, "call-unified-pre-spawn-denied");
assert_eq!(end_event.process_id.as_deref(), Some("456"));
assert_eq!(
end_event.status,
codex_protocol::protocol::ExecCommandStatus::Failed
);
assert_eq!(end_event.exit_code, 71);
assert_eq!(end_event.aggregated_output, denial);
}
#[test]
fn pruning_prefers_exited_processes_outside_recently_used() {
let now = Instant::now();