use super::CreateSeatbeltCommandArgsParams;
use super::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use super::MACOS_SEATBELT_BASE_POLICY;
use super::ProxyPolicyInputs;
use super::UnixDomainSocketPolicy;
use super::build_seatbelt_unreadable_glob_policy;
use super::create_seatbelt_command_args;
use super::create_seatbelt_command_args_for_legacy_policy;
use super::dynamic_network_policy;
use super::macos_dir_params;
use super::normalize_path_for_sandbox;
use super::seatbelt_regex_for_unreadable_glob;
use super::unix_socket_dir_params;
use super::unix_socket_policy;
use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigState;
use codex_network_proxy::NetworkMode;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyConstraints;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::build_config_state;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use tempfile::TempDir;

fn assert_seatbelt_denied(stderr: &[u8], path: &Path) {
    let stderr = String::from_utf8_lossy(stderr);
    let expected = format!("bash: {}: Operation not permitted\n", path.display());
    assert!(
        stderr == expected
            || stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted"),
        "unexpected stderr: {stderr}"
    );
}

fn absolute_path(path: &str) -> AbsolutePathBuf {
    AbsolutePathBuf::from_absolute_path(Path::new(path)).expect("absolute path")
}

fn seatbelt_policy_arg(args: &[String]) -> &str {
    let policy_index = args
        .iter()
        .position(|arg| arg == "-p")
        .expect("seatbelt args should include -p");
    args.get(policy_index + 1)
        .expect("seatbelt args should include policy text")
}

fn seatbelt_protected_metadata_name_requirements(root: &Path) -> String {
    let mut root = root.to_string_lossy().to_string();
    while root.len() > 1 && root.ends_with('/') {
        root.pop();
    }
    let root = regex_lite::escape(&root);
    PROTECTED_METADATA_PATH_NAMES
        .iter()
        .map(|name| {
            let name = regex_lite::escape(name);
            if root == "/" {
                format!(r#"(require-not (regex #"^/{name}(/.*)?$"))"#)
            } else {
                format!(r#"(require-not (regex #"^{root}/{name}(/.*)?$"))"#)
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

struct TestConfigReloader;

#[async_trait::async_trait]
impl ConfigReloader for TestConfigReloader {
    fn source_label(&self) -> String {
        "seatbelt test config".to_string()
    }

    async fn maybe_reload(&self) -> anyhow::Result<Option<ConfigState>> {
        Ok(None)
    }

    async fn reload_now(&self) -> anyhow::Result<ConfigState> {
        Err(anyhow::anyhow!("seatbelt test config cannot reload"))
    }
}

#[test]
fn base_policy_allows_node_cpu_sysctls() {
    assert!(
        MACOS_SEATBELT_BASE_POLICY.contains("(sysctl-name \"machdep.cpu.brand_string\")"),
        "base policy must allow CPU brand lookup for os.cpus()"
    );
    assert!(
        MACOS_SEATBELT_BASE_POLICY.contains("(sysctl-name \"hw.model\")"),
        "base policy must allow hardware model lookup for os.cpus()"
    );
}

#[test]
fn base_policy_allows_kmp_registration_shm_read_create_and_unlink() {
    let expected = r##"(allow ipc-posix-shm-read-data
  ipc-posix-shm-write-create
  ipc-posix-shm-write-unlink
  (ipc-posix-name-regex #"^/__KMP_REGISTERED_LIB_[0-9]+$"))"##;

    assert!(
        MACOS_SEATBELT_BASE_POLICY.contains(expected),
        "base policy must allow only KMP registration shm read/create/unlink:\n{MACOS_SEATBELT_BASE_POLICY}"
    );
}

#[test]
fn create_seatbelt_args_routes_network_through_proxy_ports() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::new_read_only_policy(),
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![43128, 48081],
            has_proxy_config: true,
            allow_local_binding: false,
            ..ProxyPolicyInputs::default()
        },
    );

    assert!(
        policy.contains("(allow network-outbound (remote ip \"localhost:43128\"))"),
        "expected HTTP proxy port allow rule in policy:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-outbound (remote ip \"localhost:48081\"))"),
        "expected SOCKS proxy port allow rule in policy:\n{policy}"
    );
    assert!(
        !policy.contains("\n(allow network-outbound)\n"),
        "policy should not include blanket outbound allowance when proxy ports are present:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-bind (local ip \"*:*\"))"),
        "policy should not allow local binding unless explicitly enabled:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-inbound (local ip \"localhost:*\"))"),
        "policy should not allow loopback inbound unless explicitly enabled:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
        "policy should not allow raw DNS unless local binding is explicitly enabled:\n{policy}"
    );
}

#[test]
fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() {
    let unreadable = absolute_path("/tmp/codex-unreadable");
    let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
        FileSystemSandboxEntry {
            path: FileSystemPath::Special {
                value: FileSystemSpecialPath::Root,
            },
            access: FileSystemAccessMode::Write,
        },
        FileSystemSandboxEntry {
            path: FileSystemPath::Path { path: unreadable },
            access: FileSystemAccessMode::None,
        },
    ]);

    let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
        command: vec!["/bin/true".to_string()],
        file_system_sandbox_policy: &file_system_policy,
        network_sandbox_policy: NetworkSandboxPolicy::Restricted,
        sandbox_policy_cwd: Path::new("/"),
        enforce_managed_network: false,
        network: None,
        extra_allow_unix_sockets: &[],
    });

    let policy = seatbelt_policy_arg(&args);
    let unreadable_roots = file_system_policy.get_unreadable_roots_with_cwd(Path::new("/"));
    let unreadable_root = unreadable_roots.first().expect("expected unreadable root");
    assert!(
        policy.contains("(require-not (literal (param \"READABLE_ROOT_0_EXCLUDED_0\")))"),
        "expected exact read carveout in policy:\n{policy}"
    );
    assert!(
        policy.contains("(require-not (subpath (param \"READABLE_ROOT_0_EXCLUDED_0\")))"),
        "expected read carveout in policy:\n{policy}"
    );
    assert!(
        policy.contains("(require-not (literal (param \"WRITABLE_ROOT_0_EXCLUDED_0\")))"),
        "expected exact write carveout in policy:\n{policy}"
    );
    assert!(
        policy.contains("(require-not (subpath (param \"WRITABLE_ROOT_0_EXCLUDED_0\")))"),
        "expected write carveout in policy:\n{policy}"
    );
    assert!(
        policy.contains(&seatbelt_protected_metadata_name_requirements(Path::new(
            "/"
        ))),
        "expected metadata protection regex deny requirements in policy:\n{policy}"
    );
    assert!(
        args.iter().any(
            |arg| arg == &format!("-DREADABLE_ROOT_0_EXCLUDED_0={}", unreadable_root.display())
        ),
        "expected read carveout parameter in args: {args:#?}"
    );
    let writable_definitions: Vec<String> = args
        .iter()
        .filter(|arg| arg.starts_with("-DWRITABLE_ROOT_"))
        .cloned()
        .collect();
    assert_eq!(
        writable_definitions,
        vec![
            "-DWRITABLE_ROOT_0=/".to_string(),
            "-DWRITABLE_ROOT_0_EXCLUDED_0=/.codex".to_string(),
            format!("-DWRITABLE_ROOT_0_EXCLUDED_1={}", unreadable_root.display()),
        ],
        "unexpected write carveout parameters in args: {args:#?}"
    );
}

