use super::ShellRequest;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::cancel_when_either;
use crate::exec::is_likely_sandbox_denied;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::guardian_rejection_message;
use crate::guardian::guardian_timeout_message;
use crate::guardian::new_guardian_review_id;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::hook_runtime::run_permission_request_hooks;
use crate::sandboxing::ExecOptions;
use crate::sandboxing::ExecRequest;
use crate::sandboxing::SandboxPermissions;
use crate::shell::ShellType;
use crate::tools::runtimes::build_sandbox_command;
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
use crate::tools::runtimes::prepend_zsh_fork_bin_to_path;
use crate::tools::sandboxing::PermissionRequestPayload;
use crate::tools::sandboxing::SandboxAttempt;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::managed_network_for_sandbox_permissions;
use crate::tools::sandboxing::sandbox_permissions_preserving_denied_reads;
use crate::tools::sandboxing::unsandboxed_execution_allowed;
use codex_execpolicy::Decision;
use codex_execpolicy::Evaluation;
use codex_execpolicy::MatchOptions;
use codex_execpolicy::Policy;
use codex_execpolicy::RuleMatch;
use codex_features::Feature;
use codex_hooks::PermissionRequestDecision;
use codex_protocol::config_types::WindowsSandboxLevel;
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::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::GuardianCommandSource;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::ReviewDecision;
use codex_sandboxing::SandboxCommand;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_shell_command::bash::parse_shell_lc_plain_commands;
use codex_shell_command::bash::parse_shell_lc_single_command_prefix;
use codex_shell_escalation::EscalateServer;
use codex_shell_escalation::EscalationDecision;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
use codex_shell_escalation::EscalationPolicy;
use codex_shell_escalation::EscalationPolicyFuture;
use codex_shell_escalation::EscalationSession;
use codex_shell_escalation::ExecParams;
use codex_shell_escalation::ExecResult;
use codex_shell_escalation::PreparedExec;
use codex_shell_escalation::ResolvedPermissionProfile;
use codex_shell_escalation::ShellCommandExecutor;
use codex_shell_escalation::ShellCommandExecutorFuture;
use codex_shell_escalation::Stopwatch;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;

pub(crate) struct PreparedUnifiedExecZshFork {
    pub(crate) exec_request: ExecRequest,
    pub(crate) escalation_session: EscalationSession,
}

const PROMPT_CONFLICT_REASON: &str =
    "approval required by policy, but AskForApproval is set to Never";
const REJECT_SANDBOX_APPROVAL_REASON: &str =
    "approval required by policy, but AskForApproval::Granular.sandbox_approval is false";
const REJECT_RULES_APPROVAL_REASON: &str =
    "approval required by policy rule, but AskForApproval::Granular.rules is false";
fn approval_sandbox_permissions(
    sandbox_permissions: SandboxPermissions,
    additional_permissions_preapproved: bool,
) -> SandboxPermissions {
    if additional_permissions_preapproved
        && matches!(
            sandbox_permissions,
            SandboxPermissions::WithAdditionalPermissions
        )
    {
        SandboxPermissions::UseDefault
    } else {
        sandbox_permissions
    }
}

