Skip to content

Commit b66bb7e

Browse files
committed
refactor: prepare unified exec for zsh-fork backend
1 parent 2e154a3 commit b66bb7e

13 files changed

Lines changed: 466 additions & 36 deletions

File tree

codex-rs/core/src/tools/runtimes/shell.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Executes shell requests under the orchestrator: asks for approval when needed,
55
builds a CommandSpec, and runs it under the current SandboxAttempt.
66
*/
77
#[cfg(unix)]
8-
mod unix_escalation;
8+
pub(crate) mod unix_escalation;
9+
pub(crate) mod zsh_fork_backend;
910

1011
use crate::command_canonicalization::canonicalize_command_for_approval;
1112
use crate::exec::ExecToolCallOutput;
@@ -80,7 +81,6 @@ pub(crate) enum ShellRuntimeBackend {
8081

8182
#[derive(Default)]
8283
pub struct ShellRuntime {
83-
#[cfg_attr(not(unix), allow(dead_code))]
8484
backend: ShellRuntimeBackend,
8585
}
8686

@@ -215,9 +215,8 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
215215
command
216216
};
217217

218-
#[cfg(unix)]
219218
if self.backend == ShellRuntimeBackend::ShellCommandZshFork {
220-
match unix_escalation::try_run_zsh_fork(req, attempt, ctx, &command).await? {
219+
match zsh_fork_backend::maybe_run_shell_command(req, attempt, ctx, &command).await? {
221220
Some(out) => return Ok(out),
222221
None => {
223222
tracing::warn!(

codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::exec::ExecToolCallOutput;
66
use crate::exec::SandboxType;
77
use crate::exec::is_likely_sandbox_denied;
88
use crate::features::Feature;
9+
use crate::sandboxing::ExecRequest;
910
use crate::sandboxing::SandboxPermissions;
1011
use crate::shell::ShellType;
1112
use crate::skills::SkillMetadata;
@@ -36,6 +37,7 @@ use codex_shell_escalation::EscalationDecision;
3637
use codex_shell_escalation::EscalationExecution;
3738
use codex_shell_escalation::EscalationPermissions;
3839
use codex_shell_escalation::EscalationPolicy;
40+
use codex_shell_escalation::EscalationSession;
3941
use codex_shell_escalation::ExecParams;
4042
use codex_shell_escalation::ExecResult;
4143
use codex_shell_escalation::Permissions as EscalatedPermissions;
@@ -51,6 +53,11 @@ use tokio::sync::RwLock;
5153
use tokio_util::sync::CancellationToken;
5254
use uuid::Uuid;
5355

56+
pub(crate) struct PreparedUnifiedExecZshFork {
57+
pub(crate) exec_request: ExecRequest,
58+
pub(crate) escalation_session: EscalationSession,
59+
}
60+
5461
pub(super) async fn try_run_zsh_fork(
5562
req: &ShellRequest,
5663
attempt: &SandboxAttempt<'_>,
@@ -95,7 +102,7 @@ pub(super) async fn try_run_zsh_fork(
95102
justification,
96103
arg0,
97104
} = sandbox_exec_request;
98-
let ParsedShellCommand { script, login } = extract_shell_script(&command)?;
105+
let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
99106
let effective_timeout = Duration::from_millis(
100107
req.timeout_ms
101108
.unwrap_or(crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS),
@@ -172,6 +179,103 @@ pub(super) async fn try_run_zsh_fork(
172179
map_exec_result(attempt.sandbox, exec_result).map(Some)
173180
}
174181

182+
pub(crate) async fn prepare_unified_exec_zsh_fork(
183+
req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest,
184+
attempt: &SandboxAttempt<'_>,
185+
ctx: &ToolCtx,
186+
exec_request: ExecRequest,
187+
) -> Result<Option<PreparedUnifiedExecZshFork>, ToolError> {
188+
let Some(shell_zsh_path) = ctx.session.services.shell_zsh_path.as_ref() else {
189+
tracing::warn!("ZshFork backend specified, but shell_zsh_path is not configured.");
190+
return Ok(None);
191+
};
192+
if !ctx.session.features().enabled(Feature::ShellZshFork) {
193+
tracing::warn!("ZshFork backend specified, but ShellZshFork feature is not enabled.");
194+
return Ok(None);
195+
}
196+
if !matches!(ctx.session.user_shell().shell_type, ShellType::Zsh) {
197+
tracing::warn!("ZshFork backend specified, but user shell is not Zsh.");
198+
return Ok(None);
199+
}
200+
201+
let parsed = match extract_shell_script(&exec_request.command) {
202+
Ok(parsed) => parsed,
203+
Err(err) => {
204+
tracing::warn!("ZshFork unified exec fallback: {err:?}");
205+
return Ok(None);
206+
}
207+
};
208+
if parsed.program != shell_zsh_path.to_string_lossy() {
209+
tracing::warn!(
210+
"ZshFork backend specified, but unified exec command targets `{}` instead of `{}`.",
211+
parsed.program,
212+
shell_zsh_path.display(),
213+
);
214+
return Ok(None);
215+
}
216+
217+
let exec_policy = Arc::new(RwLock::new(
218+
ctx.session.services.exec_policy.current().as_ref().clone(),
219+
));
220+
let command_executor = CoreShellCommandExecutor {
221+
command: exec_request.command.clone(),
222+
cwd: exec_request.cwd.clone(),
223+
sandbox_policy: exec_request.sandbox_policy.clone(),
224+
sandbox: exec_request.sandbox,
225+
env: exec_request.env.clone(),
226+
network: exec_request.network.clone(),
227+
windows_sandbox_level: exec_request.windows_sandbox_level,
228+
sandbox_permissions: exec_request.sandbox_permissions,
229+
justification: exec_request.justification.clone(),
230+
arg0: exec_request.arg0.clone(),
231+
sandbox_policy_cwd: ctx.turn.cwd.clone(),
232+
macos_seatbelt_profile_extensions: ctx
233+
.turn
234+
.config
235+
.permissions
236+
.macos_seatbelt_profile_extensions
237+
.clone(),
238+
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
239+
use_linux_sandbox_bwrap: ctx.turn.features.enabled(Feature::UseLinuxSandboxBwrap),
240+
};
241+
let main_execve_wrapper_exe = ctx
242+
.session
243+
.services
244+
.main_execve_wrapper_exe
245+
.clone()
246+
.ok_or_else(|| {
247+
ToolError::Rejected(
248+
"zsh fork feature enabled, but execve wrapper is not configured".to_string(),
249+
)
250+
})?;
251+
let escalation_policy = CoreShellActionProvider {
252+
policy: Arc::clone(&exec_policy),
253+
session: Arc::clone(&ctx.session),
254+
turn: Arc::clone(&ctx.turn),
255+
call_id: ctx.call_id.clone(),
256+
approval_policy: ctx.turn.approval_policy.value(),
257+
sandbox_policy: attempt.policy.clone(),
258+
sandbox_permissions: req.sandbox_permissions,
259+
prompt_permissions: req.additional_permissions.clone(),
260+
stopwatch: Stopwatch::unlimited(),
261+
};
262+
263+
let escalate_server = EscalateServer::new(
264+
shell_zsh_path.clone(),
265+
main_execve_wrapper_exe,
266+
escalation_policy,
267+
);
268+
let escalation_session = escalate_server
269+
.start_session(Arc::new(command_executor))
270+
.map_err(|err| ToolError::Rejected(err.to_string()))?;
271+
let mut exec_request = exec_request;
272+
exec_request.env.extend(escalation_session.env().clone());
273+
Ok(Some(PreparedUnifiedExecZshFork {
274+
exec_request,
275+
escalation_session,
276+
}))
277+
}
278+
175279
struct CoreShellActionProvider {
176280
policy: Arc<RwLock<Policy>>,
177281
session: Arc<crate::codex::Session>,
@@ -809,6 +913,7 @@ impl CoreShellCommandExecutor {
809913

810914
#[derive(Debug, Eq, PartialEq)]
811915
struct ParsedShellCommand {
916+
program: String,
812917
script: String,
813918
login: bool,
814919
}
@@ -817,12 +922,20 @@ fn extract_shell_script(command: &[String]) -> Result<ParsedShellCommand, ToolEr
817922
// Commands reaching zsh-fork can be wrapped by environment/sandbox helpers, so
818923
// we search for the first `-c`/`-lc` triple anywhere in the argv rather
819924
// than assuming it is the first positional form.
820-
if let Some((script, login)) = command.windows(3).find_map(|parts| match parts {
821-
[_, flag, script] if flag == "-c" => Some((script.to_owned(), false)),
822-
[_, flag, script] if flag == "-lc" => Some((script.to_owned(), true)),
925+
if let Some((program, script, login)) = command.windows(3).find_map(|parts| match parts {
926+
[program, flag, script] if flag == "-c" => {
927+
Some((program.to_owned(), script.to_owned(), false))
928+
}
929+
[program, flag, script] if flag == "-lc" => {
930+
Some((program.to_owned(), script.to_owned(), true))
931+
}
823932
_ => None,
824933
}) {
825-
return Ok(ParsedShellCommand { script, login });
934+
return Ok(ParsedShellCommand {
935+
program,
936+
script,
937+
login,
938+
});
826939
}
827940

828941
Err(ToolError::Rejected(

codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ fn extract_shell_script_preserves_login_flag() {
6464
assert_eq!(
6565
extract_shell_script(&["/bin/zsh".into(), "-lc".into(), "echo hi".into()]).unwrap(),
6666
ParsedShellCommand {
67+
program: "/bin/zsh".to_string(),
6768
script: "echo hi".to_string(),
6869
login: true,
6970
}
7071
);
7172
assert_eq!(
7273
extract_shell_script(&["/bin/zsh".into(), "-c".into(), "echo hi".into()]).unwrap(),
7374
ParsedShellCommand {
75+
program: "/bin/zsh".to_string(),
7476
script: "echo hi".to_string(),
7577
login: false,
7678
}
@@ -89,6 +91,7 @@ fn extract_shell_script_supports_wrapped_command_prefixes() {
8991
])
9092
.unwrap(),
9193
ParsedShellCommand {
94+
program: "/bin/zsh".to_string(),
9295
script: "echo hello".to_string(),
9396
login: true,
9497
}
@@ -105,6 +108,7 @@ fn extract_shell_script_supports_wrapped_command_prefixes() {
105108
])
106109
.unwrap(),
107110
ParsedShellCommand {
111+
program: "/bin/zsh".to_string(),
108112
script: "pwd".to_string(),
109113
login: false,
110114
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use super::ShellRequest;
2+
use crate::exec::ExecToolCallOutput;
3+
use crate::sandboxing::ExecRequest;
4+
use crate::tools::runtimes::unified_exec::UnifiedExecRequest;
5+
use crate::tools::sandboxing::SandboxAttempt;
6+
use crate::tools::sandboxing::ToolCtx;
7+
use crate::tools::sandboxing::ToolError;
8+
use crate::unified_exec::SpawnLifecycleHandle;
9+
10+
pub(crate) struct PreparedUnifiedExecSpawn {
11+
pub(crate) exec_request: ExecRequest,
12+
pub(crate) spawn_lifecycle: SpawnLifecycleHandle,
13+
}
14+
15+
/// Runs the zsh-fork shell-command backend when this request should be handled
16+
/// by executable-level escalation instead of the default shell runtime.
17+
///
18+
/// Returns `Ok(None)` when the current platform or request shape should fall
19+
/// back to the normal shell-command path.
20+
pub(crate) async fn maybe_run_shell_command(
21+
req: &ShellRequest,
22+
attempt: &SandboxAttempt<'_>,
23+
ctx: &ToolCtx,
24+
command: &[String],
25+
) -> Result<Option<ExecToolCallOutput>, ToolError> {
26+
imp::maybe_run_shell_command(req, attempt, ctx, command).await
27+
}
28+
29+
/// Prepares unified exec to launch through the zsh-fork backend when the
30+
/// request matches a wrapped `zsh -c/-lc` command on a supported platform.
31+
///
32+
/// Returns the transformed `ExecRequest` plus a spawn lifecycle that keeps the
33+
/// escalation server alive for the session and performs post-spawn cleanup.
34+
/// Returns `Ok(None)` when unified exec should use its normal spawn path.
35+
pub(crate) async fn maybe_prepare_unified_exec(
36+
req: &UnifiedExecRequest,
37+
attempt: &SandboxAttempt<'_>,
38+
ctx: &ToolCtx,
39+
exec_request: ExecRequest,
40+
) -> Result<Option<PreparedUnifiedExecSpawn>, ToolError> {
41+
imp::maybe_prepare_unified_exec(req, attempt, ctx, exec_request).await
42+
}
43+
44+
#[cfg(unix)]
45+
mod imp {
46+
use super::*;
47+
use crate::tools::runtimes::shell::unix_escalation;
48+
use crate::unified_exec::SpawnLifecycle;
49+
use codex_shell_escalation::EscalationSession;
50+
51+
#[derive(Debug)]
52+
struct ZshForkSpawnLifecycle {
53+
escalation_session: EscalationSession,
54+
}
55+
56+
impl SpawnLifecycle for ZshForkSpawnLifecycle {
57+
fn after_spawn(&mut self) {
58+
self.escalation_session.close_client_socket();
59+
}
60+
}
61+
62+
pub(super) async fn maybe_run_shell_command(
63+
req: &ShellRequest,
64+
attempt: &SandboxAttempt<'_>,
65+
ctx: &ToolCtx,
66+
command: &[String],
67+
) -> Result<Option<ExecToolCallOutput>, ToolError> {
68+
unix_escalation::try_run_zsh_fork(req, attempt, ctx, command).await
69+
}
70+
71+
pub(super) async fn maybe_prepare_unified_exec(
72+
req: &UnifiedExecRequest,
73+
attempt: &SandboxAttempt<'_>,
74+
ctx: &ToolCtx,
75+
exec_request: ExecRequest,
76+
) -> Result<Option<PreparedUnifiedExecSpawn>, ToolError> {
77+
let Some(prepared) =
78+
unix_escalation::prepare_unified_exec_zsh_fork(req, attempt, ctx, exec_request).await?
79+
else {
80+
return Ok(None);
81+
};
82+
83+
Ok(Some(PreparedUnifiedExecSpawn {
84+
exec_request: prepared.exec_request,
85+
spawn_lifecycle: Box::new(ZshForkSpawnLifecycle {
86+
escalation_session: prepared.escalation_session,
87+
}),
88+
}))
89+
}
90+
}
91+
92+
#[cfg(not(unix))]
93+
mod imp {
94+
use super::*;
95+
96+
pub(super) async fn maybe_run_shell_command(
97+
req: &ShellRequest,
98+
attempt: &SandboxAttempt<'_>,
99+
ctx: &ToolCtx,
100+
command: &[String],
101+
) -> Result<Option<ExecToolCallOutput>, ToolError> {
102+
let _ = (req, attempt, ctx, command);
103+
Ok(None)
104+
}
105+
106+
pub(super) async fn maybe_prepare_unified_exec(
107+
req: &UnifiedExecRequest,
108+
attempt: &SandboxAttempt<'_>,
109+
ctx: &ToolCtx,
110+
exec_request: ExecRequest,
111+
) -> Result<Option<PreparedUnifiedExecSpawn>, ToolError> {
112+
let _ = (req, attempt, ctx, exec_request);
113+
Ok(None)
114+
}
115+
}

0 commit comments

Comments
 (0)