Codex Handbook
core/src/tools/runtimes/apply_patch.rs 272 lines
//! 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;