pub(super) async fn try_run_zsh_fork(
    req: &ShellRequest,
    attempt: &SandboxAttempt<'_>,
    ctx: &ToolCtx,
    command: &[String],
) -> Result<Option<ExecToolCallOutput>, ToolError> {
    let Some(shell_zsh_path) = ctx.session.services.shell_zsh_path.as_ref() else {
        tracing::warn!("ZshFork backend specified, but shell_zsh_path is not configured.");
        return Ok(None);
    };
    if !ctx.session.features().enabled(Feature::ShellZshFork) {
        tracing::warn!("ZshFork backend specified, but ShellZshFork feature is not enabled.");
        return Ok(None);
    }
    if !matches!(ctx.session.user_shell().shell_type, ShellType::Zsh) {
        tracing::warn!("ZshFork backend specified, but user shell is not Zsh.");
        return Ok(None);
    }

    let (attempt_file_system_sandbox_policy, _) = attempt.permissions.to_runtime_permissions();
    let sandbox_permissions = sandbox_permissions_preserving_denied_reads(
        req.sandbox_permissions,
        &attempt_file_system_sandbox_policy,
    );
    let req = &ShellRequest {
        sandbox_permissions,
        ..req.clone()
    };
    let mut env = exec_env_for_sandbox_permissions(&req.env, req.sandbox_permissions);
    prepend_zsh_fork_bin_to_path(&mut env, shell_zsh_path);
    let command =
        build_sandbox_command(command, &req.cwd, &env, req.additional_permissions.clone())?;
    let options = ExecOptions {
        expiration: req.timeout_ms.into(),
        capture_policy: ExecCapturePolicy::ShellTool,
    };
    let sandbox_exec_request = attempt
        .env_for(
            command,
            options,
            managed_network_for_sandbox_permissions(req.network.as_ref(), req.sandbox_permissions),
        )
        .map_err(ToolError::Codex)?;
    let crate::sandboxing::ExecRequest {
        command,
        cwd: sandbox_cwd,
        env: sandbox_env,
        exec_server_env_config: _,
        network: sandbox_network,
        expiration: _sandbox_expiration,
        capture_policy: _capture_policy,
        sandbox,
        windows_sandbox_policy_cwd: sandbox_policy_cwd,
        windows_sandbox_workspace_roots,
        windows_sandbox_level,
        windows_sandbox_private_desktop: _windows_sandbox_private_desktop,
        permission_profile,
        file_system_sandbox_policy,
        network_sandbox_policy,
        windows_sandbox_filesystem_overrides: _windows_sandbox_filesystem_overrides,
        arg0,
    } = sandbox_exec_request;
    let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
    let effective_timeout = Duration::from_millis(
        req.timeout_ms
            .unwrap_or(crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS),
    );
    let exec_policy = Arc::new(RwLock::new(
        ctx.session.services.exec_policy.current().as_ref().clone(),
    ));
    let command_executor = CoreShellCommandExecutor {
        command,
        cwd: sandbox_cwd,
        permission_profile,
        file_system_sandbox_policy,
        network_sandbox_policy,
        sandbox,
        env: sandbox_env,
        network: sandbox_network,
        windows_sandbox_level,
        arg0,
        sandbox_policy_cwd,
        windows_sandbox_workspace_roots,
        codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
        use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
    };
    let main_execve_wrapper_exe = ctx
        .session
        .services
        .main_execve_wrapper_exe
        .clone()
        .ok_or_else(|| {
            ToolError::Rejected(
                "zsh fork feature enabled, but execve wrapper is not configured".to_string(),
            )
        })?;
    let exec_params = ExecParams {
        command: script,
        workdir: req.cwd.to_string_lossy().to_string(),
        timeout_ms: Some(effective_timeout.as_millis() as u64),
        login: Some(login),
    };

    // Note that Stopwatch starts immediately upon creation, so currently we try
    // to minimize the time between creating the Stopwatch and starting the
    // escalation server.
    let stopwatch = Stopwatch::new(effective_timeout);
    let mut cancel_token = stopwatch.cancellation_token();
    if let Some(cancellation) = attempt.network_denial_cancellation_token.clone() {
        cancel_token = cancel_when_either(cancel_token, cancellation);
    }
    let approval_sandbox_permissions = approval_sandbox_permissions(
        req.sandbox_permissions,
        req.additional_permissions_preapproved,
    );
    let escalation_policy = CoreShellActionProvider {
        policy: Arc::clone(&exec_policy),
        session: Arc::clone(&ctx.session),
        turn: Arc::clone(&ctx.turn),
        call_id: ctx.call_id.clone(),
        tool_name: GuardianCommandSource::Shell,
        approval_policy: ctx.turn.approval_policy.value(),
        permission_profile: command_executor.permission_profile.clone(),
        file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(),
        sandbox_permissions: req.sandbox_permissions,
        approval_sandbox_permissions,
        prompt_permissions: req.additional_permissions.clone(),
        stopwatch: stopwatch.clone(),
    };

    let escalate_server = EscalateServer::new(
        shell_zsh_path.clone(),
        main_execve_wrapper_exe,
        escalation_policy,
    );

    let exec_result = escalate_server
        .exec(exec_params, cancel_token, Arc::new(command_executor))
        .await
        .map_err(|err| ToolError::Rejected(err.to_string()))?;

    map_exec_result(attempt.sandbox, exec_result).map(Some)
}