#[test]
fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
    let root = absolute_path("/tmp/codex-readable");
    let unreadable = absolute_path("/tmp/codex-readable/private");
    let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
        FileSystemSandboxEntry {
            path: FileSystemPath::Path { path: root },
            access: FileSystemAccessMode::Read,
        },
        FileSystemSandboxEntry {
            path: FileSystemPath::Path { path: unreadable },
            access: FileSystemAccessMode::None,
        },
    ]);

    let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
        command: vec!["/bin/true".to_string()],
        file_system_sandbox_policy: &file_system_policy,
        network_sandbox_policy: NetworkSandboxPolicy::Restricted,
        sandbox_policy_cwd: Path::new("/"),
        enforce_managed_network: false,
        network: None,
        extra_allow_unix_sockets: &[],
    });

    let policy = seatbelt_policy_arg(&args);
    let readable_roots = file_system_policy.get_readable_roots_with_cwd(Path::new("/"));
    let readable_root = readable_roots.first().expect("expected readable root");
    let unreadable_roots = file_system_policy.get_unreadable_roots_with_cwd(Path::new("/"));
    let unreadable_root = unreadable_roots.first().expect("expected unreadable root");
    assert!(
        policy.contains("(require-not (literal (param \"READABLE_ROOT_0_EXCLUDED_0\")))"),
        "expected exact read carveout in policy:\n{policy}"
    );
    assert!(
        policy.contains("(require-not (subpath (param \"READABLE_ROOT_0_EXCLUDED_0\")))"),
        "expected read carveout in policy:\n{policy}"
    );
    assert!(
        args.iter()
            .any(|arg| arg == &format!("-DREADABLE_ROOT_0={}", readable_root.display())),
        "expected readable root parameter in args: {args:#?}"
    );
    assert!(
        args.iter().any(
            |arg| arg == &format!("-DREADABLE_ROOT_0_EXCLUDED_0={}", unreadable_root.display())
        ),
        "expected read carveout parameter in args: {args:#?}"
    );
}

#[test]
fn unreadable_globstar_slash_matches_zero_or_more_directories() {
    let regex = seatbelt_regex_for_unreadable_glob("/tmp/repo/**/*.env");
    assert_eq!(regex.as_deref(), Some(r"^/tmp/repo/(.*/)?[^/]*\.env$"));
    let regex = regex_lite::Regex::new(regex.as_deref().expect("glob should compile"))
        .expect("regex should compile");

    assert!(regex.is_match("/tmp/repo/.env"));
    assert!(regex.is_match("/tmp/repo/app/.env"));
    assert!(regex.is_match("/tmp/repo/app/config.env"));
    assert!(!regex.is_match("/tmp/repo/app/config.toml"));
}

#[test]
fn unreadable_globs_use_git_style_component_matching() {
    let regex = seatbelt_regex_for_unreadable_glob("/tmp/repo/*/file[0-9]?.txt");
    assert_eq!(
        regex.as_deref(),
        Some(r"^/tmp/repo/[^/]*/file[0-9][^/]\.txt$")
    );
    let regex = regex_lite::Regex::new(regex.as_deref().expect("glob should compile"))
        .expect("regex should compile");

    assert!(regex.is_match("/tmp/repo/app/file42.txt"));
    assert!(!regex.is_match("/tmp/repo/app/nested/file42.txt"));
    assert!(!regex.is_match("/tmp/repo/app/file4.txt"));
    assert!(!regex.is_match("/tmp/repo/app/fileab.txt"));
}

