sandboxing: plumb split sandbox policies through runtime (#13439)

## Why

`#13434` introduces split `FileSystemSandboxPolicy` and
`NetworkSandboxPolicy`, but the runtime still made most execution-time
sandbox decisions from the legacy `SandboxPolicy` projection.

That projection loses information about combinations like unrestricted
filesystem access with restricted network access. In practice, that
means the runtime can choose the wrong platform sandbox behavior or set
the wrong network-restriction environment for a command even when config
has already separated those concerns.

This PR carries the split policies through the runtime so sandbox
selection, process spawning, and exec handling can consult the policy
that actually matters.

## What changed

- threaded `FileSystemSandboxPolicy` and `NetworkSandboxPolicy` through
`TurnContext`, `ExecRequest`, sandbox attempts, shell escalation state,
unified exec, and app-server exec overrides
- updated sandbox selection in `core/src/sandboxing/mod.rs` and
`core/src/exec.rs` to key off `FileSystemSandboxPolicy.kind` plus
`NetworkSandboxPolicy`, rather than inferring behavior only from the
legacy `SandboxPolicy`
- updated process spawning in `core/src/spawn.rs` and the platform
wrappers to use `NetworkSandboxPolicy` when deciding whether to set
`CODEX_SANDBOX_NETWORK_DISABLED`
- kept additional-permissions handling and legacy `ExternalSandbox`
compatibility projections aligned with the split policies, including
explicit user-shell execution and Windows restricted-token routing
- updated callers across `core`, `app-server`, and `linux-sandbox` to
pass the split policies explicitly

## Verification

- added regression coverage in `core/tests/suite/user_shell_cmd.rs` to
verify `RunUserShellCommand` does not inherit
`CODEX_SANDBOX_NETWORK_DISABLED` from the active turn
- added coverage in `core/src/exec.rs` for Windows restricted-token
sandbox selection when the legacy projection is `ExternalSandbox`
- updated Linux sandbox coverage in
`linux-sandbox/tests/suite/landlock.rs` to exercise the split-policy
exec path
- verified the current PR state with `just clippy`




---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/13439).
* #13453
* #13452
* #13451
* #13449
* #13448
* #13445
* #13440
* __->__ #13439

---------