pub(crate) async fn prepare_unified_exec_zsh_fork(
    req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest,
    _attempt: &SandboxAttempt<'_>,
    ctx: &ToolCtx,
    exec_request: ExecRequest,
    shell_zsh_path: &std::path::Path,
    main_execve_wrapper_exe: &std::path::Path,
) -> Result<Option<PreparedUnifiedExecZshFork>, ToolError> {
    let parsed = match extract_shell_script(&exec_request.command) {
        Ok(parsed) => parsed,
        Err(err) => {
            tracing::warn!("ZshFork unified exec fallback: {err:?}");
            return Ok(None);
        }
    };
    if parsed.program != shell_zsh_path.to_string_lossy() {
        tracing::warn!(
            "ZshFork backend specified, but unified exec command targets `{}` instead of `{}`.",
            parsed.program,
            shell_zsh_path.display(),
        );
        return Ok(None);
    }

    let exec_policy = Arc::new(RwLock::new(
        ctx.session.services.exec_policy.current().as_ref().clone(),
    ));
    let command_executor = CoreShellCommandExecutor {
        command: exec_request.command.clone(),
        cwd: exec_request.cwd.clone(),
        permission_profile: exec_request.permission_profile.clone(),
        file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
        network_sandbox_policy: exec_request.network_sandbox_policy,
        sandbox: exec_request.sandbox,
        env: exec_request.env.clone(),
        network: exec_request.network.clone(),
        windows_sandbox_level: exec_request.windows_sandbox_level,
        arg0: exec_request.arg0.clone(),
        sandbox_policy_cwd: exec_request.windows_sandbox_policy_cwd.clone(),
        windows_sandbox_workspace_roots: exec_request.windows_sandbox_workspace_roots.clone(),
        codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
        use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
    };
    let escalation_policy = CoreShellActionProvider {
        policy: Arc::clone(&exec_policy),
        session: Arc::clone(&ctx.session),
        turn: Arc::clone(&ctx.turn),
        call_id: ctx.call_id.clone(),
        tool_name: GuardianCommandSource::UnifiedExec,
        approval_policy: ctx.turn.approval_policy.value(),
        permission_profile: exec_request.permission_profile.clone(),
        file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
        sandbox_permissions: req.sandbox_permissions,
        approval_sandbox_permissions: approval_sandbox_permissions(
            req.sandbox_permissions,
            req.additional_permissions_preapproved,
        ),
        prompt_permissions: req.additional_permissions.clone(),
        stopwatch: Stopwatch::unlimited(),
    };

    let escalate_server = EscalateServer::new(
        shell_zsh_path.to_path_buf(),
        main_execve_wrapper_exe.to_path_buf(),
        escalation_policy,
    );
    let escalation_session = escalate_server
        .start_session(CancellationToken::new(), Arc::new(command_executor))
        .map_err(|err| ToolError::Rejected(err.to_string()))?;
    let mut exec_request = exec_request;
    exec_request.env.extend(escalation_session.env().clone());
    Ok(Some(PreparedUnifiedExecZshFork {
        exec_request,
        escalation_session,
    }))
}

struct CoreShellActionProvider {
    policy: Arc<RwLock<Policy>>,
    session: Arc<crate::session::session::Session>,
    turn: Arc<crate::session::turn_context::TurnContext>,
    call_id: String,
    tool_name: GuardianCommandSource,
    approval_policy: AskForApproval,
    permission_profile: PermissionProfile,
    file_system_sandbox_policy: FileSystemSandboxPolicy,
    sandbox_permissions: SandboxPermissions,
    approval_sandbox_permissions: SandboxPermissions,
    prompt_permissions: Option<AdditionalPermissionProfile>,
    stopwatch: Stopwatch,
}

#[allow(clippy::large_enum_variant)]
enum DecisionSource {
    PrefixRule,
    /// Often, this is `is_safe_command()`.
    UnmatchedCommandFallback,
}

struct PromptDecision {
    decision: ReviewDecision,
    guardian_review_id: Option<String>,
    rejection_message: Option<String>,
}

fn execve_prompt_is_rejected_by_policy(
    approval_policy: AskForApproval,
    decision_source: &DecisionSource,
) -> Option<&'static str> {
    match (approval_policy, decision_source) {
        (AskForApproval::Never, _) => Some(PROMPT_CONFLICT_REASON),
        (AskForApproval::Granular(granular_config), DecisionSource::PrefixRule)
            if !granular_config.allows_rules_approval() =>
        {
            Some(REJECT_RULES_APPROVAL_REASON)
        }
        (AskForApproval::Granular(granular_config), DecisionSource::UnmatchedCommandFallback)
            if !granular_config.allows_sandbox_approval() =>
        {
            Some(REJECT_SANDBOX_APPROVAL_REASON)
        }
        _ => None,
    }
}

impl CoreShellActionProvider {
    fn decision_driven_by_policy(matched_rules: &[RuleMatch], decision: Decision) -> bool {
        matched_rules.iter().any(|rule_match| {
            !matches!(rule_match, RuleMatch::HeuristicsRuleMatch { .. })
                && rule_match.decision() == decision
        })
    }

