#[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_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>,
) -> anyhow::Result<()> {
    let SeatbeltCommand {
        permissions_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,
        },
        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>,
) -> 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>,
) -> anyhow::Result<()> {
    let LandlockCommand {
        permissions_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,
        },
        command,
        config_overrides,
        codex_linux_sandbox_exe,
        SandboxType::Landlock,
        /*log_denials*/ false,
        &[],
    )
    .await
}

pub async fn run_command_under_windows(
    command: WindowsCommand,
    codex_linux_sandbox_exe: Option<PathBuf>,
) -> anyhow::Result<()> {
    let WindowsCommand {
        permissions_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,
        },
        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,
}

#[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,
    )
    .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();
    // For now, we always use the same cwd for both the command and the
    // sandbox policy. In the future, we could add a CLI option to set them
    // separately.
    let sandbox_policy_cwd = cwd.clone();

    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, sandbox_policy_cwd, 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.get(),
                /*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);

    let mut child = match sandbox_type {
        #[cfg(target_os = "macos")]
        SandboxType::Seatbelt => {
            let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
            let network_sandbox_policy = config.permissions.network_sandbox_policy();
            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: false,
                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 = config.permissions.network_sandbox_policy();
            let args = create_linux_sandbox_command_args_for_permission_profile(
                command,
                cwd.as_path(),
                &config.permissions.permission_profile(),
                sandbox_policy_cwd.as_path(),
                use_legacy_landlock,
                allow_network_for_proxy(managed_network_requirements_enabled),
            );
            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,
    sandbox_policy_cwd: AbsolutePathBuf,
    env: std::collections::HashMap<String, String>,
) -> ! {
    use codex_core::windows_sandbox::WindowsSandboxLevelExt;
    use codex_protocol::config_types::WindowsSandboxLevel;
    use codex_windows_sandbox::spawn_windows_sandbox_session_elevated;
    use codex_windows_sandbox::spawn_windows_sandbox_session_legacy;

    let sandbox_policy = config
        .permissions
        .legacy_sandbox_policy(sandbox_policy_cwd.as_path());
    let policy_str = match serde_json::to_string(&sandbox_policy) {
        Ok(policy_str) => policy_str,
        Err(err) => {
            eprintln!("windows sandbox failed to serialize policy: {err}");
            std::process::exit(1);
        }
    };

    let use_elevated = matches!(
        WindowsSandboxLevel::from_config(config),
        WindowsSandboxLevel::Elevated
    );

    let spawned = if use_elevated {
        spawn_windows_sandbox_session_elevated(
            policy_str.as_str(),
            sandbox_policy_cwd.as_path(),
            config.codex_home.as_path(),
            command,
            cwd.as_path(),
            env,
            None,
            /*tty*/ false,
            /*stdin_open*/ true,
            config.permissions.windows_sandbox_private_desktop,
        )
        .await
    } else {
        spawn_windows_sandbox_session_legacy(
            policy_str.as_str(),
            sandbox_policy_cwd.as_path(),
            config.codex_home.as_path(),
            command,
            cwd.as_path(),
            env,
            None,
            /*tty*/ false,
            /*stdin_open*/ true,
            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 session = std::sync::Arc::new(spawned.session);
    let tokio_runtime = tokio::runtime::Handle::current();
    // Give large or slow tail output a better chance to finish draining
    // without letting rare EOF issues hang the wrapper indefinitely.
    let output_drain_timeout = std::time::Duration::from_secs(5);
    // A helper thread watches our stdin. When the input source closes it,
    // the thread tells the main async code so we can also close stdin for
    // the sandboxed child process.
    let (stdin_eof_tx, stdin_eof_rx) = tokio::sync::oneshot::channel();

    // Start background threads that copy stdin/stdout/stderr. We
    // intentionally do not keep their JoinHandles; dropping the handle does
    // not stop the thread, it just means we are not going to wait on it
    // later.
    drop(windows_stdio_bridge::spawn_input_forwarder(
        std::io::stdin(),
        session.writer_sender(),
        stdin_eof_tx,
    ));
    let (stdout_forwarder, stdout_forwarder_done_rx) = windows_stdio_bridge::spawn_output_forwarder(
        tokio_runtime.clone(),
        spawned.stdout_rx,
        std::io::stdout(),
    );
    drop(stdout_forwarder);
    let (stderr_forwarder, stderr_forwarder_done_rx) = windows_stdio_bridge::spawn_output_forwarder(
        tokio_runtime.clone(),
        spawned.stderr_rx,
        std::io::stderr(),
    );
    drop(stderr_forwarder);

    let stdin_close_task = tokio::spawn({
        let session = std::sync::Arc::clone(&session);
        async move {
            let _ = stdin_eof_rx.await;
            session.close_stdin();
        }
    });

    let mut exit_rx = spawned.exit_rx;
    let exit_code = tokio::select! {
        res = &mut exit_rx => res.unwrap_or(-1),
        res = tokio::signal::ctrl_c() => {
            if let Ok(()) = res {
                session.request_terminate();
            }
            exit_rx.await.unwrap_or(-1)
        }
    };

    stdin_close_task.abort();
    let _ = tokio::time::timeout(output_drain_timeout, async {
        let _ = stdout_forwarder_done_rx.await;
        let _ = stderr_forwarder_done_rx.await;
    })
    .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()
}

#[cfg(target_os = "windows")]
mod windows_stdio_bridge {
    use std::io::Read;
    use std::io::Write;

    use tokio::sync::mpsc;
    use tokio::sync::oneshot;

    const STDIN_FORWARD_CHUNK_SIZE: usize = 8 * 1024;

    pub(super) fn spawn_input_forwarder<R>(
        mut input: R,
        writer_tx: mpsc::Sender<Vec<u8>>,
        stdin_eof_tx: oneshot::Sender<()>,
    ) -> std::thread::JoinHandle<()>
    where
        R: Read + Send + 'static,
    {
        std::thread::spawn(move || {
            let mut buffer = [0_u8; STDIN_FORWARD_CHUNK_SIZE];
            loop {
                match input.read(&mut buffer) {
                    Ok(0) => break,
                    Ok(n) => {
                        if writer_tx.blocking_send(buffer[..n].to_vec()).is_err() {
                            break;
                        }
                    }
                    Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
                    Err(err) => {
                        eprintln!("windows sandbox stdin forwarder failed: {err}");
                        break;
                    }
                }
            }
            let _ = stdin_eof_tx.send(());
        })
    }

    pub(super) fn spawn_output_forwarder<W>(
        tokio_runtime: tokio::runtime::Handle,
        output_rx: mpsc::Receiver<Vec<u8>>,
        mut writer: W,
    ) -> (std::thread::JoinHandle<()>, oneshot::Receiver<()>)
    where
        W: Write + Send + 'static,
    {
        let (done_tx, done_rx) = oneshot::channel();
        // The sandbox session emits output on Tokio channels, but writing to the
        // caller's stdio is simplest from a dedicated blocking thread.
        let handle = std::thread::spawn(move || {
            let mut output_rx = output_rx;
            while let Some(chunk) = tokio_runtime.block_on(output_rx.recv()) {
                if let Err(err) = writer.write_all(&chunk) {
                    eprintln!("windows sandbox output forwarder failed to write: {err}");
                    break;
                }
                if let Err(err) = writer.flush() {
                    eprintln!("windows sandbox output forwarder failed to flush: {err}");
                    break;
                }
            }
            let _ = done_tx.send(());
        });
        (handle, done_rx)
    }

    #[cfg(test)]
    mod tests {
        use std::sync::Mutex;

        use pretty_assertions::assert_eq;

        use super::*;

        #[tokio::test]
        async fn input_forwarder_sends_chunks_and_reports_eof() -> anyhow::Result<()> {
            let (writer_tx, mut writer_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(4);
            let (stdin_closed_tx, stdin_closed_rx) = tokio::sync::oneshot::channel();
            let input = std::io::Cursor::new(b"first\nsecond\n".to_vec());

            let forwarder = spawn_input_forwarder(input, writer_tx, stdin_closed_tx);
            let mut received = Vec::new();
            while let Some(chunk) = writer_rx.recv().await {
                received.extend_from_slice(&chunk);
            }
            stdin_closed_rx.await?;
            forwarder.join().expect("stdin forwarder should finish");

            assert_eq!(received, b"first\nsecond\n".to_vec());
            Ok(())
        }

        #[tokio::test]
        async fn output_forwarder_writes_all_chunks() -> anyhow::Result<()> {
            #[derive(Clone, Default)]
            struct SharedWriter(std::sync::Arc<Mutex<Vec<u8>>>);

            impl std::io::Write for SharedWriter {
                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
                    let mut guard = self
                        .0
                        .lock()
                        .map_err(|_| std::io::Error::other("writer poisoned"))?;
                    guard.extend_from_slice(buf);
                    Ok(buf.len())
                }

                fn flush(&mut self) -> std::io::Result<()> {
                    Ok(())
                }
            }

            let runtime = tokio::runtime::Handle::current();
            let (output_tx, output_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(4);
            let writer = SharedWriter::default();
            let sink = std::sync::Arc::clone(&writer.0);

            let (forwarder, done_rx) = spawn_output_forwarder(runtime, output_rx, writer);
            output_tx.send(b"alpha".to_vec()).await?;
            output_tx.send(b"beta".to_vec()).await?;
            drop(output_tx);
            forwarder.join().expect("output forwarder should finish");
            done_rx.await?;

            let output = sink
                .lock()
                .map_err(|_| anyhow::anyhow!("writer poisoned"))?
                .clone();
            assert_eq!(output, b"alphabeta".to_vec());
            Ok(())
        }
    }
}

async fn load_debug_sandbox_config(
    cli_overrides: Vec<(String, TomlValue)>,
    codex_linux_sandbox_exe: Option<PathBuf>,
    options: DebugSandboxConfigOptions,
) -> anyhow::Result<Config> {
    load_debug_sandbox_config_with_codex_home(
        cli_overrides,
        codex_linux_sandbox_exe,
        options,
        /*codex_home*/ None,
    )
    .await
}

async fn load_debug_sandbox_config_with_codex_home(
    mut cli_overrides: Vec<(String, TomlValue)>,
    codex_linux_sandbox_exe: Option<PathBuf>,
    options: DebugSandboxConfigOptions,
    codex_home: Option<PathBuf>,
) -> anyhow::Result<Config> {
    let DebugSandboxConfigOptions {
        permissions_profile,
        cwd,
        managed_requirements_mode,
    } = options;

    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, which is now the documented writable
    // replacement for the removed `--full-auto` flag.
    let uses_legacy_sandbox_mode_override = cli_overrides_use_legacy_sandbox_mode(&cli_overrides);
    let config = build_debug_sandbox_config(
        cli_overrides.clone(),
        ConfigOverrides {
            cwd: cwd.clone(),
            codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(),
            ..Default::default()
        },
        codex_home.clone(),
        managed_requirements_mode,
    )
    .await?;

    if config_uses_permission_profiles(&config) || uses_legacy_sandbox_mode_override {
        return Ok(config);
    }

    build_debug_sandbox_config(
        cli_overrides,
        ConfigOverrides {
            sandbox_mode: Some(SandboxMode::ReadOnly),
            cwd,
            codex_linux_sandbox_exe,
            ..Default::default()
        },
        codex_home,
        managed_requirements_mode,
    )
    .await
    .map_err(Into::into)
}

async fn build_debug_sandbox_config(
    cli_overrides: Vec<(String, TomlValue)>,
    harness_overrides: ConfigOverrides,
    codex_home: Option<PathBuf>,
    managed_requirements_mode: ManagedRequirementsMode,
) -> std::io::Result<Config> {
    let mut builder = ConfigBuilder::default()
        .cli_overrides(cli_overrides)
        .harness_overrides(harness_overrides);
    if let ManagedRequirementsMode::Ignore = managed_requirements_mode {
        builder = builder.loader_overrides(LoaderOverrides {
            ignore_managed_requirements: true,
            ..Default::default()
        });
    }
    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;

    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<()> {
        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(codex_home.path().join("config.toml"), 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,
        )
        .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,
        )
        .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,
            },
            Some(codex_home_path),
        )
        .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_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,
        )
        .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,
        )
        .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,
            },
            Some(codex_home_path),
        )
        .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,
        )
        .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,
            },
            Some(codex_home_path),
        )
        .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,
            },
            Some(codex_home.path().to_path_buf()),
        )
        .await?;

        assert_eq!(
            config.permissions.file_system_sandbox_policy(),
            codex_protocol::models::PermissionProfile::workspace_write()
                .file_system_sandbox_policy()
        );

        Ok(())
    }

    #[tokio::test]
    async fn explicit_permission_profile_overrides_active_profile_sandbox_mode()
    -> anyhow::Result<()> {
        let codex_home = TempDir::new()?;
        std::fs::write(
            codex_home.path().join("config.toml"),
            "profile = \"legacy\"\n\
             \n\
             [profiles.legacy]\n\
             sandbox_mode = \"danger-full-access\"\n",
        )?;

        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,
            },
            Some(codex_home.path().to_path_buf()),
        )
        .await?;

        assert_eq!(
            config.permissions.file_system_sandbox_policy(),
            codex_protocol::models::PermissionProfile::workspace_write()
                .file_system_sandbox_policy()
        );

        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,
            },
            Some(codex_home.path().to_path_buf()),
        )
        .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,
        )
        .await?;

        assert_eq!(
            config.permissions.file_system_sandbox_policy(),
            expected.permissions.file_system_sandbox_policy()
        );

        Ok(())
    }

    #[tokio::test]
    async fn debug_sandbox_uses_explicit_profile_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,
            },
            Some(codex_home.path().to_path_buf()),
        )
        .await?;

        assert_eq!(config.cwd.as_path(), cwd.path());

        Ok(())
    }
}
