diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs
index 7e49191067..9a55badcdf 100644
--- a/codex-rs/core/src/landlock.rs
+++ b/codex-rs/core/src/landlock.rs
@@ -1,3 +1,4 @@
+use crate::protocol::FileSystemSandboxPolicy;
use crate::protocol::NetworkSandboxPolicy;
use crate::protocol::SandboxPolicy;
use crate::spawn::SpawnChildRequest;
@@ -14,9 +15,9 @@ use tokio::process::Child;
/// isolation plus seccomp for network restrictions.
///
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
-/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the
-/// public CLI. We convert the internal [`SandboxPolicy`] representation into
-/// the equivalent CLI options.
+/// helper is a separate executable. We pass the legacy [`SandboxPolicy`] plus
+/// split filesystem/network policies as JSON so the helper can migrate
+/// incrementally without breaking older call sites.
#[allow(clippy::too_many_arguments)]
pub async fn spawn_command_under_linux_sandbox
(
codex_linux_sandbox_exe: P,
@@ -32,9 +33,13 @@ pub async fn spawn_command_under_linux_sandbox
(
where
P: AsRef,
{
- let args = create_linux_sandbox_command_args(
+ let file_system_sandbox_policy = FileSystemSandboxPolicy::from(sandbox_policy);
+ let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
+ let args = create_linux_sandbox_command_args_for_policies(
command,
sandbox_policy,
+ &file_system_sandbox_policy,
+ network_sandbox_policy,
sandbox_policy_cwd,
use_bwrap_sandbox,
allow_network_for_proxy(false),
@@ -45,7 +50,7 @@ where
args,
arg0,
cwd: command_cwd,
- network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy),
+ network_sandbox_policy,
network,
stdio_policy,
env,
@@ -60,13 +65,55 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
enforce_managed_network
}
-/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`.
+/// Converts the sandbox policies into the CLI invocation for
+/// `codex-linux-sandbox`.
///
/// The helper performs the actual sandboxing (bubblewrap + seccomp) after
/// parsing these arguments. See `docs/linux_sandbox.md` for the Linux semantics.
-pub(crate) fn create_linux_sandbox_command_args(
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn create_linux_sandbox_command_args_for_policies(
command: Vec,
sandbox_policy: &SandboxPolicy,
+ file_system_sandbox_policy: &FileSystemSandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
+ sandbox_policy_cwd: &Path,
+ use_bwrap_sandbox: bool,
+ allow_network_for_proxy: bool,
+) -> Vec {
+ #[expect(clippy::expect_used)]
+ let sandbox_policy_json =
+ serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON");
+ #[expect(clippy::expect_used)]
+ let file_system_policy_json = serde_json::to_string(file_system_sandbox_policy)
+ .expect("Failed to serialize FileSystemSandboxPolicy to JSON");
+ #[expect(clippy::expect_used)]
+ let network_policy_json = serde_json::to_string(&network_sandbox_policy)
+ .expect("Failed to serialize NetworkSandboxPolicy to JSON");
+
+ let mut linux_cmd = create_linux_sandbox_command_args(
+ command,
+ sandbox_policy_cwd,
+ use_bwrap_sandbox,
+ allow_network_for_proxy,
+ );
+ linux_cmd.splice(
+ 2..2,
+ [
+ "--sandbox-policy".to_string(),
+ sandbox_policy_json,
+ "--file-system-sandbox-policy".to_string(),
+ file_system_policy_json,
+ "--network-sandbox-policy".to_string(),
+ network_policy_json,
+ ],
+ );
+ linux_cmd
+}
+
+/// Converts the sandbox cwd and execution options into the CLI invocation for
+/// `codex-linux-sandbox`.
+pub(crate) fn create_linux_sandbox_command_args(
+ command: Vec,
sandbox_policy_cwd: &Path,
use_bwrap_sandbox: bool,
allow_network_for_proxy: bool,
@@ -77,16 +124,7 @@ pub(crate) fn create_linux_sandbox_command_args(
.expect("cwd must be valid UTF-8")
.to_string();
- #[expect(clippy::expect_used)]
- let sandbox_policy_json =
- serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON");
-
- let mut linux_cmd: Vec = vec![
- "--sandbox-policy-cwd".to_string(),
- sandbox_policy_cwd,
- "--sandbox-policy".to_string(),
- sandbox_policy_json,
- ];
+ let mut linux_cmd: Vec = vec!["--sandbox-policy-cwd".to_string(), sandbox_policy_cwd];
if use_bwrap_sandbox {
linux_cmd.push("--use-bwrap-sandbox".to_string());
}
@@ -113,16 +151,14 @@ mod tests {
fn bwrap_flags_are_feature_gated() {
let command = vec!["/bin/true".to_string()];
let cwd = Path::new("/tmp");
- let policy = SandboxPolicy::new_read_only_policy();
- let with_bwrap =
- create_linux_sandbox_command_args(command.clone(), &policy, cwd, true, false);
+ let with_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, true, false);
assert_eq!(
with_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
true
);
- let without_bwrap = create_linux_sandbox_command_args(command, &policy, cwd, false, false);
+ let without_bwrap = create_linux_sandbox_command_args(command, cwd, false, false);
assert_eq!(
without_bwrap.contains(&"--use-bwrap-sandbox".to_string()),
false
@@ -133,15 +169,46 @@ mod tests {
fn proxy_flag_is_included_when_requested() {
let command = vec!["/bin/true".to_string()];
let cwd = Path::new("/tmp");
- let policy = SandboxPolicy::new_read_only_policy();
- let args = create_linux_sandbox_command_args(command, &policy, cwd, true, true);
+ let args = create_linux_sandbox_command_args(command, cwd, true, true);
assert_eq!(
args.contains(&"--allow-network-for-proxy".to_string()),
true
);
}
+ #[test]
+ fn split_policy_flags_are_included() {
+ let command = vec!["/bin/true".to_string()];
+ let cwd = Path::new("/tmp");
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
+ let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
+
+ let args = create_linux_sandbox_command_args_for_policies(
+ command,
+ &sandbox_policy,
+ &file_system_sandbox_policy,
+ network_sandbox_policy,
+ cwd,
+ true,
+ false,
+ );
+
+ assert_eq!(
+ args.windows(2).any(|window| {
+ window[0] == "--file-system-sandbox-policy" && !window[1].is_empty()
+ }),
+ true
+ );
+ assert_eq!(
+ args.windows(2)
+ .any(|window| window[0] == "--network-sandbox-policy"
+ && window[1] == "\"restricted\""),
+ true
+ );
+ }
+
#[test]
fn proxy_network_requires_managed_requirements() {
assert_eq!(allow_network_for_proxy(false), false);
diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs
index eab5d0430e..eae5554848 100644
--- a/codex-rs/core/src/sandboxing/mod.rs
+++ b/codex-rs/core/src/sandboxing/mod.rs
@@ -14,7 +14,7 @@ use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::execute_exec_request;
use crate::landlock::allow_network_for_proxy;
-use crate::landlock::create_linux_sandbox_command_args;
+use crate::landlock::create_linux_sandbox_command_args_for_policies;
use crate::protocol::FileSystemAccessMode;
use crate::protocol::FileSystemPath;
use crate::protocol::FileSystemSandboxEntry;
@@ -514,9 +514,11 @@ impl SandboxManager {
let exe = codex_linux_sandbox_exe
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
- let mut args = create_linux_sandbox_command_args(
+ let mut args = create_linux_sandbox_command_args_for_policies(
command.clone(),
&effective_policy,
+ &effective_file_system_policy,
+ effective_network_policy,
sandbox_policy_cwd,
use_linux_sandbox_bwrap,
allow_proxy_network,
diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs
index f9477a1264..a120ab7054 100644
--- a/codex-rs/linux-sandbox/src/landlock.rs
+++ b/codex-rs/linux-sandbox/src/landlock.rs
@@ -8,6 +8,7 @@ use std::path::Path;
use codex_core::error::CodexErr;
use codex_core::error::Result;
use codex_core::error::SandboxErr;
+use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -40,13 +41,14 @@ use seccompiler::apply_filter;
/// Filesystem restrictions are intentionally handled by bubblewrap.
pub(crate) fn apply_sandbox_policy_to_current_thread(
sandbox_policy: &SandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
cwd: &Path,
apply_landlock_fs: bool,
allow_network_for_proxy: bool,
proxy_routed_network: bool,
) -> Result<()> {
let network_seccomp_mode = network_seccomp_mode(
- sandbox_policy,
+ network_sandbox_policy,
allow_network_for_proxy,
proxy_routed_network,
);
@@ -91,20 +93,20 @@ enum NetworkSeccompMode {
}
fn should_install_network_seccomp(
- sandbox_policy: &SandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
allow_network_for_proxy: bool,
) -> bool {
// Managed-network sessions should remain fail-closed even for policies that
// would normally grant full network access (for example, DangerFullAccess).
- !sandbox_policy.has_full_network_access() || allow_network_for_proxy
+ !network_sandbox_policy.is_enabled() || allow_network_for_proxy
}
fn network_seccomp_mode(
- sandbox_policy: &SandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
allow_network_for_proxy: bool,
proxy_routed_network: bool,
) -> Option {
- if !should_install_network_seccomp(sandbox_policy, allow_network_for_proxy) {
+ if !should_install_network_seccomp(network_sandbox_policy, allow_network_for_proxy) {
None
} else if proxy_routed_network {
Some(NetworkSeccompMode::ProxyRouted)
diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs
index c9fdd06a32..6a624ac1dd 100644
--- a/codex-rs/linux-sandbox/src/linux_run_main.rs
+++ b/codex-rs/linux-sandbox/src/linux_run_main.rs
@@ -14,6 +14,9 @@ use crate::proxy_routing::activate_proxy_routes_in_netns;
use crate::proxy_routing::prepare_host_proxy_route_spec;
use crate::vendored_bwrap::exec_vendored_bwrap;
use crate::vendored_bwrap::run_vendored_bwrap_main;
+use codex_protocol::protocol::FileSystemSandboxPolicy;
+use codex_protocol::protocol::NetworkSandboxPolicy;
+use codex_protocol::protocol::SandboxPolicy;
#[derive(Debug, Parser)]
/// CLI surface for the Linux sandbox helper.
@@ -26,8 +29,18 @@ pub struct LandlockCommand {
#[arg(long = "sandbox-policy-cwd")]
pub sandbox_policy_cwd: PathBuf,
- #[arg(long = "sandbox-policy")]
- pub sandbox_policy: codex_protocol::protocol::SandboxPolicy,
+ /// Legacy compatibility policy.
+ ///
+ /// Newer callers pass split filesystem/network policies as well so the
+ /// helper can migrate incrementally without breaking older invocations.
+ #[arg(long = "sandbox-policy", hide = true)]
+ pub sandbox_policy: Option,
+
+ #[arg(long = "file-system-sandbox-policy", hide = true)]
+ pub file_system_sandbox_policy: Option,
+
+ #[arg(long = "network-sandbox-policy", hide = true)]
+ pub network_sandbox_policy: Option,
/// Opt-in: use the bubblewrap-based Linux sandbox pipeline.
///
@@ -77,6 +90,8 @@ pub fn run_main() -> ! {
let LandlockCommand {
sandbox_policy_cwd,
sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
use_bwrap_sandbox,
apply_seccomp_then_exec,
allow_network_for_proxy,
@@ -89,6 +104,16 @@ pub fn run_main() -> ! {
panic!("No command specified to execute.");
}
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_bwrap_sandbox);
+ let EffectiveSandboxPolicies {
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ } = resolve_sandbox_policies(
+ sandbox_policy_cwd.as_path(),
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ );
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
// established the filesystem view.
@@ -104,6 +129,7 @@ pub fn run_main() -> ! {
let proxy_routing_active = allow_network_for_proxy;
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
+ network_sandbox_policy,
&sandbox_policy_cwd,
false,
allow_network_for_proxy,
@@ -114,9 +140,10 @@ pub fn run_main() -> ! {
exec_or_panic(command);
}
- if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
+ if file_system_sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
+ network_sandbox_policy,
&sandbox_policy_cwd,
false,
allow_network_for_proxy,
@@ -142,6 +169,8 @@ pub fn run_main() -> ! {
let inner = build_inner_seccomp_command(
&sandbox_policy_cwd,
&sandbox_policy,
+ &file_system_sandbox_policy,
+ network_sandbox_policy,
use_bwrap_sandbox,
allow_network_for_proxy,
proxy_route_spec,
@@ -150,6 +179,7 @@ pub fn run_main() -> ! {
run_bwrap_with_proc_fallback(
&sandbox_policy_cwd,
&sandbox_policy,
+ network_sandbox_policy,
inner,
!no_proc,
allow_network_for_proxy,
@@ -159,6 +189,7 @@ pub fn run_main() -> ! {
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
+ network_sandbox_policy,
&sandbox_policy_cwd,
true,
allow_network_for_proxy,
@@ -169,6 +200,53 @@ pub fn run_main() -> ! {
exec_or_panic(command);
}
+#[derive(Debug, Clone)]
+struct EffectiveSandboxPolicies {
+ sandbox_policy: SandboxPolicy,
+ file_system_sandbox_policy: FileSystemSandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
+}
+
+fn resolve_sandbox_policies(
+ sandbox_policy_cwd: &Path,
+ sandbox_policy: Option,
+ file_system_sandbox_policy: Option,
+ network_sandbox_policy: Option,
+) -> EffectiveSandboxPolicies {
+ match (
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ ) {
+ (Some(sandbox_policy), Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => {
+ EffectiveSandboxPolicies {
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ }
+ }
+ (Some(sandbox_policy), None, None) => EffectiveSandboxPolicies {
+ file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
+ network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
+ sandbox_policy,
+ },
+ (None, Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => {
+ let sandbox_policy = file_system_sandbox_policy
+ .to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd)
+ .unwrap_or_else(|err| {
+ panic!("failed to derive legacy sandbox policy from split policies: {err}")
+ });
+ EffectiveSandboxPolicies {
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ }
+ }
+ (None, None, None) => panic!("missing sandbox policy configuration"),
+ _ => panic!("file-system and network sandbox policies must be provided together"),
+ }
+}
+
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_sandbox: bool) {
if apply_seccomp_then_exec && !use_bwrap_sandbox {
panic!("--apply-seccomp-then-exec requires --use-bwrap-sandbox");
@@ -177,12 +255,13 @@ fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_san
fn run_bwrap_with_proc_fallback(
sandbox_policy_cwd: &Path,
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+ sandbox_policy: &SandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
inner: Vec,
mount_proc: bool,
allow_network_for_proxy: bool,
) -> ! {
- let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy);
+ let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy);
let mut mount_proc = mount_proc;
if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy, network_mode)
@@ -200,12 +279,12 @@ fn run_bwrap_with_proc_fallback(
}
fn bwrap_network_mode(
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
allow_network_for_proxy: bool,
) -> BwrapNetworkMode {
if allow_network_for_proxy {
BwrapNetworkMode::ProxyOnly
- } else if sandbox_policy.has_full_network_access() {
+ } else if network_sandbox_policy.is_enabled() {
BwrapNetworkMode::FullAccess
} else {
BwrapNetworkMode::Isolated
@@ -214,7 +293,7 @@ fn bwrap_network_mode(
fn build_bwrap_argv(
inner: Vec,
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+ sandbox_policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
options: BwrapOptions,
) -> Vec {
@@ -237,7 +316,7 @@ fn build_bwrap_argv(
fn preflight_proc_mount_support(
sandbox_policy_cwd: &Path,
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+ sandbox_policy: &SandboxPolicy,
network_mode: BwrapNetworkMode,
) -> bool {
let preflight_argv =
@@ -248,7 +327,7 @@ fn preflight_proc_mount_support(
fn build_preflight_bwrap_argv(
sandbox_policy_cwd: &Path,
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+ sandbox_policy: &SandboxPolicy,
network_mode: BwrapNetworkMode,
) -> Vec {
let preflight_command = vec![resolve_true_command()];
@@ -361,7 +440,9 @@ fn is_proc_mount_failure(stderr: &str) -> bool {
/// Build the inner command that applies seccomp after bubblewrap.
fn build_inner_seccomp_command(
sandbox_policy_cwd: &Path,
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+ sandbox_policy: &SandboxPolicy,
+ file_system_sandbox_policy: &FileSystemSandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
use_bwrap_sandbox: bool,
allow_network_for_proxy: bool,
proxy_route_spec: Option,
@@ -375,6 +456,14 @@ fn build_inner_seccomp_command(
Ok(json) => json,
Err(err) => panic!("failed to serialize sandbox policy: {err}"),
};
+ let file_system_policy_json = match serde_json::to_string(file_system_sandbox_policy) {
+ Ok(json) => json,
+ Err(err) => panic!("failed to serialize filesystem sandbox policy: {err}"),
+ };
+ let network_policy_json = match serde_json::to_string(&network_sandbox_policy) {
+ Ok(json) => json,
+ Err(err) => panic!("failed to serialize network sandbox policy: {err}"),
+ };
let mut inner = vec![
current_exe.to_string_lossy().to_string(),
@@ -382,6 +471,10 @@ fn build_inner_seccomp_command(
sandbox_policy_cwd.to_string_lossy().to_string(),
"--sandbox-policy".to_string(),
policy_json,
+ "--file-system-sandbox-policy".to_string(),
+ file_system_policy_json,
+ "--network-sandbox-policy".to_string(),
+ network_policy_json,
];
if use_bwrap_sandbox {
inner.push("--use-bwrap-sandbox".to_string());
diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs
index cda5090306..c466de5e27 100644
--- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs
+++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs
@@ -1,7 +1,13 @@
#[cfg(test)]
use super::*;
#[cfg(test)]
+use codex_protocol::protocol::FileSystemSandboxPolicy;
+#[cfg(test)]
+use codex_protocol::protocol::NetworkSandboxPolicy;
+#[cfg(test)]
use codex_protocol::protocol::SandboxPolicy;
+#[cfg(test)]
+use pretty_assertions::assert_eq;
#[test]
fn detects_proc_mount_invalid_argument_failure() {
@@ -91,22 +97,25 @@ fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
#[test]
fn proxy_only_mode_takes_precedence_over_full_network_policy() {
- let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
+ let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true);
assert_eq!(mode, BwrapNetworkMode::ProxyOnly);
}
#[test]
fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
- let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
+ let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true);
let argv = build_preflight_bwrap_argv(Path::new("/"), &SandboxPolicy::DangerFullAccess, mode);
assert!(argv.iter().any(|arg| arg == "--"));
}
#[test]
fn managed_proxy_inner_command_includes_route_spec() {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
let args = build_inner_seccomp_command(
Path::new("/tmp"),
- &SandboxPolicy::new_read_only_policy(),
+ &sandbox_policy,
+ &FileSystemSandboxPolicy::from(&sandbox_policy),
+ NetworkSandboxPolicy::Restricted,
true,
true,
Some("{\"routes\":[]}".to_string()),
@@ -118,10 +127,31 @@ fn managed_proxy_inner_command_includes_route_spec() {
}
#[test]
-fn non_managed_inner_command_omits_route_spec() {
+fn inner_command_includes_split_policy_flags() {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
let args = build_inner_seccomp_command(
Path::new("/tmp"),
- &SandboxPolicy::new_read_only_policy(),
+ &sandbox_policy,
+ &FileSystemSandboxPolicy::from(&sandbox_policy),
+ NetworkSandboxPolicy::Restricted,
+ true,
+ false,
+ None,
+ vec!["/bin/true".to_string()],
+ );
+
+ assert!(args.iter().any(|arg| arg == "--file-system-sandbox-policy"));
+ assert!(args.iter().any(|arg| arg == "--network-sandbox-policy"));
+}
+
+#[test]
+fn non_managed_inner_command_omits_route_spec() {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ let args = build_inner_seccomp_command(
+ Path::new("/tmp"),
+ &sandbox_policy,
+ &FileSystemSandboxPolicy::from(&sandbox_policy),
+ NetworkSandboxPolicy::Restricted,
true,
false,
None,
@@ -134,9 +164,12 @@ fn non_managed_inner_command_omits_route_spec() {
#[test]
fn managed_proxy_inner_command_requires_route_spec() {
let result = std::panic::catch_unwind(|| {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
build_inner_seccomp_command(
Path::new("/tmp"),
- &SandboxPolicy::new_read_only_policy(),
+ &sandbox_policy,
+ &FileSystemSandboxPolicy::from(&sandbox_policy),
+ NetworkSandboxPolicy::Restricted,
true,
true,
None,
@@ -146,6 +179,59 @@ fn managed_proxy_inner_command_requires_route_spec() {
assert!(result.is_err());
}
+#[test]
+fn resolve_sandbox_policies_derives_split_policies_from_legacy_policy() {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+
+ let resolved =
+ resolve_sandbox_policies(Path::new("/tmp"), Some(sandbox_policy.clone()), None, None);
+
+ assert_eq!(resolved.sandbox_policy, sandbox_policy.clone());
+ assert_eq!(
+ resolved.file_system_sandbox_policy,
+ FileSystemSandboxPolicy::from(&sandbox_policy)
+ );
+ assert_eq!(
+ resolved.network_sandbox_policy,
+ NetworkSandboxPolicy::from(&sandbox_policy)
+ );
+}
+
+#[test]
+fn resolve_sandbox_policies_derives_legacy_policy_from_split_policies() {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
+ let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
+
+ let resolved = resolve_sandbox_policies(
+ Path::new("/tmp"),
+ None,
+ Some(file_system_sandbox_policy.clone()),
+ Some(network_sandbox_policy),
+ );
+
+ assert_eq!(resolved.sandbox_policy, sandbox_policy);
+ assert_eq!(
+ resolved.file_system_sandbox_policy,
+ file_system_sandbox_policy
+ );
+ assert_eq!(resolved.network_sandbox_policy, network_sandbox_policy);
+}
+
+#[test]
+fn resolve_sandbox_policies_rejects_partial_split_policies() {
+ let result = std::panic::catch_unwind(|| {
+ resolve_sandbox_policies(
+ Path::new("/tmp"),
+ Some(SandboxPolicy::new_read_only_policy()),
+ Some(FileSystemSandboxPolicy::default()),
+ None,
+ )
+ });
+
+ assert!(result.is_err());
+}
+
#[test]
fn apply_seccomp_then_exec_without_bwrap_panics() {
let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, false));
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index f4ec554dbb..b482d5d3c9 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -728,6 +728,22 @@ impl FromStr for SandboxPolicy {
}
}
+impl FromStr for FileSystemSandboxPolicy {
+ type Err = serde_json::Error;
+
+ fn from_str(s: &str) -> Result {
+ serde_json::from_str(s)
+ }
+}
+
+impl FromStr for NetworkSandboxPolicy {
+ type Err = serde_json::Error;
+
+ fn from_str(s: &str) -> Result {
+ serde_json::from_str(s)
+ }
+}
+
impl SandboxPolicy {
/// Returns a policy with read-only disk access and no network.
pub fn new_read_only_policy() -> Self {