#[test]
fn unreadable_globs_treat_unclosed_character_classes_as_literals() {
    let regex = seatbelt_regex_for_unreadable_glob("/tmp/repo/[*.env");
    assert_eq!(regex.as_deref(), Some(r"^/tmp/repo/\[[^/]*\.env$"));
    let regex = regex_lite::Regex::new(regex.as_deref().expect("glob should compile"))
        .expect("regex should compile");

    assert!(regex.is_match("/tmp/repo/[local.env"));
    assert!(regex.is_match("/tmp/repo/[.env"));
    assert!(!regex.is_match("/tmp/repo/local.env"));
}

#[cfg(unix)]
#[test]
fn unreadable_glob_policy_includes_canonicalized_static_prefix() {
    use std::os::unix::fs::symlink;

    let temp_dir = TempDir::new().expect("temp dir");
    let real_root = temp_dir.path().join("real-root");
    let link_root = temp_dir.path().join("link-root");
    fs::create_dir(&real_root).expect("create real root");
    symlink(&real_root, &link_root).expect("create symlinked root");

    let pattern = format!("{}/**/*.env", link_root.display());
    let canonical_pattern = format!(
        "{}/**/*.env",
        real_root
            .canonicalize()
            .expect("canonicalize real root")
            .display()
    );
    let expected_regex = seatbelt_regex_for_unreadable_glob(&canonical_pattern)
        .expect("canonical glob should compile");
    let mut policy = FileSystemSandboxPolicy::default();
    policy.entries.push(FileSystemSandboxEntry {
        path: FileSystemPath::GlobPattern { pattern },
        access: FileSystemAccessMode::None,
    });

    let seatbelt_policy = build_seatbelt_unreadable_glob_policy(&policy, temp_dir.path());

    assert!(
        seatbelt_policy.contains(&format!(r#"(deny file-read* (regex #"{expected_regex}"))"#)),
        "expected canonicalized glob regex in policy:\n{seatbelt_policy}"
    );
}

#[test]
fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() {
    let cwd = std::env::temp_dir();
    let args = create_seatbelt_command_args_for_legacy_policy(
        vec!["echo".to_string(), "ok".to_string()],
        &SandboxPolicy::new_read_only_policy(),
        cwd.as_path(),
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );
    let policy = &args[1];
    assert!(policy.contains("(allow user-preference-read)"));
    assert!(!policy.contains("(allow user-preference-write)"));
}

#[test]
fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::new_read_only_policy(),
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![43128],
            has_proxy_config: true,
            allow_local_binding: true,
            ..ProxyPolicyInputs::default()
        },
    );

    assert!(
        policy.contains("(allow network-bind (local ip \"*:*\"))"),
        "policy should allow loopback local binding when explicitly enabled:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-inbound (local ip \"localhost:*\"))"),
        "policy should allow loopback inbound when explicitly enabled:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-outbound (remote ip \"localhost:*\"))"),
        "policy should allow loopback outbound when explicitly enabled:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
        "policy should allow DNS egress when local binding is explicitly enabled:\n{policy}"
    );
    assert!(
        !policy.contains("\n(allow network-outbound)\n"),
        "policy should keep proxy-routed behavior without blanket outbound allowance:\n{policy}"
    );
}

#[test]
fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ports() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::WorkspaceWrite {
            writable_roots: vec![],
            network_access: true,
            exclude_tmpdir_env_var: false,
            exclude_slash_tmp: false,
        },
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![],
            has_proxy_config: true,
            allow_local_binding: false,
            ..ProxyPolicyInputs::default()
        },
    );

    assert!(
        policy.contains("(socket-domain AF_SYSTEM)"),
        "policy should keep the restricted network profile when proxy config is present without ports:\n{policy}"
    );
    assert!(
        !policy.contains("\n(allow network-outbound)\n"),
        "policy should not include blanket outbound allowance when proxy config is present without ports:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-outbound (remote ip \"localhost:"),
        "policy should not include proxy port allowance when proxy config is present without ports:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
        "policy should stay fail-closed for DNS when no proxy ports are available:\n{policy}"
    );
}

#[test]
fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::WorkspaceWrite {
            writable_roots: vec![],
            network_access: true,
            exclude_tmpdir_env_var: false,
            exclude_slash_tmp: false,
        },
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![],
            has_proxy_config: true,
            allow_local_binding: true,
            ..ProxyPolicyInputs::default()
        },
    );

    assert!(
        policy.contains("(allow network-bind (local ip \"*:*\"))"),
        "policy should still allow explicitly configured local binding:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
        "policy should not allow DNS egress when no proxy ports are available:\n{policy}"
    );
}

#[test]
fn dynamic_network_policy_preserves_restricted_policy_for_managed_network_without_proxy_config() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::WorkspaceWrite {
            writable_roots: vec![],
            network_access: true,
            exclude_tmpdir_env_var: false,
            exclude_slash_tmp: false,
        },
        /*enforce_managed_network*/ true,
        &ProxyPolicyInputs {
            ports: vec![],
            has_proxy_config: false,
            allow_local_binding: false,
            ..ProxyPolicyInputs::default()
        },
    );

    assert!(
        policy.contains("(socket-domain AF_SYSTEM)"),
        "policy should keep the restricted network profile when managed network is active without proxy endpoints:\n{policy}"
    );
    assert!(
        !policy.contains("\n(allow network-outbound)\n"),
        "policy should not include blanket outbound allowance when managed network is active without proxy endpoints:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network-outbound (remote ip \"*:53\"))"),
        "policy should stay fail-closed for DNS when no proxy endpoints are available:\n{policy}"
    );
}

