Compare commits

...

4 Commits

Author SHA1 Message Date
kevin zhao
7309b0f8e8 attempt 1 2025-11-03 09:20:45 -08:00
zhao-oai
b81cb7ceb3 Fix sandbox detection for user shell commands (#6094) 2025-11-01 17:27:03 -04:00
kevin zhao
c7a3428986 fix 2025-10-31 17:27:35 -04:00
kevin zhao
d609dfa2fc escalating permissions 2025-10-31 17:15:50 -04:00
5 changed files with 68 additions and 37 deletions

View File

@@ -1,27 +1,26 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use codex_protocol::models::ShellToolCallParams;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::error; use tracing::error;
use uuid::Uuid; use uuid::Uuid;
use crate::codex::TurnContext; use crate::codex::TurnContext;
use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec_env::create_env;
use crate::protocol::EventMsg; use crate::protocol::EventMsg;
use crate::protocol::TaskStartedEvent; use crate::protocol::TaskStartedEvent;
use crate::sandboxing::ExecEnv;
use crate::state::TaskKind; use crate::state::TaskKind;
use crate::tools::context::ToolPayload; use crate::tools::events::ToolEmitter;
use crate::tools::parallel::ToolCallRuntime; use crate::tools::events::ToolEventCtx;
use crate::tools::router::ToolCall; use crate::tools::sandboxing::ToolError;
use crate::tools::router::ToolRouter;
use crate::turn_diff_tracker::TurnDiffTracker;
use super::SessionTask; use super::SessionTask;
use super::SessionTaskContext; use super::SessionTaskContext;
const USER_SHELL_TOOL_NAME: &str = "local_shell";
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct UserShellCommandTask { pub(crate) struct UserShellCommandTask {
@@ -53,9 +52,8 @@ impl SessionTask for UserShellCommandTask {
let session = session.clone_session(); let session = session.clone_session();
session.send_event(turn_context.as_ref(), event).await; session.send_event(turn_context.as_ref(), event).await;
// Execute the user's script under their default shell when known; this // Execute the user's script under their default shell when known. Use
// allows commands that use shell features (pipes, &&, redirects, etc.). // execute_exec_env directly to avoid approvals/sandbox overhead.
// We do not source rc files or otherwise reformat the script.
let shell_invocation = match session.user_shell() { let shell_invocation = match session.user_shell() {
crate::shell::Shell::Zsh(zsh) => vec![ crate::shell::Shell::Zsh(zsh) => vec![
zsh.shell_path.clone(), zsh.shell_path.clone(),
@@ -78,35 +76,56 @@ impl SessionTask for UserShellCommandTask {
} }
}; };
let params = ShellToolCallParams { // Emit shell begin event to keep UI consistent with tool runs.
let call_id = Uuid::new_v4().to_string();
let emitter = ToolEmitter::shell(shell_invocation.clone(), turn_context.cwd.clone(), true);
let event_ctx = ToolEventCtx::new(session.as_ref(), turn_context.as_ref(), &call_id, None);
emitter.begin(event_ctx).await;
let env = ExecEnv {
command: shell_invocation, command: shell_invocation,
workdir: None, cwd: turn_context.cwd.clone(),
env: create_env(&turn_context.shell_environment_policy),
timeout_ms: None, timeout_ms: None,
sandbox: SandboxType::None,
with_escalated_permissions: None, with_escalated_permissions: None,
justification: None, justification: None,
arg0: None,
}; };
let stream = Some(StdoutStream {
sub_id: turn_context.sub_id.clone(),
call_id: call_id.clone(),
tx_event: session.get_tx_event(),
});
let tool_call = ToolCall { let policy = turn_context.sandbox_policy.clone();
tool_name: USER_SHELL_TOOL_NAME.to_string(), let mut exec_task = tokio::spawn(async move {
call_id: Uuid::new_v4().to_string(), crate::exec::execute_exec_env(env, &policy, stream).await
payload: ToolPayload::LocalShell { params }, });
};
let router = Arc::new(ToolRouter::from_config(&turn_context.tools_config, None)); tokio::select! {
let tracker = Arc::new(Mutex::new(TurnDiffTracker::new())); res = &mut exec_task => {
let runtime = ToolCallRuntime::new( match res {
Arc::clone(&router), Ok(Ok(output)) => {
Arc::clone(&session), let event_ctx = ToolEventCtx::new(session.as_ref(), turn_context.as_ref(), &call_id, None);
Arc::clone(&turn_context), let _ = emitter.finish(event_ctx, Ok(output)).await;
Arc::clone(&tracker), }
); Ok(Err(err)) => {
let event_ctx = ToolEventCtx::new(session.as_ref(), turn_context.as_ref(), &call_id, None);
if let Err(err) = runtime let _ = emitter.finish(event_ctx, Err(ToolError::Codex(err))).await;
.handle_tool_call(tool_call, cancellation_token) }
.await Err(join_err) => {
{ error!("user shell exec task join error: {join_err:?}");
error!("user shell command failed: {err:?}"); }
}
}
_ = cancellation_token.cancelled() => {
exec_task.abort();
// Session will emit TurnAborted; do not finish emitter to avoid
// emitting ExecCommandEnd for an interrupted run.
}
} }
None None
} }
} }

View File

@@ -40,6 +40,7 @@ pub enum ToolPayload {
}, },
LocalShell { LocalShell {
params: ShellToolCallParams, params: ShellToolCallParams,
is_user_shell_command: bool,
}, },
UnifiedExec { UnifiedExec {
arguments: String, arguments: String,
@@ -56,7 +57,7 @@ impl ToolPayload {
match self { match self {
ToolPayload::Function { arguments } => Cow::Borrowed(arguments), ToolPayload::Function { arguments } => Cow::Borrowed(arguments),
ToolPayload::Custom { input } => Cow::Borrowed(input), ToolPayload::Custom { input } => Cow::Borrowed(input),
ToolPayload::LocalShell { params } => Cow::Owned(params.command.join(" ")), ToolPayload::LocalShell { params, .. } => Cow::Owned(params.command.join(" ")),
ToolPayload::UnifiedExec { arguments } => Cow::Borrowed(arguments), ToolPayload::UnifiedExec { arguments } => Cow::Borrowed(arguments),
ToolPayload::Mcp { raw_arguments, .. } => Cow::Borrowed(raw_arguments), ToolPayload::Mcp { raw_arguments, .. } => Cow::Borrowed(raw_arguments),
} }

View File

@@ -82,7 +82,10 @@ impl ToolHandler for ShellHandler {
) )
.await .await
} }
ToolPayload::LocalShell { params } => { ToolPayload::LocalShell {
params,
is_user_shell_command,
} => {
let exec_params = Self::to_exec_params(params, turn.as_ref()); let exec_params = Self::to_exec_params(params, turn.as_ref());
Self::run_exec_like( Self::run_exec_like(
tool_name.as_str(), tool_name.as_str(),
@@ -91,7 +94,7 @@ impl ToolHandler for ShellHandler {
turn, turn,
tracker, tracker,
call_id, call_id,
true, is_user_shell_command,
) )
.await .await
} }
@@ -219,6 +222,7 @@ impl ShellHandler {
env: exec_params.env.clone(), env: exec_params.env.clone(),
with_escalated_permissions: exec_params.with_escalated_permissions, with_escalated_permissions: exec_params.with_escalated_permissions,
justification: exec_params.justification.clone(), justification: exec_params.justification.clone(),
is_user_shell_command,
}; };
let mut orchestrator = ToolOrchestrator::new(); let mut orchestrator = ToolOrchestrator::new();
let mut runtime = ShellRuntime::new(); let mut runtime = ShellRuntime::new();

