mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
This PR adds support for a model-based summary and risk assessment for commands that violate the sandbox policy and require user approval. This aids the user in evaluating whether the command should be approved. The feature works by taking a failed command and passing it back to the model and asking it to summarize the command, give it a risk level (low, medium, high) and a risk category (e.g. "data deletion" or "data exfiltration"). It uses a new conversation thread so the context in the existing thread doesn't influence the answer. If the call to the model fails or takes longer than 5 seconds, it falls back to the current behavior. For now, this is an experimental feature and is gated by a config key `experimental_sandbox_command_assessment`. Here is a screen shot of the approval prompt showing the risk assessment and summary. <img width="723" height="282" alt="image" src="https://github.com/user-attachments/assets/4597dd7c-d5a0-4e9f-9d13-414bd082fd6b" />
177 lines
5.6 KiB
Rust
177 lines
5.6 KiB
Rust
/*
|
||
Runtime: shell
|
||
|
||
Executes shell requests under the orchestrator: asks for approval when needed,
|
||
builds a CommandSpec, and runs it under the current SandboxAttempt.
|
||
*/
|
||
use crate::command_safety::is_dangerous_command::command_might_be_dangerous;
|
||
use crate::command_safety::is_safe_command::is_known_safe_command;
|
||
use crate::exec::ExecToolCallOutput;
|
||
use crate::protocol::SandboxPolicy;
|
||
use crate::sandboxing::execute_env;
|
||
use crate::tools::runtimes::build_command_spec;
|
||
use crate::tools::sandboxing::Approvable;
|
||
use crate::tools::sandboxing::ApprovalCtx;
|
||
use crate::tools::sandboxing::ProvidesSandboxRetryData;
|
||
use crate::tools::sandboxing::SandboxAttempt;
|
||
use crate::tools::sandboxing::SandboxRetryData;
|
||
use crate::tools::sandboxing::Sandboxable;
|
||
use crate::tools::sandboxing::SandboxablePreference;
|
||
use crate::tools::sandboxing::ToolCtx;
|
||
use crate::tools::sandboxing::ToolError;
|
||
use crate::tools::sandboxing::ToolRuntime;
|
||
use crate::tools::sandboxing::with_cached_approval;
|
||
use codex_protocol::protocol::AskForApproval;
|
||
use codex_protocol::protocol::ReviewDecision;
|
||
use futures::future::BoxFuture;
|
||
use std::path::PathBuf;
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct ShellRequest {
|
||
pub command: Vec<String>,
|
||
pub cwd: PathBuf,
|
||
pub timeout_ms: Option<u64>,
|
||
pub env: std::collections::HashMap<String, String>,
|
||
pub with_escalated_permissions: Option<bool>,
|
||
pub justification: Option<String>,
|
||
}
|
||
|
||
impl ProvidesSandboxRetryData for ShellRequest {
|
||
fn sandbox_retry_data(&self) -> Option<SandboxRetryData> {
|
||
Some(SandboxRetryData {
|
||
command: self.command.clone(),
|
||
cwd: self.cwd.clone(),
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Default)]
|
||
pub struct ShellRuntime;
|
||
|
||
#[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||
pub(crate) struct ApprovalKey {
|
||
command: Vec<String>,
|
||
cwd: PathBuf,
|
||
escalated: bool,
|
||
}
|
||
|
||
impl ShellRuntime {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
|
||
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_key(&self, req: &ShellRequest) -> Self::ApprovalKey {
|
||
ApprovalKey {
|
||
command: req.command.clone(),
|
||
cwd: req.cwd.clone(),
|
||
escalated: req.with_escalated_permissions.unwrap_or(false),
|
||
}
|
||
}
|
||
|
||
fn start_approval_async<'a>(
|
||
&'a mut self,
|
||
req: &'a ShellRequest,
|
||
ctx: ApprovalCtx<'a>,
|
||
) -> BoxFuture<'a, ReviewDecision> {
|
||
let key = self.approval_key(req);
|
||
let command = req.command.clone();
|
||
let cwd = req.cwd.clone();
|
||
let reason = ctx
|
||
.retry_reason
|
||
.clone()
|
||
.or_else(|| req.justification.clone());
|
||
let risk = ctx.risk.clone();
|
||
let session = ctx.session;
|
||
let turn = ctx.turn;
|
||
let call_id = ctx.call_id.to_string();
|
||
Box::pin(async move {
|
||
with_cached_approval(&session.services, key, move || async move {
|
||
session
|
||
.request_command_approval(turn, call_id, command, cwd, reason, risk)
|
||
.await
|
||
})
|
||
.await
|
||
})
|
||
}
|
||
|
||
fn wants_initial_approval(
|
||
&self,
|
||
req: &ShellRequest,
|
||
policy: AskForApproval,
|
||
sandbox_policy: &SandboxPolicy,
|
||
) -> bool {
|
||
if is_known_safe_command(&req.command) {
|
||
return false;
|
||
}
|
||
match policy {
|
||
AskForApproval::Never | AskForApproval::OnFailure => false,
|
||
AskForApproval::OnRequest => {
|
||
// In DangerFullAccess, only prompt if the command looks dangerous.
|
||
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
|
||
return command_might_be_dangerous(&req.command);
|
||
}
|
||
|
||
// In restricted sandboxes (ReadOnly/WorkspaceWrite), do not prompt for
|
||
// non‑escalated, non‑dangerous commands — let the sandbox enforce
|
||
// restrictions (e.g., block network/write) without a user prompt.
|
||
let wants_escalation = req.with_escalated_permissions.unwrap_or(false);
|
||
if wants_escalation {
|
||
return true;
|
||
}
|
||
command_might_be_dangerous(&req.command)
|
||
}
|
||
AskForApproval::UnlessTrusted => !is_known_safe_command(&req.command),
|
||
}
|
||
}
|
||
|
||
fn wants_escalated_first_attempt(&self, req: &ShellRequest) -> bool {
|
||
req.with_escalated_permissions.unwrap_or(false)
|
||
}
|
||
}
|
||
|
||
impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||
async fn run(
|
||
&mut self,
|
||
req: &ShellRequest,
|
||
attempt: &SandboxAttempt<'_>,
|
||
ctx: &ToolCtx<'_>,
|
||
) -> Result<ExecToolCallOutput, ToolError> {
|
||
let spec = build_command_spec(
|
||
&req.command,
|
||
&req.cwd,
|
||
&req.env,
|
||
req.timeout_ms,
|
||
req.with_escalated_permissions,
|
||
req.justification.clone(),
|
||
)?;
|
||
let env = attempt
|
||
.env_for(&spec)
|
||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||
let out = execute_env(&env, attempt.policy, Self::stdout_stream(ctx))
|
||
.await
|
||
.map_err(ToolError::Codex)?;
|
||
Ok(out)
|
||
}
|
||
}
|