#[test]
fn create_seatbelt_args_allowlists_unix_socket_paths() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::new_read_only_policy(),
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![43128],
            has_proxy_config: true,
            allow_local_binding: false,
            unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
                allowed: vec![absolute_path("/tmp/example.sock")],
            },
        },
    );

    assert!(
        policy.contains("(allow system-socket (socket-domain AF_UNIX))"),
        "policy should allow AF_UNIX socket creation for configured unix sockets:\n{policy}"
    );
    assert!(
        policy.contains(
            "(allow network-bind (local unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))"
        ),
        "policy should allow binding explicitly configured unix sockets:\n{policy}"
    );
    assert!(
        policy.contains(
            "(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))"
        ),
        "policy should allow connecting to explicitly configured unix sockets:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network* (subpath"),
        "policy should no longer use the generic subpath unix-socket rules:\n{policy}"
    );
}

#[test]
fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() {
    let cwd = TempDir::new().expect("temp cwd");
    let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
        &SandboxPolicy::new_read_only_policy(),
        cwd.path(),
    );
    let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")];
    let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
        command: vec!["/usr/bin/true".to_string()],
        file_system_sandbox_policy: &file_system_policy,
        network_sandbox_policy: NetworkSandboxPolicy::Restricted,
        sandbox_policy_cwd: cwd.path(),
        enforce_managed_network: false,
        network: None,
        extra_allow_unix_sockets: &extra_allow_unix_sockets,
    });
    let policy = seatbelt_policy_arg(&args);

    assert!(
        policy.contains("(allow system-socket (socket-domain AF_UNIX))"),
        "policy should allow AF_UNIX when explicit socket paths are requested:\n{policy}"
    );
    assert!(
        policy.contains(
            "(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))"
        ),
        "policy should allow outbound AF_UNIX traffic for explicit socket paths:\n{policy}"
    );
    let expected_socket_root = normalize_path_for_sandbox(Path::new("/tmp/codex-browser-use"))
        .expect("socket root should normalize")
        .to_string_lossy()
        .into_owned();
    assert!(
        args.iter()
            .any(|arg| arg == &format!("-DUNIX_SOCKET_PATH_0={expected_socket_root}")),
        "seatbelt args should pass the configured socket root as a sandbox param: {args:?}"
    );
}

#[tokio::test]
async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> anyhow::Result<()> {
    let cwd = TempDir::new().expect("temp cwd");
    let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
        &SandboxPolicy::new_read_only_policy(),
        cwd.path(),
    );
    let network_socket = "/tmp/codex-proxy-use";
    let explicit_socket = "/tmp/codex-browser-use";
    let mut network_config = NetworkProxyConfig::default();
    network_config.network.enabled = true;
    network_config.network.mode = NetworkMode::Full;
    network_config
        .network
        .set_allow_unix_sockets(vec![network_socket.to_string()]);
    let state = build_config_state(network_config, NetworkProxyConstraints::default())?;
    let network_proxy = NetworkProxy::builder()
        .state(Arc::new(NetworkProxyState::with_reloader(
            state,
            Arc::new(TestConfigReloader),
        )))
        .managed_by_codex(/*managed_by_codex*/ false)
        .build()
        .await?;
    let extra_allow_unix_sockets = vec![absolute_path(explicit_socket)];

    let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
        command: vec!["/usr/bin/true".to_string()],
        file_system_sandbox_policy: &file_system_policy,
        network_sandbox_policy: NetworkSandboxPolicy::Restricted,
        sandbox_policy_cwd: cwd.path(),
        enforce_managed_network: false,
        network: Some(&network_proxy),
        extra_allow_unix_sockets: &extra_allow_unix_sockets,
    });

    let expected_explicit_socket = normalize_path_for_sandbox(Path::new(explicit_socket))
        .expect("explicit socket root should normalize");
    let expected_network_socket = normalize_path_for_sandbox(Path::new(network_socket))
        .expect("network socket root should normalize");
    let unix_socket_definitions = args
        .iter()
        .filter(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_"))
        .cloned()
        .collect::<Vec<_>>();
    assert_eq!(
        unix_socket_definitions,
        vec![
            format!(
                "-DUNIX_SOCKET_PATH_0={}",
                expected_explicit_socket.display()
            ),
            format!("-DUNIX_SOCKET_PATH_1={}", expected_network_socket.display()),
        ],
        "seatbelt args should include both explicit and network proxy socket roots: {args:?}"
    );
    Ok(())
}

#[test]
fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths() {
    let cwd = TempDir::new().expect("temp cwd");
    let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
        &SandboxPolicy::new_read_only_policy(),
        cwd.path(),
    );
    let extra_allow_unix_sockets = vec![absolute_path("/tmp/codex-browser-use")];
    let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
        command: vec!["/usr/bin/true".to_string()],
        file_system_sandbox_policy: &file_system_policy,
        network_sandbox_policy: NetworkSandboxPolicy::Enabled,
        sandbox_policy_cwd: cwd.path(),
        enforce_managed_network: false,
        network: None,
        extra_allow_unix_sockets: &extra_allow_unix_sockets,
    });
    let policy = seatbelt_policy_arg(&args);

    assert!(
        policy.contains("(allow network-outbound)\n"),
        "policy should preserve full outbound network access:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-inbound)\n"),
        "policy should preserve full inbound network access:\n{policy}"
    );
    assert!(
        policy.contains(
            "(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))"
        ),
        "policy should still allow outbound AF_UNIX traffic for explicit socket paths:\n{policy}"
    );
}