Co-authored-by: viyatb-oai <viyatb@openai.com>
This commit is contained in:
Michael Bolin
2026-03-06 18:30:21 -08:00
committed by GitHub
Unverified
parent 25fa974166
commit 22ac6b9aaa
20 changed files with 913 additions and 90 deletions
+4 -1
View File
@@ -852,7 +852,8 @@ impl JsReplManager {
.network
.is_some();
let sandbox_type = sandbox.select_initial(
&turn.sandbox_policy,
&turn.file_system_sandbox_policy,
turn.network_sandbox_policy,
SandboxablePreference::Auto,
turn.windows_sandbox_level,
has_managed_network_requirements,
@@ -861,6 +862,8 @@ impl JsReplManager {
.transform(crate::sandboxing::SandboxTransformRequest {
spec,
policy: &turn.sandbox_policy,
file_system_policy: &turn.file_system_sandbox_policy,
network_policy: turn.network_sandbox_policy,
sandbox: sandbox_type,
enforce_managed_network: has_managed_network_requirements,
network: None,
+6 -1
View File
@@ -169,7 +169,8 @@ impl ToolOrchestrator {
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial(
&turn_ctx.sandbox_policy,
&turn_ctx.file_system_sandbox_policy,
turn_ctx.network_sandbox_policy,
tool.sandbox_preference(),
turn_ctx.windows_sandbox_level,
has_managed_network_requirements,
@@ -182,6 +183,8 @@ impl ToolOrchestrator {
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
policy: &turn_ctx.sandbox_policy,
file_system_policy: &turn_ctx.file_system_sandbox_policy,
network_policy: turn_ctx.network_sandbox_policy,
enforce_managed_network: has_managed_network_requirements,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
@@ -296,6 +299,8 @@ impl ToolOrchestrator {
let escalated_attempt = SandboxAttempt {
sandbox: crate::exec::SandboxType::None,
policy: &turn_ctx.sandbox_policy,
file_system_policy: &turn_ctx.file_system_sandbox_policy,
network_policy: turn_ctx.network_sandbox_policy,
enforce_managed_network: has_managed_network_requirements,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
@@ -24,6 +24,8 @@ use codex_execpolicy::RuleMatch;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::RejectConfig;
@@ -98,6 +100,8 @@ pub(super) async fn try_run_zsh_fork(
windows_sandbox_level,
sandbox_permissions,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
justification,
arg0,
} = sandbox_exec_request;
@@ -113,6 +117,8 @@ pub(super) async fn try_run_zsh_fork(
command,
cwd: sandbox_cwd,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
sandbox,
env: sandbox_env,
network: sandbox_network,
@@ -158,7 +164,9 @@ pub(super) async fn try_run_zsh_fork(
turn: Arc::clone(&ctx.turn),
call_id: ctx.call_id.clone(),
approval_policy: ctx.turn.approval_policy.value(),
sandbox_policy: attempt.policy.clone(),
sandbox_policy: command_executor.sandbox_policy.clone(),
file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(),
network_sandbox_policy: command_executor.network_sandbox_policy,
sandbox_permissions: req.sandbox_permissions,
prompt_permissions: req.additional_permissions.clone(),
stopwatch: stopwatch.clone(),
@@ -180,7 +188,7 @@ pub(super) async fn try_run_zsh_fork(
pub(crate) async fn prepare_unified_exec_zsh_fork(
req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest,
attempt: &SandboxAttempt<'_>,
_attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
) -> Result<Option<PreparedUnifiedExecZshFork>, ToolError> {
@@ -220,6 +228,8 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
command: exec_request.command.clone(),
cwd: exec_request.cwd.clone(),
sandbox_policy: exec_request.sandbox_policy.clone(),
file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
network_sandbox_policy: exec_request.network_sandbox_policy,
sandbox: exec_request.sandbox,
env: exec_request.env.clone(),
network: exec_request.network.clone(),
@@ -253,7 +263,9 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
turn: Arc::clone(&ctx.turn),
call_id: ctx.call_id.clone(),
approval_policy: ctx.turn.approval_policy.value(),
sandbox_policy: attempt.policy.clone(),
sandbox_policy: exec_request.sandbox_policy.clone(),
file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
network_sandbox_policy: exec_request.network_sandbox_policy,
sandbox_permissions: req.sandbox_permissions,
prompt_permissions: req.additional_permissions.clone(),
stopwatch: Stopwatch::unlimited(),
@@ -282,6 +294,8 @@ struct CoreShellActionProvider {
call_id: String,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
file_system_sandbox_policy: FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
sandbox_permissions: SandboxPermissions,
prompt_permissions: Option<PermissionProfile>,
stopwatch: Stopwatch,
@@ -308,6 +322,8 @@ impl CoreShellActionProvider {
fn shell_request_escalation_execution(
sandbox_permissions: SandboxPermissions,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
additional_permissions: Option<&PermissionProfile>,
macos_seatbelt_profile_extensions: Option<&MacOsSeatbeltProfileExtensions>,
) -> EscalationExecution {
@@ -321,6 +337,8 @@ impl CoreShellActionProvider {
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: file_system_sandbox_policy.clone(),
network_sandbox_policy,
macos_seatbelt_profile_extensions: macos_seatbelt_profile_extensions
.cloned(),
},
@@ -634,6 +652,8 @@ impl EscalationPolicy for CoreShellActionProvider {
DecisionSource::UnmatchedCommandFallback => Self::shell_request_escalation_execution(
self.sandbox_permissions,
&self.sandbox_policy,
&self.file_system_sandbox_policy,
self.network_sandbox_policy,
self.prompt_permissions.as_ref(),
self.turn
.config
@@ -741,6 +761,8 @@ struct CoreShellCommandExecutor {
command: Vec<String>,
cwd: PathBuf,
sandbox_policy: SandboxPolicy,
file_system_sandbox_policy: FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
sandbox: SandboxType,
env: HashMap<String, String>,
network: Option<codex_network_proxy::NetworkProxy>,
@@ -760,6 +782,8 @@ struct PrepareSandboxedExecParams<'a> {
workdir: &'a AbsolutePathBuf,
env: HashMap<String, String>,
sandbox_policy: &'a SandboxPolicy,
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
additional_permissions: Option<PermissionProfile>,
#[cfg(target_os = "macos")]
macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
@@ -795,6 +819,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
windows_sandbox_level: self.windows_sandbox_level,
sandbox_permissions: self.sandbox_permissions,
sandbox_policy: self.sandbox_policy.clone(),
file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
network_sandbox_policy: self.network_sandbox_policy,
justification: self.justification.clone(),
arg0: self.arg0.clone(),
},
@@ -841,6 +867,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
workdir,
env,
sandbox_policy: &self.sandbox_policy,
file_system_sandbox_policy: &self.file_system_sandbox_policy,
network_sandbox_policy: self.network_sandbox_policy,
additional_permissions: None,
#[cfg(target_os = "macos")]
macos_seatbelt_profile_extensions: self
@@ -858,6 +886,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
workdir,
env,
sandbox_policy: &self.sandbox_policy,
file_system_sandbox_policy: &self.file_system_sandbox_policy,
network_sandbox_policy: self.network_sandbox_policy,
additional_permissions: Some(permission_profile),
#[cfg(target_os = "macos")]
macos_seatbelt_profile_extensions: self
@@ -872,6 +902,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
workdir,
env,
sandbox_policy: &permissions.sandbox_policy,
file_system_sandbox_policy: &permissions.file_system_sandbox_policy,
network_sandbox_policy: permissions.network_sandbox_policy,
additional_permissions: None,
#[cfg(target_os = "macos")]
macos_seatbelt_profile_extensions: permissions
@@ -886,6 +918,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
}
impl CoreShellCommandExecutor {
#[allow(clippy::too_many_arguments)]
fn prepare_sandboxed_exec(
&self,
params: PrepareSandboxedExecParams<'_>,
@@ -895,6 +928,8 @@ impl CoreShellCommandExecutor {
workdir,
env,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
additional_permissions,
#[cfg(target_os = "macos")]
macos_seatbelt_profile_extensions,
@@ -904,7 +939,8 @@ impl CoreShellCommandExecutor {
.ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?;
let sandbox_manager = crate::sandboxing::SandboxManager::new();
let sandbox = sandbox_manager.select_initial(
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
SandboxablePreference::Auto,
self.windows_sandbox_level,
self.network.is_some(),
@@ -926,6 +962,8 @@ impl CoreShellCommandExecutor {
justification: self.justification.clone(),
},
policy: sandbox_policy,
file_system_policy: file_system_sandbox_policy,
network_policy: network_sandbox_policy,
sandbox,
enforce_managed_network: self.network.is_some(),
network: self.network.as_ref(),
@@ -31,6 +31,11 @@ use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SkillScope;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
@@ -223,6 +228,21 @@ fn shell_request_escalation_execution_is_explicit() {
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: AbsolutePathBuf::from_absolute_path("/tmp/secret").unwrap(),
},
access: FileSystemAccessMode::None,
},
]);
let network_sandbox_policy = NetworkSandboxPolicy::Restricted;
let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
@@ -232,6 +252,8 @@ fn shell_request_escalation_execution_is_explicit() {
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::UseDefault,
&sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
None,
Some(&macos_seatbelt_profile_extensions),
),
@@ -241,6 +263,8 @@ fn shell_request_escalation_execution_is_explicit() {
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::RequireEscalated,
&sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
None,
Some(&macos_seatbelt_profile_extensions),
),
@@ -250,12 +274,16 @@ fn shell_request_escalation_execution_is_explicit() {
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::WithAdditionalPermissions,
&sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
Some(&requested_permissions),
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
macos_seatbelt_profile_extensions: Some(macos_seatbelt_profile_extensions),
},
)),
@@ -474,6 +502,10 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
&SandboxPolicy::new_read_only_policy(),
),
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
@@ -524,6 +556,8 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
network_sandbox_policy: NetworkSandboxPolicy::Enabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
@@ -560,6 +594,8 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy: permissions.sandbox_policy.get().clone(),
file_system_sandbox_policy: permissions.file_system_sandbox_policy.clone(),
network_sandbox_policy: permissions.network_sandbox_policy,
macos_seatbelt_profile_extensions: permissions
.macos_seatbelt_profile_extensions
.clone(),
@@ -588,13 +624,16 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
#[tokio::test]
async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_macos_extensions() {
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let executor = CoreShellCommandExecutor {
command: vec!["echo".to_string(), "ok".to_string()],
cwd: cwd.to_path_buf(),
env: HashMap::new(),
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
+6
View File
@@ -17,6 +17,8 @@ use crate::tools::network_approval::NetworkApprovalSpec;
use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkApprovalContext;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewDecision;
use futures::Future;
@@ -318,6 +320,8 @@ pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
pub(crate) struct SandboxAttempt<'a> {
pub sandbox: crate::exec::SandboxType,
pub policy: &'a crate::protocol::SandboxPolicy,
pub file_system_policy: &'a FileSystemSandboxPolicy,
pub network_policy: NetworkSandboxPolicy,
pub enforce_managed_network: bool,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a Path,
@@ -336,6 +340,8 @@ impl<'a> SandboxAttempt<'a> {
.transform(crate::sandboxing::SandboxTransformRequest {
spec,
policy: self.policy,
file_system_policy: self.file_system_policy,
network_policy: self.network_policy,
sandbox: self.sandbox,
enforce_managed_network: self.enforce_managed_network,
network,