Codex Handbook
cli/src/debug_sandbox.rs 952 lines
#[cfg(target_os = "macos")]mod pid_tracker;#[cfg(target_os = "macos")]mod seatbelt;use std::path::PathBuf;use std::process::Stdio;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::exec_env::create_env;#[cfg(target_os = "macos")]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::allow_network_for_proxy;use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;#[cfg(target_os = "macos")]use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams;#[cfg(target_os = "macos")]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;#[cfg(target_os = "macos")]use seatbelt::DenialLogger;#[cfg(target_os = "macos")]pub async fn run_command_under_seatbelt(    command: SeatbeltCommand,    codex_linux_sandbox_exe: Option<PathBuf>,    loader_overrides: LoaderOverrides,) -> anyhow::Result<()> {    let SeatbeltCommand {        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 {            permissions_profile,            cwd,            managed_requirements_mode,            loader_overrides,        },        command,        config_overrides,        codex_linux_sandbox_exe,        SandboxType::Seatbelt,        log_denials,        &allow_unix_sockets,    )    .await}#[cfg(not(target_os = "macos"))]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 {        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 {            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 {        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 {            permissions_profile,            cwd,            managed_requirements_mode,            loader_overrides,        },        command,        config_overrides,        codex_linux_sandbox_exe,        SandboxType::Windows,        /*log_denials*/ false,        &[],    )    .await}enum SandboxType {    #[cfg(target_os = "macos")]    Seatbelt,    Landlock,    Windows,}#[derive(Debug)]struct DebugSandboxConfigOptions {    permissions_profile: Option<String>,    cwd: Option<PathBuf>,    managed_requirements_mode: ManagedRequirementsMode,    loader_overrides: LoaderOverrides,}#[derive(Debug, Clone, Copy)]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(    config_options: DebugSandboxConfigOptions,    command: Vec<String>,    config_overrides: CliConfigOverrides,    codex_linux_sandbox_exe: Option<PathBuf>,    sandbox_type: SandboxType,    log_denials: bool,    #[cfg_attr(not(target_os = "macos"), allow(unused_variables))]    allow_unix_sockets: &[AbsolutePathBuf],) -> anyhow::Result<()> {    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();    #[cfg(target_os = "windows")]    let workspace_roots = config.effective_workspace_roots();    let env = create_env(        &config.permissions.shell_environment_policy,        /*thread_id*/ None,    );    // Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.    if let SandboxType::Windows = sandbox_type {        #[cfg(target_os = "windows")]        {            run_command_under_windows_session(&config, command, cwd, workspace_roots, env).await;        }        #[cfg(not(target_os = "windows"))]        {            anyhow::bail!("Windows sandbox is only available on Windows");        }    }    #[cfg(target_os = "macos")]    let mut denial_logger = log_denials.then(DenialLogger::new).flatten();    #[cfg(not(target_os = "macos"))]    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(                config.permissions.permission_profile(),                /*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(        config.permissions.effective_permission_profile(),        managed_mitm_ca_trust_bundle_path.as_ref(),        sandbox_policy_cwd.as_path(),    );    let mut child = match sandbox_type {        #[cfg(target_os = "macos")]        SandboxType::Seatbelt => {            let (file_system_sandbox_policy, network_sandbox_policy) =                runtime_permission_profile.to_runtime_permissions();            let 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,                network: network.as_ref(),                extra_allow_unix_sockets: allow_unix_sockets,            });            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 => {            #[expect(clippy::expect_used)]            let codex_linux_sandbox_exe = config                .codex_linux_sandbox_exe                .expect("codex-linux-sandbox executable not found");            let use_legacy_landlock = config.features.use_legacy_landlock();            let network_sandbox_policy = runtime_permission_profile.network_sandbox_policy();            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,                allow_network_for_proxy(enforce_managed_network),            );            spawn_debug_sandbox_child(                codex_linux_sandbox_exe,                args,                Some("codex-linux-sandbox"),                cwd.to_path_buf(),                network_sandbox_policy,                env,                |env_map| {                    if let Some(network) = network.as_ref() {                        network.apply_to_env(env_map);                    }                },            )            .await?        }        SandboxType::Windows => {            unreachable!("Windows sandbox should have been handled above");        }    };    #[cfg(target_os = "macos")]    if let Some(denial_logger) = &mut denial_logger {        denial_logger.on_child_spawn(&child);    }    let status = child.wait().await?;    #[cfg(target_os = "macos")]    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);}#[cfg(target_os = "windows")]async fn run_command_under_windows_session(    config: &Config,    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::WindowsSandboxSessionRequest;    use codex_windows_sandbox::spawn_windows_sandbox_session_for_level;    let permission_profile = config.permissions.effective_permission_profile();    let empty_paths: &[AbsolutePathBuf] = &[];    let spawned = spawn_windows_sandbox_session_for_level(WindowsSandboxSessionRequest {        permission_profile: &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_enforced: false,        timeout_ms: None,        read_roots_override: None,        read_roots_include_platform_defaults: false,        write_roots_override: None,        deny_read_paths_override: empty_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);    #[cfg(unix)]    cmd.arg0(arg0.map_or_else(|| program.to_string_lossy().to_string(), String::from));    #[cfg(not(unix))]    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> {    load_debug_sandbox_config_with_codex_home(        cli_overrides,        codex_linux_sandbox_exe,        options,        /*codex_home*/ None,        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>,    strict_config: bool,) -> anyhow::Result<Config> {    let DebugSandboxConfigOptions {        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(),        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,        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,    strict_config: bool,) -> std::io::Result<Config> {    let mut builder = ConfigBuilder::default()        .cli_overrides(cli_overrides)        .harness_overrides(harness_overrides)        .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")}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    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(),            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(())    }    #[tokio::test]    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 {                permissions_profile: None,                cwd: None,                managed_requirements_mode: ManagedRequirementsMode::Include,                loader_overrides: LoaderOverrides::default(),            },            Some(codex_home_path),            /*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(())    }    #[tokio::test]    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(),            /*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 {                permissions_profile: None,                cwd: None,                managed_requirements_mode: ManagedRequirementsMode::Include,                loader_overrides,            },            Some(codex_home_path),            /*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(())    }    #[tokio::test]    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 {                permissions_profile: None,                cwd: None,                managed_requirements_mode: ManagedRequirementsMode::Include,                loader_overrides: LoaderOverrides::default(),            },            Some(codex_home_path),            /*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(())    }    #[tokio::test]    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 {                permissions_profile: None,                cwd: None,                managed_requirements_mode: ManagedRequirementsMode::Include,                loader_overrides: LoaderOverrides::default(),            },            Some(codex_home_path),            /*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(())    }    #[tokio::test]    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 {                permissions_profile: Some(":workspace".to_string()),                cwd: None,                managed_requirements_mode: ManagedRequirementsMode::Ignore,                loader_overrides: LoaderOverrides::default(),            },            Some(codex_home.path().to_path_buf()),            /*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(())    }    #[tokio::test]    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 {                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()),            /*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(())    }    #[tokio::test]    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 {                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()),            /*strict_config*/ false,        )        .await?;        assert_eq!(config.cwd.as_path(), cwd.path());        Ok(())    }}