#[test]
fn unix_socket_policy_non_empty_output_is_newline_terminated() {
    let allowlist_policy = unix_socket_policy(&ProxyPolicyInputs {
        unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
            allowed: vec![absolute_path("/tmp/example.sock")],
        },
        ..ProxyPolicyInputs::default()
    });
    assert!(
        allowlist_policy.ends_with('\n'),
        "allowlist unix socket policy should end with a newline:\n{allowlist_policy}"
    );

    let allow_all_policy = unix_socket_policy(&ProxyPolicyInputs {
        unix_domain_socket_policy: UnixDomainSocketPolicy::AllowAll,
        ..ProxyPolicyInputs::default()
    });
    assert!(
        allow_all_policy.ends_with('\n'),
        "allow-all unix socket policy should end with a newline:\n{allow_all_policy}"
    );
}

#[test]
fn unix_socket_dir_params_use_stable_param_names() {
    let params = unix_socket_dir_params(&ProxyPolicyInputs {
        unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
            allowed: vec![
                absolute_path("/tmp/b.sock"),
                absolute_path("/tmp/a.sock"),
                absolute_path("/tmp/a.sock"),
            ],
        },
        ..ProxyPolicyInputs::default()
    });

    assert_eq!(
        params,
        vec![
            (
                "UNIX_SOCKET_PATH_0".to_string(),
                PathBuf::from("/tmp/a.sock")
            ),
            (
                "UNIX_SOCKET_PATH_1".to_string(),
                PathBuf::from("/tmp/b.sock")
            ),
        ]
    );
}

#[test]
fn normalize_path_for_sandbox_rejects_relative_paths() {
    assert_eq!(normalize_path_for_sandbox(Path::new("relative.sock")), None);
}

#[test]
fn create_seatbelt_args_allows_all_unix_sockets_when_enabled() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::new_read_only_policy(),
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![43128],
            has_proxy_config: true,
            allow_local_binding: false,
            unix_domain_socket_policy: UnixDomainSocketPolicy::AllowAll,
        },
    );

    assert!(
        policy.contains("(allow system-socket (socket-domain AF_UNIX))"),
        "policy should allow AF_UNIX socket creation when unix sockets are enabled:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-bind (local unix-socket))"),
        "policy should allow binding unix sockets when enabled:\n{policy}"
    );
    assert!(
        policy.contains("(allow network-outbound (remote unix-socket))"),
        "policy should allow connecting to unix sockets when enabled:\n{policy}"
    );
    assert!(
        !policy.contains("(allow network* (subpath"),
        "policy should no longer use the generic subpath unix-socket rules:\n{policy}"
    );
}

#[test]
fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() {
    let policy = dynamic_network_policy(
        &SandboxPolicy::WorkspaceWrite {
            writable_roots: vec![],
            network_access: true,
            exclude_tmpdir_env_var: false,
            exclude_slash_tmp: false,
        },
        /*enforce_managed_network*/ false,
        &ProxyPolicyInputs {
            ports: vec![43128],
            has_proxy_config: true,
            allow_local_binding: false,
            ..ProxyPolicyInputs::default()
        },
    );

    assert!(
        policy.contains("(allow network-outbound (remote ip \"localhost:43128\"))"),
        "expected proxy endpoint allow rule in policy:\n{policy}"
    );
    assert!(
        !policy.contains("\n(allow network-outbound)\n"),
        "policy should not include blanket outbound allowance when proxy is configured:\n{policy}"
    );
    assert!(
        !policy.contains("\n(allow network-inbound)\n"),
        "policy should not include blanket inbound allowance when proxy is configured:\n{policy}"
    );
}

