Conversation
65be9c0 to
c04ba90
Compare
jif-oai
approved these changes
Mar 4, 2026
29419f7 to
1b5a00e
Compare
e95b1a1 to
58049ba
Compare
5c8732e to
8222c01
Compare
3960093 to
cbf6429
Compare
bolinfest
added a commit
that referenced
this pull request
Mar 5, 2026
## Why `shell_zsh_fork` already provides stronger guarantees around which executables receive elevated permissions. To reuse that machinery from unified exec without pushing Unix-specific escalation details through generic runtime code, the escalation bootstrap and session lifetime handling need a cleaner boundary. That boundary also needs to be safe for long-lived sessions: when an intercepted shell session is closed or pruned, any in-flight approval workers and any already-approved escalated child they spawned must be torn down with the session, and the inherited escalation socket must not leak into unrelated subprocesses. ## What Changed - Extracted a reusable `EscalationSession` and `EscalateServer::start_session(...)` in `shell-escalation` so callers can get the wrapper/socket env overlay and keep the escalation server alive without immediately running a one-shot command. - Documented that `EscalationSession::env()` and `ShellCommandExecutor::run(...)` exchange only that env overlay, which callers must merge into their own base shell environment. - Clarified the prepared-exec helper boundary in `core` by naming the new helper APIs around `ExecRequest`, while keeping the legacy `execute_env(...)` entrypoints as thin compatibility wrappers for existing callers that still use the older naming. - Added a small post-spawn hook on the prepared execution path so the parent copy of the inheritable escalation socket is closed immediately after both the existing one-shot shell-command spawn and the unified-exec spawn. - Made session teardown explicit with session-scoped cancellation: dropping an `EscalationSession` or canceling its parent request now stops intercept workers, and the server-spawned escalated child uses `kill_on_drop(true)` so teardown cannot orphan an already-approved child. - Added `UnifiedExecBackendConfig` plumbing through `ToolsConfig`, a `shell::zsh_fork_backend` facade, and an opaque unified-exec spawn-lifecycle hook so unified exec can prepare a wrapped `zsh -c/-lc` request without storing `EscalationSession` directly in generic process/runtime code. - Kept the existing `shell_command` zsh-fork behavior intact on top of the new bootstrap path. Tool selection is unchanged in this PR: when `shell_zsh_fork` is enabled, `ShellCommand` still wins over `exec_command`. ## Verification - `cargo test -p codex-shell-escalation` - includes coverage for `start_session_exposes_wrapper_env_overlay` - includes coverage for `exec_closes_parent_socket_after_shell_spawn` - includes coverage for `dropping_session_aborts_intercept_workers_and_kills_spawned_child` - `cargo test -p codex-core shell_zsh_fork_prefers_shell_command_over_unified_exec` - `cargo test -p codex-core --test all shell_zsh_fork_prompts_for_skill_script_execution` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13392). * #13432 * __->__ #13392
184d0e7 to
8b935f8
Compare
f3f72e3 to
e2d0232
Compare
6764932 to
61fcd3e
Compare
83eb97f to
c7ce647
Compare
75d57a9 to
6a3ad79
Compare
1b7e3ec to
b23aeee
Compare
2be2c19 to
374a9a6
Compare
84ec355 to
ab58df4
Compare
This was referenced Mar 13, 2026
jif-oai
reviewed
Mar 17, 2026
| .as_ref() | ||
| .map(|preamble| format!("{preamble}\n\n{}", shell_command.script)) | ||
| .unwrap_or_else(|| shell_command.script.clone()); | ||
| let exact_intercepted_command = vec![ |
Collaborator
There was a problem hiding this comment.
I think the exact match will create some issues... what if you don't have a shell snapshot available for example? Then we can still have a -lc
This will basically break the pre-approved path
| req.sandbox_permissions, | ||
| SandboxPermissions::RequireEscalated | ||
| ) { | ||
| let mut explicit_env_overrides = req.env.clone(); |
Collaborator
There was a problem hiding this comment.
this changes the contract of the shell snapshot. It means you will restore something coming from before the shell_snapshot on top of the shell_snapshot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When
unified_execandshell_zsh_forkare enabled together, unified-exec sessions need to stay on the configured zsh-fork binary so exec interception remains reliable for approvals, execpolicy, and escalation behavior.In practice, those sessions could drift away from the configured
zsh_pathin a few ways:exec_command.shelloverrides could request a different shellexec zshcalls could hop onto the hostzsh/usr/libexec/path_helperas separate escalated execsexec_commandsessions could let laterwrite_stdinsubcommands inherit that broader privilege instead of dropping back to turn-default policyThat weakens the guarantees the zsh-fork path is supposed to provide and makes privileged interactive sessions much noisier than intended.
What Changed
exec_command/ unified exec when both features are enabled, and hid theshellparameter from theexec_commandschema when the session is actively using zsh-fork (core/src/tools/spec.rs,core/src/tools/handlers/unified_exec.rs).zsh_path(core/src/tools/handlers/unified_exec.rs).program,-c/-lc,script) before preparing the zsh-fork unified-exec path (core/src/tools/runtimes/shell/unix_escalation.rs,core/src/tools/runtimes/shell/zsh_fork_backend.rs,core/src/tools/runtimes/unified_exec.rs).zsh -c ...target instead of heuristically matching later startup execs. When a matching shell snapshot exists for a login-shell request, that inner command now sources the snapshot, reapplies explicit env overrides, and then runs the original script. This preserves login-derived shell state without rerunning startup helpers under zsh-fork interception (core/src/tools/runtimes/mod.rs,core/src/tools/runtimes/shell/unix_escalation.rs).zshexecs on zsh-fork by:zshinstallationszshinstead of the configuredzsh_pathzsh_path(
core/src/tools/runtimes/shell/unix_escalation.rs).cwdand re-read execpolicy for intercepted execs so approved amendments continue to apply to later matching subcommands in the same session (core/src/tools/runtimes/shell/unix_escalation.rs).write_stdinsubcommands in a privileged zsh-fork unified-exec TTY fall back to turn-default sandbox policy unless separately allowed, while still honoring matchingprefix_rule()approvals (core/src/tools/runtimes/shell/unix_escalation.rs,core/tests/suite/unified_exec.rs).core/src/unified_exec/process.rs,core/src/tools/runtimes/shell/zsh_fork_backend.rs).app-server/tests/suite/v2/turn_start_zsh_fork.rs)core/tests/suite/approvals.rs)cwdbehavior (core/tests/suite/skill_approval.rs)zshrewrite coverage, and the privileged-write_stdinsandbox regression (core/tests/suite/unified_exec.rs)zshrewrite predicates (core/src/tools/handlers/unified_exec_tests.rs,core/src/tools/runtimes/shell/unix_escalation_tests.rs)#[cfg(unix)]usage around zsh-fork-only integration coverage.Verification
cargo test -p codex-core build_exact_match_zsh_fork_reexec --libcargo test -p codex-core maybe_wrap_shell_lc_with_snapshot --libcargo test -p codex-core --test all unified_exec_zsh_fork_write_stdin_reverts_to_default_sandbox_and_honors_prefix_rulecargo test -p codex-app-server --test all turn_start_shell_zsh_fork_login_startup_helper_does_not_prompt_separately_v2Manual Testing
Run Codex built from source with both features enabled as follows:
just codex --enable unified_exec --enable shell_zsh_fork --config zsh_path=$(realpath codex-rs/app-server/tests/suite/zsh)Then ask Codex to do something like:
Then tell it:
In my case, it replied with:
Then over in a plain shell, I checked
pstreeand confirmed that the Python process was still running under the configured zsh-fork shell rather than the host shell.