@@ -6,6 +6,7 @@ use crate::exec::ExecToolCallOutput;
66use crate :: exec:: SandboxType ;
77use crate :: exec:: is_likely_sandbox_denied;
88use crate :: features:: Feature ;
9+ use crate :: sandboxing:: ExecRequest ;
910use crate :: sandboxing:: SandboxPermissions ;
1011use crate :: shell:: ShellType ;
1112use crate :: skills:: SkillMetadata ;
@@ -36,6 +37,7 @@ use codex_shell_escalation::EscalationDecision;
3637use codex_shell_escalation:: EscalationExecution ;
3738use codex_shell_escalation:: EscalationPermissions ;
3839use codex_shell_escalation:: EscalationPolicy ;
40+ use codex_shell_escalation:: EscalationSession ;
3941use codex_shell_escalation:: ExecParams ;
4042use codex_shell_escalation:: ExecResult ;
4143use codex_shell_escalation:: Permissions as EscalatedPermissions ;
@@ -51,6 +53,11 @@ use tokio::sync::RwLock;
5153use tokio_util:: sync:: CancellationToken ;
5254use uuid:: Uuid ;
5355
56+ pub ( crate ) struct PreparedUnifiedExecZshFork {
57+ pub ( crate ) exec_request : ExecRequest ,
58+ pub ( crate ) escalation_session : EscalationSession ,
59+ }
60+
5461pub ( 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 ( CancellationToken :: new ( ) , 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+
175279struct CoreShellActionProvider {
176280 policy : Arc < RwLock < Policy > > ,
177281 session : Arc < crate :: codex:: Session > ,
@@ -648,17 +752,20 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
648752 & self ,
649753 _command : Vec < String > ,
650754 _cwd : PathBuf ,
651- env : HashMap < String , String > ,
755+ env_overlay : HashMap < String , String > ,
652756 cancel_rx : CancellationToken ,
757+ after_spawn : Option < Box < dyn FnOnce ( ) + Send > > ,
653758 ) -> anyhow:: Result < ExecResult > {
654759 let mut exec_env = self . env . clone ( ) ;
760+ // `env_overlay` comes from `EscalationSession::env()`, so merge only the
761+ // wrapper/socket variables into the base shell environment.
655762 for var in [ "CODEX_ESCALATE_SOCKET" , "EXEC_WRAPPER" , "BASH_EXEC_WRAPPER" ] {
656- if let Some ( value) = env . get ( var) {
763+ if let Some ( value) = env_overlay . get ( var) {
657764 exec_env. insert ( var. to_string ( ) , value. clone ( ) ) ;
658765 }
659766 }
660767
661- let result = crate :: sandboxing:: execute_env (
768+ let result = crate :: sandboxing:: execute_env_after_spawn (
662769 crate :: sandboxing:: ExecRequest {
663770 command : self . command . clone ( ) ,
664771 cwd : self . cwd . clone ( ) ,
@@ -673,6 +780,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
673780 arg0 : self . arg0 . clone ( ) ,
674781 } ,
675782 None ,
783+ after_spawn. unwrap_or_else ( || Box :: new ( || { } ) ) ,
676784 )
677785 . await ?;
678786
@@ -809,6 +917,7 @@ impl CoreShellCommandExecutor {
809917
810918#[ derive( Debug , Eq , PartialEq ) ]
811919struct ParsedShellCommand {
920+ program : String ,
812921 script : String ,
813922 login : bool ,
814923}
@@ -817,12 +926,20 @@ fn extract_shell_script(command: &[String]) -> Result<ParsedShellCommand, ToolEr
817926 // Commands reaching zsh-fork can be wrapped by environment/sandbox helpers, so
818927 // we search for the first `-c`/`-lc` triple anywhere in the argv rather
819928 // 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 ) ) ,
929+ if let Some ( ( program, script, login) ) = command. windows ( 3 ) . find_map ( |parts| match parts {
930+ [ program, flag, script] if flag == "-c" => {
931+ Some ( ( program. to_owned ( ) , script. to_owned ( ) , false ) )
932+ }
933+ [ program, flag, script] if flag == "-lc" => {
934+ Some ( ( program. to_owned ( ) , script. to_owned ( ) , true ) )
935+ }
823936 _ => None ,
824937 } ) {
825- return Ok ( ParsedShellCommand { script, login } ) ;
938+ return Ok ( ParsedShellCommand {
939+ program,
940+ script,
941+ login,
942+ } ) ;
826943 }
827944
828945 Err ( ToolError :: Rejected (
0 commit comments