mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
## Why This is effectively a follow-up to [#15812](https://github.com/openai/codex/pull/15812). That change removed the special skill-script exec path, but `skill_metadata` was still being threaded through command-approval payloads even though the approval flow no longer uses it to render prompts or resolve decisions. Keeping it around added extra protocol, schema, and client surface area without changing behavior. Removing it keeps the command-approval contract smaller and avoids carrying a dead field through app-server, TUI, and MCP boundaries. ## What changed - removed `ExecApprovalRequestSkillMetadata` and the corresponding `skillMetadata` field from core approval events and the v2 app-server protocol - removed the generated JSON and TypeScript schema output for that field - updated app-server, MCP server, TUI, and TUI app-server approval plumbing to stop forwarding the field - cleaned up tests that previously constructed or asserted `skillMetadata` ## Testing - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-protocol` - `cargo test -p codex-app-server-test-client` - `cargo test -p codex-mcp-server` - `just argument-comment-lint`
266 lines
9.1 KiB
Rust
266 lines
9.1 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::exec::ExecToolCallOutput;
|
|
use crate::guardian::GuardianApprovalRequest;
|
|
use crate::guardian::review_approval_request;
|
|
use crate::guardian::routes_approval_to_guardian;
|
|
use crate::powershell::prefix_powershell_script_with_utf8;
|
|
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::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::sandbox_override_for_first_attempt;
|
|
use crate::tools::sandboxing::with_cached_approval;
|
|
use codex_network_proxy::NetworkProxy;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::protocol::ReviewDecision;
|
|
use codex_sandboxing::SandboxablePreference;
|
|
use futures::future::BoxFuture;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ShellRequest {
|
|
pub command: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
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<PermissionProfile>,
|
|
#[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: PathBuf,
|
|
sandbox_permissions: SandboxPermissions,
|
|
additional_permissions: Option<PermissionProfile>,
|
|
}
|
|
|
|
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(),
|
|
})
|
|
}
|
|
}
|
|
|
|
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 command = req.command.clone();
|
|
let cwd = req.cwd.clone();
|
|
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 call_id = ctx.call_id.to_string();
|
|
Box::pin(async move {
|
|
if routes_approval_to_guardian(turn) {
|
|
return review_approval_request(
|
|
session,
|
|
turn,
|
|
GuardianApprovalRequest::Shell {
|
|
id: call_id,
|
|
command,
|
|
cwd,
|
|
sandbox_permissions: req.sandbox_permissions,
|
|
additional_permissions: req.additional_permissions.clone(),
|
|
justification: req.justification.clone(),
|
|
},
|
|
retry_reason,
|
|
)
|
|
.await;
|
|
}
|
|
with_cached_approval(&session.services, "shell", keys, move || async move {
|
|
let available_decisions = None;
|
|
session
|
|
.request_command_approval(
|
|
turn,
|
|
call_id,
|
|
/*approval_id*/ None,
|
|
command,
|
|
cwd,
|
|
reason,
|
|
ctx.network_approval_context.clone(),
|
|
req.exec_approval_requirement
|
|
.proposed_execpolicy_amendment()
|
|
.cloned(),
|
|
req.additional_permissions.clone(),
|
|
available_decisions,
|
|
)
|
|
.await
|
|
})
|
|
.await
|
|
})
|
|
}
|
|
|
|
fn exec_approval_requirement(&self, req: &ShellRequest) -> Option<ExecApprovalRequirement> {
|
|
Some(req.exec_approval_requirement.clone())
|
|
}
|
|
|
|
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> {
|
|
req.network.as_ref()?;
|
|
Some(NetworkApprovalSpec {
|
|
network: req.network.clone(),
|
|
mode: NetworkApprovalMode::Immediate,
|
|
})
|
|
}
|
|
|
|
async fn run(
|
|
&mut self,
|
|
req: &ShellRequest,
|
|
attempt: &SandboxAttempt<'_>,
|
|
ctx: &ToolCtx,
|
|
) -> Result<ExecToolCallOutput, ToolError> {
|
|
let session_shell = ctx.session.user_shell();
|
|
let command = maybe_wrap_shell_lc_with_snapshot(
|
|
&req.command,
|
|
session_shell.as_ref(),
|
|
&req.cwd,
|
|
&req.explicit_env_overrides,
|
|
);
|
|
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,
|
|
&req.env,
|
|
req.additional_permissions.clone(),
|
|
)?;
|
|
let options = ExecOptions {
|
|
expiration: req.timeout_ms.into(),
|
|
capture_policy: ExecCapturePolicy::ShellTool,
|
|
};
|
|
let env = attempt
|
|
.env_for(command, options, req.network.as_ref())
|
|
.map_err(|err| ToolError::Codex(err.into()))?;
|
|
let out = execute_env(env, Self::stdout_stream(ctx))
|
|
.await
|
|
.map_err(ToolError::Codex)?;
|
|
Ok(out)
|
|
}
|
|
}
|