    fn shell_request_escalation_execution(
        sandbox_permissions: SandboxPermissions,
        permission_profile: &PermissionProfile,
        file_system_sandbox_policy: &FileSystemSandboxPolicy,
        additional_permissions: Option<&AdditionalPermissionProfile>,
    ) -> EscalationExecution {
        match sandbox_permissions {
            SandboxPermissions::UseDefault => EscalationExecution::TurnDefault,
            SandboxPermissions::RequireEscalated => {
                if unsandboxed_execution_allowed(file_system_sandbox_policy) {
                    EscalationExecution::Unsandboxed
                } else {
                    EscalationExecution::TurnDefault
                }
            }
            SandboxPermissions::WithAdditionalPermissions => additional_permissions
                .map(|_| {
                    // Shell request additional permissions were already normalized and
                    // merged into the first-attempt sandbox policy.
                    EscalationExecution::Permissions(
                        EscalationPermissions::ResolvedPermissionProfile(
                            ResolvedPermissionProfile {
                                permission_profile: permission_profile.clone(),
                            },
                        ),
                    )
                })
                .unwrap_or(EscalationExecution::TurnDefault),
        }
    }

    async fn prompt(
        &self,
        program: &AbsolutePathBuf,
        argv: &[String],
        workdir: &AbsolutePathBuf,
        stopwatch: &Stopwatch,
        additional_permissions: Option<AdditionalPermissionProfile>,
    ) -> anyhow::Result<PromptDecision> {
        let command = join_program_and_argv(program, argv);
        let workdir = workdir.clone();
        let session = self.session.clone();
        let turn = self.turn.clone();
        let call_id = self.call_id.clone();
        let approval_id = Some(Uuid::new_v4().to_string());
        let source = self.tool_name;
        let guardian_review_id = routes_approval_to_guardian(&turn).then(new_guardian_review_id);
        Ok(stopwatch
            .pause_for(async move {
                // 1) Run PermissionRequest hooks
                let permission_request = PermissionRequestPayload::bash(
                    codex_shell_command::parse_command::shlex_join(&command),
                    /*description*/ None,
                );
                let effective_approval_id = approval_id.clone().unwrap_or_else(|| call_id.clone());
                match run_permission_request_hooks(
                    &session,
                    &turn,
                    &effective_approval_id,
                    permission_request,
                )
                .await
                {
                    Some(PermissionRequestDecision::Allow) => {
                        return PromptDecision {
                            decision: ReviewDecision::Approved,
                            guardian_review_id: None,
                            rejection_message: None,
                        };
                    }
                    Some(PermissionRequestDecision::Deny { message }) => {
                        return PromptDecision {
                            decision: ReviewDecision::Denied,
                            guardian_review_id: None,
                            rejection_message: Some(message),
                        };
                    }
                    None => {}
                }

                // 2) Route to Guardian if configured
                if let Some(review_id) = guardian_review_id.clone() {
                    let decision = review_approval_request(
                        &session,
                        &turn,
                        review_id.clone(),
                        GuardianApprovalRequest::Execve {
                            id: call_id.clone(),
                            source,
                            program: program.to_string_lossy().into_owned(),
                            argv: argv.to_vec(),
                            cwd: workdir.clone(),
                            additional_permissions,
                        },
                        /*retry_reason*/ None,
                    )
                    .await;
                    return PromptDecision {
                        decision,
                        guardian_review_id,
                        rejection_message: None,
                    };
                }

                // 3) Fall back to regular user prompt
                let decision = session
                    .request_command_approval(
                        &turn,
                        call_id,
                        approval_id,
                        command,
                        workdir.clone(),
                        /*reason*/ None,
                        /*network_approval_context*/ None,
                        /*proposed_execpolicy_amendment*/ None,
                        additional_permissions,
                        Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
                    )
                    .await;
                PromptDecision {
                    decision,
                    guardian_review_id: None,
                    rejection_message: None,
                }
            })
            .await)
    }