#[test]
fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() {
    // Create a temporary workspace with two writable roots: one containing
    // top-level workspace metadata paths and one without them.
    let tmp = TempDir::new().expect("tempdir");
    let PopulatedTmp {
        vulnerable_root,
        vulnerable_root_canonical,
        dot_git_canonical,
        dot_agents_canonical: _,
        dot_codex_canonical,
        empty_root,
        empty_root_canonical,
    } = populate_tmpdir(tmp.path());
    let cwd = tmp.path().join("cwd");
    fs::create_dir_all(&cwd).expect("create cwd");

    // Build a policy that only includes the two test roots as writable and
    // does not automatically include defaults TMPDIR or /tmp.
    let policy = SandboxPolicy::WorkspaceWrite {
        writable_roots: vec![vulnerable_root, empty_root]
            .into_iter()
            .map(|p| p.try_into().unwrap())
            .collect(),
        network_access: false,
        exclude_tmpdir_env_var: true,
        exclude_slash_tmp: true,
    };

    // Create the Seatbelt command to wrap a shell command that tries to
    // write to .codex/config.toml in the vulnerable root.
    let shell_command: Vec<String> = [
        "bash",
        "-c",
        "echo 'sandbox_mode = \"danger-full-access\"' > \"$1\"",
        "bash",
        dot_codex_canonical
            .join("config.toml")
            .to_string_lossy()
            .as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let args = create_seatbelt_command_args_for_legacy_policy(
        shell_command.clone(),
        &policy,
        &cwd,
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );

    let policy_text = seatbelt_policy_arg(&args);
    assert!(
        policy_text.contains("(require-all (subpath (param \"WRITABLE_ROOT_0\"))"),
        "expected cwd writable root to carry protected carveouts:\n{policy_text}",
    );
    assert!(
        policy_text.contains("WRITABLE_ROOT_0_EXCLUDED_0"),
        "expected cwd metadata carveouts in policy:\n{policy_text}",
    );
    assert!(
        policy_text.contains("WRITABLE_ROOT_0_EXCLUDED_1")
            && policy_text.contains("WRITABLE_ROOT_0_EXCLUDED_2"),
        "expected symbolic cwd .git/.agents carveouts in policy:\n{policy_text}",
    );
    assert!(
        policy_text.contains("WRITABLE_ROOT_1_EXCLUDED_0")
            && policy_text.contains("WRITABLE_ROOT_1_EXCLUDED_1"),
        "expected explicit writable root .git/.codex carveouts in policy:\n{policy_text}",
    );
    assert!(
        policy_text.contains(&seatbelt_protected_metadata_name_requirements(
            &cwd.canonicalize().expect("canonicalize cwd")
        )),
        "expected cwd metadata protection regex requirements in policy:\n{policy_text}",
    );
    assert!(
        policy_text.contains(&seatbelt_protected_metadata_name_requirements(
            &vulnerable_root_canonical
        )),
        "expected populated root metadata protection regex requirements in policy:\n{policy_text}",
    );
    assert!(
        policy_text.contains(&seatbelt_protected_metadata_name_requirements(
            &empty_root_canonical
        )),
        "expected empty root metadata protection regex requirements in policy:\n{policy_text}",
    );

    let expected_definitions = [
        format!(
            "-DWRITABLE_ROOT_0={}",
            cwd.canonicalize()
                .expect("canonicalize cwd")
                .to_string_lossy()
        ),
        format!(
            "-DWRITABLE_ROOT_0_EXCLUDED_0={}",
            cwd.canonicalize()
                .expect("canonicalize cwd")
                .join(".codex")
                .display()
        ),
        format!(
            "-DWRITABLE_ROOT_0_EXCLUDED_1={}",
            cwd.canonicalize()
                .expect("canonicalize cwd")
                .join(".git")
                .display()
        ),
        format!(
            "-DWRITABLE_ROOT_0_EXCLUDED_2={}",
            cwd.canonicalize()
                .expect("canonicalize cwd")
                .join(".agents")
                .display()
        ),
        format!(
            "-DWRITABLE_ROOT_1={}",
            vulnerable_root_canonical.to_string_lossy()
        ),
        format!(
            "-DWRITABLE_ROOT_1_EXCLUDED_0={}",
            dot_git_canonical.to_string_lossy()
        ),
        format!(
            "-DWRITABLE_ROOT_1_EXCLUDED_1={}",
            dot_codex_canonical.to_string_lossy()
        ),
        format!(
            "-DWRITABLE_ROOT_2={}",
            empty_root_canonical.to_string_lossy()
        ),
    ];
    let writable_definitions: Vec<String> = args
        .iter()
        .filter(|arg| arg.starts_with("-DWRITABLE_ROOT_"))
        .cloned()
        .collect();
    assert_eq!(
        writable_definitions, expected_definitions,
        "unexpected writable-root parameter definitions in {args:#?}"
    );
    for (key, value) in macos_dir_params() {
        let expected_definition = format!("-D{key}={}", value.to_string_lossy());
        assert!(
            args.contains(&expected_definition),
            "expected definition arg `{expected_definition}` in {args:#?}"
        );
    }

    let command_index = args
        .iter()
        .position(|arg| arg == "--")
        .expect("seatbelt args should include command separator");
    assert_eq!(args[command_index + 1..], shell_command);

    // Verify that .codex/config.toml cannot be modified under the generated
    // Seatbelt policy.
    let config_toml = dot_codex_canonical.join("config.toml");
    let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
        .args(&args)
        .current_dir(&cwd)
        .output()
        .expect("execute seatbelt command");
    assert_eq!(
        "sandbox_mode = \"read-only\"\n",
        String::from_utf8_lossy(&fs::read(&config_toml).expect("read config.toml")),
        "config.toml should contain its original contents because it should not have been modified"
    );
    assert!(
        !output.status.success(),
        "command to write {} should fail under seatbelt",
        &config_toml.display()
    );
    assert_seatbelt_denied(&output.stderr, &config_toml);

    // Create a similar Seatbelt command that tries to write to a file in
    // the .git folder, which should also be blocked.
    let pre_commit_hook = dot_git_canonical.join("hooks").join("pre-commit");
    let shell_command_git: Vec<String> = [
        "bash",
        "-c",
        "echo 'pwned!' > \"$1\"",
        "bash",
        pre_commit_hook.to_string_lossy().as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let write_hooks_file_args = create_seatbelt_command_args_for_legacy_policy(
        shell_command_git,
        &policy,
        &cwd,
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );
    let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
        .args(&write_hooks_file_args)
        .current_dir(&cwd)
        .output()
        .expect("execute seatbelt command");
    assert!(
        !fs::exists(&pre_commit_hook).expect("exists pre-commit hook"),
        "{} should not exist because it should not have been created",
        pre_commit_hook.display()
    );
    assert!(
        !output.status.success(),
        "command to write {} should fail under seatbelt",
        &pre_commit_hook.display()
    );
    assert_seatbelt_denied(&output.stderr, &pre_commit_hook);

    // Verify that writing a file to the folder containing .git and .codex is allowed.
    let allowed_file = vulnerable_root_canonical.join("allowed.txt");
    let shell_command_allowed: Vec<String> = [
        "bash",
        "-c",
        "echo 'this is allowed' > \"$1\"",
        "bash",
        allowed_file.to_string_lossy().as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let write_allowed_file_args = create_seatbelt_command_args_for_legacy_policy(
        shell_command_allowed,
        &policy,
        &cwd,
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );
    let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
        .args(&write_allowed_file_args)
        .current_dir(&cwd)
        .output()
        .expect("execute seatbelt command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    if !output.status.success()
        && stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted")
    {
        return;
    }
    assert!(
        output.status.success(),
        "command to write {} should succeed under seatbelt",
        &allowed_file.display()
    );
    assert_eq!(
        "this is allowed\n",
        String::from_utf8_lossy(&fs::read(&allowed_file).expect("read allowed.txt")),
        "{} should contain the written text",
        allowed_file.display()
    );
}

#[test]
fn create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_regex() {
    let tmp = TempDir::new().expect("tempdir");
    let repo_root = tmp.path().join("repo");
    fs::create_dir_all(&repo_root).expect("create repo root");

    Command::new("git")
        .arg("init")
        .arg(".")
        .current_dir(&repo_root)
        .output()
        .expect("git init .");

    let dot_codex = repo_root.join(".codex");
    let config_toml = dot_codex.join("config.toml");
    let policy = SandboxPolicy::WorkspaceWrite {
        writable_roots: vec![repo_root.as_path().try_into().expect("absolute repo root")],
        network_access: false,
        exclude_tmpdir_env_var: true,
        exclude_slash_tmp: true,
    };

    let shell_command: Vec<String> = [
        "bash",
        "-c",
        "mkdir -p \"$1\" && echo 'sandbox_mode = \"danger-full-access\"' > \"$2\"",
        "bash",
        dot_codex.to_string_lossy().as_ref(),
        config_toml.to_string_lossy().as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let args = create_seatbelt_command_args_for_legacy_policy(
        shell_command,
        &policy,
        repo_root.as_path(),
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );

    let policy_text = seatbelt_policy_arg(&args);
    assert!(
        policy_text.contains(&seatbelt_protected_metadata_name_requirements(
            &repo_root.canonicalize().expect("canonicalize repo root")
        )),
        "expected metadata protection regex requirements in policy:\n{policy_text}"
    );
}

#[test]
fn create_seatbelt_args_with_read_only_git_pointer_file() {
    let tmp = TempDir::new().expect("tempdir");
    let worktree_root = tmp.path().join("worktree_root");
    fs::create_dir_all(&worktree_root).expect("create worktree_root");
    let gitdir = worktree_root.join("actual-gitdir");
    fs::create_dir_all(&gitdir).expect("create gitdir");
    let gitdir_config = gitdir.join("config");
    let gitdir_config_contents = "[core]\n";
    fs::write(&gitdir_config, gitdir_config_contents).expect("write gitdir config");

    let dot_git = worktree_root.join(".git");
    let dot_git_contents = format!("gitdir: {}\n", gitdir.to_string_lossy());
    fs::write(&dot_git, &dot_git_contents).expect("write .git pointer");

    let cwd = tmp.path().join("cwd");
    fs::create_dir_all(&cwd).expect("create cwd");

    let policy = SandboxPolicy::WorkspaceWrite {
        writable_roots: vec![worktree_root.try_into().expect("worktree_root is absolute")],
        network_access: false,
        exclude_tmpdir_env_var: true,
        exclude_slash_tmp: true,
    };

    let shell_command: Vec<String> = [
        "bash",
        "-c",
        "echo 'pwned!' > \"$1\"",
        "bash",
        dot_git.to_string_lossy().as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let args = create_seatbelt_command_args_for_legacy_policy(
        shell_command,
        &policy,
        &cwd,
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );

    let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
        .args(&args)
        .current_dir(&cwd)
        .output()
        .expect("execute seatbelt command");

    assert_eq!(
        dot_git_contents,
        String::from_utf8_lossy(&fs::read(&dot_git).expect("read .git pointer")),
        ".git pointer file should not be modified under seatbelt"
    );
    assert!(
        !output.status.success(),
        "command to write {} should fail under seatbelt",
        dot_git.display()
    );
    assert_seatbelt_denied(&output.stderr, &dot_git);

    let shell_command_gitdir: Vec<String> = [
        "bash",
        "-c",
        "echo 'pwned!' > \"$1\"",
        "bash",
        gitdir_config.to_string_lossy().as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let gitdir_args = create_seatbelt_command_args_for_legacy_policy(
        shell_command_gitdir,
        &policy,
        &cwd,
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );
    let output = Command::new(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
        .args(&gitdir_args)
        .current_dir(&cwd)
        .output()
        .expect("execute seatbelt command");

    assert_eq!(
        gitdir_config_contents,
        String::from_utf8_lossy(&fs::read(&gitdir_config).expect("read gitdir config")),
        "gitdir config should contain its original contents because it should not have been modified"
    );
    assert!(
        !output.status.success(),
        "command to write {} should fail under seatbelt",
        gitdir_config.display()
    );
    assert_seatbelt_denied(&output.stderr, &gitdir_config);
}

#[test]
fn create_seatbelt_args_for_cwd_as_git_repo() {
    // Create a temporary workspace with two writable roots: one containing
    // top-level workspace metadata paths and one without them.
    let tmp = TempDir::new().expect("tempdir");
    let PopulatedTmp {
        vulnerable_root,
        vulnerable_root_canonical,
        dot_git_canonical,
        dot_agents_canonical,
        dot_codex_canonical,
        ..
    } = populate_tmpdir(tmp.path());

    // Build a policy that does not specify any writable_roots, but does
    // use the default ones (cwd and TMPDIR) and verifies the protected
    // metadata checks are done properly for cwd.
    let policy = SandboxPolicy::WorkspaceWrite {
        writable_roots: vec![],
        network_access: false,
        exclude_tmpdir_env_var: false,
        exclude_slash_tmp: false,
    };

    let shell_command: Vec<String> = [
        "bash",
        "-c",
        "echo 'sandbox_mode = \"danger-full-access\"' > \"$1\"",
        "bash",
        dot_codex_canonical
            .join("config.toml")
            .to_string_lossy()
            .as_ref(),
    ]
    .iter()
    .map(std::string::ToString::to_string)
    .collect();
    let args = create_seatbelt_command_args_for_legacy_policy(
        shell_command.clone(),
        &policy,
        vulnerable_root.as_path(),
        /*enforce_managed_network*/ false,
        /*network*/ None,
    );

    let slash_tmp = PathBuf::from("/tmp")
        .canonicalize()
        .expect("canonicalize /tmp");
    let policy_text = seatbelt_policy_arg(&args);
    assert!(
        policy_text.contains(&seatbelt_protected_metadata_name_requirements(
            &vulnerable_root_canonical
        )),
        "expected cwd metadata protection regex requirements in policy:\n{policy_text}",
    );
    assert!(
        policy_text.contains(&seatbelt_protected_metadata_name_requirements(&slash_tmp)),
        "expected /tmp metadata protection regex requirements in policy:\n{policy_text}",
    );
    if let Some(tmpdir_env_var) = std::env::var("TMPDIR")
        .ok()
        .map(PathBuf::from)
        .and_then(|p| p.canonicalize().ok())
    {
        assert!(
            policy_text.contains(&seatbelt_protected_metadata_name_requirements(
                &tmpdir_env_var
            )),
            "expected TMPDIR metadata protection regex requirements in policy:\n{policy_text}",
        );
    }

    let expected_root = format!(
        "-DWRITABLE_ROOT_0={}",
        vulnerable_root_canonical.to_string_lossy()
    );
    assert!(
        args.contains(&expected_root),
        "missing {expected_root}: {args:#?}"
    );
    let expected_dot_git = format!(
        "-DWRITABLE_ROOT_0_EXCLUDED_0={}",
        dot_git_canonical.to_string_lossy()
    );
    assert!(
        args.contains(&expected_dot_git),
        "missing {expected_dot_git}: {args:#?}"
    );
    let expected_dot_codex = format!(
        "-DWRITABLE_ROOT_0_EXCLUDED_1={}",
        dot_codex_canonical.to_string_lossy()
    );
    assert!(
        args.contains(&expected_dot_codex),
        "missing {expected_dot_codex}: {args:#?}"
    );
    let unexpected_dot_agents = format!(
        "-DWRITABLE_ROOT_0_EXCLUDED_1={}",
        dot_agents_canonical.to_string_lossy()
    );
    assert!(
        !args.contains(&unexpected_dot_agents),
        "missing .agents should be handled by regex rather than materialized as a path param: {args:#?}"
    );
    let expected_slash_tmp = format!("-DWRITABLE_ROOT_1={}", slash_tmp.to_string_lossy());
    assert!(
        args.contains(&expected_slash_tmp),
        "missing {expected_slash_tmp}: {args:#?}"
    );
    for (key, value) in macos_dir_params() {
        let expected_definition = format!("-D{key}={}", value.to_string_lossy());
        assert!(
            args.contains(&expected_definition),
            "expected definition arg `{expected_definition}` in {args:#?}"
        );
    }

    let command_index = args
        .iter()
        .position(|arg| arg == "--")
        .expect("seatbelt args should include command separator");
    assert_eq!(args[command_index + 1..], shell_command);
}

struct PopulatedTmp {
    /// Path containing protected metadata subfolders.
    /// For the purposes of this test, we consider this a "vulnerable" root
    /// because a bad actor could write to .git/hooks/pre-commit so an
    /// unsuspecting user would run code as privileged the next time they
    /// ran `git commit` themselves, or modified .codex/config.toml to
    /// contain `sandbox_mode = "danger-full-access"` so the agent would
    /// have full privileges the next time it ran in that repo.
    vulnerable_root: PathBuf,
    vulnerable_root_canonical: PathBuf,
    dot_git_canonical: PathBuf,
    dot_agents_canonical: PathBuf,
    dot_codex_canonical: PathBuf,

    /// Path without protected metadata subfolders.
    empty_root: PathBuf,
    /// Canonicalized version of `empty_root`.
    empty_root_canonical: PathBuf,
}

fn populate_tmpdir(tmp: &Path) -> PopulatedTmp {
    let vulnerable_root = tmp.join("vulnerable_root");
    fs::create_dir_all(&vulnerable_root).expect("create vulnerable_root");

    // TODO(mbolin): Should also support the case where `.git` is a file
    // with a gitdir: ... line.
    Command::new("git")
        .arg("init")
        .arg(".")
        .current_dir(&vulnerable_root)
        .output()
        .expect("git init .");

    fs::create_dir_all(vulnerable_root.join(".codex")).expect("create .codex");
    fs::write(
        vulnerable_root.join(".codex").join("config.toml"),
        "sandbox_mode = \"read-only\"\n",
    )
    .expect("write .codex/config.toml");

    let empty_root = tmp.join("empty_root");
    fs::create_dir_all(&empty_root).expect("create empty_root");

    // Ensure we have canonical paths for -D parameter matching.
    let vulnerable_root_canonical = vulnerable_root
        .canonicalize()
        .expect("canonicalize vulnerable_root");
    let dot_git_canonical = vulnerable_root_canonical.join(".git");
    let dot_agents_canonical = vulnerable_root_canonical.join(".agents");
    let dot_codex_canonical = vulnerable_root_canonical.join(".codex");
    let empty_root_canonical = empty_root.canonicalize().expect("canonicalize empty_root");
    PopulatedTmp {
        vulnerable_root,
        vulnerable_root_canonical,
        dot_git_canonical,
        dot_agents_canonical,
        dot_codex_canonical,
        empty_root,
        empty_root_canonical,
    }
}
