diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8acf8b20..93ba73eb 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -916,6 +916,7 @@ impl Session { duration, exit_code, timed_out: _, + .. } = output; // Send full stdout/stderr to clients; do not truncate. let stdout = stdout.text.clone(); @@ -980,15 +981,28 @@ impl Session { let sub_id = context.sub_id.clone(); let call_id = context.call_id.clone(); - self.on_exec_command_begin(turn_diff_tracker.clone(), context.clone()) - .await; - + let begin_turn_diff = turn_diff_tracker.clone(); + let begin_context = context.clone(); + let session = self; let result = self .services .executor - .run(request, self, approval_policy, &context) + .run(request, self, approval_policy, &context, move || { + let turn_diff = begin_turn_diff.clone(); + let ctx = begin_context.clone(); + async move { + session.on_exec_command_begin(turn_diff, ctx).await; + } + }) .await; + if matches!( + &result, + Err(ExecError::Function(FunctionCallError::Denied(_))) + ) { + return result; + } + let normalized = normalize_exec_result(&result); let borrowed = normalized.event_output(); @@ -2262,7 +2276,8 @@ async fn try_run_turn( response: Some(response), }); } - Err(FunctionCallError::RespondToModel(message)) => { + Err(FunctionCallError::RespondToModel(message)) + | Err(FunctionCallError::Denied(message)) => { let response = ResponseInputItem::FunctionCallOutput { call_id: String::new(), output: FunctionCallOutputPayload { diff --git a/codex-rs/core/src/executor/mod.rs b/codex-rs/core/src/executor/mod.rs index 97d7b292..3f1e02df 100644 --- a/codex-rs/core/src/executor/mod.rs +++ b/codex-rs/core/src/executor/mod.rs @@ -60,5 +60,9 @@ pub mod errors { pub(crate) fn rejection(msg: impl Into) -> Self { FunctionCallError::RespondToModel(msg.into()).into() } + + pub(crate) fn denied(msg: impl Into) -> Self { + FunctionCallError::Denied(msg.into()).into() + } } } diff --git a/codex-rs/core/src/executor/runner.rs b/codex-rs/core/src/executor/runner.rs index e13016e3..072debb5 100644 --- a/codex-rs/core/src/executor/runner.rs +++ b/codex-rs/core/src/executor/runner.rs @@ -1,3 +1,4 @@ +use std::future::Future; use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; @@ -74,13 +75,18 @@ impl Executor { /// Runs a prepared execution request end-to-end: prepares parameters, decides on /// sandbox placement (prompting the user when necessary), launches the command, /// and lets the backend post-process the final output. - pub(crate) async fn run( + pub(crate) async fn run( &self, mut request: ExecutionRequest, session: &Session, approval_policy: AskForApproval, context: &ExecCommandContext, - ) -> Result { + on_exec_begin: F, + ) -> Result + where + F: FnOnce() -> Fut, + Fut: Future, + { if matches!(request.mode, ExecutionMode::Shell) { request.params = maybe_translate_shell_command(request.params, session, request.use_shell_profile); @@ -119,7 +125,7 @@ impl Executor { if sandbox_decision.record_session_approval { self.approval_cache.insert(request.approval_command.clone()); } - + on_exec_begin().await; // Step 4: Launch the command within the chosen sandbox. let first_attempt = self .spawn( @@ -210,7 +216,7 @@ impl Executor { Ok(retry_output) } ReviewDecision::Denied | ReviewDecision::Abort => { - Err(ExecError::rejection("exec command rejected by user")) + Err(ExecError::denied("exec command rejected by user")) } } } @@ -301,7 +307,8 @@ pub(crate) fn normalize_exec_result( } Err(err) => { let message = match err { - ExecError::Function(FunctionCallError::RespondToModel(msg)) => msg.clone(), + ExecError::Function(FunctionCallError::RespondToModel(msg)) + | ExecError::Function(FunctionCallError::Denied(msg)) => msg.clone(), ExecError::Codex(e) => get_error_message_ui(e), err => err.to_string(), }; diff --git a/codex-rs/core/src/executor/sandbox.rs b/codex-rs/core/src/executor/sandbox.rs index 5c01ff69..f0a421a2 100644 --- a/codex-rs/core/src/executor/sandbox.rs +++ b/codex-rs/core/src/executor/sandbox.rs @@ -149,7 +149,7 @@ async fn select_shell_sandbox( ReviewDecision::Approved => Ok(SandboxDecision::user_override(false)), ReviewDecision::ApprovedForSession => Ok(SandboxDecision::user_override(true)), ReviewDecision::Denied | ReviewDecision::Abort => { - Err(ExecError::rejection("exec command rejected by user")) + Err(ExecError::denied("exec command rejected by user")) } } } diff --git a/codex-rs/core/src/function_tool.rs b/codex-rs/core/src/function_tool.rs index 240e0436..a25fff61 100644 --- a/codex-rs/core/src/function_tool.rs +++ b/codex-rs/core/src/function_tool.rs @@ -4,6 +4,8 @@ use thiserror::Error; pub enum FunctionCallError { #[error("{0}")] RespondToModel(String), + #[error("{0}")] + Denied(String), #[error("LocalShellCall without call_id or id")] MissingLocalShellCallId, #[error("Fatal error: {0}")] diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 691c6dc0..607697e0 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -238,6 +238,7 @@ fn truncate_function_error(err: FunctionCallError) -> FunctionCallError { FunctionCallError::RespondToModel(msg) => { FunctionCallError::RespondToModel(format_exec_output(&msg)) } + FunctionCallError::Denied(msg) => FunctionCallError::Denied(format_exec_output(&msg)), FunctionCallError::Fatal(msg) => FunctionCallError::Fatal(format_exec_output(&msg)), other => other, }