    #[allow(clippy::too_many_arguments)]
    async fn process_decision(
        &self,
        decision: Decision,
        needs_escalation: bool,
        program: &AbsolutePathBuf,
        argv: &[String],
        workdir: &AbsolutePathBuf,
        prompt_permissions: Option<AdditionalPermissionProfile>,
        escalation_execution: EscalationExecution,
        decision_source: DecisionSource,
    ) -> anyhow::Result<EscalationDecision> {
        let action = match decision {
            Decision::Forbidden => {
                EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
            }
            Decision::Prompt => {
                if execve_prompt_is_rejected_by_policy(self.approval_policy, &decision_source)
                    .is_some()
                {
                    EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
                } else {
                    let prompt_decision = self
                        .prompt(program, argv, workdir, &self.stopwatch, prompt_permissions)
                        .await?;
                    match prompt_decision.decision {
                        ReviewDecision::Approved
                        | ReviewDecision::ApprovedForSession
                        | ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
                            if needs_escalation {
                                EscalationDecision::escalate(escalation_execution.clone())
                            } else {
                                EscalationDecision::run()
                            }
                        }
                        ReviewDecision::NetworkPolicyAmendment {
                            network_policy_amendment,
                        } => match network_policy_amendment.action {
                            NetworkPolicyRuleAction::Allow => {
                                if needs_escalation {
                                    EscalationDecision::escalate(escalation_execution.clone())
                                } else {
                                    EscalationDecision::run()
                                }
                            }
                            NetworkPolicyRuleAction::Deny => {
                                EscalationDecision::deny(Some("User denied execution".to_string()))
                            }
                        },
                        ReviewDecision::Denied => {
                            let message = if let Some(message) =
                                prompt_decision.rejection_message.clone()
                            {
                                message
                            } else if let Some(review_id) =
                                prompt_decision.guardian_review_id.as_deref()
                            {
                                guardian_rejection_message(self.session.as_ref(), review_id).await
                            } else {
                                "User denied execution".to_string()
                            };
                            EscalationDecision::deny(Some(message))
                        }
                        ReviewDecision::TimedOut => {
                            EscalationDecision::deny(Some(guardian_timeout_message()))
                        }
                        ReviewDecision::Abort => {
                            EscalationDecision::deny(Some("User cancelled execution".to_string()))
                        }
                    }
                }
            }
            Decision::Allow => {
                if needs_escalation {
                    EscalationDecision::escalate(escalation_execution)
                } else {
                    EscalationDecision::run()
                }
            }
        };
        tracing::debug!(
            "Policy decision for command {program:?} is {decision:?}, leading to escalation action {action:?}",
        );
        Ok(action)
    }
}

// Shell-wrapper parsing is weaker than direct exec interception because it can
// only see the script text, not the final resolved executable path. Keep it
// disabled by default so path-sensitive rules rely on the later authoritative
// execve interception.
const ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING: bool = false;

impl CoreShellActionProvider {
    async fn determine_action(
        &self,
        program: &AbsolutePathBuf,
        argv: &[String],
        workdir: &AbsolutePathBuf,
    ) -> anyhow::Result<EscalationDecision> {
        tracing::debug!(
            "Determining escalation action for command {program:?} with args {argv:?} in {workdir:?}"
        );

        let evaluation = {
            let policy = self.policy.read().await;
            evaluate_intercepted_exec_policy(
                &policy,
                program,
                argv,
                InterceptedExecPolicyContext {
                    approval_policy: self.approval_policy,
                    permission_profile: self.permission_profile.clone(),
                    windows_sandbox_level: self.turn.windows_sandbox_level,
                    sandbox_permissions: self.approval_sandbox_permissions,
                    enable_shell_wrapper_parsing:
                        ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING,
                },
            )
        };
        // When true, means the Evaluation was due to *.rules, not the
        // fallback function.
        let decision_driven_by_policy =
            Self::decision_driven_by_policy(&evaluation.matched_rules, evaluation.decision);
        let unsandboxed_allowed = unsandboxed_execution_allowed(&self.file_system_sandbox_policy);
        let needs_escalation = match self.sandbox_permissions {
            SandboxPermissions::UseDefault => unsandboxed_allowed && decision_driven_by_policy,
            SandboxPermissions::RequireEscalated => unsandboxed_allowed,
            SandboxPermissions::WithAdditionalPermissions => true,
        };

        let decision_source = if decision_driven_by_policy {
            DecisionSource::PrefixRule
        } else {
            DecisionSource::UnmatchedCommandFallback
        };
        let escalation_execution = match decision_source {
            DecisionSource::PrefixRule if unsandboxed_allowed => EscalationExecution::Unsandboxed,
            DecisionSource::PrefixRule => EscalationExecution::TurnDefault,
            DecisionSource::UnmatchedCommandFallback => Self::shell_request_escalation_execution(
                self.sandbox_permissions,
                &self.permission_profile,
                &self.file_system_sandbox_policy,
                self.prompt_permissions.as_ref(),
            ),
        };
        self.process_decision(
            evaluation.decision,
            needs_escalation,
            program,
            argv,
            workdir,
            self.prompt_permissions.clone(),
            escalation_execution,
            decision_source,
        )
        .await
    }
}

