This commit is contained in:
jif-oai
2025-10-15 11:40:07 +01:00
parent f61c7aa160
commit 703c3825ce
6 changed files with 115 additions and 103 deletions
+80 -51
View File
@@ -18,13 +18,14 @@ use tokio::process::Child;
use crate::error::CodexErr;
use crate::error::Result;
use crate::error::SandboxErr;
use crate::landlock::spawn_command_under_linux_sandbox;
use crate::executor::SandboxLaunch;
use crate::executor::SandboxLaunchError;
use crate::executor::sandbox::build_launch_for_sandbox;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::ExecCommandOutputDeltaEvent;
use crate::protocol::ExecOutputStream;
use crate::protocol::SandboxPolicy;
use crate::seatbelt::spawn_command_under_seatbelt;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
@@ -86,58 +87,36 @@ pub async fn process_exec_tool_call(
sandbox_cwd: &Path,
codex_linux_sandbox_exe: &Option<PathBuf>,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
let launch = build_launch_for_sandbox(
sandbox_type,
&params.command,
sandbox_policy,
sandbox_cwd,
codex_linux_sandbox_exe.as_ref(),
)
.map_err(sandbox_launch_error_to_codex)?;
execute_sandbox_launch(params, launch, sandbox_type, sandbox_policy, stdout_stream).await
}
pub(crate) async fn execute_sandbox_launch(
params: ExecParams,
launch: SandboxLaunch,
sandbox_type: SandboxType,
sandbox_policy: &SandboxPolicy,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
let start = Instant::now();
let timeout_duration = params.timeout_duration();
let raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr> = match sandbox_type
{
SandboxType::None => exec(params, sandbox_policy, stdout_stream.clone()).await,
SandboxType::MacosSeatbelt => {
let ExecParams {
command,
cwd: command_cwd,
env,
..
} = params;
let child = spawn_command_under_seatbelt(
command,
command_cwd,
sandbox_policy,
sandbox_cwd,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, timeout_duration, stdout_stream.clone()).await
}
SandboxType::LinuxSeccomp => {
let ExecParams {
command,
cwd: command_cwd,
env,
..
} = params;
let codex_linux_sandbox_exe = codex_linux_sandbox_exe
.as_ref()
.ok_or(CodexErr::LandlockSandboxExecutableNotProvided)?;
let child = spawn_command_under_linux_sandbox(
codex_linux_sandbox_exe,
command,
command_cwd,
sandbox_policy,
sandbox_cwd,
StdioPolicy::RedirectForShellTool,
env,
)
.await?;
consume_truncated_output(child, timeout_duration, stdout_stream).await
}
};
let raw_output_result = spawn_with_launch(params, launch, sandbox_policy, stdout_stream).await;
let duration = start.elapsed();
finalize_exec_result(raw_output_result, sandbox_type, duration)
}
fn finalize_exec_result(
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
sandbox_type: SandboxType,
duration: Duration,
) -> Result<ExecToolCallOutput> {
match raw_output_result {
Ok(raw_output) => {
#[allow(unused_mut)]
@@ -192,6 +171,56 @@ pub async fn process_exec_tool_call(
}
}
pub(crate) fn sandbox_launch_error_to_codex(err: SandboxLaunchError) -> CodexErr {
match err {
SandboxLaunchError::MissingCommandLine => CodexErr::Io(io::Error::new(
io::ErrorKind::InvalidInput,
"command args are empty",
)),
SandboxLaunchError::MissingLinuxSandboxExecutable => {
CodexErr::LandlockSandboxExecutableNotProvided
}
}
}
async fn spawn_with_launch(
params: ExecParams,
launch: SandboxLaunch,
sandbox_policy: &SandboxPolicy,
stdout_stream: Option<StdoutStream>,
) -> std::result::Result<RawExecToolCallOutput, CodexErr> {
let ExecParams {
command: _,
cwd,
timeout_ms,
mut env,
with_escalated_permissions,
justification,
} = params;
let SandboxLaunch {
program,
args,
env: launch_env,
} = launch;
env.extend(launch_env);
let mut command = Vec::with_capacity(1 + args.len());
command.push(program);
command.extend(args);
let updated_params = ExecParams {
command,
cwd,
timeout_ms,
env,
with_escalated_permissions,
justification,
};
exec(updated_params, sandbox_policy, stdout_stream).await
}
/// We don't have a fully deterministic way to tell if our command failed
/// because of the sandbox - a command in the user's zshrc file might hit an
/// error, but the command itself might fail or succeed for other reasons.
+1
View File
@@ -11,6 +11,7 @@ pub(crate) use runner::ExecutorConfig;
pub(crate) use runner::normalize_exec_result;
pub(crate) use sandbox::SandboxLaunch;
pub(crate) use sandbox::SandboxLaunchError;
pub(crate) use sandbox::build_launch_for_sandbox;
pub(crate) mod linkers {
use crate::exec::ExecParams;
+20 -29
View File
@@ -15,7 +15,8 @@ use crate::exec::ExecToolCallOutput;
use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::StreamOutput;
use crate::exec::process_exec_tool_call;
use crate::exec::execute_sandbox_launch;
use crate::exec::sandbox_launch_error_to_codex;
use crate::executor::errors::ExecError;
use crate::executor::sandbox::RetrySandboxContext;
use crate::executor::sandbox::SandboxDecision;
@@ -225,14 +226,18 @@ impl Executor {
.await?;
let stdout_stream = plan.stdout_stream();
let first_attempt = self
.spawn(
plan.request().params.clone(),
plan.initial_sandbox(),
plan.config(),
stdout_stream.clone(),
)
.await;
let sandbox_policy = plan.config().sandbox_policy.clone();
let initial_launch = plan
.initial_launch()
.map_err(|err| ExecError::Codex(sandbox_launch_error_to_codex(err)))?;
let first_attempt = execute_sandbox_launch(
plan.request().params.clone(),
initial_launch,
plan.initial_sandbox(),
&sandbox_policy,
stdout_stream.clone(),
)
.await;
match first_attempt {
Ok(output) => Ok(output),
@@ -245,10 +250,14 @@ impl Executor {
.prompt_retry_without_sandbox(session, format!("Execution failed: {error}"))
.await
{
self.spawn(
let retry_launch = plan
.retry_launch()
.map_err(|err| ExecError::Codex(sandbox_launch_error_to_codex(err)))?;
execute_sandbox_launch(
plan.request().params.clone(),
retry_launch,
SandboxType::None,
plan.config(),
&sandbox_policy,
stdout_stream,
)
.await
@@ -264,24 +273,6 @@ impl Executor {
Err(err) => Err(err.into()),
}
}
async fn spawn(
&self,
params: ExecParams,
sandbox: SandboxType,
config: &ExecutorConfig,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput, CodexErr> {
process_exec_tool_call(
params,
sandbox,
&config.sandbox_policy,
&config.sandbox_cwd,
&config.codex_linux_sandbox_exe,
stdout_stream,
)
.await
}
}
fn maybe_translate_shell_command(
@@ -79,16 +79,10 @@ impl ToolHandler for UnifiedExecHandler {
};
let request = UnifiedExecRequest {
session_id: parsed_session_id,
input_chunks: &input,
timeout_ms,
};
session
.services
.executor
.update_environment(turn.sandbox_policy.clone(), turn.cwd.clone());
let value = session
.services
.unified_exec_manager
@@ -100,6 +94,7 @@ impl ToolHandler for UnifiedExecHandler {
sub_id: &sub_id,
call_id: &call_id,
tool_name: &tool_name,
session_id: parsed_session_id,
},
)
.await
+4 -7
View File
@@ -3,11 +3,8 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub(crate) enum UnifiedExecError {
#[error("Failed to create unified exec session: {pty_error}")]
CreateSession {
#[source]
pty_error: anyhow::Error,
},
#[error("Failed to create unified exec session: {message}")]
CreateSession { message: String },
#[error("Unknown session id {session_id}")]
UnknownSessionId { session_id: i32 },
#[error("failed to write to stdin")]
@@ -21,8 +18,8 @@ pub(crate) enum UnifiedExecError {
}
impl UnifiedExecError {
pub(crate) fn create_session(error: anyhow::Error) -> Self {
Self::CreateSession { pty_error: error }
pub(crate) fn create_session(message: String) -> Self {
Self::CreateSession { message }
}
}
+9 -10
View File
@@ -1,4 +1,3 @@
use anyhow::anyhow;
use portable_pty::CommandBuilder;
use portable_pty::PtySize;
use portable_pty::native_pty_system;
@@ -43,11 +42,11 @@ pub(crate) struct UnifiedExecContext<'a> {
pub sub_id: &'a str,
pub call_id: &'a str,
pub tool_name: &'a str,
pub session_id: Option<i32>,
}
#[derive(Debug)]
pub(crate) struct UnifiedExecRequest<'a> {
pub session_id: Option<i32>,
pub input_chunks: &'a [String],
pub timeout_ms: Option<u64>,
}
@@ -224,7 +223,7 @@ impl UnifiedExecSessionManager {
&exec_context,
)
.await
.map_err(|err| UnifiedExecError::create_session(anyhow!(err.to_string())))?;
.map_err(|err| UnifiedExecError::create_session(err.to_string()))?;
let launch = plan.initial_launch()?;
@@ -270,7 +269,7 @@ impl UnifiedExecSessionManager {
let output_buffer;
let output_notify;
if let Some(existing_id) = request.session_id {
if let Some(existing_id) = context.session_id {
let mut sessions = self.sessions.lock().await;
match sessions.get(&existing_id) {
Some(session) => {
@@ -307,7 +306,7 @@ impl UnifiedExecSessionManager {
new_session = Some(managed_session);
};
if request.session_id.is_some() {
if context.session_id.is_some() {
let joined_input = request.input_chunks.join(" ");
if !joined_input.is_empty() && writer_tx.send(joined_input.into_bytes()).await.is_err()
{
@@ -366,7 +365,7 @@ impl UnifiedExecSessionManager {
let should_store_session = if let Some(session) = new_session.as_ref() {
!session.has_exited()
} else if request.session_id.is_some() {
} else if context.session_id.is_some() {
let mut sessions = self.sessions.lock().await;
if let Some(existing) = sessions.get(&session_id) {
if existing.has_exited() {
@@ -421,7 +420,7 @@ async fn create_unified_exec_session(
pixel_width: 0,
pixel_height: 0,
})
.map_err(UnifiedExecError::create_session)?;
.map_err(|err| UnifiedExecError::create_session(err.to_string()))?;
// Safe thanks to the check at the top of the function.
let mut command_builder = CommandBuilder::new(launch.program.clone());
@@ -435,7 +434,7 @@ async fn create_unified_exec_session(
let mut child = pair
.slave
.spawn_command(command_builder)
.map_err(UnifiedExecError::create_session)?;
.map_err(|err| UnifiedExecError::create_session(err.to_string()))?;
let killer = child.clone_killer();
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(128);
@@ -444,7 +443,7 @@ async fn create_unified_exec_session(
let mut reader = pair
.master
.try_clone_reader()
.map_err(UnifiedExecError::create_session)?;
.map_err(|err| UnifiedExecError::create_session(err.to_string()))?;
let output_tx_clone = output_tx.clone();
let reader_handle = tokio::task::spawn_blocking(move || {
let mut buf = [0u8; 8192];
@@ -467,7 +466,7 @@ async fn create_unified_exec_session(
let writer = pair
.master
.take_writer()
.map_err(UnifiedExecError::create_session)?;
.map_err(|err| UnifiedExecError::create_session(err.to_string()))?;
let writer = Arc::new(StdMutex::new(writer));
let writer_handle = tokio::spawn({
let writer = writer.clone();