View File

@@ -120,7 +120,10 @@ impl ToolRouter {
Ok(Some(ToolCall { Ok(Some(ToolCall {
tool_name: "local_shell".to_string(), tool_name: "local_shell".to_string(),
call_id, call_id,
payload: ToolPayload::LocalShell { params }, payload: ToolPayload::LocalShell {
params,
is_user_shell_command: false,
},
})) }))
} }
} }

View File

@@ -34,6 +34,7 @@ pub struct ShellRequest {
pub env: std::collections::HashMap<String, String>, pub env: std::collections::HashMap<String, String>,
pub with_escalated_permissions: Option<bool>, pub with_escalated_permissions: Option<bool>,
pub justification: Option<String>, pub justification: Option<String>,
pub is_user_shell_command: bool,
} }
impl ProvidesSandboxRetryData for ShellRequest { impl ProvidesSandboxRetryData for ShellRequest {
@@ -121,6 +122,9 @@ impl Approvable<ShellRequest> for ShellRuntime {
policy: AskForApproval, policy: AskForApproval,
sandbox_policy: &SandboxPolicy, sandbox_policy: &SandboxPolicy,
) -> bool { ) -> bool {
if req.is_user_shell_command {
return false;
}
if is_known_safe_command(&req.command) { if is_known_safe_command(&req.command) {
return false; return false;
} }
@@ -146,7 +150,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
} }
fn wants_escalated_first_attempt(&self, req: &ShellRequest) -> bool { fn wants_escalated_first_attempt(&self, req: &ShellRequest) -> bool {
req.with_escalated_permissions.unwrap_or(false) req.is_user_shell_command || req.with_escalated_permissions.unwrap_or(false)
} }
} }