impl EscalationPolicy for CoreShellActionProvider {
    fn determine_action<'a>(
        &'a self,
        program: &'a AbsolutePathBuf,
        argv: &'a [String],
        workdir: &'a AbsolutePathBuf,
    ) -> EscalationPolicyFuture<'a> {
        Box::pin(CoreShellActionProvider::determine_action(
            self, program, argv, workdir,
        ))
    }
}

fn evaluate_intercepted_exec_policy(
    policy: &Policy,
    program: &AbsolutePathBuf,
    argv: &[String],
    context: InterceptedExecPolicyContext,
) -> Evaluation {
    let InterceptedExecPolicyContext {
        approval_policy,
        permission_profile,
        windows_sandbox_level,
        sandbox_permissions,
        enable_shell_wrapper_parsing,
    } = context;
    let CandidateCommands {
        commands,
        used_complex_parsing,
    } = if enable_shell_wrapper_parsing {
        // In this codepath, the first argument in `commands` could be a bare
        // name like `find` instead of an absolute path like `/usr/bin/find`.
        // It could also be a shell built-in like `echo`.
        commands_for_intercepted_exec_policy(program, argv)
    } else {
        // In this codepath, `commands` has a single entry where the program
        // is always an absolute path.
        CandidateCommands {
            commands: vec![join_program_and_argv(program, argv)],
            used_complex_parsing: false,
        }
    };

    let fallback = |cmd: &[String]| {
        crate::exec_policy::render_decision_for_unmatched_command(
            cmd,
            crate::exec_policy::UnmatchedCommandContext {
                approval_policy,
                permission_profile: &permission_profile,
                windows_sandbox_level,
                sandbox_permissions,
                used_complex_parsing,
                command_origin: crate::exec_policy::ExecPolicyCommandOrigin::Generic,
            },
        )
    };

    policy.check_multiple_with_options(
        commands.iter(),
        &fallback,
        &MatchOptions {
            resolve_host_executables: true,
        },
    )
}

#[derive(Clone)]
struct InterceptedExecPolicyContext {
    approval_policy: AskForApproval,
    permission_profile: PermissionProfile,
    windows_sandbox_level: WindowsSandboxLevel,
    sandbox_permissions: SandboxPermissions,
    enable_shell_wrapper_parsing: bool,
}

struct CandidateCommands {
    commands: Vec<Vec<String>>,
    used_complex_parsing: bool,
}

fn commands_for_intercepted_exec_policy(
    program: &AbsolutePathBuf,
    argv: &[String],
) -> CandidateCommands {
    if let [_, flag, script] = argv {
        let shell_command = [
            program.to_string_lossy().to_string(),
            flag.clone(),
            script.clone(),
        ];
        if let Some(commands) = parse_shell_lc_plain_commands(&shell_command) {
            return CandidateCommands {
                commands,
                used_complex_parsing: false,
            };
        }
        if let Some(single_command) = parse_shell_lc_single_command_prefix(&shell_command) {
            return CandidateCommands {
                commands: vec![single_command],
                used_complex_parsing: true,
            };
        }
    }

    CandidateCommands {
        commands: vec![join_program_and_argv(program, argv)],
        used_complex_parsing: false,
    }
}

struct CoreShellCommandExecutor {
    command: Vec<String>,
    cwd: AbsolutePathBuf,
    permission_profile: PermissionProfile,
    file_system_sandbox_policy: FileSystemSandboxPolicy,
    network_sandbox_policy: NetworkSandboxPolicy,
    sandbox: SandboxType,
    env: HashMap<String, String>,
    network: Option<codex_network_proxy::NetworkProxy>,
    windows_sandbox_level: WindowsSandboxLevel,
    arg0: Option<String>,
    sandbox_policy_cwd: AbsolutePathBuf,
    windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
    codex_linux_sandbox_exe: Option<PathBuf>,
    use_legacy_landlock: bool,
}

struct PrepareSandboxedExecParams<'a> {
    command: Vec<String>,
    workdir: &'a AbsolutePathBuf,
    env: HashMap<String, String>,
    permission_profile: &'a PermissionProfile,
    additional_permissions: Option<AdditionalPermissionProfile>,
}

