diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 778dee3ea..f9a523dd9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1678,6 +1678,7 @@ dependencies = [ "codex-protocol", "codex-responses-api-proxy", "codex-rmcp-client", + "codex-sandboxing", "codex-state", "codex-stdio-to-uds", "codex-terminal-detection", @@ -1885,6 +1886,7 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-rmcp-client", + "codex-sandboxing", "codex-secrets", "codex-shell-command", "codex-shell-escalation", @@ -2462,6 +2464,15 @@ dependencies = [ "which 8.0.0", ] +[[package]] +name = "codex-sandboxing" +version = "0.0.0" +dependencies = [ + "codex-protocol", + "pretty_assertions", + "serde_json", +] + [[package]] name = "codex-secrets" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 524b61e3b..db203d83b 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -42,6 +42,7 @@ members = [ "protocol", "rmcp-client", "responses-api-proxy", + "sandboxing", "stdio-to-uds", "otel", "tui", @@ -130,6 +131,7 @@ codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-responses-api-proxy = { path = "responses-api-proxy" } codex-rmcp-client = { path = "rmcp-client" } +codex-sandboxing = { path = "sandboxing" } codex-secrets = { path = "secrets" } codex-shell-command = { path = "shell-command" } codex-shell-escalation = { path = "shell-escalation" } diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index c2fd1300c..7e703efe1 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -36,6 +36,7 @@ codex-mcp-server = { workspace = true } codex-protocol = { workspace = true } codex-responses-api-proxy = { workspace = true } codex-rmcp-client = { workspace = true } +codex-sandboxing = { workspace = true } codex-state = { workspace = true } codex-stdio-to-uds = { workspace = true } codex-terminal-detection = { workspace = true } diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index c65b6dcad..71a266101 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -11,7 +11,6 @@ use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::exec_env::create_env; -use codex_core::landlock::create_linux_sandbox_command_args_for_policies; #[cfg(target_os = "macos")] use codex_core::seatbelt::create_seatbelt_command_args_for_policies_with_extensions; #[cfg(target_os = "macos")] @@ -19,6 +18,7 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_protocol::config_types::SandboxMode; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; use codex_utils_cli::CliConfigOverrides; use tokio::process::Child; use tokio::process::Command as TokioCommand; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 6386c8d42..9c2034517 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -48,6 +48,7 @@ codex-otel = { workspace = true } codex-artifacts = { workspace = true } codex-protocol = { workspace = true } codex-rmcp-client = { workspace = true } +codex-sandboxing = { workspace = true } codex-state = { workspace = true } codex-terminal-detection = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 19b3f7c6a..c1dc4ba54 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -5,6 +5,8 @@ use crate::spawn::spawn_child_async; use codex_network_proxy::NetworkProxy; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::landlock::allow_network_for_proxy; +use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -59,112 +61,3 @@ where }) .await } - -pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool { - // When managed network requirements are active, request proxy-only - // networking from the Linux sandbox helper. Without managed requirements, - // preserve existing behavior. - enforce_managed_network -} - -/// Converts the sandbox policies into the CLI invocation for -/// `codex-linux-sandbox`. -/// -/// The helper performs the actual sandboxing (bubblewrap by default + seccomp) after -/// 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 fn create_linux_sandbox_command_args_for_policies( - command: Vec, - command_cwd: &Path, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, - sandbox_policy_cwd: &Path, - use_legacy_landlock: bool, - allow_network_for_proxy: bool, -) -> Vec { - 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() - .unwrap_or_else(|| panic!("cwd must be valid UTF-8")) - .to_string(); - let command_cwd = command_cwd - .to_str() - .unwrap_or_else(|| panic!("command cwd must be valid UTF-8")) - .to_string(); - - let mut linux_cmd: Vec = vec![ - "--sandbox-policy-cwd".to_string(), - sandbox_policy_cwd, - "--command-cwd".to_string(), - command_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_legacy_landlock { - linux_cmd.push("--use-legacy-landlock".to_string()); - } - 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, - command_cwd: &Path, - sandbox_policy_cwd: &Path, - use_legacy_landlock: bool, - allow_network_for_proxy: bool, -) -> Vec { - let command_cwd = command_cwd - .to_str() - .unwrap_or_else(|| panic!("command cwd must be valid UTF-8")) - .to_string(); - 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, - "--command-cwd".to_string(), - command_cwd, - ]; - if use_legacy_landlock { - linux_cmd.push("--use-legacy-landlock".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. - linux_cmd.push("--".to_string()); - - // Append the original tool command. - linux_cmd.extend(command); - - linux_cmd -} - -#[cfg(test)] -#[path = "landlock_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 277ff2b24..fd055d4de 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -14,8 +14,6 @@ use crate::exec::ExecToolCallOutput; 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_for_policies; use crate::protocol::SandboxPolicy; #[cfg(target_os = "macos")] use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; @@ -40,6 +38,8 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::ReadOnlyAccess; +use codex_sandboxing::landlock::allow_network_for_proxy; +use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; use macos_permissions::intersect_macos_seatbelt_profile_extensions; diff --git a/codex-rs/sandboxing/BUILD.bazel b/codex-rs/sandboxing/BUILD.bazel new file mode 100644 index 000000000..b4b3377a7 --- /dev/null +++ b/codex-rs/sandboxing/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "sandboxing", + crate_name = "codex_sandboxing", +) diff --git a/codex-rs/sandboxing/Cargo.toml b/codex-rs/sandboxing/Cargo.toml new file mode 100644 index 000000000..dc6f4b122 --- /dev/null +++ b/codex-rs/sandboxing/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "codex-sandboxing" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +name = "codex_sandboxing" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-protocol = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/sandboxing/src/landlock.rs b/codex-rs/sandboxing/src/landlock.rs new file mode 100644 index 000000000..8636f9ed0 --- /dev/null +++ b/codex-rs/sandboxing/src/landlock.rs @@ -0,0 +1,112 @@ +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::SandboxPolicy; +use std::path::Path; +pub fn allow_network_for_proxy(enforce_managed_network: bool) -> bool { + // When managed network requirements are active, request proxy-only + // networking from the Linux sandbox helper. Without managed requirements, + // preserve existing behavior. + enforce_managed_network +} + +/// Converts the sandbox policies into the CLI invocation for +/// `codex-linux-sandbox`. +/// +/// The helper performs the actual sandboxing (bubblewrap by default + seccomp) after +/// 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 fn create_linux_sandbox_command_args_for_policies( + command: Vec, + command_cwd: &Path, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + sandbox_policy_cwd: &Path, + use_legacy_landlock: bool, + allow_network_for_proxy: bool, +) -> Vec { + 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() + .unwrap_or_else(|| panic!("cwd must be valid UTF-8")) + .to_string(); + let command_cwd = command_cwd + .to_str() + .unwrap_or_else(|| panic!("command cwd must be valid UTF-8")) + .to_string(); + + let mut linux_cmd: Vec = vec![ + "--sandbox-policy-cwd".to_string(), + sandbox_policy_cwd, + "--command-cwd".to_string(), + command_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_legacy_landlock { + linux_cmd.push("--use-legacy-landlock".to_string()); + } + 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_attr(not(test), allow(dead_code))] +fn create_linux_sandbox_command_args( + command: Vec, + command_cwd: &Path, + sandbox_policy_cwd: &Path, + use_legacy_landlock: bool, + allow_network_for_proxy: bool, +) -> Vec { + let command_cwd = command_cwd + .to_str() + .unwrap_or_else(|| panic!("command cwd must be valid UTF-8")) + .to_string(); + 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, + "--command-cwd".to_string(), + command_cwd, + ]; + if use_legacy_landlock { + linux_cmd.push("--use-legacy-landlock".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. + linux_cmd.push("--".to_string()); + + // Append the original tool command. + linux_cmd.extend(command); + + linux_cmd +} + +#[cfg(test)] +#[path = "landlock_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/landlock_tests.rs b/codex-rs/sandboxing/src/landlock_tests.rs similarity index 100% rename from codex-rs/core/src/landlock_tests.rs rename to codex-rs/sandboxing/src/landlock_tests.rs diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs new file mode 100644 index 000000000..517579e28 --- /dev/null +++ b/codex-rs/sandboxing/src/lib.rs @@ -0,0 +1 @@ +pub mod landlock;