Codex Handbook
core/src/tools/handlers/shell.rs 242 lines
use codex_features::Feature;use codex_protocol::models::ShellCommandToolCallParams;use serde_json::Value as JsonValue;use std::sync::Arc;use tokio_util::sync::CancellationToken;use crate::exec::ExecParams;use crate::exec_policy::ExecApprovalRequest;use crate::function_tool::FunctionCallError;use crate::session::turn_context::TurnContext;use crate::shell::ShellType;use crate::tools::context::FunctionToolOutput;use crate::tools::context::ToolPayload;use crate::tools::events::ToolEmitter;use crate::tools::events::ToolEventCtx;use crate::tools::handlers::apply_granted_turn_permissions;use crate::tools::handlers::apply_patch::intercept_apply_patch;use crate::tools::handlers::implicit_granted_permissions;use crate::tools::handlers::normalize_and_validate_additional_permissions;use crate::tools::handlers::parse_arguments;use crate::tools::orchestrator::ToolOrchestrator;use crate::tools::runtimes::shell::ShellRequest;use crate::tools::runtimes::shell::ShellRuntime;use crate::tools::runtimes::shell::ShellRuntimeBackend;use crate::tools::sandboxing::ToolCtx;use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::protocol::ExecCommandSource;use codex_tools::ToolName;mod shell_command;pub use shell_command::ShellCommandHandler;pub(crate) use shell_command::ShellCommandHandlerOptions;fn shell_command_payload_command(payload: &ToolPayload) -> Option<String> {    let ToolPayload::Function { arguments } = payload else {        return None;    };    parse_arguments::<ShellCommandToolCallParams>(arguments)        .ok()        .map(|params| params.command)}struct RunExecLikeArgs {    tool_name: ToolName,    exec_params: ExecParams,    cancellation_token: CancellationToken,    hook_command: String,    shell_type: Option<ShellType>,    additional_permissions: Option<AdditionalPermissionProfile>,    prefix_rule: Option<Vec<String>>,    session: Arc<crate::session::session::Session>,    turn: Arc<TurnContext>,    tracker: crate::tools::context::SharedTurnDiffTracker,    call_id: String,    shell_runtime_backend: ShellRuntimeBackend,}async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, FunctionCallError> {    let RunExecLikeArgs {        tool_name,        exec_params,        cancellation_token,        hook_command,        shell_type,        additional_permissions,        prefix_rule,        session,        turn,        tracker,        call_id,        shell_runtime_backend,    } = args;    let Some(turn_environment) = turn.environments.primary() else {        return Err(FunctionCallError::RespondToModel(            "shell is unavailable in this session".to_string(),        ));    };    let fs = turn_environment.environment.get_filesystem();    let explicit_env_overrides = turn.shell_environment_policy.r#set.clone();    let exec_permission_approvals_enabled =        session.features().enabled(Feature::ExecPermissionApprovals);    let requested_additional_permissions = additional_permissions.clone();    let effective_additional_permissions = apply_granted_turn_permissions(        session.as_ref(),        &turn_environment.environment_id,        exec_params.cwd.as_path(),        exec_params.sandbox_permissions,        additional_permissions,    )    .await;    let additional_permissions_allowed = exec_permission_approvals_enabled        || (session.features().enabled(Feature::RequestPermissionsTool)            && effective_additional_permissions.permissions_preapproved);    let normalized_additional_permissions = implicit_granted_permissions(        exec_params.sandbox_permissions,        requested_additional_permissions.as_ref(),        &effective_additional_permissions,    )    .map_or_else(        || {            normalize_and_validate_additional_permissions(                additional_permissions_allowed,                turn.approval_policy.value(),                effective_additional_permissions.sandbox_permissions,                effective_additional_permissions.additional_permissions,                effective_additional_permissions.permissions_preapproved,                &exec_params.cwd,            )        },        |permissions| Ok(Some(permissions)),    )    .map_err(FunctionCallError::RespondToModel)?;    // Approval policy guard for explicit escalation in non-OnRequest modes.    // Sticky turn permissions have already been approved, so they should    // continue through the normal exec approval flow for the command.    if effective_additional_permissions        .sandbox_permissions        .requests_sandbox_override()        && !effective_additional_permissions.permissions_preapproved        && !matches!(            turn.approval_policy.value(),            codex_protocol::protocol::AskForApproval::OnRequest        )    {        let approval_policy = turn.approval_policy.value();        return Err(FunctionCallError::RespondToModel(format!(            "approval policy is {approval_policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {approval_policy:?}"        )));    }    // Intercept apply_patch if present.    if let Some(output) = intercept_apply_patch(        &exec_params.command,        &exec_params.cwd,        fs.as_ref(),        turn_environment.clone(),        session.clone(),        turn.clone(),        Some(&tracker),        &call_id,        tool_name.name.as_str(),    )    .await?    {        return Ok(output);    }    let source = ExecCommandSource::Agent;    let emitter = ToolEmitter::shell(exec_params.command.clone(), exec_params.cwd.clone(), source);    let event_ctx = ToolEventCtx::new(        session.as_ref(),        turn.as_ref(),        &call_id,        /*turn_diff_tracker*/ None,    );    emitter.begin(event_ctx).await;    let exec_approval_requirement = session        .services        .exec_policy        .create_exec_approval_requirement_for_command(ExecApprovalRequest {            command: &exec_params.command,            approval_policy: turn.approval_policy.value(),            permission_profile: turn.permission_profile(),            windows_sandbox_level: turn.windows_sandbox_level,            sandbox_permissions: if effective_additional_permissions.permissions_preapproved {                codex_protocol::models::SandboxPermissions::UseDefault            } else {                effective_additional_permissions.sandbox_permissions            },            prefix_rule,        })        .await;    let req = ShellRequest {        command: exec_params.command.clone(),        turn_environment: turn_environment.clone(),        shell_type,        hook_command,        cwd: exec_params.cwd.clone(),        timeout_ms: exec_params.expiration.timeout_ms(),        cancellation_token,        env: exec_params.env.clone(),        explicit_env_overrides,        network: exec_params.network.clone(),        sandbox_permissions: effective_additional_permissions.sandbox_permissions,        additional_permissions: normalized_additional_permissions,        #[cfg(unix)]        additional_permissions_preapproved: effective_additional_permissions            .permissions_preapproved,        justification: exec_params.justification.clone(),        exec_approval_requirement,    };    let mut orchestrator = ToolOrchestrator::new();    let mut runtime = ShellRuntime::for_shell_command(shell_runtime_backend);    let tool_ctx = ToolCtx {        session: session.clone(),        turn: turn.clone(),        call_id: call_id.clone(),        tool_name,    };    let out = orchestrator        .run(            &mut runtime,            &req,            &tool_ctx,            &turn,            turn.approval_policy.value(),        )        .await        .map(|result| result.output);    let event_ctx = ToolEventCtx::new(        session.as_ref(),        turn.as_ref(),        &call_id,        /*turn_diff_tracker*/ None,    );    let post_tool_use_response = out        .as_ref()        .ok()        .map(|output| crate::tools::format_exec_output_str(output, turn.truncation_policy))        .map(JsonValue::String);    let content = emitter        .finish(event_ctx, out, /*applied_patch_delta*/ None)        .await?;    Ok(FunctionToolOutput {        body: vec![            codex_protocol::models::FunctionCallOutputContentItem::InputText { text: content },        ],        success: Some(true),        post_tool_use_response,    })}#[cfg(test)]#[path = "shell_tests.rs"]mod tests;