mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
296 lines
11 KiB
Rust
296 lines
11 KiB
Rust
/*
|
|
Runtime: shell
|
|
|
|
Executes shell requests under the orchestrator: asks for approval when needed,
|
|
builds sandbox transform inputs, and runs them under the current SandboxAttempt.
|
|
*/
|
|
#[cfg(unix)]
|
|
pub(crate) mod unix_escalation;
|
|
pub(crate) mod zsh_fork_backend;
|
|
|
|
use crate::command_canonicalization::canonicalize_command_for_approval;
|
|
use crate::exec::ExecCapturePolicy;
|
|
use crate::guardian::GuardianApprovalRequest;
|
|
use crate::guardian::GuardianNetworkAccessTrigger;
|
|
use crate::guardian::review_approval_request;
|
|
use crate::sandboxing::ExecOptions;
|
|
use crate::sandboxing::SandboxPermissions;
|
|
use crate::sandboxing::execute_env;
|
|
use crate::shell::ShellType;
|
|
use crate::tools::network_approval::NetworkApprovalMode;
|
|
use crate::tools::network_approval::NetworkApprovalSpec;
|
|
use crate::tools::runtimes::build_sandbox_command;
|
|
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
|
|
use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
|
|
use crate::tools::sandboxing::Approvable;
|
|
use crate::tools::sandboxing::ApprovalCtx;
|
|
use crate::tools::sandboxing::ExecApprovalRequirement;
|
|
use crate::tools::sandboxing::SandboxAttempt;
|
|
use crate::tools::sandboxing::SandboxOverride;
|
|
use crate::tools::sandboxing::Sandboxable;
|
|
use crate::tools::sandboxing::ToolCtx;
|
|
use crate::tools::sandboxing::ToolError;
|
|
use crate::tools::sandboxing::ToolRuntime;
|
|
use crate::tools::sandboxing::managed_network_for_sandbox_permissions;
|
|
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
|
|
use crate::tools::sandboxing::with_cached_approval;
|
|
use codex_network_proxy::NetworkProxy;
|
|
use codex_protocol::exec_output::ExecToolCallOutput;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::protocol::ReviewDecision;
|
|
use codex_sandboxing::SandboxablePreference;
|
|
use codex_shell_command::powershell::prefix_powershell_script_with_utf8;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use futures::future::BoxFuture;
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ShellRequest {
|
|
pub command: Vec<String>,
|
|
pub hook_command: String,
|
|
pub cwd: AbsolutePathBuf,
|
|
pub timeout_ms: Option<u64>,
|
|
pub env: HashMap<String, String>,
|
|
pub explicit_env_overrides: HashMap<String, String>,
|
|
pub network: Option<NetworkProxy>,
|
|
pub sandbox_permissions: SandboxPermissions,
|
|
pub additional_permissions: Option<AdditionalPermissionProfile>,
|
|
#[cfg(unix)]
|
|
pub additional_permissions_preapproved: bool,
|
|
pub justification: Option<String>,
|
|
pub exec_approval_requirement: ExecApprovalRequirement,
|
|
}
|
|
|
|
/// Selects `ShellRuntime` behavior for different callers.
|
|
///
|
|
/// Note: `Generic` is not the same as `ShellCommandClassic`.
|
|
/// `Generic` means "no `shell_command`-specific backend behavior" (used by the
|
|
/// generic `shell` tool path). The `ShellCommand*` variants are only for the
|
|
/// `shell_command` tool family.
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
pub(crate) enum ShellRuntimeBackend {
|
|
/// Tool-agnostic/default runtime path.
|
|
///
|
|
/// Uses the normal `ShellRuntime` execution flow without enabling any
|
|
/// `shell_command`-specific backend selection.
|
|
#[default]
|
|
Generic,
|
|
/// Legacy backend for the `shell_command` tool.
|
|
///
|
|
/// Keeps `shell_command` on the standard shell runtime flow without the
|
|
/// zsh-fork shell-escalation adapter.
|
|
ShellCommandClassic,
|
|
/// zsh-fork backend for the `shell_command` tool.
|
|
///
|
|
/// On Unix, attempts to run via the zsh-fork + `codex-shell-escalation`
|
|
/// adapter, with fallback to the standard shell runtime flow if
|
|
/// prerequisites are not met.
|
|
ShellCommandZshFork,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct ShellRuntime {
|
|
backend: ShellRuntimeBackend,
|
|
}
|
|
|
|
#[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)]
|
|
pub(crate) struct ApprovalKey {
|
|
command: Vec<String>,
|
|
cwd: AbsolutePathBuf,
|
|
sandbox_permissions: SandboxPermissions,
|
|
additional_permissions: Option<AdditionalPermissionProfile>,
|
|
}
|
|
|
|
impl ShellRuntime {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
backend: ShellRuntimeBackend::Generic,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn for_shell_command(backend: ShellRuntimeBackend) -> Self {
|
|
Self { backend }
|
|
}
|
|
|
|
fn stdout_stream(ctx: &ToolCtx) -> Option<crate::exec::StdoutStream> {
|
|
Some(crate::exec::StdoutStream {
|
|
sub_id: ctx.turn.sub_id.clone(),
|
|
call_id: ctx.call_id.clone(),
|
|
tx_event: ctx.session.get_tx_event(),
|
|
})
|
|
}
|
|
|
|
fn build_approval_request(req: &ShellRequest, call_id: String) -> GuardianApprovalRequest {
|
|
GuardianApprovalRequest::Shell {
|
|
id: call_id,
|
|
command: req.command.clone(),
|
|
hook_command: req.hook_command.clone(),
|
|
cwd: req.cwd.clone(),
|
|
sandbox_permissions: req.sandbox_permissions,
|
|
additional_permissions: req.additional_permissions.clone(),
|
|
justification: req.justification.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Sandboxable for ShellRuntime {
|
|
fn sandbox_preference(&self) -> SandboxablePreference {
|
|
SandboxablePreference::Auto
|
|
}
|
|
fn escalate_on_failure(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl Approvable<ShellRequest> for ShellRuntime {
|
|
type ApprovalKey = ApprovalKey;
|
|
|
|
fn approval_keys(&self, req: &ShellRequest) -> Vec<Self::ApprovalKey> {
|
|
vec![ApprovalKey {
|
|
command: canonicalize_command_for_approval(&req.command),
|
|
cwd: req.cwd.clone(),
|
|
sandbox_permissions: req.sandbox_permissions,
|
|
additional_permissions: req.additional_permissions.clone(),
|
|
}]
|
|
}
|
|
|
|
fn start_approval_async<'a>(
|
|
&'a mut self,
|
|
req: &'a ShellRequest,
|
|
ctx: ApprovalCtx<'a>,
|
|
) -> BoxFuture<'a, ReviewDecision> {
|
|
let keys = self.approval_keys(req);
|
|
let retry_reason = ctx.retry_reason.clone();
|
|
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
|
let session = ctx.session;
|
|
let turn = ctx.turn;
|
|
let approval_request = Self::build_approval_request(req, ctx.call_id.to_string());
|
|
let guardian_review_id = ctx.guardian_review_id.clone();
|
|
Box::pin(async move {
|
|
if let Some(review_id) = guardian_review_id {
|
|
return review_approval_request(
|
|
session,
|
|
turn,
|
|
review_id,
|
|
approval_request.clone(),
|
|
retry_reason,
|
|
)
|
|
.await;
|
|
}
|
|
with_cached_approval(&session.services, "shell", keys, move || async move {
|
|
let available_decisions = None;
|
|
session
|
|
.request_command_approval_for_request(
|
|
turn,
|
|
approval_request.clone(),
|
|
/*approval_id*/ None,
|
|
reason,
|
|
ctx.network_approval_context.clone(),
|
|
req.exec_approval_requirement
|
|
.proposed_execpolicy_amendment()
|
|
.cloned(),
|
|
available_decisions,
|
|
/*fallback_cwd*/ None,
|
|
)
|
|
.await
|
|
})
|
|
.await
|
|
})
|
|
}
|
|
|
|
fn exec_approval_requirement(&self, req: &ShellRequest) -> Option<ExecApprovalRequirement> {
|
|
Some(req.exec_approval_requirement.clone())
|
|
}
|
|
|
|
fn approval_request(
|
|
&self,
|
|
req: &ShellRequest,
|
|
ctx: &ApprovalCtx<'_>,
|
|
) -> Option<GuardianApprovalRequest> {
|
|
Some(Self::build_approval_request(req, ctx.call_id.to_string()))
|
|
}
|
|
|
|
fn sandbox_mode_for_first_attempt(&self, req: &ShellRequest) -> SandboxOverride {
|
|
sandbox_override_for_first_attempt(req.sandbox_permissions, &req.exec_approval_requirement)
|
|
}
|
|
}
|
|
|
|
impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
|
fn network_approval_spec(
|
|
&self,
|
|
req: &ShellRequest,
|
|
ctx: &ToolCtx,
|
|
) -> Option<NetworkApprovalSpec> {
|
|
let network =
|
|
managed_network_for_sandbox_permissions(req.network.as_ref(), req.sandbox_permissions)?;
|
|
Some(NetworkApprovalSpec {
|
|
network: Some(network.clone()),
|
|
mode: NetworkApprovalMode::Immediate,
|
|
trigger: GuardianNetworkAccessTrigger {
|
|
call_id: ctx.call_id.clone(),
|
|
tool_name: ctx.tool_name.clone(),
|
|
command: req.command.clone(),
|
|
cwd: req.cwd.clone(),
|
|
sandbox_permissions: req.sandbox_permissions,
|
|
additional_permissions: req.additional_permissions.clone(),
|
|
justification: req.justification.clone(),
|
|
tty: None,
|
|
},
|
|
command: req.hook_command.clone(),
|
|
})
|
|
}
|
|
|
|
async fn run(
|
|
&mut self,
|
|
req: &ShellRequest,
|
|
attempt: &SandboxAttempt<'_>,
|
|
ctx: &ToolCtx,
|
|
) -> Result<ExecToolCallOutput, ToolError> {
|
|
let session_shell = ctx.session.user_shell();
|
|
let managed_network =
|
|
managed_network_for_sandbox_permissions(req.network.as_ref(), req.sandbox_permissions);
|
|
let env = exec_env_for_sandbox_permissions(&req.env, req.sandbox_permissions);
|
|
let command = maybe_wrap_shell_lc_with_snapshot(
|
|
&req.command,
|
|
session_shell.as_ref(),
|
|
&req.cwd,
|
|
&req.explicit_env_overrides,
|
|
&env,
|
|
);
|
|
let command = if matches!(session_shell.shell_type, ShellType::PowerShell) {
|
|
prefix_powershell_script_with_utf8(&command)
|
|
} else {
|
|
command
|
|
};
|
|
|
|
if self.backend == ShellRuntimeBackend::ShellCommandZshFork {
|
|
match zsh_fork_backend::maybe_run_shell_command(req, attempt, ctx, &command).await? {
|
|
Some(out) => return Ok(out),
|
|
None => {
|
|
tracing::warn!(
|
|
"ZshFork backend specified, but conditions for using it were not met, falling back to normal execution",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let command =
|
|
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())?;
|
|
let mut expiration: crate::exec::ExecExpiration = req.timeout_ms.into();
|
|
if let Some(cancellation) = attempt.network_denial_cancellation_token.clone() {
|
|
expiration = expiration.with_cancellation(cancellation);
|
|
}
|
|
let options = ExecOptions {
|
|
expiration,
|
|
capture_policy: ExecCapturePolicy::ShellTool,
|
|
};
|
|
let env = attempt
|
|
.env_for(command, options, managed_network)
|
|
.map_err(|err| ToolError::Codex(err.into()))?;
|
|
let out = execute_env(env, Self::stdout_stream(ctx))
|
|
.await
|
|
.map_err(ToolError::Codex)?;
|
|
Ok(out)
|
|
}
|
|
}
|