Download codex-rs/cli/src/debug_sandbox.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 44.8 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/debug_sandbox.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/cli/src/debug_sandbox.rs
-
curl -L -o debug_sandbox.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/debug_sandbox.rs
44.8 kB
| mod cloud_config; | |
| mod pid_tracker; | |
| mod seatbelt; | |
| use std::path::PathBuf; | |
| use std::process::Stdio; | |
| use anyhow::Context as _; | |
| use codex_config::CloudConfigBundleLoader; | |
| use codex_config::LoaderOverrides; | |
| use codex_core::config::Config; | |
| use codex_core::config::ConfigBuilder; | |
| use codex_core::config::ConfigOverrides; | |
| use codex_core::config::NetworkProxyAuditMetadata; | |
| use codex_core::config::find_codex_home; | |
| use codex_core::exec_env::create_env; | |
| use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; | |
| use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; | |
| use codex_core::windows_sandbox::managed_proxy_routing_for_windows_sandbox; | |
| use codex_protocol::config_types::SandboxMode; | |
| use codex_protocol::models::PermissionProfile; | |
| use codex_protocol::models::SandboxEnforcement; | |
| use codex_protocol::permissions::NetworkSandboxPolicy; | |
| use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile; | |
| use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams; | |
| use codex_sandboxing::seatbelt::create_seatbelt_command_args; | |
| use codex_sandboxing::with_managed_mitm_ca_readable_root; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use codex_utils_cli::CliConfigOverrides; | |
| use tokio::process::Child; | |
| use tokio::process::Command as TokioCommand; | |
| use toml::Value as TomlValue; | |
| use crate::LandlockCommand; | |
| use crate::SeatbeltCommand; | |
| use crate::WindowsCommand; | |
| use crate::exit_status::handle_exit_status; | |
| use seatbelt::DenialLogger; | |
| pub async fn run_command_under_seatbelt( | |
| command: SeatbeltCommand, | |
| codex_linux_sandbox_exe: Option<PathBuf>, | |
| loader_overrides: LoaderOverrides, | |
| ) -> anyhow::Result<()> { | |
| let SeatbeltCommand { | |
| sandbox_state, | |
| permissions_profile, | |
| config_profile: _, | |
| cwd, | |
| include_managed_config, | |
| allow_unix_sockets, | |
| log_denials, | |
| config_overrides, | |
| command, | |
| } = command; | |
| let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation( | |
| &permissions_profile, | |
| include_managed_config, | |
| ); | |
| run_command_under_sandbox( | |
| DebugSandboxConfigOptions { | |
| sandbox_state, | |
| permissions_profile, | |
| cwd, | |
| managed_requirements_mode, | |
| loader_overrides, | |
| }, | |
| command, | |
| config_overrides, | |
| codex_linux_sandbox_exe, | |
| SandboxType::Seatbelt, | |
| log_denials, | |
| &allow_unix_sockets, | |
| ) | |
| .await | |
| } | |
| pub async fn run_command_under_seatbelt( | |
| _command: SeatbeltCommand, | |
| _codex_linux_sandbox_exe: Option<PathBuf>, | |
| _loader_overrides: LoaderOverrides, | |
| ) -> anyhow::Result<()> { | |
| anyhow::bail!("Seatbelt sandbox is only available on macOS"); | |
| } | |
| pub async fn run_command_under_landlock( | |
| command: LandlockCommand, | |
| codex_linux_sandbox_exe: Option<PathBuf>, | |
| loader_overrides: LoaderOverrides, | |
| ) -> anyhow::Result<()> { | |
| let LandlockCommand { | |
| sandbox_state, | |
| permissions_profile, | |
| config_profile: _, | |
| cwd, | |
| include_managed_config, | |
| config_overrides, | |
| command, | |
| } = command; | |
| let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation( | |
| &permissions_profile, | |
| include_managed_config, | |
| ); | |
| run_command_under_sandbox( | |
| DebugSandboxConfigOptions { | |
| sandbox_state, | |
| permissions_profile, | |
| cwd, | |
| managed_requirements_mode, | |
| loader_overrides, | |
| }, | |
| command, | |
| config_overrides, | |
| codex_linux_sandbox_exe, | |
| SandboxType::Landlock, | |
| /*log_denials*/ false, | |
| &[], | |
| ) | |
| .await | |
| } | |
| pub async fn run_command_under_windows_sandbox( | |
| command: WindowsCommand, | |
| codex_linux_sandbox_exe: Option<PathBuf>, | |
| loader_overrides: LoaderOverrides, | |
| ) -> anyhow::Result<()> { | |
| let WindowsCommand { | |
| sandbox_state, | |
| permissions_profile, | |
| config_profile: _, | |
| cwd, | |
| include_managed_config, | |
| config_overrides, | |
| command, | |
| } = command; | |
| let managed_requirements_mode = ManagedRequirementsMode::for_profile_invocation( | |
| &permissions_profile, | |
| include_managed_config, | |
| ); | |
| run_command_under_sandbox( | |
| DebugSandboxConfigOptions { | |
| sandbox_state, | |
| permissions_profile, | |
| cwd, | |
| managed_requirements_mode, | |
| loader_overrides, | |
| }, | |
| command, | |
| config_overrides, | |
| codex_linux_sandbox_exe, | |
| SandboxType::Windows, | |
| /*log_denials*/ false, | |
| &[], | |
| ) | |
| .await | |
| } | |
| enum SandboxType { | |
| Seatbelt, | |
| Landlock, | |
| Windows, | |
| } | |
| struct DebugSandboxConfigOptions { | |
| sandbox_state: crate::SandboxStateArgs, | |
| permissions_profile: Option<String>, | |
| cwd: Option<PathBuf>, | |
| managed_requirements_mode: ManagedRequirementsMode, | |
| loader_overrides: LoaderOverrides, | |
| } | |
| enum ManagedRequirementsMode { | |
| Include, | |
| Ignore, | |
| } | |
| impl ManagedRequirementsMode { | |
| fn for_profile_invocation( | |
| permissions_profile: &Option<String>, | |
| include_managed_config: bool, | |
| ) -> Self { | |
| if permissions_profile.is_some() && !include_managed_config { | |
| Self::Ignore | |
| } else { | |
| Self::Include | |
| } | |
| } | |
| } | |
| async fn run_command_under_sandbox( | |
| mut config_options: DebugSandboxConfigOptions, | |
| command: Vec<String>, | |
| config_overrides: CliConfigOverrides, | |
| codex_linux_sandbox_exe: Option<PathBuf>, | |
| sandbox_type: SandboxType, | |
| log_denials: bool, | |
| allow_unix_sockets: &[AbsolutePathBuf], | |
| ) -> anyhow::Result<()> { | |
| let sandbox_state = config_options | |
| .sandbox_state | |
| .sandbox_state_json | |
| .as_deref() | |
| .map(serde_json::from_str::<codex_mcp::SandboxState>) | |
| .transpose() | |
| .map_err(|err| anyhow::anyhow!("invalid --sandbox-state-json value: {err}"))?; | |
| let sandbox_state_readable_root = config_options | |
| .sandbox_state | |
| .sandbox_state_readable_root | |
| .clone(); | |
| let sandbox_state_disable_network = config_options.sandbox_state.sandbox_state_disable_network; | |
| let codex_linux_sandbox_exe = match sandbox_state.as_ref() { | |
| Some(state) => { | |
| config_options.cwd = Some( | |
| state | |
| .sandbox_cwd | |
| .to_abs_path() | |
| .context("sandbox state cwd is not native to this host")? | |
| .to_path_buf(), | |
| ); | |
| state | |
| .codex_linux_sandbox_exe | |
| .clone() | |
| .or(codex_linux_sandbox_exe) | |
| } | |
| None => codex_linux_sandbox_exe, | |
| }; | |
| let config = load_debug_sandbox_config( | |
| config_overrides | |
| .parse_overrides() | |
| .map_err(anyhow::Error::msg)?, | |
| codex_linux_sandbox_exe, | |
| config_options, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| // In practice, this should be `std::env::current_dir()` because this CLI | |
| // does not support `--cwd`, but let's use the config value for consistency. | |
| let cwd = config.cwd.clone(); | |
| // Non-Windows sandbox launchers still use `sandbox_policy_cwd` for any | |
| // remaining cwd-dependent policy resolution. `:workspace_roots` entries in | |
| // the effective profile have already been materialized from config roots. | |
| let sandbox_policy_cwd = cwd.clone(); | |
| let env = create_env( | |
| &config.permissions.shell_environment_policy, | |
| /*thread_id*/ None, | |
| ); | |
| let mut permission_profile = match sandbox_state.as_ref() { | |
| Some(state) => match &state.permission_profile { | |
| PermissionProfile::External { .. } => { | |
| // `External` only says that the producer relies on an outer sandbox; it does not | |
| // include filesystem permissions we can recreate here. The consumer may not share | |
| // that sandbox, so use a locally enforceable read-only profile instead of spawning | |
| // without a sandbox. | |
| PermissionProfile::read_only() | |
| } | |
| permission_profile => permission_profile.clone(), | |
| }, | |
| None => config.permissions.effective_permission_profile(), | |
| }; | |
| if matches!(permission_profile, PermissionProfile::Disabled) && sandbox_state_disable_network { | |
| anyhow::bail!( | |
| "--sandbox-state-disable-network cannot be applied to a disabled permission profile" | |
| ); | |
| } | |
| if !matches!(permission_profile, PermissionProfile::Disabled) | |
| && (!sandbox_state_readable_root.is_empty() || sandbox_state_disable_network) | |
| { | |
| let file_system = permission_profile | |
| .file_system_sandbox_policy() | |
| .with_additional_readable_roots(&cwd, &sandbox_state_readable_root); | |
| let network = if sandbox_state_disable_network { | |
| NetworkSandboxPolicy::Restricted | |
| } else { | |
| permission_profile.network_sandbox_policy() | |
| }; | |
| permission_profile = PermissionProfile::from_runtime_permissions(&file_system, network); | |
| } | |
| let use_legacy_landlock = sandbox_state.as_ref().map_or_else( | |
| || config.features.use_legacy_landlock(), | |
| |state| state.use_legacy_landlock, | |
| ); | |
| match permission_profile.enforcement() { | |
| SandboxEnforcement::Managed => {} | |
| SandboxEnforcement::Disabled | SandboxEnforcement::External => { | |
| let (program, args) = command | |
| .split_first() | |
| .context("sandbox command must not be empty")?; | |
| let mut child = spawn_debug_sandbox_child( | |
| PathBuf::from(program), | |
| args.to_vec(), | |
| /*arg0*/ None, | |
| cwd.to_path_buf(), | |
| permission_profile.network_sandbox_policy(), | |
| env, | |
| |_| {}, | |
| ) | |
| .await?; | |
| handle_exit_status(child.wait().await?); | |
| } | |
| } | |
| // Legacy Windows sandbox sessions emulate inherited stdio and exit the process. | |
| if let SandboxType::Windows = sandbox_type { | |
| { | |
| if config.permissions.windows_sandbox_type != codex_sandboxing::SandboxType::WindowsMxc | |
| { | |
| let workspace_roots = config | |
| .effective_workspace_roots() | |
| .iter() | |
| .map(codex_utils_path_uri::PathUri::to_abs_path) | |
| .collect::<std::io::Result<Vec<_>>>() | |
| .context("invalid Windows sandbox workspace roots")?; | |
| run_command_under_windows_session( | |
| &config, | |
| &permission_profile, | |
| command, | |
| cwd, | |
| workspace_roots, | |
| env, | |
| ) | |
| .await; | |
| } | |
| } | |
| { | |
| anyhow::bail!("Windows sandbox is only available on Windows"); | |
| } | |
| } | |
| let mut denial_logger = log_denials.then(DenialLogger::new).flatten(); | |
| let _ = log_denials; | |
| let managed_network_requirements_enabled = config.managed_network_requirements_enabled(); | |
| // This proxy should only live for the lifetime of the child process. | |
| let network_proxy = match config.permissions.network.as_ref() { | |
| Some(spec) => Some( | |
| spec.start_proxy( | |
| &permission_profile, | |
| managed_proxy_routing_for_windows_sandbox(config.permissions.windows_sandbox_type), | |
| /*policy_decider*/ None, | |
| /*blocked_request_observer*/ None, | |
| managed_network_requirements_enabled, | |
| NetworkProxyAuditMetadata::default(), | |
| ) | |
| .await | |
| .map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?, | |
| ), | |
| None => None, | |
| }; | |
| let network = network_proxy | |
| .as_ref() | |
| .map(codex_core::config::StartedNetworkProxy::proxy); | |
| // Proxy containment depends on whether a proxy is active, not whether its | |
| // policy came from managed requirements. | |
| let enforce_managed_network = network.is_some(); | |
| let managed_mitm_ca_trust_bundle_path = match network.as_ref() { | |
| Some(network) => network.managed_mitm_ca_trust_bundle_path(), | |
| None => None, | |
| }; | |
| let runtime_permission_profile = with_managed_mitm_ca_readable_root( | |
| permission_profile, | |
| managed_mitm_ca_trust_bundle_path.as_ref(), | |
| sandbox_policy_cwd.as_path(), | |
| ); | |
| let mut child = match sandbox_type { | |
| SandboxType::Seatbelt => { | |
| let (file_system_sandbox_policy, network_sandbox_policy) = | |
| runtime_permission_profile.to_runtime_permissions(); | |
| let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { | |
| command, | |
| file_system_sandbox_policy: &file_system_sandbox_policy, | |
| network_sandbox_policy, | |
| sandbox_policy_cwd: sandbox_policy_cwd.as_path(), | |
| enforce_managed_network, | |
| managed_network: None, | |
| environment_id: None, | |
| network: network.as_ref(), | |
| extra_allow_unix_sockets: allow_unix_sockets, | |
| }) | |
| .map_err(|err| anyhow::anyhow!(err))?; | |
| // This CLI inherits the user's controlling terminal. Keep this deny | |
| // after every shared policy allowance so the child cannot queue input | |
| // for the unsandboxed shell that resumes when Codex exits. | |
| match args.as_mut_slice() { | |
| [flag, policy, ..] if flag.as_str() == "-p" => { | |
| policy.push_str("\n(deny file-ioctl (ioctl-command TIOCSTI))"); | |
| } | |
| _ => anyhow::bail!("Seatbelt command is missing its generated policy"), | |
| } | |
| spawn_debug_sandbox_child( | |
| PathBuf::from("/usr/bin/sandbox-exec"), | |
| args, | |
| /*arg0*/ None, | |
| cwd.to_path_buf(), | |
| network_sandbox_policy, | |
| env, | |
| |env_map| { | |
| env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); | |
| if let Some(network) = network.as_ref() { | |
| network.apply_to_env(env_map); | |
| } | |
| }, | |
| ) | |
| .await? | |
| } | |
| SandboxType::Landlock => { | |
| let codex_linux_sandbox_exe = config | |
| .codex_linux_sandbox_exe | |
| .expect("codex-linux-sandbox executable not found"); | |
| let network_sandbox_policy = runtime_permission_profile.network_sandbox_policy(); | |
| let (env, managed_network) = if let Some(network) = network.as_ref() { | |
| let prepared = | |
| network.prepare_for_optional_environment(env, /*environment_id*/ None)?; | |
| (prepared.env, Some(prepared.sandbox_context)) | |
| } else { | |
| (env, None) | |
| }; | |
| let args = create_linux_sandbox_command_args_for_permission_profile( | |
| command, | |
| cwd.as_path(), | |
| &runtime_permission_profile, | |
| sandbox_policy_cwd.as_path(), | |
| use_legacy_landlock, | |
| managed_network.as_ref(), | |
| ); | |
| spawn_debug_sandbox_child( | |
| codex_linux_sandbox_exe, | |
| args, | |
| Some("codex-linux-sandbox"), | |
| cwd.to_path_buf(), | |
| network_sandbox_policy, | |
| env, | |
| |_| {}, | |
| ) | |
| .await? | |
| } | |
| SandboxType::Windows => { | |
| unreachable!("Windows sandbox should have been handled above"); | |
| } | |
| SandboxType::Windows => { | |
| use codex_sandboxing::SandboxCommand; | |
| use codex_sandboxing::SandboxManager; | |
| use codex_sandboxing::SandboxTransformRequest; | |
| use codex_utils_path_uri::PathUri; | |
| let (program, args) = command | |
| .split_first() | |
| .context("sandbox command must not be empty")?; | |
| let sandbox_policy_cwd = PathUri::from_abs_path(&sandbox_policy_cwd); | |
| let codex_self_exe = std::env::current_exe()?; | |
| let request = SandboxManager::new().transform(SandboxTransformRequest { | |
| command: SandboxCommand { | |
| program: program.into(), | |
| args: args.to_vec(), | |
| cwd: PathUri::from_abs_path(&cwd), | |
| env, | |
| managed_network: None, | |
| additional_permissions: None, | |
| }, | |
| permissions: &runtime_permission_profile, | |
| sandbox: codex_sandboxing::SandboxType::WindowsMxc, | |
| enforce_managed_network, | |
| environment_id: None, | |
| network: network.as_ref(), | |
| sandbox_policy_cwd: &sandbox_policy_cwd, | |
| sandbox_exe: Some(codex_self_exe.as_path()), | |
| use_legacy_landlock: false, | |
| windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, | |
| windows_sandbox_private_desktop: false, | |
| })?; | |
| let (program, args) = request | |
| .command | |
| .split_first() | |
| .context("missing MXC wrapper")?; | |
| spawn_debug_sandbox_child( | |
| PathBuf::from(program), | |
| args.to_vec(), | |
| /*arg0*/ None, | |
| cwd.to_path_buf(), | |
| runtime_permission_profile.network_sandbox_policy(), | |
| request.env, | |
| |_| {}, | |
| ) | |
| .await? | |
| } | |
| }; | |
| if let Some(denial_logger) = &mut denial_logger { | |
| denial_logger.on_child_spawn(&child); | |
| } | |
| let status = child.wait().await?; | |
| if let Some(denial_logger) = denial_logger { | |
| let denials = denial_logger.finish().await; | |
| eprintln!("\n=== Sandbox denials ==="); | |
| if denials.is_empty() { | |
| eprintln!("None found."); | |
| } else { | |
| for seatbelt::SandboxDenial { name, capability } in denials { | |
| eprintln!("({name}) {capability}"); | |
| } | |
| } | |
| } | |
| handle_exit_status(status); | |
| } | |
| async fn run_command_under_windows_session( | |
| config: &Config, | |
| permission_profile: &PermissionProfile, | |
| command: Vec<String>, | |
| cwd: AbsolutePathBuf, | |
| workspace_roots: Vec<AbsolutePathBuf>, | |
| env: std::collections::HashMap<String, String>, | |
| ) -> ! { | |
| use codex_core::windows_sandbox::WindowsSandboxLevelExt; | |
| use codex_protocol::config_types::WindowsSandboxLevel; | |
| use codex_windows_sandbox::WindowsSandboxProxySettingsMode; | |
| use codex_windows_sandbox::WindowsSandboxSessionRequest; | |
| use codex_windows_sandbox::resolve_windows_deny_read_paths; | |
| use codex_windows_sandbox::spawn_windows_sandbox_session_for_level; | |
| // Setup reconciles persistent deny ACLs against this list. An empty list | |
| // would discard the profile's denies, including on subsequent launches. | |
| let (mut file_system, _) = permission_profile.to_runtime_permissions(); | |
| file_system.remove_skip_missing_path_entries(); | |
| let file_system = file_system.materialize_project_roots_with_workspace_roots(&workspace_roots); | |
| let deny_read_paths = match resolve_windows_deny_read_paths(&file_system, &cwd) { | |
| Ok(paths) => paths, | |
| Err(err) => { | |
| eprintln!("windows sandbox failed: {err}"); | |
| std::process::exit(1); | |
| } | |
| }; | |
| let empty_paths: &[AbsolutePathBuf] = &[]; | |
| let spawned = spawn_windows_sandbox_session_for_level(WindowsSandboxSessionRequest { | |
| permission_profile, | |
| workspace_roots: workspace_roots.as_slice(), | |
| codex_home: config.codex_home.as_path(), | |
| command, | |
| cwd: cwd.as_path(), | |
| env_map: env, | |
| windows_sandbox_level: WindowsSandboxLevel::from_config(config), | |
| proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve, | |
| proxy_enforced: false, | |
| network_proxy_restricting_sid: None, | |
| timeout_ms: None, | |
| read_roots_override: None, | |
| read_roots_include_platform_defaults: false, | |
| write_roots_override: None, | |
| deny_read_paths_override: &deny_read_paths, | |
| deny_write_paths_override: empty_paths, | |
| tty: false, | |
| stdin_open: true, | |
| use_private_desktop: config.permissions.windows_sandbox_private_desktop, | |
| }) | |
| .await; | |
| let spawned = match spawned { | |
| Ok(spawned) => spawned, | |
| Err(err) => { | |
| eprintln!("windows sandbox failed: {err}"); | |
| std::process::exit(1); | |
| } | |
| }; | |
| let exit_code = codex_windows_sandbox::forward_sandbox_session_stdio(spawned).await; | |
| std::process::exit(exit_code); | |
| } | |
| async fn spawn_debug_sandbox_child( | |
| program: PathBuf, | |
| args: Vec<String>, | |
| arg0: Option<&str>, | |
| cwd: PathBuf, | |
| network_sandbox_policy: NetworkSandboxPolicy, | |
| mut env: std::collections::HashMap<String, String>, | |
| apply_env: impl FnOnce(&mut std::collections::HashMap<String, String>), | |
| ) -> std::io::Result<Child> { | |
| let mut cmd = TokioCommand::new(&program); | |
| cmd.arg0(arg0.map_or_else(|| program.to_string_lossy().to_string(), String::from)); | |
| let _ = arg0; | |
| cmd.args(args); | |
| cmd.current_dir(cwd); | |
| apply_env(&mut env); | |
| cmd.env_clear(); | |
| cmd.envs(env); | |
| if !network_sandbox_policy.is_enabled() { | |
| cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1"); | |
| } | |
| cmd.stdin(Stdio::inherit()) | |
| .stdout(Stdio::inherit()) | |
| .stderr(Stdio::inherit()) | |
| .kill_on_drop(true) | |
| .spawn() | |
| } | |
| async fn load_debug_sandbox_config( | |
| cli_overrides: Vec<(String, TomlValue)>, | |
| codex_linux_sandbox_exe: Option<PathBuf>, | |
| options: DebugSandboxConfigOptions, | |
| strict_config: bool, | |
| ) -> anyhow::Result<Config> { | |
| let cloud_config_bundle = cloud_config::bootstrap_cloud_config_bundle( | |
| &cli_overrides, | |
| &options, | |
| find_codex_home, | |
| strict_config, | |
| ) | |
| .await?; | |
| load_debug_sandbox_config_with_codex_home( | |
| cli_overrides, | |
| codex_linux_sandbox_exe, | |
| options, | |
| /*codex_home*/ None, | |
| cloud_config_bundle, | |
| strict_config, | |
| ) | |
| .await | |
| } | |
| async fn load_debug_sandbox_config_with_codex_home( | |
| cli_overrides: Vec<(String, TomlValue)>, | |
| codex_linux_sandbox_exe: Option<PathBuf>, | |
| options: DebugSandboxConfigOptions, | |
| codex_home: Option<PathBuf>, | |
| cloud_config_bundle: CloudConfigBundleLoader, | |
| strict_config: bool, | |
| ) -> anyhow::Result<Config> { | |
| let DebugSandboxConfigOptions { | |
| sandbox_state: _, | |
| permissions_profile, | |
| cwd, | |
| managed_requirements_mode, | |
| loader_overrides, | |
| } = options; | |
| let mut cli_overrides = cli_overrides; | |
| if let Some(permissions_profile) = permissions_profile { | |
| cli_overrides.push(( | |
| "default_permissions".to_string(), | |
| TomlValue::String(permissions_profile), | |
| )); | |
| } | |
| // For legacy configs, `codex sandbox` historically defaulted to read-only | |
| // instead of inheriting ambient `sandbox_mode` settings from user/system | |
| // config. Keep that behavior unless this invocation explicitly passes a | |
| // legacy `sandbox_mode` CLI override for compatibility with older callers. | |
| let uses_legacy_sandbox_mode_override = cli_overrides_use_legacy_sandbox_mode(&cli_overrides); | |
| let config = build_debug_sandbox_config_with_loader_overrides( | |
| cli_overrides.clone(), | |
| ConfigOverrides { | |
| cwd: cwd.clone(), | |
| codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(), | |
| ..Default::default() | |
| }, | |
| codex_home.clone(), | |
| managed_requirements_mode, | |
| loader_overrides.clone(), | |
| cloud_config_bundle.clone(), | |
| strict_config, | |
| ) | |
| .await?; | |
| if config_uses_permission_profiles(&config) || uses_legacy_sandbox_mode_override { | |
| return Ok(config); | |
| } | |
| build_debug_sandbox_config_with_loader_overrides( | |
| cli_overrides, | |
| ConfigOverrides { | |
| sandbox_mode: Some(SandboxMode::ReadOnly), | |
| cwd, | |
| codex_linux_sandbox_exe, | |
| ..Default::default() | |
| }, | |
| codex_home, | |
| managed_requirements_mode, | |
| loader_overrides, | |
| cloud_config_bundle, | |
| strict_config, | |
| ) | |
| .await | |
| .map_err(Into::into) | |
| } | |
| async fn build_debug_sandbox_config_with_loader_overrides( | |
| cli_overrides: Vec<(String, TomlValue)>, | |
| harness_overrides: ConfigOverrides, | |
| codex_home: Option<PathBuf>, | |
| managed_requirements_mode: ManagedRequirementsMode, | |
| mut loader_overrides: LoaderOverrides, | |
| cloud_config_bundle: CloudConfigBundleLoader, | |
| strict_config: bool, | |
| ) -> std::io::Result<Config> { | |
| let mut builder = ConfigBuilder::default() | |
| .cli_overrides(cli_overrides) | |
| .harness_overrides(harness_overrides) | |
| .cloud_config_bundle(cloud_config_bundle) | |
| .strict_config(strict_config); | |
| if matches!(managed_requirements_mode, ManagedRequirementsMode::Ignore) { | |
| loader_overrides.ignore_managed_requirements = true; | |
| } | |
| builder = builder.loader_overrides(loader_overrides); | |
| if let Some(codex_home) = codex_home { | |
| builder = builder | |
| .codex_home(codex_home.clone()) | |
| .fallback_cwd(Some(codex_home)); | |
| } | |
| builder.build().await | |
| } | |
| fn config_uses_permission_profiles(config: &Config) -> bool { | |
| config | |
| .config_layer_stack | |
| .effective_config() | |
| .get("default_permissions") | |
| .is_some() | |
| } | |
| fn cli_overrides_use_legacy_sandbox_mode(cli_overrides: &[(String, TomlValue)]) -> bool { | |
| cli_overrides.iter().any(|(key, _)| key == "sandbox_mode") | |
| } | |
| mod tests { | |
| use super::*; | |
| use codex_config::ConfigRequirementsToml; | |
| use codex_config::test_support::CloudConfigBundleFixture; | |
| use pretty_assertions::assert_eq; | |
| use tempfile::TempDir; | |
| const CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS: &str = r#" | |
| default_permissions = "managed-cloud" | |
| [allowed_permission_profiles] | |
| managed-cloud = true | |
| [permissions.managed-cloud] | |
| extends = ":workspace" | |
| [permissions.managed-cloud.network] | |
| enabled = true | |
| "#; | |
| async fn build_debug_sandbox_config( | |
| cli_overrides: Vec<(String, TomlValue)>, | |
| harness_overrides: ConfigOverrides, | |
| codex_home: Option<PathBuf>, | |
| managed_requirements_mode: ManagedRequirementsMode, | |
| strict_config: bool, | |
| ) -> std::io::Result<Config> { | |
| build_debug_sandbox_config_with_loader_overrides( | |
| cli_overrides, | |
| harness_overrides, | |
| codex_home, | |
| managed_requirements_mode, | |
| LoaderOverrides::default(), | |
| CloudConfigBundleLoader::default(), | |
| strict_config, | |
| ) | |
| .await | |
| } | |
| fn escape_toml_path(path: &std::path::Path) -> String { | |
| path.display().to_string().replace('\\', "\\\\") | |
| } | |
| fn write_permissions_profile_config( | |
| codex_home: &TempDir, | |
| docs: &std::path::Path, | |
| private: &std::path::Path, | |
| ) -> std::io::Result<()> { | |
| write_permissions_profile_config_to_path( | |
| &codex_home.path().join("config.toml"), | |
| docs, | |
| private, | |
| ) | |
| } | |
| fn write_permissions_profile_config_to_path( | |
| config_path: &std::path::Path, | |
| docs: &std::path::Path, | |
| private: &std::path::Path, | |
| ) -> std::io::Result<()> { | |
| std::fs::create_dir_all(private)?; | |
| let config = format!( | |
| "default_permissions = \"limited-read-test\"\n\ | |
| [permissions.limited-read-test.filesystem]\n\ | |
| \":minimal\" = \"read\"\n\ | |
| \"{}\" = \"read\"\n\ | |
| \"{}\" = \"none\"\n\ | |
| \n\ | |
| [permissions.limited-read-test.network]\n\ | |
| enabled = true\n", | |
| escape_toml_path(docs), | |
| escape_toml_path(private), | |
| ); | |
| std::fs::write(config_path, config)?; | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_honors_active_permission_profiles() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let sandbox_paths = TempDir::new()?; | |
| let docs = sandbox_paths.path().join("docs"); | |
| let private = docs.join("private"); | |
| write_permissions_profile_config(&codex_home, &docs, &private)?; | |
| let codex_home_path = codex_home.path().to_path_buf(); | |
| let profile_config = build_debug_sandbox_config( | |
| Vec::new(), | |
| ConfigOverrides::default(), | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let legacy_config = build_debug_sandbox_config( | |
| Vec::new(), | |
| ConfigOverrides { | |
| sandbox_mode: Some(SandboxMode::ReadOnly), | |
| ..Default::default() | |
| }, | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: None, | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Include, | |
| loader_overrides: LoaderOverrides::default(), | |
| }, | |
| Some(codex_home_path), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert!(config_uses_permission_profiles(&config)); | |
| assert!( | |
| profile_config.permissions.file_system_sandbox_policy() | |
| != legacy_config.permissions.file_system_sandbox_policy(), | |
| "test fixture should distinguish profile syntax from legacy sandbox_mode" | |
| ); | |
| assert_eq!( | |
| config.permissions.file_system_sandbox_policy(), | |
| profile_config.permissions.file_system_sandbox_policy(), | |
| ); | |
| assert_ne!( | |
| config.permissions.file_system_sandbox_policy(), | |
| legacy_config.permissions.file_system_sandbox_policy(), | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_honors_config_profile_loader_overrides() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let sandbox_paths = TempDir::new()?; | |
| let docs = sandbox_paths.path().join("docs"); | |
| let private = docs.join("private"); | |
| let profile_path = codex_home.path().join("work.config.toml"); | |
| write_permissions_profile_config_to_path(&profile_path, &docs, &private)?; | |
| let codex_home_path = codex_home.path().to_path_buf(); | |
| let loader_overrides = LoaderOverrides { | |
| user_config_path: Some(AbsolutePathBuf::from_absolute_path(&profile_path)?), | |
| user_config_profile: Some("work".parse().expect("profile name should parse")), | |
| ..LoaderOverrides::default() | |
| }; | |
| let profile_config = build_debug_sandbox_config_with_loader_overrides( | |
| Vec::new(), | |
| ConfigOverrides::default(), | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| loader_overrides.clone(), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let read_only_config = build_debug_sandbox_config( | |
| Vec::new(), | |
| ConfigOverrides { | |
| sandbox_mode: Some(SandboxMode::ReadOnly), | |
| ..Default::default() | |
| }, | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: None, | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Include, | |
| loader_overrides, | |
| }, | |
| Some(codex_home_path), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert!(config_uses_permission_profiles(&config)); | |
| assert_ne!( | |
| profile_config.permissions.file_system_sandbox_policy(), | |
| read_only_config.permissions.file_system_sandbox_policy(), | |
| "test fixture should distinguish the profile config from read-only" | |
| ); | |
| assert_eq!( | |
| config.permissions.file_system_sandbox_policy(), | |
| profile_config.permissions.file_system_sandbox_policy(), | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_honors_explicit_legacy_sandbox_mode() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let codex_home_path = codex_home.path().to_path_buf(); | |
| let cli_overrides = vec![( | |
| "sandbox_mode".to_string(), | |
| TomlValue::String("workspace-write".to_string()), | |
| )]; | |
| let workspace_write_config = build_debug_sandbox_config( | |
| cli_overrides.clone(), | |
| ConfigOverrides::default(), | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let read_only_config = build_debug_sandbox_config( | |
| Vec::new(), | |
| ConfigOverrides { | |
| sandbox_mode: Some(SandboxMode::ReadOnly), | |
| ..Default::default() | |
| }, | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| cli_overrides, | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: None, | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Include, | |
| loader_overrides: LoaderOverrides::default(), | |
| }, | |
| Some(codex_home_path), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| if cfg!(target_os = "windows") { | |
| assert_eq!( | |
| workspace_write_config | |
| .permissions | |
| .file_system_sandbox_policy(), | |
| read_only_config.permissions.file_system_sandbox_policy(), | |
| "workspace-write downgrades to read-only when the Windows sandbox is disabled" | |
| ); | |
| } else { | |
| assert_ne!( | |
| workspace_write_config | |
| .permissions | |
| .file_system_sandbox_policy(), | |
| read_only_config.permissions.file_system_sandbox_policy(), | |
| "test fixture should distinguish explicit workspace-write from read-only" | |
| ); | |
| } | |
| assert_eq!( | |
| config.permissions.file_system_sandbox_policy(), | |
| workspace_write_config | |
| .permissions | |
| .file_system_sandbox_policy(), | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_defaults_legacy_configs_to_read_only() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let codex_home_path = codex_home.path().to_path_buf(); | |
| let read_only_config = build_debug_sandbox_config( | |
| Vec::new(), | |
| ConfigOverrides { | |
| sandbox_mode: Some(SandboxMode::ReadOnly), | |
| ..Default::default() | |
| }, | |
| Some(codex_home_path.clone()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: None, | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Include, | |
| loader_overrides: LoaderOverrides::default(), | |
| }, | |
| Some(codex_home_path), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert!(!config_uses_permission_profiles(&config)); | |
| assert_eq!( | |
| config.permissions.file_system_sandbox_policy(), | |
| read_only_config.permissions.file_system_sandbox_policy(), | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_honors_explicit_builtin_permission_profile() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: Some(":workspace".to_string()), | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Ignore, | |
| loader_overrides: LoaderOverrides::default(), | |
| }, | |
| Some(codex_home.path().to_path_buf()), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let actual = config | |
| .permissions | |
| .permission_profile() | |
| .file_system_sandbox_policy(); | |
| let expected = codex_protocol::models::PermissionProfile::workspace_write() | |
| .file_system_sandbox_policy(); | |
| assert!( | |
| expected | |
| .entries | |
| .iter() | |
| .all(|entry| actual.entries.contains(entry)), | |
| "explicit workspace profile should preserve the built-in workspace rules" | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_honors_explicit_cloud_managed_permission_profile() -> anyhow::Result<()> | |
| { | |
| let codex_home = TempDir::new()?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: Some("managed-cloud".to_string()), | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Include, | |
| loader_overrides: LoaderOverrides::without_managed_config_for_tests(), | |
| }, | |
| Some(codex_home.path().to_path_buf()), | |
| CloudConfigBundleFixture::loader_with_enterprise_requirement( | |
| CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS, | |
| ), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert_eq!( | |
| config | |
| .permissions | |
| .active_permission_profile() | |
| .map(|profile| profile.id), | |
| Some("managed-cloud".to_string()), | |
| ); | |
| assert_eq!( | |
| config.permissions.network_sandbox_policy(), | |
| NetworkSandboxPolicy::Enabled, | |
| ); | |
| assert_eq!( | |
| config.config_layer_stack.requirements_toml(), | |
| &toml::from_str::<ConfigRequirementsToml>( | |
| CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS, | |
| )?, | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_ignores_cloud_managed_permission_profiles_by_default() | |
| -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: Some(":workspace".to_string()), | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Ignore, | |
| loader_overrides: LoaderOverrides::without_managed_config_for_tests(), | |
| }, | |
| Some(codex_home.path().to_path_buf()), | |
| CloudConfigBundleFixture::loader_with_enterprise_requirement( | |
| CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS, | |
| ), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert_eq!( | |
| config | |
| .permissions | |
| .active_permission_profile() | |
| .map(|profile| profile.id), | |
| Some(":workspace".to_string()), | |
| ); | |
| assert_eq!( | |
| config.permissions.network_sandbox_policy(), | |
| NetworkSandboxPolicy::Restricted, | |
| ); | |
| assert_eq!( | |
| config.config_layer_stack.requirements_toml(), | |
| &ConfigRequirementsToml::default(), | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_honors_explicit_named_permission_profile() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let sandbox_paths = TempDir::new()?; | |
| let docs = sandbox_paths.path().join("docs"); | |
| let private = docs.join("private"); | |
| write_permissions_profile_config(&codex_home, &docs, &private)?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: Some("limited-read-test".to_string()), | |
| cwd: None, | |
| managed_requirements_mode: ManagedRequirementsMode::Ignore, | |
| loader_overrides: LoaderOverrides::default(), | |
| }, | |
| Some(codex_home.path().to_path_buf()), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| let expected = build_debug_sandbox_config( | |
| vec![( | |
| "default_permissions".to_string(), | |
| TomlValue::String("limited-read-test".to_string()), | |
| )], | |
| ConfigOverrides::default(), | |
| Some(codex_home.path().to_path_buf()), | |
| ManagedRequirementsMode::Include, | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert_eq!( | |
| config.permissions.file_system_sandbox_policy(), | |
| expected.permissions.file_system_sandbox_policy() | |
| ); | |
| Ok(()) | |
| } | |
| async fn debug_sandbox_uses_explicit_cwd() -> anyhow::Result<()> { | |
| let codex_home = TempDir::new()?; | |
| let cwd = TempDir::new()?; | |
| let config = load_debug_sandbox_config_with_codex_home( | |
| Vec::new(), | |
| /*codex_linux_sandbox_exe*/ None, | |
| DebugSandboxConfigOptions { | |
| sandbox_state: Default::default(), | |
| permissions_profile: Some(":workspace".to_string()), | |
| cwd: Some(cwd.path().to_path_buf()), | |
| managed_requirements_mode: ManagedRequirementsMode::Ignore, | |
| loader_overrides: LoaderOverrides::default(), | |
| }, | |
| Some(codex_home.path().to_path_buf()), | |
| CloudConfigBundleLoader::default(), | |
| /*strict_config*/ false, | |
| ) | |
| .await?; | |
| assert_eq!(config.cwd.as_path(), cwd.path()); | |
| Ok(()) | |
| } | |
| } | |