//! Apply Patch runtime: executes verified patches under the orchestrator.//!//! Assumes `apply_patch` verification/approval happened upstream. Reuses the//! selected turn environment filesystem for both local and remote turns, with//! sandboxing enforced by the explicit filesystem sandbox context.use crate::exec::is_likely_sandbox_denied;use crate::guardian::GuardianApprovalRequest;use crate::guardian::review_approval_request;use crate::session::turn_context::TurnEnvironment;use crate::tools::hook_names::HookToolName;use crate::tools::sandboxing::Approvable;use crate::tools::sandboxing::ApprovalCtx;use crate::tools::sandboxing::ExecApprovalRequirement;use crate::tools::sandboxing::PermissionRequestPayload;use crate::tools::sandboxing::SandboxAttempt;use crate::tools::sandboxing::Sandboxable;use crate::tools::sandboxing::ToolCtx;use crate::tools::sandboxing::ToolError;use crate::tools::sandboxing::ToolRuntime;use crate::tools::sandboxing::with_cached_approval;use codex_apply_patch::AppliedPatchDelta;use codex_apply_patch::ApplyPatchAction;use codex_exec_server::FileSystemSandboxContext;use codex_protocol::error::CodexErr;use codex_protocol::error::SandboxErr;use codex_protocol::exec_output::ExecToolCallOutput;use codex_protocol::exec_output::StreamOutput;use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::FileChange;use codex_protocol::protocol::ReviewDecision;use codex_sandboxing::SandboxType;use codex_sandboxing::SandboxablePreference;use codex_sandboxing::policy_transforms::effective_permission_profile;use codex_utils_absolute_path::AbsolutePathBuf;use futures::future::BoxFuture;use std::path::PathBuf;use std::time::Instant;#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize)]pub(crate) struct ApplyPatchApprovalKey { environment_id: String, path: AbsolutePathBuf,}#[derive(Debug)]pub struct ApplyPatchRequest { pub turn_environment: TurnEnvironment, pub action: ApplyPatchAction, pub file_paths: Vec<AbsolutePathBuf>, pub changes: std::collections::HashMap<PathBuf, FileChange>, pub exec_approval_requirement: ExecApprovalRequirement, pub additional_permissions: Option<AdditionalPermissionProfile>, pub permissions_preapproved: bool,}#[derive(Default)]pub struct ApplyPatchRuntime { committed_delta: AppliedPatchDelta,}#[derive(Debug)]pub struct ApplyPatchRuntimeOutput { pub exec_output: ExecToolCallOutput, pub delta: AppliedPatchDelta,}impl ApplyPatchRuntime { pub fn new() -> Self { Self::default() } pub fn committed_delta(&self) -> &AppliedPatchDelta { &self.committed_delta } fn build_guardian_review_request( req: &ApplyPatchRequest, call_id: &str, ) -> GuardianApprovalRequest { GuardianApprovalRequest::ApplyPatch { id: call_id.to_string(), cwd: req.action.cwd.clone(), files: req.file_paths.clone(), patch: req.action.patch.clone(), } } fn file_system_sandbox_context_for_attempt( req: &ApplyPatchRequest, attempt: &SandboxAttempt<'_>, ) -> Option<FileSystemSandboxContext> { if attempt.sandbox == SandboxType::None { return None; } let permissions = effective_permission_profile(attempt.permissions, req.additional_permissions.as_ref()); Some(FileSystemSandboxContext { permissions: permissions.into(), cwd: Some(attempt.sandbox_cwd.clone()), windows_sandbox_level: attempt.windows_sandbox_level, windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop, use_legacy_landlock: attempt.use_legacy_landlock, }) }}impl Sandboxable for ApplyPatchRuntime { fn sandbox_preference(&self) -> SandboxablePreference { SandboxablePreference::Auto } fn escalate_on_failure(&self) -> bool { true }}impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime { type ApprovalKey = ApplyPatchApprovalKey; fn approval_keys(&self, req: &ApplyPatchRequest) -> Vec<Self::ApprovalKey> { req.file_paths .iter() .cloned() .map(|path| ApplyPatchApprovalKey { environment_id: req.turn_environment.environment_id.clone(), path, }) .collect() } fn start_approval_async<'a>( &'a mut self, req: &'a ApplyPatchRequest, ctx: ApprovalCtx<'a>, ) -> BoxFuture<'a, ReviewDecision> { let session = ctx.session; let turn = ctx.turn; let call_id = ctx.call_id.to_string(); let retry_reason = ctx.retry_reason.clone(); let approval_keys = self.approval_keys(req); let changes = req.changes.clone(); let guardian_review_id = ctx.guardian_review_id.clone(); Box::pin(async move { if let Some(review_id) = guardian_review_id { let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id); return review_approval_request(session, turn, review_id, action, retry_reason) .await; } if req.permissions_preapproved && retry_reason.is_none() { return ReviewDecision::Approved; } if let Some(reason) = retry_reason { let rx_approve = session .request_patch_approval( turn, call_id, changes.clone(), Some(reason), /*grant_root*/ None, ) .await; return rx_approve.await.unwrap_or_default(); } with_cached_approval( &session.services, "apply_patch", approval_keys, || async move { let rx_approve = session .request_patch_approval( turn, call_id, changes, /*reason*/ None, /*grant_root*/ None, ) .await; rx_approve.await.unwrap_or_default() }, ) .await }) } fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool { match policy { AskForApproval::Never => false, AskForApproval::Granular(granular_config) => granular_config.allows_sandbox_approval(), AskForApproval::OnFailure => true, AskForApproval::OnRequest => true, AskForApproval::UnlessTrusted => true, } } // apply_patch approvals are decided upstream by assess_patch_safety. // // This override ensures the orchestrator runs the patch approval flow when required instead // of falling back to the global exec approval policy. fn exec_approval_requirement( &self, req: &ApplyPatchRequest, ) -> Option<ExecApprovalRequirement> { Some(req.exec_approval_requirement.clone()) } fn permission_request_payload( &self, req: &ApplyPatchRequest, ) -> Option<PermissionRequestPayload> { Some(PermissionRequestPayload { tool_name: HookToolName::apply_patch(), tool_input: serde_json::json!({ "command": req.action.patch }), }) }}impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRuntime { fn sandbox_cwd<'a>(&self, req: &'a ApplyPatchRequest) -> Option<&'a AbsolutePathBuf> { Some(&req.action.cwd) } async fn run( &mut self, req: &ApplyPatchRequest, attempt: &SandboxAttempt<'_>, _ctx: &ToolCtx, ) -> Result<ApplyPatchRuntimeOutput, ToolError> { let started_at = Instant::now(); let fs = req.turn_environment.environment.get_filesystem(); let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let result = codex_apply_patch::apply_patch( &req.action.patch, &req.action.cwd, &mut stdout, &mut stderr, fs.as_ref(), sandbox.as_ref(), ) .await; let stdout = String::from_utf8_lossy(&stdout).into_owned(); let stderr = String::from_utf8_lossy(&stderr).into_owned(); let failed = result.is_err(); let exit_code = if failed { 1 } else { 0 }; let delta = match result { Ok(delta) => delta, Err(failure) => failure.into_parts().1, }; self.committed_delta.append(delta); let output = ExecToolCallOutput { exit_code, stdout: StreamOutput::new(stdout.clone()), stderr: StreamOutput::new(stderr.clone()), aggregated_output: StreamOutput::new(format!("{stdout}{stderr}")), duration: started_at.elapsed(), timed_out: false, }; if failed && is_likely_sandbox_denied(attempt.sandbox, &output) { return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output: Box::new(output), network_policy_decision: None, }))); } Ok(ApplyPatchRuntimeOutput { exec_output: output, delta: self.committed_delta.clone(), }) }}#[cfg(test)]#[path = "apply_patch_tests.rs"]mod tests;