impl ShellCommandExecutor for CoreShellCommandExecutor {
    fn run(
        &self,
        _command: Vec<String>,
        _cwd: PathBuf,
        env_overlay: HashMap<String, String>,
        cancel_rx: CancellationToken,
        after_spawn: Option<Box<dyn FnOnce() + Send>>,
    ) -> ShellCommandExecutorFuture<'_, ExecResult> {
        Box::pin(CoreShellCommandExecutor::run(
            self,
            env_overlay,
            cancel_rx,
            after_spawn,
        ))
    }

    fn prepare_escalated_exec<'a>(
        &'a self,
        program: &'a AbsolutePathBuf,
        argv: &'a [String],
        workdir: &'a AbsolutePathBuf,
        env: HashMap<String, String>,
        execution: EscalationExecution,
    ) -> ShellCommandExecutorFuture<'a, PreparedExec> {
        Box::pin(CoreShellCommandExecutor::prepare_escalated_exec(
            self, program, argv, workdir, env, execution,
        ))
    }
}

impl CoreShellCommandExecutor {
    async fn run(
        &self,
        env_overlay: HashMap<String, String>,
        cancel_rx: CancellationToken,
        after_spawn: Option<Box<dyn FnOnce() + Send>>,
    ) -> anyhow::Result<ExecResult> {
        let mut exec_env = self.env.clone();
        // `env_overlay` comes from `EscalationSession::env()`, so merge only the
        // wrapper/socket variables into the base shell environment.
        for var in ["CODEX_ESCALATE_SOCKET", "EXEC_WRAPPER"] {
            if let Some(value) = env_overlay.get(var) {
                exec_env.insert(var.to_string(), value.clone());
            }
        }

        let result = crate::sandboxing::execute_exec_request_with_after_spawn(
            crate::sandboxing::ExecRequest {
                command: self.command.clone(),
                cwd: self.cwd.clone(),
                env: exec_env,
                exec_server_env_config: None,
                network: self.network.clone(),
                expiration: ExecExpiration::Cancellation(cancel_rx),
                capture_policy: ExecCapturePolicy::ShellTool,
                sandbox: self.sandbox,
                windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone(),
                windows_sandbox_workspace_roots: self.windows_sandbox_workspace_roots.clone(),
                windows_sandbox_level: self.windows_sandbox_level,
                windows_sandbox_private_desktop: false,
                permission_profile: self.permission_profile.clone(),
                file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
                network_sandbox_policy: self.network_sandbox_policy,
                windows_sandbox_filesystem_overrides: None,
                arg0: self.arg0.clone(),
            },
            /*stdout_stream*/ None,
            after_spawn,
        )
        .await?;

        Ok(ExecResult {
            exit_code: result.exit_code,
            stdout: result.stdout.text,
            stderr: result.stderr.text,
            output: result.aggregated_output.text,
            duration: result.duration,
            timed_out: result.timed_out,
        })
    }

