diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs
index fdb8a52af..d022adbc7 100644
--- a/codex-rs/core/src/landlock.rs
+++ b/codex-rs/core/src/landlock.rs
@@ -3,6 +3,7 @@ use crate::spawn::SpawnChildRequest;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use codex_network_proxy::NetworkProxy;
+use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use std::collections::HashMap;
use std::path::Path;
@@ -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,32 +65,43 @@ 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(
+/// parsing these arguments. Policy JSON flags are emitted before helper feature
+/// flags so the argv order matches the helper's CLI shape. See
+/// `docs/linux_sandbox.md` for the Linux semantics.
+#[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)
+ .unwrap_or_else(|err| panic!("failed to serialize sandbox policy: {err}"));
+ let file_system_policy_json = serde_json::to_string(file_system_sandbox_policy)
+ .unwrap_or_else(|err| panic!("failed to serialize filesystem sandbox policy: {err}"));
+ let network_policy_json = serde_json::to_string(&network_sandbox_policy)
+ .unwrap_or_else(|err| panic!("failed to serialize network sandbox policy: {err}"));
let sandbox_policy_cwd = sandbox_policy_cwd
.to_str()
- .expect("cwd must be valid UTF-8")
+ .unwrap_or_else(|| panic!("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,
+ "--file-system-sandbox-policy".to_string(),
+ file_system_policy_json,
+ "--network-sandbox-policy".to_string(),
+ network_policy_json,
];
if use_bwrap_sandbox {
linux_cmd.push("--use-bwrap-sandbox".to_string());
@@ -93,6 +109,32 @@ pub(crate) fn create_linux_sandbox_command_args(
if allow_network_for_proxy {
linux_cmd.push("--allow-network-for-proxy".to_string());
}
+ linux_cmd.push("--".to_string());
+ linux_cmd.extend(command);
+ linux_cmd
+}
+
+/// Converts the sandbox cwd and execution options into the CLI invocation for
+/// `codex-linux-sandbox`.
+#[cfg(test)]
+pub(crate) fn create_linux_sandbox_command_args(
+ command: Vec,
+ sandbox_policy_cwd: &Path,
+ use_bwrap_sandbox: bool,
+ allow_network_for_proxy: bool,
+) -> Vec {
+ let sandbox_policy_cwd = sandbox_policy_cwd
+ .to_str()
+ .unwrap_or_else(|| panic!("cwd must be valid UTF-8"))
+ .to_string();
+
+ 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());
+ }
+ if allow_network_for_proxy {
+ linux_cmd.push("--allow-network-for-proxy".to_string());
+ }
// Separator so that command arguments starting with `-` are not parsed as
// options of the helper itself.
@@ -113,16 +155,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 +173,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 49ecef923..69ab438e1 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::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
@@ -516,9 +516,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 f9477a126..307f956a4 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)
@@ -266,13 +268,13 @@ mod tests {
use super::NetworkSeccompMode;
use super::network_seccomp_mode;
use super::should_install_network_seccomp;
- use codex_protocol::protocol::SandboxPolicy;
+ use codex_protocol::protocol::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn managed_network_enforces_seccomp_even_for_full_network_policy() {
assert_eq!(
- should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, true),
+ should_install_network_seccomp(NetworkSandboxPolicy::Enabled, true),
true
);
}
@@ -280,7 +282,7 @@ mod tests {
#[test]
fn full_network_policy_without_managed_network_skips_seccomp() {
assert_eq!(
- should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, false),
+ should_install_network_seccomp(NetworkSandboxPolicy::Enabled, false),
false
);
}
@@ -288,11 +290,11 @@ mod tests {
#[test]
fn restricted_network_policy_always_installs_seccomp() {
assert!(should_install_network_seccomp(
- &SandboxPolicy::new_read_only_policy(),
+ NetworkSandboxPolicy::Restricted,
false
));
assert!(should_install_network_seccomp(
- &SandboxPolicy::new_read_only_policy(),
+ NetworkSandboxPolicy::Restricted,
true
));
}
@@ -300,7 +302,7 @@ mod tests {
#[test]
fn managed_proxy_routes_use_proxy_routed_seccomp_mode() {
assert_eq!(
- network_seccomp_mode(&SandboxPolicy::DangerFullAccess, true, true),
+ network_seccomp_mode(NetworkSandboxPolicy::Enabled, true, true),
Some(NetworkSeccompMode::ProxyRouted)
);
}
@@ -308,7 +310,7 @@ mod tests {
#[test]
fn restricted_network_without_proxy_routing_uses_restricted_mode() {
assert_eq!(
- network_seccomp_mode(&SandboxPolicy::new_read_only_policy(), false, false),
+ network_seccomp_mode(NetworkSandboxPolicy::Restricted, false, false),
Some(NetworkSeccompMode::Restricted)
);
}
@@ -316,7 +318,7 @@ mod tests {
#[test]
fn full_network_without_managed_proxy_skips_network_seccomp_mode() {
assert_eq!(
- network_seccomp_mode(&SandboxPolicy::DangerFullAccess, false, false),
+ network_seccomp_mode(NetworkSandboxPolicy::Enabled, false, false),
None
);
}
diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs
index c9fdd06a3..dad4a6d3a 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,
@@ -139,17 +166,20 @@ pub fn run_main() -> ! {
} else {
None
};
- let inner = build_inner_seccomp_command(
- &sandbox_policy_cwd,
- &sandbox_policy,
+ let inner = build_inner_seccomp_command(InnerSeccompCommandArgs {
+ sandbox_policy_cwd: &sandbox_policy_cwd,
+ sandbox_policy: &sandbox_policy,
+ file_system_sandbox_policy: &file_system_sandbox_policy,
+ network_sandbox_policy,
use_bwrap_sandbox,
allow_network_for_proxy,
proxy_route_spec,
command,
- );
+ });
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,59 @@ 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 {
+ // Accept either a fully legacy policy, a fully split policy pair, or all
+ // three views together. Reject partial split-policy input so the helper
+ // never runs with mismatched filesystem/network state.
+ let split_policies = match (file_system_sandbox_policy, network_sandbox_policy) {
+ (Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => {
+ Some((file_system_sandbox_policy, network_sandbox_policy))
+ }
+ (None, None) => None,
+ _ => panic!("file-system and network sandbox policies must be provided together"),
+ };
+
+ match (sandbox_policy, split_policies) {
+ (Some(sandbox_policy), Some((file_system_sandbox_policy, network_sandbox_policy))) => {
+ EffectiveSandboxPolicies {
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ }
+ }
+ (Some(sandbox_policy), 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, 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) => panic!("missing sandbox policy configuration"),
+ }
+}
+
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 +261,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 +285,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 +299,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 +322,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 +333,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()];
@@ -358,15 +443,29 @@ fn is_proc_mount_failure(stderr: &str) -> bool {
|| stderr.contains("Permission denied"))
}
-/// Build the inner command that applies seccomp after bubblewrap.
-fn build_inner_seccomp_command(
- sandbox_policy_cwd: &Path,
- sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
+struct InnerSeccompCommandArgs<'a> {
+ sandbox_policy_cwd: &'a Path,
+ sandbox_policy: &'a SandboxPolicy,
+ file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
+ network_sandbox_policy: NetworkSandboxPolicy,
use_bwrap_sandbox: bool,
allow_network_for_proxy: bool,
proxy_route_spec: Option,
command: Vec,
-) -> Vec {
+}
+
+/// Build the inner command that applies seccomp after bubblewrap.
+fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec {
+ let InnerSeccompCommandArgs {
+ sandbox_policy_cwd,
+ sandbox_policy,
+ file_system_sandbox_policy,
+ network_sandbox_policy,
+ use_bwrap_sandbox,
+ allow_network_for_proxy,
+ proxy_route_spec,
+ command,
+ } = args;
let current_exe = match std::env::current_exe() {
Ok(path) => path,
Err(err) => panic!("failed to resolve current executable path: {err}"),
@@ -375,6 +474,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 +489,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 cda509030..35c60f364 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,42 +97,66 @@ 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 args = build_inner_seccomp_command(
- Path::new("/tmp"),
- &SandboxPolicy::new_read_only_policy(),
- true,
- true,
- Some("{\"routes\":[]}".to_string()),
- vec!["/bin/true".to_string()],
- );
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
+ sandbox_policy_cwd: Path::new("/tmp"),
+ sandbox_policy: &sandbox_policy,
+ file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
+ network_sandbox_policy: NetworkSandboxPolicy::Restricted,
+ use_bwrap_sandbox: true,
+ allow_network_for_proxy: true,
+ proxy_route_spec: Some("{\"routes\":[]}".to_string()),
+ command: vec!["/bin/true".to_string()],
+ });
assert!(args.iter().any(|arg| arg == "--proxy-route-spec"));
assert!(args.iter().any(|arg| arg == "{\"routes\":[]}"));
}
+#[test]
+fn inner_command_includes_split_policy_flags() {
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
+ sandbox_policy_cwd: Path::new("/tmp"),
+ sandbox_policy: &sandbox_policy,
+ file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
+ network_sandbox_policy: NetworkSandboxPolicy::Restricted,
+ use_bwrap_sandbox: true,
+ allow_network_for_proxy: false,
+ proxy_route_spec: None,
+ command: 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 args = build_inner_seccomp_command(
- Path::new("/tmp"),
- &SandboxPolicy::new_read_only_policy(),
- true,
- false,
- None,
- vec!["/bin/true".to_string()],
- );
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
+ sandbox_policy_cwd: Path::new("/tmp"),
+ sandbox_policy: &sandbox_policy,
+ file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
+ network_sandbox_policy: NetworkSandboxPolicy::Restricted,
+ use_bwrap_sandbox: true,
+ allow_network_for_proxy: false,
+ proxy_route_spec: None,
+ command: vec!["/bin/true".to_string()],
+ });
assert!(!args.iter().any(|arg| arg == "--proxy-route-spec"));
}
@@ -134,15 +164,71 @@ fn non_managed_inner_command_omits_route_spec() {
#[test]
fn managed_proxy_inner_command_requires_route_spec() {
let result = std::panic::catch_unwind(|| {
- build_inner_seccomp_command(
+ let sandbox_policy = SandboxPolicy::new_read_only_policy();
+ build_inner_seccomp_command(InnerSeccompCommandArgs {
+ sandbox_policy_cwd: Path::new("/tmp"),
+ sandbox_policy: &sandbox_policy,
+ file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
+ network_sandbox_policy: NetworkSandboxPolicy::Restricted,
+ use_bwrap_sandbox: true,
+ allow_network_for_proxy: true,
+ proxy_route_spec: None,
+ command: vec!["/bin/true".to_string()],
+ })
+ });
+ 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);
+ 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"),
- &SandboxPolicy::new_read_only_policy(),
- true,
- true,
+ Some(SandboxPolicy::new_read_only_policy()),
+ Some(FileSystemSandboxPolicy::default()),
None,
- vec!["/bin/true".to_string()],
)
});
+
assert!(result.is_err());
}
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 420345c21..de899ab65 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -727,6 +727,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 {
@@ -3177,6 +3193,7 @@ mod tests {
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
+ use std::path::PathBuf;
use tempfile::NamedTempFile;
use tempfile::TempDir;