    async fn prepare_escalated_exec(
        &self,
        program: &AbsolutePathBuf,
        argv: &[String],
        workdir: &AbsolutePathBuf,
        env: HashMap<String, String>,
        execution: EscalationExecution,
    ) -> anyhow::Result<PreparedExec> {
        let command = join_program_and_argv(program, argv);
        let Some(first_arg) = argv.first() else {
            return Err(anyhow::anyhow!(
                "intercepted exec request must contain argv[0]"
            ));
        };

        let prepared = match execution {
            EscalationExecution::Unsandboxed => PreparedExec {
                command,
                cwd: workdir.to_path_buf(),
                env: exec_env_for_sandbox_permissions(&env, SandboxPermissions::RequireEscalated),
                arg0: Some(first_arg.clone()),
            },
            EscalationExecution::TurnDefault => {
                self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
                    command,
                    workdir,
                    env,
                    permission_profile: &self.permission_profile,
                    additional_permissions: None,
                })?
            }
            EscalationExecution::Permissions(
                EscalationPermissions::AdditionalPermissionProfile(permission_profile),
            ) => {
                // Merge additive permissions into the existing turn/request sandbox policy.
                self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
                    command,
                    workdir,
                    env,
                    permission_profile: &self.permission_profile,
                    additional_permissions: Some(permission_profile),
                })?
            }
            EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile(
                permissions,
            )) => {
                // Use a fully specified permission profile instead of merging into the turn policy.
                self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
                    command,
                    workdir,
                    env,
                    permission_profile: &permissions.permission_profile,
                    additional_permissions: None,
                })?
            }
        };

        Ok(prepared)
    }

    #[allow(clippy::too_many_arguments)]
    fn prepare_sandboxed_exec(
        &self,
        params: PrepareSandboxedExecParams<'_>,
    ) -> anyhow::Result<PreparedExec> {
        let PrepareSandboxedExecParams {
            command,
            workdir,
            env,
            permission_profile,
            additional_permissions,
        } = params;
        let (file_system_sandbox_policy, network_sandbox_policy) =
            permission_profile.to_runtime_permissions();
        let (program, args) = command
            .split_first()
            .ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?;
        let sandbox_manager = SandboxManager::new();
        let sandbox = sandbox_manager.select_initial(
            &file_system_sandbox_policy,
            network_sandbox_policy,
            SandboxablePreference::Auto,
            self.windows_sandbox_level,
            self.network.is_some(),
        );
        let cwd = PathUri::from_abs_path(workdir);
        let sandbox_policy_cwd = PathUri::from_abs_path(&self.sandbox_policy_cwd);
        let command = SandboxCommand {
            program: program.clone().into(),
            args: args.to_vec(),
            cwd,
            env,
            additional_permissions,
        };
        let options = ExecOptions {
            expiration: ExecExpiration::DefaultTimeout,
            capture_policy: ExecCapturePolicy::ShellTool,
        };
        let exec_request = sandbox_manager.transform(SandboxTransformRequest {
            command,
            permissions: permission_profile,
            sandbox,
            enforce_managed_network: self.network.is_some(),
            network: self.network.as_ref(),
            sandbox_policy_cwd: &sandbox_policy_cwd,
            codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(),
            use_legacy_landlock: self.use_legacy_landlock,
            windows_sandbox_level: self.windows_sandbox_level,
            windows_sandbox_private_desktop: false,
        })?;
        let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
            exec_request,
            options,
            self.windows_sandbox_workspace_roots.clone(),
        );
        if let Some(network) = exec_request.network.as_ref() {
            network.apply_to_env(&mut exec_request.env);
        }

        Ok(PreparedExec {
            command: exec_request.command,
            cwd: exec_request.cwd.to_path_buf(),
            env: exec_request.env,
            arg0: exec_request.arg0,
        })
    }
}

#[derive(Debug, Eq, PartialEq)]
struct ParsedShellCommand {
    program: String,
    script: String,
    login: bool,
}

fn extract_shell_script(command: &[String]) -> Result<ParsedShellCommand, ToolError> {
    // Commands reaching zsh-fork can be wrapped by environment/sandbox helpers, so
    // we search for the first `-c`/`-lc` triple anywhere in the argv rather
    // than assuming it is the first positional form.
    if let Some((program, script, login)) = command.windows(3).find_map(|parts| match parts {
        [program, flag, script] if flag == "-c" => {
            Some((program.to_owned(), script.to_owned(), false))
        }
        [program, flag, script] if flag == "-lc" => {
            Some((program.to_owned(), script.to_owned(), true))
        }
        _ => None,
    }) {
        return Ok(ParsedShellCommand {
            program,
            script,
            login,
        });
    }

    Err(ToolError::Rejected(
        "unexpected shell command format for zsh-fork execution".to_string(),
    ))
}

fn map_exec_result(
    sandbox: SandboxType,
    result: ExecResult,
) -> Result<ExecToolCallOutput, ToolError> {
    let output = ExecToolCallOutput {
        exit_code: result.exit_code,
        stdout: StreamOutput::new(result.stdout.clone()),
        stderr: StreamOutput::new(result.stderr.clone()),
        aggregated_output: StreamOutput::new(result.output.clone()),
        duration: result.duration,
        timed_out: result.timed_out,
    };

    if result.timed_out {
        return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout {
            output: Box::new(output),
        })));
    }

    if is_likely_sandbox_denied(sandbox, &output) {
        return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
            output: Box::new(output),
            network_policy_decision: None,
        })));
    }

    Ok(output)
}

/// Convert an intercepted exec `(program, argv)` into a command vector suitable
/// for display and policy parsing.
///
/// The intercepted `argv` includes `argv[0]`, but once we have normalized the
/// executable path in `program`, we should replace the original `argv[0]`
/// rather than duplicating it as an apparent user argument.
fn join_program_and_argv(program: &AbsolutePathBuf, argv: &[String]) -> Vec<String> {
    std::iter::once(program.to_string_lossy().to_string())
        .chain(argv.iter().skip(1).cloned())
        .collect::<Vec<_>>()
}

#[cfg(test)]
#[path = "unix_escalation_tests.rs"]
mod tests;
