//! Windows sandbox integration via WSL.
//!
//! Sandboxed Windows terminal commands are routed through WSL and then executed
//! under Bubblewrap inside Linux. Projects may be opened either from native
//! Windows paths (`C:\Users\...`) or WSL UNC paths
//! (`\\wsl.localhost\Ubuntu\home\...`). Native drive-letter paths are
//! translated into the distro's filesystem view with `wslpath` (falling back
//! to the conventional `/mnt/<drive>/...` mapping if that fails) and use the
//! user's default WSL distro unless a WSL UNC path in the request pins a
//! specific distro.
//!
//! Errors fall into two classes the agent treats differently:
//!
//! * **Environment unavailable** — WSL missing or failing to start, no
//!   usable `bwrap`, or the probe/path-resolution stdout protocol breaking
//!   down. These are returned as a [`WslSandboxUnavailable`] (whose `Display`
//!   carries
//!   [`WSL_SANDBOX_UNAVAILABLE_PREFIX`](crate::WSL_SANDBOX_UNAVAILABLE_PREFIX)),
//!   so the agent recognizes them *by type* and offers the same
//!   retry / run-unsandboxed fallback it offers on Linux, rather than matching
//!   on message text.
//! * **Bad request** — a specific path that doesn't exist or can't be mapped
//!   into WSL, or a request mixing distros. These are ordinary `anyhow` errors
//!   *without* [`WslSandboxUnavailable`], and are reported back to the model,
//!   which can fix the request and retry.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};

use smol::process::{Command, Stdio};

use anyhow::{Context as _, Result, bail, ensure};

use crate::WSL_SANDBOX_UNAVAILABLE_PREFIX;

/// Per-command relaxations of the WSL/Bubblewrap sandbox. Windows can only
/// toggle network access wholesale (no loopback-proxy confinement yet), so this
/// is a plain bool rather than the richer cross-platform [`crate::SandboxNetPolicy`].
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct SandboxPermissions {
    pub(crate) allow_network: bool,
    pub(crate) allow_fs_write: bool,
}

pub(crate) struct WritablePath {
    pub(crate) requested: PathBuf,
    pub(crate) canonical: PathBuf,
}

/// Exit code the environment probe script uses to signal that `bwrap` is not
/// installed, distinguishing that from WSL itself failing to start a shell.
/// Chosen to be unlikely to collide with `wsl.exe`'s own failure codes.
const BWRAP_MISSING_EXIT_CODE: i32 = 41;

/// Exit code the environment probe script uses to signal that `bwrap` is
/// installed but failed the sandbox smoke test — typically because the
/// distro restricts unprivileged user namespaces (e.g. Ubuntu 24.04's
/// default AppArmor policy), which every namespace flag we pass depends on.
const BWRAP_UNUSABLE_EXIT_CODE: i32 = 42;

/// Prefix of the probe script's single result line, so it can be picked out
/// of any stdout noise printed by the login shell's profile scripts.
const PROBE_RESULT_PREFIX: &str = "zed-wsl-probe:";

/// Prefix of the helper-provisioning script's single result line (the absolute
/// in-WSL path of the Linux `zed` to run as the sandbox helper), picked out of
/// login-shell stdout noise just like [`PROBE_RESULT_PREFIX`].
const HELPER_RESULT_PREFIX: &str = "zed-wsl-helper:";

/// Ensures a Linux `zed` matching the running release is available inside WSL to
/// act as the sandbox helper (`--wsl-sandbox-helper`), and prints its absolute
/// in-WSL path on a [`HELPER_RESULT_PREFIX`] line. `$1` is the release channel,
/// `$2` the version (`latest` for dev builds, which have no matching release and
/// so track the latest nightly); both are passed as argv, never interpolated, so
/// a version/channel string can't inject shell.
///
/// Unlike a normal Linux install, this deliberately does **not** consult the WSL
/// `PATH`: inside WSL `zed` typically resolves to the *Windows* `zed.exe` via
/// interop, which is not a Linux binary and so can't be the helper. It also does
/// not use the public install script (`install.sh`), which puts `zed` on the
/// user's `PATH` and writes desktop entries we don't want. Instead the Windows
/// side resolves the exact channel+version (see `wsl_zed_release`) and this
/// script downloads that release's Linux tarball straight from
/// `cloud.zed.dev/releases` and unpacks it into a private, off-`PATH` location
/// (`~/.local/libexec/zed/<channel>`, the conventional spot for executables run
/// by other programs rather than directly by the user). One managed copy per
/// channel is kept, tracked by a marker file so an exact channel+version match
/// is reused rather than re-downloaded. The floating `latest` version (dev
/// builds) is the exception: it always re-downloads so it tracks the newest
/// nightly rather than pinning to the first copy fetched.
///
/// We ship no `zed` (nor `bwrap`) into WSL ourselves; this downloads `zed` on
/// demand. A missing `curl`/`wget` (or a failed download) is a hard error the
/// caller surfaces to the user, exactly like a missing `bwrap`.
const HELPER_PROVISION_SCRIPT: &str = r#"
set -eu
channel="$1"
version="$2"
dest="$HOME/.local/libexec/zed/$channel"
marker="$dest/.zed-wsl-helper-version"
want="$channel $version"

# Reuse an exact, already-installed channel+version — but never for the floating
# "latest" tag (dev builds), which must always re-fetch so they track the most
# recent nightly instead of pinning to whatever was downloaded first.
if [ "$version" != "latest" ] && [ "$(cat "$marker" 2>/dev/null || true)" = "$want" ]; then
    helper=$(find "$dest" -type f -path '*/libexec/zed-editor' -print 2>/dev/null | head -n 1 || true)
    if [ -n "$helper" ] && [ -x "$helper" ]; then
        printf 'zed-wsl-helper: %s\n' "$helper"
        exit 0
    fi
fi

arch=$(uname -m)
case "$arch" in
    x86_64 | amd64) arch="x86_64" ;;
    aarch64 | arm64) arch="aarch64" ;;
    *) echo "unsupported WSL architecture for the zed sandbox helper: $arch" >&2; exit 1 ;;
esac
url="https://cloud.zed.dev/releases/$channel/$version/download?asset=zed&arch=$arch&os=linux&source=zed-wsl-sandbox"

tmp=$(mktemp -d "${TMPDIR:-/tmp}/zed-wsl-helper-XXXXXX")
trap 'rm -rf "$tmp"' EXIT
tarball="$tmp/zed.tar.gz"
if command -v curl >/dev/null 2>&1; then
    curl -fL "$url" -o "$tarball"
elif command -v wget >/dev/null 2>&1; then
    wget -O "$tarball" "$url"
else
    echo 'neither curl nor wget is available in WSL to download zed' >&2
    exit 1
fi

mkdir -p "$tmp/unpacked"
tar -xzf "$tarball" -C "$tmp/unpacked"
helper_src=$(find "$tmp/unpacked" -type f -path '*/libexec/zed-editor' -print 2>/dev/null | head -n 1 || true)
if [ -z "$helper_src" ]; then
    echo 'the downloaded zed tarball did not contain a libexec/zed-editor binary' >&2
    exit 1
fi
app=$(dirname "$(dirname "$helper_src")")

# Install atomically: stage the unpacked app next to the destination on the same
# filesystem, then swap it into place so a concurrent run never sees (or execs) a
# partially-written install. The whole app dir is kept so the binary's bundled
# libraries ($ORIGIN/../lib) remain alongside it.
mkdir -p "$(dirname "$dest")"
rm -rf "$dest.new" "$dest.old"
cp -a "$app" "$dest.new"
if [ -e "$dest" ]; then mv "$dest" "$dest.old"; fi
mv "$dest.new" "$dest"
rm -rf "$dest.old"
printf '%s' "$want" > "$marker"

helper=$(find "$dest" -type f -path '*/libexec/zed-editor' -print 2>/dev/null | head -n 1 || true)
if [ -z "$helper" ] || [ ! -x "$helper" ]; then
    echo "the installed zed sandbox helper is missing or not executable under $dest" >&2
    exit 1
fi
printf 'zed-wsl-helper: %s\n' "$helper"
exit 0
"#;

/// Marks a failure of the Windows WSL sandboxing *environment*: WSL is missing
/// or won't start, there's no usable `bwrap`, or the probe / path-resolution
/// stdout protocol broke down. Returned as the root of the `anyhow::Error` so
/// callers classify it by type ([`anyhow::Error::downcast_ref`]) instead of by
/// matching message text. Per-request failures (a missing writable path, paths
/// mixing distros) are ordinary `anyhow` errors *without* this type, so they
/// never match — the agent returns those to the model rather than offering to
/// run unsandboxed.
#[derive(Debug, Clone)]
pub struct WslSandboxUnavailable(String);

impl WslSandboxUnavailable {
    /// Build an environment-unavailable error from a human-readable reason
    /// (without the [`WSL_SANDBOX_UNAVAILABLE_PREFIX`], which `Display` adds).
    pub fn new(message: impl Into<String>) -> Self {
        Self(message.into())
    }

    /// The reason, without the leading [`WSL_SANDBOX_UNAVAILABLE_PREFIX`].
    #[cfg(test)]
    pub fn message(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for WslSandboxUnavailable {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{WSL_SANDBOX_UNAVAILABLE_PREFIX}: {}", self.0)
    }
}

impl std::error::Error for WslSandboxUnavailable {}

/// Shorthand for an [`anyhow::Error`] wrapping a [`WslSandboxUnavailable`].
fn unavailable(message: impl Into<String>) -> anyhow::Error {
    anyhow::Error::new(WslSandboxUnavailable::new(message))
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct WslPath {
    distro: Option<String>,
    path: String,
}

/// A path mapped for use inside WSL.
///
/// WSL UNC and WSL-absolute paths can be mapped structurally up front. Native
/// drive-letter paths depend on the distro's automount configuration
/// (`/etc/wsl.conf` can move the `/mnt` root), so they are translated with
/// `wslpath` inside the distro — but a distro can only be chosen after every
/// path has been parsed (WSL UNC paths pin one), hence this two-stage shape:
/// parse structurally first, then resolve native paths via [`resolve_paths`]
/// once the distro is known.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum PathMapping {
    Wsl(WslPath),
    NativeDrive {
        /// The `\\?\`-stripped, forward-slashed form that `wslpath -u`
        /// accepts (`wslpath` is a Linux binary and doesn't understand
        /// backslash separators).
        windows_path: String,
        /// The conventional `/mnt/<drive>/...` mapping, used when `wslpath`
        /// translation fails.
        fallback: WslPath,
    },
}

impl PathMapping {
    fn distro(&self) -> Option<&str> {
        match self {
            PathMapping::Wsl(path) => path.distro.as_deref(),
            PathMapping::NativeDrive { .. } => None,
        }
    }
}

/// The result of resolving a write grant inside WSL: its canonical target, plus
/// whether that target lives on a Windows-hosted (DrvFs) filesystem whose
/// sandbox-integrity guarantees are weaker than the distro's native filesystem.
pub struct ResolvedGrant {
    pub canonical: PathBuf,
    pub on_windows_fs: bool,
}

/// Whether `path` names a location on a Windows drive (reached inside WSL via
/// DrvFs), as opposed to a WSL-native (ext4) path. This is a cheap *structural*
/// classification of the path shape (a drive-letter/verbatim path maps to a
/// `NativeDrive`; WSL UNC and Linux-absolute paths are distro-native). It is
/// used to decide whether to warn about the weaker DrvFs sandbox guarantees for
/// default project paths without a WSL round-trip. For explicit grants the
/// authoritative filesystem class comes from the in-WSL resolver instead (see
/// [`resolve_canonical_for_grant`]).
pub fn path_is_on_windows_drive(path: &Path) -> bool {
    matches!(
        path_to_wsl_allowing_missing(path),
        Ok(PathMapping::NativeDrive { .. })
    )
}

/// Whether WSL is structurally present: at least one distro is registered
/// under `HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss`. A cheap
/// registry read (no `wsl.exe` spawn), suitable for UI gating decisions like
/// suppressing sandbox-integrity warnings on machines where no WSL sandbox
/// could ever be constructed. It is *not* an availability guarantee: a
/// registered distro can still fail the real environment probe (no `bwrap`,
/// unhealthy WSL service, …), so enforcement paths must keep relying on
/// [`probe_environment`]'s in-WSL round-trip.
pub fn wsl_distro_registered() -> bool {
    windows_registry::CURRENT_USER
        .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss")
        .and_then(|lxss_key| {
            Ok(lxss_key.keys()?.any(|key| {
                lxss_key
                    .open(&key)
                    .and_then(|distro| distro.get_string("DistributionName"))
                    .is_ok()
            }))
        })
        .unwrap_or(false)
}

/// Resolve a requested write grant to the path we persist for it, in
/// **platform-native** form: a Windows drive path stays `C:\...` (NTFS) and a
/// WSL path stays a Linux-absolute `/...`. In both cases the path is resolved to
/// its symlink/junction-free canonical target (so a granted junction or symlink
/// is followed and shown/stored as its real target), and its backing filesystem
/// is classified.
///
/// Crucially, *each side canonicalizes what it is authoritative about*:
///
/// * A **Windows drive path** is canonicalized on the **Windows host**. A
///   junction (or symlink) on a Windows drive is a Windows filesystem object;
///   WSL's DrvFs surfaces it as a plain symlink but cannot reliably resolve it
///   (the `/proc/self/fd` readback that follows an ext4 symlink to its real
///   target does not follow a DrvFs junction). Resolving it WSL-side would
///   therefore store the junction unresolved and then reject it at enforcement,
///   where [`crate::util::CanonicalPathBuf::from_canonical`] refuses a symlink
///   leaf. `std::fs::canonicalize` follows the junction/symlink chain and
///   confirms the target exists.
/// * A **WSL path** is canonicalized inside **WSL** via the in-distro helper —
///   Windows can't follow ext4 symlinks.
///
/// Either way the persisted canonical is re-verified in the enforcement
/// namespace by `from_canonical` (which opens it `O_NOFOLLOW` and pins the
/// inode), so a component swapped for a symlink/junction after approval still
/// fails closed.
///
/// A local project (the only kind that is sandboxed) has NTFS files, so grants
/// are either those `C:\...` paths or WSL-native scratch locations named with
/// `/...`; `\\wsl.localhost\...` and other shapes are rejected. There is no
/// distro to pin — a sandboxed command runs in exactly one distro (the project's
/// or WSL's default), so a bare Linux path is unambiguous.
pub async fn resolve_canonical_for_grant(
    requested: PathBuf,
    wsl_zed_release: (String, String),
) -> Result<ResolvedGrant> {
    let path_string = requested.to_string_lossy();

    // A Windows drive grant (NTFS) is resolved on the Windows host, which is
    // authoritative about junctions and symlinks (see the doc comment above).
    if parse_native_drive_path(&path_string).is_ok() {
        let canonical = smol::fs::canonicalize(&requested).await.with_context(|| {
            format!(
                "failed to resolve writable path `{}` on the Windows host",
                requested.display()
            )
        })?;
        // `canonicalize` yields a `\\?\` verbatim path; store the plain native
        // spelling so a non-junction grant compares equal to what was requested.
        // A junction resolving off the local drives (onto a UNC/WSL path) is
        // rejected — only a Windows drive path maps to a DrvFs bind.
        let canonical = strip_windows_verbatim_prefix(canonical);
        ensure!(
            parse_native_drive_path(&canonical.to_string_lossy()).is_ok(),
            "cannot grant sandbox write access to `{}`: it resolves to `{}`, \
             which is not a Windows drive path",
            requested.display(),
            canonical.display()
        );
        // A Windows drive is reached inside WSL via DrvFs (weaker guarantees), so
        // a native-drive grant is on a Windows filesystem by definition.
        return Ok(ResolvedGrant {
            canonical,
            on_windows_fs: true,
        });
    }

    // Otherwise the grant must be a Linux-absolute WSL path, which only WSL can
    // canonicalize. `\\wsl.localhost\...` and other shapes are rejected.
    let wsl_path = parse_wsl_absolute_path(&path_string).map_err(|_| {
        anyhow::anyhow!(
            "cannot grant sandbox write access to `{}`: only Windows drive paths \
             (`C:\\...`) and WSL absolute paths (`/...`) are supported",
            requested.display()
        )
    })?;
    let mapping = PathMapping::Wsl(wsl_path);

    let wsl_exe = wsl_exe_path();
    if !wsl_exe.is_file() {
        return Err(unavailable(format!(
            "WSL (`wsl.exe`) was not found at `{}`",
            wsl_exe.display()
        )));
    }

    // Translate + existence-check the path inside WSL, then resolve its
    // symlink-free canonical (and classify its filesystem) via the helper. No
    // distro is pinned — a sandboxed command runs in one distro.
    let translated = resolve_paths(&wsl_exe, None, &[(mapping, "writable path", true)])
        .await?
        .into_iter()
        .next()
        .context("bug: missing resolved writable path")?
        .context("bug: required writable path resolved as missing")?;
    let (channel, version) = wsl_zed_release;
    let helper = ensure_wsl_zed_helper(&wsl_exe, None, &channel, &version).await?;
    let output = run_wsl_command(
        &wsl_exe,
        None,
        [
            "--exec",
            helper.as_str(),
            crate::WSL_SANDBOX_RESOLVE_FLAG,
            translated.as_str(),
        ],
        "resolve a sandbox write grant",
    )
    .await?;
    if !output.status.success() {
        return Err(anyhow::anyhow!(
            "failed to resolve writable path `{}`{}",
            requested.display(),
            command_failure_details(output.status.code(), &output.stderr)
        ));
    }
    let stdout = String::from_utf8(output.stdout)
        .context("WSL sandbox helper returned a non-UTF-8 canonical path")?;
    parse_canonical_grant_output(&stdout)
}

/// Strip the `\\?\` verbatim prefix that `std::fs::canonicalize` prepends to a
/// Windows drive path, leaving `\\?\UNC\...` untouched (it is not a drive path,
/// and is rejected by the caller). We persist and display the plain `C:\...`
/// spelling so a non-junction grant compares equal to the requested path.
fn strip_windows_verbatim_prefix(path: PathBuf) -> PathBuf {
    let text = path.to_string_lossy();
    match text.strip_prefix(r"\\?\") {
        Some(rest) if !rest.starts_with("UNC\\") => PathBuf::from(rest),
        _ => path,
    }
}

/// `on_windows_fs` word emitted by the resolver for a Windows-hosted (DrvFs)
/// canonical target.
const RESOLVE_WINDOWS_FS: &str = "windows-fs";
/// `on_windows_fs` word emitted by the resolver for a distro-native canonical
/// target.
const RESOLVE_NATIVE_FS: &str = "native-fs";

/// Wrap a Linux process invocation so it runs under Bubblewrap inside WSL.
///
/// `program` and `args` must name a Linux executable and Linux argv, not a
/// Windows executable. The caller is expected to convert the model's command
/// into a Linux shell invocation (typically `/bin/sh -c ...`) before calling
/// this function.
///
/// All writable paths, protected paths, and the cwd must be paths that can be
/// mapped into WSL. The cwd and writable paths must exist; protected paths may
/// be missing because Bubblewrap cannot bind a missing source. WSL UNC paths
/// may specify a distro; native drive-letter paths are translated with `wslpath`
/// inside either that distro or the default distro (falling back to
/// `/mnt/<drive>/...` if translation fails).
///
/// `env` is forwarded into the sandboxed command via `bwrap --setenv` rather
/// than being set on the `wsl.exe` process. Windows environment variables
/// don't cross the WSL boundary unless they're listed in `WSLENV`, so without
/// this the command would lose `PAGER` (used to stop `git` from paging into
/// the PTY) and the rest of the project environment. Variables whose Windows
/// values are meaningless or harmful inside Linux are dropped (see
/// [`is_forwardable_env_var`]).
///
/// This function performs up to two `wsl.exe` round-trips (environment probe
/// and path resolution, each cached) plus filesystem stats of WSL UNC paths,
/// any of which can take seconds when the WSL VM is cold (and the stats can
/// stall on a slow `\\wsl.localhost` filesystem). Run it on a background
/// executor, never on the UI thread, and bound it with a timeout — a wedged
/// `wsl.exe` (a real failure mode when the WSL service is unhealthy)
/// otherwise stalls the returned future forever. This crate deliberately has
/// no timer of its own (timers come from the caller's executor so tests stay
/// deterministic); instead it guarantees that dropping the future kills any
/// in-flight `wsl.exe` child, so a caller-side timeout that drops the future
/// also reaps the process. Parameters are owned so the returned future is
/// `Send + 'static`.
pub async fn wrap_invocation<S: std::hash::BuildHasher>(
    program: String,
    args: Vec<String>,
    writable_paths: Vec<WritablePath>,
    protected_paths: Vec<PathBuf>,
    permissions: SandboxPermissions,
    cwd: Option<PathBuf>,
    env: HashMap<String, String, S>,
    // `(release channel, version)` of the Linux `zed` to provision inside WSL as
    // the `--wsl-sandbox-helper` (the version is `latest` for dev builds). When
    // `None`, no helper is used and bwrap is exec'd directly — the legacy path,
    // which binds writable paths by string and so carries the bind-source TOCTOU
    // the helper closes. Callers that can determine the running release should
    // always pass `Some`.
    wsl_zed_release: Option<(String, String)>,
) -> Result<(String, Vec<String>)> {
    // Mapping failures are bad requests (a path that doesn't exist or has a
    // shape WSL can't address), not environment problems, so no
    // `WSL_SANDBOX_UNAVAILABLE_PREFIX` here.
    let cwd_mapping =
        match &cwd {
            Some(cwd) => Some(directory_to_wsl(cwd).with_context(|| {
                format!("failed to map terminal cwd `{}` into WSL", cwd.display())
            })?),
            None => None,
        };

    let writable_requested_mappings = writable_paths
        .iter()
        .map(|path| {
            path_to_wsl(&path.requested).with_context(|| {
                format!(
                    "failed to map writable path `{}` into WSL",
                    path.requested.display()
                )
            })
        })
        .collect::<Result<Vec<_>>>()?;
    let writable_mappings = writable_paths
        .iter()
        .map(|path| {
            path_to_wsl(&path.canonical).with_context(|| {
                format!(
                    "failed to map canonical writable path `{}` into WSL",
                    path.canonical.display()
                )
            })
        })
        .collect::<Result<Vec<_>>>()?;
    let protected_mappings = protected_paths
        .iter()
        .map(|path| {
            path_to_wsl_allowing_missing(path).with_context(|| {
                format!("failed to map protected path `{}` into WSL", path.display())
            })
        })
        .collect::<Result<Vec<_>>>()?;

    let distro = select_distro(
        cwd_mapping.as_ref(),
        writable_requested_mappings
            .iter()
            .chain(writable_mappings.iter())
            .chain(protected_mappings.iter()),
    )?;
    let wsl_exe = wsl_exe_path();
    if !wsl_exe.is_file() {
        return Err(unavailable(format!(
            "WSL (`wsl.exe`) was not found at `{}`",
            wsl_exe.display()
        )));
    }
    let environment = probe_environment(&wsl_exe, distro.as_deref()).await?;

    // Resolve all paths (translating native drive-letter paths with `wslpath`
    // now that the distro is known) in a single WSL round-trip. The cwd and
    // ordinary writable paths must exist; protected paths may be missing because,
    // as with Linux bwrap, a not-yet-created path can't be overlaid.
    let has_cwd = cwd_mapping.is_some();
    let writable_path_count = writable_mappings.len();
    let mut mappings = Vec::with_capacity(writable_mappings.len() + protected_mappings.len() + 1);
    if let Some(mapping) = cwd_mapping {
        mappings.push((mapping, "terminal cwd", true));
    }
    mappings.extend(
        writable_mappings
            .into_iter()
            .map(|mapping| (mapping, "writable path", true)),
    );
    mappings.extend(
        protected_mappings
            .into_iter()
            .map(|mapping| (mapping, "protected path", false)),
    );
    let resolved = resolve_paths(&wsl_exe, distro.as_deref(), &mappings).await?;
    let (cwd, writable_paths, protected_paths) =
        split_resolved_paths(has_cwd, writable_path_count, resolved)?;

    let mut wsl_args = Vec::new();
    if let Some(distro) = distro.as_deref() {
        wsl_args.extend(["-d".to_string(), distro.to_string()]);
    }
    if let Some(cwd) = &cwd {
        wsl_args.extend(["--cd".to_string(), cwd.clone()]);
    }

    // The bwrap *options* (everything before the trailing `-- cmd`): root bind,
    // `/tmp` tmpfs, writable binds, interop blocking, `--setenv`s, `--chdir`,
    // namespace flags. Identical whether or not we route through the helper.
    let bwrap_options = build_bwrap_args(
        &writable_paths,
        &protected_paths,
        permissions,
        cwd.as_deref(),
        environment.mask_interop_dir,
        &env,
    );

    match wsl_zed_release {
        // Preferred path: run the in-WSL `zed` as the sandbox helper, which
        // verifies and pins the persisted canonical writable paths WSL-side,
        // then validates them again after bwrap's mounts.
        Some((channel, version)) => {
            let helper =
                ensure_wsl_zed_helper(&wsl_exe, distro.as_deref(), &channel, &version).await?;
            wsl_args.extend(["--exec".to_string(), helper]);
            // Protocol (decoded by `linux_bubblewrap::decode_wsl_helper_args`):
            //   <flag> <bwrap_path> <n_base> <base...> <n_writable> <writable...> -- <prog> <args>
            wsl_args.push(crate::WSL_SANDBOX_HELPER_FLAG.to_string());
            wsl_args.push(environment.bwrap_path.clone());
            wsl_args.push(bwrap_options.len().to_string());
            wsl_args.extend(bwrap_options);
            wsl_args.push(writable_paths.len().to_string());
            wsl_args.extend(writable_paths.iter().cloned());
            wsl_args.push("--".to_string());
            wsl_args.push(program);
            wsl_args.extend(args);
        }
        // Legacy path: exec bwrap directly (no in-sandbox bind validation). Use
        // the absolute path the probe validated, since `wsl --exec` searches
        // only the default WSL PATH.
        None => {
            wsl_args.extend(["--exec".to_string(), environment.bwrap_path.clone()]);
            wsl_args.extend(bwrap_options);
            wsl_args.push("--".to_string());
            wsl_args.push(program);
            wsl_args.extend(args);
        }
    }

    Ok((wsl_exe.to_string_lossy().into_owned(), wsl_args))
}

fn parse_canonical_grant_output(stdout: &str) -> Result<ResolvedGrant> {
    let mut lines = stdout.lines();
    let distro = lines
        .next()
        .context("WSL sandbox helper omitted the distro name")?;
    let canonical = lines
        .next()
        .context("WSL sandbox helper omitted the canonical path")?;
    let fs_class = lines
        .next()
        .context("WSL sandbox helper omitted the filesystem class")?;
    let on_windows_fs = match fs_class {
        RESOLVE_WINDOWS_FS => true,
        RESOLVE_NATIVE_FS => false,
        _ => bail!("WSL sandbox helper returned an invalid filesystem class: {fs_class:?}"),
    };
    ensure!(
        lines.next().is_none()
            && !distro.is_empty()
            && !distro.contains(['/', '\\'])
            && canonical.starts_with('/'),
        "WSL sandbox helper returned invalid canonical-path output: {stdout:?}"
    );
    // Store the canonical as the Linux path bwrap actually binds. The distro is
    // pinned at enforcement from the *requested* path (see `wrap_invocation`),
    // so it is not re-encoded here: spelling the canonical as
    // `\\wsl.localhost\<distro>\...` would resurface the Windows-style path we
    // avoid and, because it names the same object as the request, show as a
    // spurious redirect in the approval UI.
    Ok(ResolvedGrant {
        canonical: PathBuf::from(canonical),
        on_windows_fs,
    })
}

fn split_resolved_paths(
    has_cwd: bool,
    writable_path_count: usize,
    resolved: Vec<Option<String>>,
) -> Result<(Option<String>, Vec<String>, Vec<String>)> {
    let mut resolved = resolved.into_iter();
    let cwd = if has_cwd {
        Some(
            resolved
                .next()
                .context("bug: missing resolved terminal cwd")?
                .context("bug: required terminal cwd resolved as missing")?,
        )
    } else {
        None
    };

    let mut writable_paths = Vec::with_capacity(writable_path_count);
    for _ in 0..writable_path_count {
        writable_paths.push(
            resolved
                .next()
                .context("bug: missing resolved writable path")?
                .context("bug: required writable path resolved as missing")?,
        );
    }

    Ok((cwd, writable_paths, resolved.flatten().collect()))
}

fn select_distro<'a>(
    cwd: Option<&PathMapping>,
    paths: impl IntoIterator<Item = &'a PathMapping>,
) -> Result<Option<String>> {
    let mut distro = cwd.and_then(|mapping| mapping.distro().map(str::to_string));
    for mapping in paths {
        let Some(path_distro) = mapping.distro() else {
            continue;
        };
        match distro.as_deref() {
            // A bad request, not an environment problem: the model (or
            // project layout) asked for paths spanning two distros, which a
            // single bwrap invocation can't serve.
            Some(distro) => ensure!(
                distro == path_distro,
                "cannot sandbox a command whose paths mix WSL distros `{}` and `{}`",
                distro,
                path_distro
            ),
            None => distro = Some(path_distro.to_string()),
        }
    }
    Ok(distro)
}

/// What [`probe_environment`] learned about a WSL distro.
#[derive(Clone, Debug, Eq, PartialEq)]
struct EnvironmentProbe {
    /// Whether the WSL interop socket directory (`/run/WSL`) exists and so
    /// must (and can) be masked — see [`build_bwrap_args`].
    mask_interop_dir: bool,
    /// Absolute path of the `bwrap` binary the smoke test validated. The real
    /// invocation must exec this exact path: `wsl --exec` searches only the
    /// default WSL PATH, so a bare `bwrap` could miss (or differ from) the
    /// binary the probe's login shell found.
    bwrap_path: String,
}

/// Shell script run by [`probe_environment`]. Resolves `bwrap` to an absolute
/// path (exit [`BWRAP_MISSING_EXIT_CODE`] if absent), rejects setuid-root
/// binaries, then smoke-tests a real minimal sandbox (exit
/// [`BWRAP_UNUSABLE_EXIT_CODE`] on failure) using the same mount and namespace
/// flags as [`build_bwrap_args`] — presence isn't
/// enough, because unprivileged user namespaces can be disabled by the
/// distro's kernel, sysctl, or AppArmor policy (notably Ubuntu 24.04, the
/// current default WSL distro), in which case `bwrap` exists but every
/// sandboxed command would fail. The interop mask is included in the smoke
/// test when `/run/WSL` exists so the exact mount we later perform is
/// exercised too. On success, one [`PROBE_RESULT_PREFIX`]-marked result line
/// reports the interop state and the resolved `bwrap` path.
fn probe_script() -> String {
    format!(
        "bwrap_path=$(command -v bwrap) || exit {BWRAP_MISSING_EXIT_CODE}; \
         if [ -u \"$bwrap_path\" ] && [ \"$(stat -c %u \"$bwrap_path\" 2>/dev/null)\" = 0 ]; then \
         echo 'setuid-root bwrap is not supported' >&2; \
         exit {BWRAP_UNUSABLE_EXIT_CODE}; fi; \
         if [ -d /run/WSL ]; then interop=interop; mask='--tmpfs /run/WSL'; \
         else interop=no-interop; mask=''; fi; \
         \"$bwrap_path\" --ro-bind / / --tmpfs /tmp $mask --dev /dev --proc /proc \
         --unshare-net --unshare-user --unshare-ipc --unshare-uts --unshare-pid \
         --unshare-cgroup-try --die-with-parent -- true >/dev/null \
         || exit {BWRAP_UNUSABLE_EXIT_CODE}; \
         printf '{PROBE_RESULT_PREFIX} %s %s\\n' \"$interop\" \"$bwrap_path\""
    )
}

/// Probe a distro's sandbox environment in one `wsl.exe` round-trip: confirm
/// a shell starts, confirm `bwrap` is installed *and can actually set up an
/// unprivileged sandbox* (see [`probe_script`]), and report whether the
/// interop socket directory exists.
///
/// Successful results are cached per distro for the life of the process —
/// like `linux_bubblewrap::is_available`, the answers can't realistically
/// change while Zed runs. Failures are deliberately *not* cached so a user
/// who installs `bwrap` (or lifts a user-namespace restriction) after seeing
/// the error can retry the command without restarting Zed.
async fn probe_environment(wsl_exe: &Path, distro: Option<&str>) -> Result<EnvironmentProbe> {
    static CACHE: OnceLock<Mutex<HashMap<Option<String>, EnvironmentProbe>>> = OnceLock::new();
    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));

    let key = distro.map(str::to_string);
    if let Some(probe) = cache
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .get(&key)
    {
        return Ok(probe.clone());
    }

    // A login shell (`-lc`) is used so a `bwrap` reachable only through a
    // profile-managed PATH is still found; the resolved absolute path is
    // reported back so the real invocation execs the same binary.
    let script = probe_script();
    let output = run_wsl_command(
        wsl_exe,
        distro,
        ["--exec", "sh", "-lc", &script],
        "probe the sandbox environment",
    )
    .await?;
    if output.status.code() == Some(BWRAP_MISSING_EXIT_CODE) {
        return Err(unavailable(format!(
            "Bubblewrap (`bwrap`) is not installed in {}",
            wsl_distro_label(distro)
        )));
    }
    if output.status.code() == Some(BWRAP_UNUSABLE_EXIT_CODE) {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stderr = stderr.trim();
        return Err(unavailable(format!(
            "Bubblewrap (`bwrap`) is installed in {} but could not set up a sandbox — the \
             distro may restrict unprivileged user namespaces (as Ubuntu 24.04's default \
             AppArmor policy does){}",
            wsl_distro_label(distro),
            if stderr.is_empty() {
                String::new()
            } else {
                format!(": {stderr}")
            }
        )));
    }
    if !output.status.success() {
        return Err(unavailable(format!(
            "failed to start a shell in {}{}",
            wsl_distro_label(distro),
            command_failure_details(output.status.code(), &output.stderr)
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let probe = parse_probe_output(&stdout).map_err(|error| {
        unavailable(format!(
            "unexpected sandbox probe output from {}: {error:#}",
            wsl_distro_label(distro)
        ))
    })?;
    cache
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .insert(key, probe.clone());
    Ok(probe)
}

/// Parse [`probe_script`] output: the last [`PROBE_RESULT_PREFIX`]-marked
/// line wins, so stdout noise from login-shell profile scripts (which runs
/// before the script body) is ignored.
fn parse_probe_output(stdout: &str) -> Result<EnvironmentProbe> {
    let line = stdout
        .lines()
        .rev()
        .find_map(|line| line.strip_prefix(PROBE_RESULT_PREFIX))
        .with_context(|| format!("no probe result line in: {stdout:?}"))?;
    let (interop, bwrap_path) = line
        .trim_start()
        .split_once(' ')
        .with_context(|| format!("malformed probe result line: {line:?}"))?;
    let mask_interop_dir = match interop {
        "interop" => true,
        "no-interop" => false,
        _ => bail!("malformed probe result line: {line:?}"),
    };
    ensure!(
        bwrap_path.starts_with('/'),
        "`bwrap` resolved to {bwrap_path:?} rather than an absolute path; a shell \
         alias or function named `bwrap` cannot be run with `wsl --exec`"
    );
    Ok(EnvironmentProbe {
        mask_interop_dir,
        bwrap_path: bwrap_path.to_string(),
    })
}

/// Ensure a Linux `zed` of the given release `channel`/`version` is available
/// inside WSL and return its absolute in-WSL path, to be `--exec`'d as the
/// `--wsl-sandbox-helper`. Runs [`HELPER_PROVISION_SCRIPT`] (which downloads the
/// matching release tarball into an off-`PATH` location on first use).
///
/// Successful resolutions are cached per `(distro, channel, version)` for the
/// life of the process — once provisioned, the path won't change. Failures are
/// not cached, so a user who installs `curl` (or fixes networking) after an
/// error can retry without restarting Zed.
async fn ensure_wsl_zed_helper(
    wsl_exe: &Path,
    distro: Option<&str>,
    channel: &str,
    version: &str,
) -> Result<String> {
    // TODO: Remove this development override once WSL canonical-path handling is released.
    if let Some(helper) = std::env::var_os("ZED_WSL_SANDBOX_HELPER") {
        let helper = helper
            .into_string()
            .map_err(|_| anyhow::anyhow!("ZED_WSL_SANDBOX_HELPER is not valid UTF-8"))?;
        ensure!(
            helper.starts_with('/'),
            "ZED_WSL_SANDBOX_HELPER must be an absolute path inside WSL"
        );
        return Ok(helper);
    }

    type HelperCache = HashMap<(Option<String>, String, String), String>;
    static CACHE: OnceLock<Mutex<HelperCache>> = OnceLock::new();
    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));

    let key = (
        distro.map(str::to_string),
        channel.to_string(),
        version.to_string(),
    );
    if let Some(path) = cache
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .get(&key)
    {
        return Ok(path.clone());
    }

    // A login shell (`-lc`) is used so a profile-managed PATH (where `zed` or
    // `curl` may live) is honored. `channel`/`version` are passed as positional
    // args (`$1`/`$2`), never interpolated into the script body.
    let output = run_wsl_command(
        wsl_exe,
        distro,
        [
            "--exec",
            "sh",
            "-lc",
            HELPER_PROVISION_SCRIPT,
            "zed-wsl-sandbox-helper",
            channel,
            version,
        ],
        "provision the Linux `zed` sandbox helper",
    )
    .await?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stderr = stderr.trim();
        return Err(unavailable(format!(
            "failed to provision a Linux `zed` sandbox helper in {}{}",
            wsl_distro_label(distro),
            if stderr.is_empty() {
                String::new()
            } else {
                format!(": {stderr}")
            }
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let path = stdout
        .lines()
        .rev()
        .find_map(|line| line.strip_prefix(HELPER_RESULT_PREFIX))
        .map(|path| path.trim().to_string())
        .with_context(|| {
            unavailable(format!(
                "no helper result line in sandbox-helper provisioning output from {}: {stdout:?}",
                wsl_distro_label(distro)
            ))
        })?;
    ensure!(
        path.starts_with('/'),
        "the WSL `zed` sandbox helper resolved to {path:?} rather than an absolute path"
    );

    cache
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .insert(key, path.clone());
    Ok(path)
}

/// Shell script that resolves and existence-checks paths in a single WSL
/// round-trip. Arguments come in triples `(kind, path, fallback)`: kind `W`
/// is a native Windows path to translate with `wslpath -u` (falling back to
/// the precomputed `/mnt/<drive>/...` mapping when translation fails), kind
/// `L` is an already-Linux path with an empty fallback. One result line is
/// printed per triple: `<ok|fallback> <ok|missing> <resolved path>`.
const PATH_RESOLUTION_SCRIPT: &str = "\
    while [ \"$#\" -ge 3 ]; do \
        kind=$1; path=$2; fallback=$3; shift 3; translate=ok; \
        if [ \"$kind\" = W ]; then \
            resolved=$(wslpath -u \"$path\" 2>/dev/null) || { resolved=$fallback; translate=fallback; }; \
        else resolved=$path; fi; \
        exists=ok; [ -e \"$resolved\" ] || exists=missing; \
        printf '%s %s %s\\n' \"$translate\" \"$exists\" \"$resolved\"; \
    done";

/// A line of [`PATH_RESOLUTION_SCRIPT`] output, parsed.
#[derive(Debug, Eq, PartialEq)]
struct ResolvedPath {
    path: String,
    used_fallback: bool,
    exists: bool,
}

/// Resolve path mappings into final WSL paths and confirm required paths exist.
/// Native drive-letter paths are translated with `wslpath -u` inside the
/// chosen distro so its actual automount configuration is honored, falling
/// back to the structural `/mnt/<drive>` mapping when translation fails
/// (e.g. a distro without `wslpath`); a wrong fallback is still caught by
/// the existence check.
///
/// Successful resolutions are memoized per `(distro, mapping)` for the life
/// of the process, so a steady-state command whose paths have all been seen
/// before resolves with zero `wsl.exe` round-trips; at most one round-trip
/// handles all cache misses ([`resolve_uncached_paths`]). A hit reuses the
/// translation — which only changes if the distro's automount configuration
/// is edited and the distro restarted — and also skips the WSL-side
/// existence re-check. That staleness is acceptable: if a cached path
/// disappears mid-session bwrap fails closed on the missing bind source rather
/// than running the command unsandboxed. Optional missing paths are not cached,
/// so a protected Git path can be created and then included by a later command.
///
/// Each mapping is paired with a human-readable description used in errors and
/// a flag for whether the path is required to exist. The returned paths are in
/// the same order as `mappings`; optional missing paths are returned as `None`.
async fn resolve_paths(
    wsl_exe: &Path,
    distro: Option<&str>,
    mappings: &[(PathMapping, &str, bool)],
) -> Result<Vec<Option<String>>> {
    type ResolutionCache = HashMap<Option<String>, HashMap<PathMapping, String>>;
    static CACHE: OnceLock<Mutex<ResolutionCache>> = OnceLock::new();
    let cache = CACHE.get_or_init(Default::default);

    let distro_key = distro.map(str::to_string);
    let mut resolved: Vec<Option<Option<String>>> = {
        let cache = cache
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        let per_distro = cache.get(&distro_key);
        mappings
            .iter()
            .map(|(mapping, _, _)| {
                per_distro
                    .and_then(|cached| cached.get(mapping))
                    .cloned()
                    .map(Some)
            })
            .collect()
    };

    let misses: Vec<usize> = (0..mappings.len())
        .filter(|&index| resolved[index].is_none())
        .collect();
    if !misses.is_empty() {
        let miss_mappings: Vec<&(PathMapping, &str, bool)> =
            misses.iter().map(|&index| &mappings[index]).collect();
        let miss_resolved = resolve_uncached_paths(wsl_exe, distro, &miss_mappings).await?;
        let mut cache = cache
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner());
        let per_distro = cache.entry(distro_key).or_default();
        for (&index, path) in misses.iter().zip(miss_resolved) {
            if let Some(path) = &path {
                per_distro.insert(mappings[index].0.clone(), path.clone());
            }
            resolved[index] = Some(path);
        }
    }

    let mut paths = Vec::with_capacity(resolved.len());
    for path in resolved {
        paths.push(path.context("bug: a path mapping was left unresolved")?);
    }
    Ok(paths)
}

/// Resolve and existence-check mappings that weren't in the cache, in a
/// single `wsl.exe` round-trip. A non-login shell runs the script so profile
/// scripts can't pollute the stdout protocol.
async fn resolve_uncached_paths(
    wsl_exe: &Path,
    distro: Option<&str>,
    mappings: &[&(PathMapping, &str, bool)],
) -> Result<Vec<Option<String>>> {
    let mut args = vec![
        "--exec".to_string(),
        "sh".to_string(),
        "-c".to_string(),
        PATH_RESOLUTION_SCRIPT.to_string(),
        // argv[0] for the script; the path triples follow as "$@".
        "zed-resolve-paths".to_string(),
    ];
    args.extend(path_resolution_args(
        mappings.iter().map(|mapping| &mapping.0),
    ));
    let output = run_wsl_command(wsl_exe, distro, &args, "resolve sandbox paths").await?;
    if !output.status.success() {
        return Err(unavailable(format!(
            "failed to resolve sandbox paths in {}{}",
            wsl_distro_label(distro),
            command_failure_details(output.status.code(), &output.stderr)
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let resolved = parse_path_resolution_output(&stdout, mappings.len()).map_err(|error| {
        unavailable(format!(
            "failed to resolve sandbox paths in {}: {error:#}",
            wsl_distro_label(distro)
        ))
    })?;

    mappings
        .iter()
        .zip(resolved)
        .map(|((mapping, description, required), resolved)| {
            if resolved.used_fallback
                && let PathMapping::NativeDrive { windows_path, .. } = mapping
            {
                log::warn!(
                    "failed to translate `{windows_path}` with wslpath in {}; \
                     falling back to `{}`",
                    wsl_distro_label(distro),
                    resolved.path
                );
            }
            if !resolved.exists {
                // A bad request (the path simply isn't there), not an
                // environment problem — the model can create it or fix the path
                // and retry, so no `WSL_SANDBOX_UNAVAILABLE_PREFIX`. Protected
                // Git paths are allowed to be absent, matching Linux bwrap's
                // behavior: a missing path cannot be overlaid, so it is skipped.
                ensure!(
                    !required,
                    "mapped {description} `{}` does not exist in {}",
                    resolved.path,
                    wsl_distro_label(distro)
                );
                return Ok(None);
            }
            Ok(Some(resolved.path))
        })
        .collect()
}

/// Flatten path mappings into the `(kind, path, fallback)` argument triples
/// consumed by [`PATH_RESOLUTION_SCRIPT`].
fn path_resolution_args<'a>(mappings: impl Iterator<Item = &'a PathMapping>) -> Vec<String> {
    let mut args = Vec::new();
    for mapping in mappings {
        match mapping {
            PathMapping::Wsl(path) => {
                args.extend(["L".to_string(), path.path.clone(), String::new()]);
            }
            PathMapping::NativeDrive {
                windows_path,
                fallback,
            } => {
                args.extend(["W".to_string(), windows_path.clone(), fallback.path.clone()]);
            }
        }
    }
    args
}

/// Parse [`PATH_RESOLUTION_SCRIPT`] output: one strictly-formatted line per
/// input triple. Anything else (wrong line count, unknown status words, a
/// non-absolute path) means the stdout protocol was corrupted and is an error.
fn parse_path_resolution_output(stdout: &str, expected: usize) -> Result<Vec<ResolvedPath>> {
    let lines: Vec<&str> = stdout.lines().collect();
    ensure!(
        lines.len() == expected,
        "expected {expected} result lines from the path resolution script, got {}: {stdout:?}",
        lines.len()
    );
    lines
        .into_iter()
        .map(|line| {
            let mut parts = line.splitn(3, ' ');
            let (Some(translate), Some(exists), Some(path)) =
                (parts.next(), parts.next(), parts.next())
            else {
                bail!("malformed line from the path resolution script: {line:?}");
            };
            let used_fallback = match translate {
                "ok" => false,
                "fallback" => true,
                _ => bail!("malformed line from the path resolution script: {line:?}"),
            };
            let exists = match exists {
                "ok" => true,
                "missing" => false,
                _ => bail!("malformed line from the path resolution script: {line:?}"),
            };
            ensure!(
                path.starts_with('/'),
                "unexpected resolved path from the path resolution script: {path:?}"
            );
            Ok(ResolvedPath {
                path: path.to_string(),
                used_fallback,
                exists,
            })
        })
        .collect()
}

/// `CREATE_NO_WINDOW` process creation flag. `wsl.exe` is a console-subsystem
/// binary, so spawning it from a GUI process without this flag flashes a
/// console window. Defined locally because this crate doesn't depend on
/// `util` (whose command helpers normally take care of this).
const CREATE_NO_WINDOW: u32 = 0x0800_0000;

/// Invoke `wsl.exe` with the given args and return its raw output.
///
/// Only spawn failures become errors here; callers interpret the exit status
/// themselves. stdout, when used, is decoded as UTF-8 (lossily) — that's
/// only valid for `--exec`'d programs whose output we control, not for
/// `wsl.exe`'s own diagnostics (which are UTF-16LE).
///
/// `output()` spawns the child eagerly and the returned future owns it, so
/// with `kill_on_drop` the child can't outlive this future: a caller-side
/// timeout or cancellation that drops us also terminates a wedged `wsl.exe`
/// instead of leaking it.
async fn run_wsl_command(
    wsl_exe: &Path,
    distro: Option<&str>,
    args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>,
    description: &str,
) -> Result<std::process::Output> {
    use smol::process::windows::CommandExt as _;

    let mut command = Command::new(wsl_exe);
    if let Some(distro) = distro {
        command.args(["-d", distro]);
    }
    command
        .args(args)
        .stdin(Stdio::null())
        .kill_on_drop(true)
        .creation_flags(CREATE_NO_WINDOW);

    command.output().await.map_err(|error| {
        unavailable(format!(
            "failed to invoke WSL while trying to {description}: {error:#}"
        ))
    })
}

fn command_failure_details(exit_code: Option<i32>, stderr: &[u8]) -> String {
    let stderr = String::from_utf8_lossy(stderr);
    let stderr = stderr.trim();
    let exit_status = match exit_code {
        Some(code) => format!("exit code {code}"),
        None => "terminated by signal".to_string(),
    };
    if stderr.is_empty() {
        format!(" ({exit_status})")
    } else {
        format!(" ({exit_status}; stderr: {stderr})")
    }
}

fn wsl_distro_label(distro: Option<&str>) -> String {
    match distro {
        Some(distro) => format!("WSL distro `{distro}`"),
        None => "the default WSL distro".to_string(),
    }
}

fn wsl_exe_path() -> PathBuf {
    std::env::var_os("SystemRoot")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(r"C:\Windows"))
        .join("System32")
        .join("wsl.exe")
}

fn build_bwrap_args<S: std::hash::BuildHasher>(
    writable_paths: &[String],
    protected_paths: &[String],
    permissions: SandboxPermissions,
    cwd: Option<&str>,
    mask_interop_dir: bool,
    env: &HashMap<String, String, S>,
) -> Vec<String> {
    let mut args = Vec::new();

    if permissions.allow_fs_write {
        push_bind(&mut args, "--bind", "/", "/");
    } else {
        push_bind(&mut args, "--ro-bind", "/", "/");
        args.extend(["--tmpfs".to_string(), "/tmp".to_string()]);
        for path in writable_paths {
            push_bind(&mut args, "--bind", path, path);
        }
    }

    // Protect requested paths by re-binding them read-only over any writable
    // binds above (order matters: later binds win).
    for path in protected_paths {
        push_bind(&mut args, "--ro-bind", path, path);
    }

    // Block WSL's Windows interop, regardless of the requested permissions.
    // Without this, a sandboxed process can exec a Windows binary (e.g.
    // /mnt/c/Windows/System32/cmd.exe), which the kernel's binfmt handler
    // (`/init`) hands off to the Windows host over an AF_UNIX socket — running
    // fully outside bwrap and defeating both the filesystem and the network
    // restrictions. `/init` locates that socket via the $WSL_INTEROP
    // environment variable, so we drop it; and we mask the socket directory
    // (when it exists) so the value can't be rediscovered by listing
    // /run/WSL and re-exporting it. Both steps are required: unsetting the
    // variable alone is bypassable, and masking alone leaves the inherited
    // variable usable.
    args.extend(["--unsetenv".to_string(), "WSL_INTEROP".to_string()]);
    args.extend(["--unsetenv".to_string(), "WSLENV".to_string()]);
    if mask_interop_dir {
        args.extend(["--tmpfs".to_string(), "/run/WSL".to_string()]);
    }

    args.extend([
        "--dev".to_string(),
        "/dev".to_string(),
        "--proc".to_string(),
        "/proc".to_string(),
    ]);

    if !permissions.allow_network {
        args.push("--unshare-net".to_string());
    }

    args.extend([
        "--unshare-user".to_string(),
        "--unshare-ipc".to_string(),
        "--unshare-uts".to_string(),
        "--unshare-pid".to_string(),
        "--unshare-cgroup-try".to_string(),
        "--die-with-parent".to_string(),
    ]);

    // Forward the caller-provided environment into the command. Windows env
    // set on the `wsl.exe` process doesn't reach the Linux command, so we
    // re-apply it here on the sandbox's child instead.
    for (name, value) in env {
        if is_forwardable_env_var(name) {
            args.extend(["--setenv".to_string(), name.clone(), value.clone()]);
        }
    }

    if let Some(cwd) = cwd {
        args.extend(["--chdir".to_string(), cwd.to_string()]);
    }

    args
}

/// Whether an environment variable should be forwarded into the Linux sandbox.
///
/// `bwrap --setenv` calls `setenv(3)`, which rejects names that are empty or
/// contain `=`. Windows process environments include such entries — most
/// notably the per-drive current-directory pseudo-variables (`=C:`, `=D:`,
/// ...) Windows keeps in the environment block — so they must be skipped or
/// bwrap aborts with "setenv failed".
///
/// Beyond that, many variables hold Windows-specific values that would be
/// meaningless or actively break the command inside WSL, so they are dropped
/// rather than forwarded: `PATH`/`PATHEXT` would shadow WSL's own `PATH` and
/// stop the shell from finding Linux executables, the temp-dir variables point
/// at Windows paths that don't exist in WSL (bwrap provides a fresh tmpfs
/// `/tmp` instead), the WSL interop variables would undermine the explicit
/// interop block above, a Windows-set `HOME` would clobber the distro's own
/// `$HOME` and break the shell, and the rest are Windows system locations,
/// shell settings, host/session identity, and CPU descriptors that are either
/// wrong or misleading inside Linux. This is a blocklist rather than an allowlist so
/// genuinely portable variables (e.g. `LANG`, `CARGO_TERM_COLOR`, `PAGER`)
/// still reach the command; it only needs to cover the variables Windows
/// populates by default. Matched case-insensitively because Windows
/// environment variable names are.
fn is_forwardable_env_var(name: &str) -> bool {
    if name.is_empty() || name.contains('=') {
        return false;
    }
    const BLOCKED: &[&str] = &[
        // Shadow or break the Linux process environment.
        "PATH",
        "PATHEXT",
        "TMPDIR",
        "TMP",
        "TEMP",
        // Would undermine the explicit WSL interop block above.
        "WSL_INTEROP",
        "WSLENV",
        // Windows system locations and shell, meaningless inside Linux.
        "OS",
        "COMSPEC",
        "WINDIR",
        "SYSTEMROOT",
        "SYSTEMDRIVE",
        // When Windows has `HOME` set (e.g. for git), it's a Windows path that
        // would clobber the distro's correct Linux `$HOME` and break the shell.
        "HOME",
        "HOMEDRIVE",
        "HOMEPATH",
        "HOMESHARE",
        "USERPROFILE",
        "PUBLIC",
        "ALLUSERSPROFILE",
        "APPDATA",
        "LOCALAPPDATA",
        "PROGRAMDATA",
        "PROGRAMFILES",
        "PROGRAMFILES(X86)",
        "PROGRAMW6432",
        "COMMONPROGRAMFILES",
        "COMMONPROGRAMFILES(X86)",
        "COMMONPROGRAMW6432",
        "PSMODULEPATH",
        "DRIVERDATA",
        "ONEDRIVE",
        // Windows host/session identity, misleading inside Linux.
        "COMPUTERNAME",
        "USERNAME",
        "USERDOMAIN",
        "USERDOMAIN_ROAMINGPROFILE",
        "LOGONSERVER",
        "SESSIONNAME",
        // Windows CPU descriptors.
        "NUMBER_OF_PROCESSORS",
        "PROCESSOR_ARCHITECTURE",
        "PROCESSOR_ARCHITEW6432",
        "PROCESSOR_IDENTIFIER",
        "PROCESSOR_LEVEL",
        "PROCESSOR_REVISION",
    ];
    !BLOCKED
        .iter()
        .any(|blocked| name.eq_ignore_ascii_case(blocked))
}

fn push_bind(args: &mut Vec<String>, flag: &str, source: &str, destination: &str) {
    args.extend([
        flag.to_string(),
        source.to_string(),
        destination.to_string(),
    ]);
}

fn directory_to_wsl(path: &Path) -> Result<PathMapping> {
    ensure!(
        path.is_dir(),
        "Windows sandboxing via WSL can only use an existing directory as cwd: {}",
        path.display()
    );
    map_path_to_wsl(path)
}

fn path_to_wsl(path: &Path) -> Result<PathMapping> {
    let path_string = path.to_string_lossy();
    if let Ok(path) = parse_wsl_absolute_path(&path_string) {
        return Ok(PathMapping::Wsl(path));
    }

    ensure!(
        path.is_dir() || path.is_file(),
        "Windows sandboxing via WSL can only grant existing files or directories: {}",
        path.display()
    );
    map_path_to_wsl(path)
}

fn path_to_wsl_allowing_missing(path: &Path) -> Result<PathMapping> {
    let path_string = path.to_string_lossy();
    if let Ok(path) = parse_wsl_absolute_path(&path_string) {
        return Ok(PathMapping::Wsl(path));
    }
    map_path_to_wsl(path)
}

fn map_path_to_wsl(path: &Path) -> Result<PathMapping> {
    let path_string = path.to_string_lossy();
    if let Ok(path) = parse_wsl_unc_path(&path_string) {
        return Ok(PathMapping::Wsl(path));
    }
    let fallback = parse_native_drive_path(&path_string)?;
    let windows_path = path_string
        .strip_prefix(r"\\?\")
        .unwrap_or(&path_string)
        .replace('\\', "/");
    Ok(PathMapping::NativeDrive {
        windows_path,
        fallback,
    })
}

fn parse_wsl_absolute_path(path: &str) -> Result<WslPath> {
    let path = path.replace('\\', "/");
    ensure!(
        path.starts_with('/') && !path.starts_with("//"),
        "path is not a WSL absolute path: {path}"
    );
    Ok(WslPath { distro: None, path })
}

fn parse_wsl_unc_path(path: &str) -> Result<WslPath> {
    let path = path.replace('/', "\\");
    let remainder = path
        .strip_prefix("\\\\wsl.localhost\\")
        .or_else(|| path.strip_prefix("\\\\wsl$\\"))
        .or_else(|| path.strip_prefix("\\\\?\\UNC\\wsl.localhost\\"))
        .or_else(|| path.strip_prefix("\\\\?\\UNC\\wsl$\\"))
        .with_context(|| format!("path is not a WSL UNC path: {path}"))?;

    let (distro, rest) = remainder
        .split_once('\\')
        .map(|(distro, rest)| (distro, Some(rest)))
        .unwrap_or((remainder, None));
    ensure!(
        !distro.is_empty(),
        "WSL UNC path is missing a distro name: {path}"
    );

    let linux_path = match rest {
        Some(rest) if !rest.is_empty() => format!("/{}", rest.replace('\\', "/")),
        _ => "/".to_string(),
    };

    Ok(WslPath {
        distro: Some(distro.to_string()),
        path: linux_path,
    })
}

fn parse_native_drive_path(path: &str) -> Result<WslPath> {
    let path = path
        .strip_prefix("\\\\?\\")
        .unwrap_or(path)
        .replace('\\', "/");
    let mut chars = path.chars();
    let Some(drive) = chars.next().filter(|drive| drive.is_ascii_alphabetic()) else {
        bail!("path is not a drive-letter Windows path: {path}");
    };
    ensure!(chars.next() == Some(':'), "path is not absolute: {path}");
    let rest = chars.as_str().trim_start_matches('/');
    let drive = drive.to_ascii_lowercase();
    let linux_path = if rest.is_empty() {
        format!("/mnt/{drive}")
    } else {
        format!("/mnt/{drive}/{rest}")
    };
    Ok(WslPath {
        distro: None,
        path: linux_path,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wrap_invocation_future_is_send() {
        // Callers run `wrap_invocation` via `background_spawn`, which
        // requires a `Send` future. This fails to compile if, for example, a
        // cache `MutexGuard` is ever held across an await point.
        fn assert_send<T: Send>(_: T) {}
        assert_send(wrap_invocation(
            String::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            SandboxPermissions::default(),
            None,
            HashMap::<String, String>::new(),
            None,
        ));
    }

    #[test]
    fn parse_wsl_localhost_path() {
        let path = parse_wsl_unc_path(r"\\wsl.localhost\Ubuntu\home\me\project").unwrap();
        assert_eq!(path.distro.as_deref(), Some("Ubuntu"));
        assert_eq!(path.path, "/home/me/project");
    }

    #[test]
    fn parse_wsl_dollar_path() {
        let path = parse_wsl_unc_path(r"\\wsl$\Debian\tmp").unwrap();
        assert_eq!(path.distro.as_deref(), Some("Debian"));
        assert_eq!(path.path, "/tmp");
    }

    #[test]
    fn parse_native_windows_path() {
        let path = parse_native_drive_path(r"C:\Users\me\project").unwrap();
        assert_eq!(path.distro, None);
        assert_eq!(path.path, "/mnt/c/Users/me/project");
    }

    #[test]
    fn parse_wsl_absolute_path_keeps_linux_path() {
        let path = parse_wsl_absolute_path("/home/me").unwrap();
        assert_eq!(path.distro, None);
        assert_eq!(path.path, "/home/me");
    }

    #[test]
    fn parse_wsl_absolute_path_rejects_unc_paths() {
        assert!(parse_wsl_absolute_path(r"\\server\share").is_err());
    }

    #[test]
    fn parse_verbatim_native_windows_path() {
        let path = parse_native_drive_path(r"\\?\D:\workspace").unwrap();
        assert_eq!(path.distro, None);
        assert_eq!(path.path, "/mnt/d/workspace");
    }

    #[test]
    fn rejects_unc_non_wsl_path() {
        assert!(parse_native_drive_path(r"\\server\share\project").is_err());
    }

    #[test]
    fn probe_output_reports_interop_and_bwrap_path() {
        let probe = parse_probe_output("zed-wsl-probe: interop /usr/bin/bwrap\n").unwrap();
        assert_eq!(
            probe,
            EnvironmentProbe {
                mask_interop_dir: true,
                bwrap_path: "/usr/bin/bwrap".to_string(),
            }
        );

        let probe =
            parse_probe_output("zed-wsl-probe: no-interop /home/me/.nix-profile/bin/bwrap\n")
                .unwrap();
        assert_eq!(
            probe,
            EnvironmentProbe {
                mask_interop_dir: false,
                bwrap_path: "/home/me/.nix-profile/bin/bwrap".to_string(),
            }
        );
    }

    #[test]
    fn probe_output_ignores_profile_noise_even_mentioning_interop() {
        // Login-shell profile scripts run before the probe body and may print
        // arbitrary text; only the marked result line counts.
        let probe = parse_probe_output(
            "welcome to my shell, interop fans\nzed-wsl-probe: no-interop /usr/bin/bwrap\n",
        )
        .unwrap();
        assert!(!probe.mask_interop_dir);
    }

    #[test]
    fn probe_output_rejects_missing_or_malformed_result_line() {
        assert!(parse_probe_output("").is_err());
        assert!(parse_probe_output("profile noise only\n").is_err());
        assert!(parse_probe_output("zed-wsl-probe: interop\n").is_err());
        assert!(parse_probe_output("zed-wsl-probe: maybe /usr/bin/bwrap\n").is_err());
    }

    #[test]
    fn probe_output_rejects_non_absolute_bwrap_path() {
        // `command -v` reports a bare name for shell functions and aliases,
        // which `wsl --exec` could never run.
        assert!(parse_probe_output("zed-wsl-probe: interop bwrap\n").is_err());
    }

    #[test]
    fn probe_script_smoke_tests_the_namespaces_the_real_invocation_uses() {
        // Presence isn't enough: unprivileged user namespaces can be
        // restricted (e.g. Ubuntu 24.04's AppArmor policy), so the probe must
        // actually exercise the namespace flags `build_bwrap_args` emits.
        let script = probe_script();
        for flag in [
            "--unshare-user",
            "--unshare-net",
            "--unshare-ipc",
            "--unshare-uts",
            "--unshare-pid",
            "--unshare-cgroup-try",
            "--ro-bind / /",
        ] {
            assert!(script.contains(flag), "probe script must contain {flag}");
        }
        assert!(script.contains("exit 41"));
        assert!(script.contains("exit 42"));
    }

    #[test]
    fn probe_script_rejects_setuid_root_bwrap_before_smoke_test() {
        let script = probe_script();
        let guard =
            "[ -u \"$bwrap_path\" ] && [ \"$(stat -c %u \"$bwrap_path\" 2>/dev/null)\" = 0 ]";
        let smoke_test = "\"$bwrap_path\" --ro-bind / /";
        let Some(guard_index) = script.find(guard) else {
            panic!("probe script must contain setuid-root guard: {script}");
        };
        let Some(smoke_test_index) = script.find(smoke_test) else {
            panic!("probe script must contain bwrap smoke test: {script}");
        };

        assert!(script.contains("setuid-root bwrap is not supported"));
        assert!(script.contains(&format!("exit {BWRAP_UNUSABLE_EXIT_CODE}; fi")));
        assert!(guard_index < smoke_test_index);
    }

    #[test]
    fn canonical_grant_output_returns_the_linux_canonical_path() {
        let resolved =
            parse_canonical_grant_output("Ubuntu-24.04\n/home/me/real target\nnative-fs\n")
                .expect("valid helper output");
        // The bare Linux path, not a `\\wsl.localhost\...` spelling.
        assert_eq!(resolved.canonical, PathBuf::from("/home/me/real target"));
        assert!(!resolved.on_windows_fs);

        let resolved = parse_canonical_grant_output("Ubuntu\n/mnt/c/Users/me/proj\nwindows-fs\n")
            .expect("valid helper output");
        assert_eq!(resolved.canonical, PathBuf::from("/mnt/c/Users/me/proj"));
        assert!(resolved.on_windows_fs);
    }

    #[test]
    fn canonical_grant_output_rejects_malformed_protocol() {
        // Missing filesystem-class line.
        assert!(parse_canonical_grant_output("Ubuntu\n/home/me/project\n").is_err());
        // Non-absolute canonical path.
        assert!(parse_canonical_grant_output("Ubuntu\nrelative\nnative-fs\n").is_err());
        // Distro name containing a separator.
        assert!(parse_canonical_grant_output("Ubuntu/bad\n/home/me/project\nnative-fs\n").is_err());
        // Unknown filesystem class.
        assert!(parse_canonical_grant_output("Ubuntu\n/home/me/project\nmaybe-fs\n").is_err());
        // Trailing junk after the filesystem class.
        assert!(
            parse_canonical_grant_output("Ubuntu\n/home/me/project\nnative-fs\nnoise\n").is_err()
        );
    }

    #[test]
    fn split_resolved_paths_keeps_existing_protected_paths_and_skips_missing_ones() {
        let (cwd, writable_paths, protected_paths) = split_resolved_paths(
            true,
            1,
            vec![
                Some("/home/me/project".to_string()),
                Some("/home/me/project".to_string()),
                None,
                Some("/mnt/c/external/.git".to_string()),
            ],
        )
        .unwrap();

        assert_eq!(cwd.as_deref(), Some("/home/me/project"));
        assert_eq!(writable_paths, vec!["/home/me/project".to_string()]);
        assert_eq!(protected_paths, vec!["/mnt/c/external/.git".to_string()]);
    }

    #[test]
    fn split_resolved_paths_rejects_missing_required_writable_paths() {
        let error = split_resolved_paths(false, 1, vec![None]).unwrap_err();
        assert!(
            error
                .to_string()
                .contains("required writable path resolved as missing"),
            "unexpected error: {error:#}"
        );
    }

    #[test]
    fn bwrap_denies_network_by_default() {
        let args = build_bwrap_args(
            &["/home/me/project".to_string()],
            &[],
            SandboxPermissions::default(),
            Some("/home/me/project"),
            true,
            &HashMap::new(),
        );
        assert!(args.iter().any(|arg| arg == "--unshare-net"));
        assert!(
            args.windows(3)
                .any(|window| window == ["--bind", "/home/me/project", "/home/me/project"])
        );
    }

    #[test]
    fn bwrap_allows_network_when_requested() {
        let args = build_bwrap_args(
            &[],
            &[],
            SandboxPermissions {
                allow_network: true,
                allow_fs_write: false,
            },
            None,
            true,
            &HashMap::new(),
        );
        assert!(!args.iter().any(|arg| arg == "--unshare-net"));
    }

    #[test]
    fn bwrap_binds_explicit_writable_file_paths() {
        let args = build_bwrap_args(
            &["/mnt/c/Users/me/AppData/Roaming/Zed/AGENTS.md".to_string()],
            &[],
            SandboxPermissions::default(),
            None,
            true,
            &HashMap::new(),
        );
        assert!(args.windows(3).any(|window| window
            == [
                "--bind",
                "/mnt/c/Users/me/AppData/Roaming/Zed/AGENTS.md",
                "/mnt/c/Users/me/AppData/Roaming/Zed/AGENTS.md"
            ]));
    }

    #[test]
    fn bwrap_protects_paths_after_writable_paths() {
        let args = build_bwrap_args(
            &["/home/me/project".to_string()],
            &["/home/me/project/.git".to_string()],
            SandboxPermissions::default(),
            Some("/home/me/project"),
            true,
            &HashMap::new(),
        );
        let writable_index = args
            .windows(3)
            .position(|window| window == ["--bind", "/home/me/project", "/home/me/project"])
            .expect("project should be writable");
        let protected_index = args
            .windows(3)
            .position(|window| {
                window
                    == [
                        "--ro-bind",
                        "/home/me/project/.git",
                        "/home/me/project/.git",
                    ]
            })
            .expect("protected path should be bound read-only");
        assert!(protected_index > writable_index);
    }

    #[test]
    fn bwrap_protects_paths_when_fs_writes_are_unrestricted() {
        let args = build_bwrap_args(
            &[],
            &["/home/me/project/.git".to_string()],
            SandboxPermissions {
                allow_network: false,
                allow_fs_write: true,
            },
            None,
            true,
            &HashMap::new(),
        );
        let unrestricted_write_index = args
            .windows(3)
            .position(|window| window == ["--bind", "/", "/"])
            .expect("root should be writable");
        let protected_index = args
            .windows(3)
            .position(|window| {
                window
                    == [
                        "--ro-bind",
                        "/home/me/project/.git",
                        "/home/me/project/.git",
                    ]
            })
            .expect("protected path should be bound read-only");
        assert!(protected_index > unrestricted_write_index);
    }

    #[test]
    fn bwrap_blocks_wsl_interop_by_default() {
        let args = build_bwrap_args(
            &["/home/me/project".to_string()],
            &[],
            SandboxPermissions::default(),
            Some("/home/me/project"),
            true,
            &HashMap::new(),
        );
        assert!(
            args.windows(2)
                .any(|window| window == ["--unsetenv", "WSL_INTEROP"])
        );
        assert!(
            args.windows(2)
                .any(|window| window == ["--tmpfs", "/run/WSL"])
        );
    }

    #[test]
    fn bwrap_blocks_wsl_interop_even_with_fs_write() {
        let args = build_bwrap_args(
            &[],
            &[],
            SandboxPermissions {
                allow_network: true,
                allow_fs_write: true,
            },
            None,
            true,
            &HashMap::new(),
        );
        // Interop is host code execution, not just a filesystem write, so it
        // stays blocked even when the user has granted unrestricted writes
        // and network.
        assert!(
            args.windows(2)
                .any(|window| window == ["--unsetenv", "WSL_INTEROP"])
        );
        assert!(
            args.windows(2)
                .any(|window| window == ["--tmpfs", "/run/WSL"])
        );
    }

    #[test]
    fn bwrap_skips_interop_dir_mask_when_absent() {
        // When the interop socket directory doesn't exist (interop disabled),
        // there's nothing to mask and a `--tmpfs /run/WSL` would abort bwrap,
        // so the mount must be omitted. Unsetting the variable is harmless and
        // stays.
        let args = build_bwrap_args(
            &[],
            &[],
            SandboxPermissions::default(),
            None,
            false,
            &HashMap::new(),
        );
        assert!(
            args.windows(2)
                .any(|window| window == ["--unsetenv", "WSL_INTEROP"])
        );
        assert!(!args.iter().any(|arg| arg == "/run/WSL"));
    }

    #[test]
    fn bwrap_forwards_env_via_setenv() {
        let env = HashMap::from([
            ("PAGER".to_string(), String::new()),
            ("CARGO_TERM_COLOR".to_string(), "always".to_string()),
        ]);
        let args = build_bwrap_args(&[], &[], SandboxPermissions::default(), None, false, &env);
        assert!(
            args.windows(3)
                .any(|window| window == ["--setenv", "PAGER", ""])
        );
        assert!(
            args.windows(3)
                .any(|window| window == ["--setenv", "CARGO_TERM_COLOR", "always"])
        );
    }

    #[test]
    fn bwrap_does_not_forward_wsl_interop_env() {
        let env = HashMap::from([
            (
                "WSL_INTEROP".to_string(),
                "/run/WSL/123_interop".to_string(),
            ),
            ("WsLeNv".to_string(), "WSL_INTEROP/u".to_string()),
            ("PAGER".to_string(), String::new()),
        ]);
        let args = build_bwrap_args(&[], &[], SandboxPermissions::default(), None, false, &env);

        assert!(
            args.windows(2)
                .any(|window| window == ["--unsetenv", "WSL_INTEROP"])
        );
        assert!(
            args.windows(2)
                .any(|window| window == ["--unsetenv", "WSLENV"])
        );
        assert!(
            args.windows(3)
                .any(|window| window == ["--setenv", "PAGER", ""])
        );
        assert!(!args.windows(3).any(|window| {
            matches!(window, [flag, name, _]
                if flag.as_str() == "--setenv"
                    && name.eq_ignore_ascii_case("WSL_INTEROP"))
        }));
        assert!(!args.windows(3).any(|window| {
            matches!(window, [flag, name, _]
                if flag.as_str() == "--setenv"
                    && name.eq_ignore_ascii_case("WSLENV"))
        }));
    }

    #[test]
    fn bwrap_does_not_forward_windows_specific_env() {
        // These hold Windows paths/values that would break or be meaningless
        // inside WSL, so they must never cross the boundary. Names are matched
        // case-insensitively, as Windows env var names are. `(x86)` variants
        // contain parentheses but no `=`, so they'd pass the `setenv` filter
        // and must be blocked by name.
        let env = HashMap::from([
            ("Path".to_string(), r"C:\Windows\System32".to_string()),
            ("PATHEXT".to_string(), ".COM;.EXE;.BAT".to_string()),
            (
                "TEMP".to_string(),
                r"C:\Users\me\AppData\Local\Temp".to_string(),
            ),
            (
                "Tmp".to_string(),
                r"C:\Users\me\AppData\Local\Temp".to_string(),
            ),
            ("TMPDIR".to_string(), r"C:\tmp".to_string()),
            ("OS".to_string(), "Windows_NT".to_string()),
            (
                "ComSpec".to_string(),
                r"C:\Windows\system32\cmd.exe".to_string(),
            ),
            ("windir".to_string(), r"C:\Windows".to_string()),
            ("SystemRoot".to_string(), r"C:\Windows".to_string()),
            ("HOME".to_string(), r"C:\Users\me".to_string()),
            ("USERPROFILE".to_string(), r"C:\Users\me".to_string()),
            (
                "APPDATA".to_string(),
                r"C:\Users\me\AppData\Roaming".to_string(),
            ),
            (
                "LOCALAPPDATA".to_string(),
                r"C:\Users\me\AppData\Local".to_string(),
            ),
            (
                "ProgramFiles(x86)".to_string(),
                r"C:\Program Files (x86)".to_string(),
            ),
            ("USERNAME".to_string(), "me".to_string()),
            ("COMPUTERNAME".to_string(), "DESKTOP-ABC".to_string()),
            ("PROCESSOR_ARCHITECTURE".to_string(), "AMD64".to_string()),
            ("NUMBER_OF_PROCESSORS".to_string(), "16".to_string()),
        ]);
        let args = build_bwrap_args(&[], &[], SandboxPermissions::default(), None, false, &env);
        assert!(!args.iter().any(|arg| arg == "--setenv"));
    }

    #[test]
    fn bwrap_forwards_portable_env_alongside_windows_specific_env() {
        // A blocklist (not an allowlist) means genuinely portable variables
        // still reach the command even when Windows-only ones are present.
        let env = HashMap::from([
            ("USERPROFILE".to_string(), r"C:\Users\me".to_string()),
            ("LANG".to_string(), "en_US.UTF-8".to_string()),
            ("CARGO_TERM_COLOR".to_string(), "always".to_string()),
        ]);
        let args = build_bwrap_args(&[], &[], SandboxPermissions::default(), None, false, &env);
        assert!(
            args.windows(3)
                .any(|window| window == ["--setenv", "LANG", "en_US.UTF-8"])
        );
        assert!(
            args.windows(3)
                .any(|window| window == ["--setenv", "CARGO_TERM_COLOR", "always"])
        );
        assert!(!args.windows(3).any(|window| {
            matches!(window, [flag, name, _]
                if flag.as_str() == "--setenv"
                    && name.eq_ignore_ascii_case("USERPROFILE"))
        }));
    }

    #[test]
    fn bwrap_skips_env_names_setenv_would_reject() {
        // bwrap's `--setenv` calls `setenv(3)`, which rejects empty names and
        // names containing `=`. Windows environments include the per-drive
        // current-directory pseudo-variables (`=C:`, ...); forwarding them
        // would abort bwrap with "setenv failed".
        let env = HashMap::from([
            ("=C:".to_string(), r"C:\Users\me".to_string()),
            (String::new(), "value".to_string()),
            ("OK".to_string(), "value".to_string()),
        ]);
        let args = build_bwrap_args(&[], &[], SandboxPermissions::default(), None, false, &env);
        assert!(
            args.windows(3)
                .any(|window| window == ["--setenv", "OK", "value"])
        );
        assert_eq!(args.iter().filter(|arg| *arg == "--setenv").count(), 1);
    }

    #[test]
    fn select_distro_uses_wsl_distro_when_present() {
        let distro = select_distro(
            None,
            &[
                PathMapping::NativeDrive {
                    windows_path: "C:/project".to_string(),
                    fallback: WslPath {
                        distro: None,
                        path: "/mnt/c/project".to_string(),
                    },
                },
                PathMapping::Wsl(WslPath {
                    distro: Some("Ubuntu".to_string()),
                    path: "/home/me/project".to_string(),
                }),
            ],
        )
        .unwrap();
        assert_eq!(distro.as_deref(), Some("Ubuntu"));
    }

    #[test]
    fn bad_request_errors_do_not_claim_sandboxing_is_unavailable() {
        // Mixed distros and missing/unmappable paths are model-fixable bad
        // requests. They must not be typed as `WslSandboxUnavailable` (nor
        // carry its prefix), since the agent uses that type to offer the
        // run-unsandboxed fallback only for genuine environment failures.
        let mixed_distros = select_distro(
            Some(&PathMapping::Wsl(WslPath {
                distro: Some("Ubuntu".to_string()),
                path: "/home/me".to_string(),
            })),
            &[PathMapping::Wsl(WslPath {
                distro: Some("Debian".to_string()),
                path: "/home/me".to_string(),
            })],
        )
        .unwrap_err();
        assert!(
            mixed_distros
                .downcast_ref::<WslSandboxUnavailable>()
                .is_none()
        );
        assert!(!format!("{mixed_distros:#}").contains(WSL_SANDBOX_UNAVAILABLE_PREFIX));

        let missing_path =
            path_to_wsl(Path::new(r"C:\zed-test\definitely\does\not\exist-2769")).unwrap_err();
        assert!(
            missing_path
                .downcast_ref::<WslSandboxUnavailable>()
                .is_none()
        );
        assert!(!format!("{missing_path:#}").contains(WSL_SANDBOX_UNAVAILABLE_PREFIX));

        let unmappable_cwd = directory_to_wsl(Path::new(r"\\server\share\project")).unwrap_err();
        assert!(
            unmappable_cwd
                .downcast_ref::<WslSandboxUnavailable>()
                .is_none()
        );
        assert!(!format!("{unmappable_cwd:#}").contains(WSL_SANDBOX_UNAVAILABLE_PREFIX));
    }

    #[test]
    fn unavailable_errors_are_typed_and_prefixed() {
        // Environment failures are recognizable by type (so the agent doesn't
        // depend on message text) and still render with the shared prefix.
        let error = unavailable("Bubblewrap (`bwrap`) is not installed in the default WSL distro");
        let typed = error
            .downcast_ref::<WslSandboxUnavailable>()
            .expect("environment failure should downcast to WslSandboxUnavailable");
        assert_eq!(
            typed.message(),
            "Bubblewrap (`bwrap`) is not installed in the default WSL distro"
        );
        assert!(format!("{error:#}").starts_with(WSL_SANDBOX_UNAVAILABLE_PREFIX));
    }

    #[test]
    fn map_path_to_wsl_keeps_unc_paths_structural() {
        let mapping = map_path_to_wsl(Path::new(r"\\wsl.localhost\Ubuntu\home\me")).unwrap();
        assert_eq!(
            mapping,
            PathMapping::Wsl(WslPath {
                distro: Some("Ubuntu".to_string()),
                path: "/home/me".to_string(),
            })
        );
    }

    #[test]
    fn map_path_to_wsl_defers_native_paths_to_wslpath() {
        let mapping = map_path_to_wsl(Path::new(r"C:\Users\me\project")).unwrap();
        assert_eq!(
            mapping,
            PathMapping::NativeDrive {
                windows_path: "C:/Users/me/project".to_string(),
                fallback: WslPath {
                    distro: None,
                    path: "/mnt/c/Users/me/project".to_string(),
                },
            }
        );
    }

    #[test]
    fn strip_windows_verbatim_prefix_only_strips_drive_paths() {
        // `canonicalize` yields `\\?\C:\...`; the plain native spelling is stored.
        assert_eq!(
            strip_windows_verbatim_prefix(PathBuf::from(r"\\?\C:\Users\me\dir")),
            PathBuf::from(r"C:\Users\me\dir")
        );
        // A `\\?\UNC\...` path is not a drive path and is left intact (the caller
        // rejects it).
        assert_eq!(
            strip_windows_verbatim_prefix(PathBuf::from(r"\\?\UNC\wsl.localhost\Ubuntu\home")),
            PathBuf::from(r"\\?\UNC\wsl.localhost\Ubuntu\home")
        );
        // A path without the prefix is unchanged.
        assert_eq!(
            strip_windows_verbatim_prefix(PathBuf::from(r"C:\already\plain")),
            PathBuf::from(r"C:\already\plain")
        );
    }

    #[test]
    fn map_path_to_wsl_strips_verbatim_prefix_for_wslpath() {
        let mapping = map_path_to_wsl(Path::new(r"\\?\D:\workspace")).unwrap();
        assert_eq!(
            mapping,
            PathMapping::NativeDrive {
                windows_path: "D:/workspace".to_string(),
                fallback: WslPath {
                    distro: None,
                    path: "/mnt/d/workspace".to_string(),
                },
            }
        );
    }

    #[test]
    fn path_resolution_args_flattens_mappings_into_triples() {
        let mappings = [
            PathMapping::NativeDrive {
                windows_path: "C:/Users/me/project".to_string(),
                fallback: WslPath {
                    distro: None,
                    path: "/mnt/c/Users/me/project".to_string(),
                },
            },
            PathMapping::Wsl(WslPath {
                distro: Some("Ubuntu".to_string()),
                path: "/home/me/project".to_string(),
            }),
        ];
        assert_eq!(
            path_resolution_args(mappings.iter()),
            [
                "W",
                "C:/Users/me/project",
                "/mnt/c/Users/me/project",
                "L",
                "/home/me/project",
                "",
            ]
        );
    }

    #[test]
    fn parse_path_resolution_output_reads_one_line_per_path() {
        let resolved = parse_path_resolution_output(
            "ok ok /mnt/c/Users/me/project\nfallback missing /mnt/d/workspace\n",
            2,
        )
        .unwrap();
        assert_eq!(
            resolved,
            [
                ResolvedPath {
                    path: "/mnt/c/Users/me/project".to_string(),
                    used_fallback: false,
                    exists: true,
                },
                ResolvedPath {
                    path: "/mnt/d/workspace".to_string(),
                    used_fallback: true,
                    exists: false,
                },
            ]
        );
    }

    #[test]
    fn parse_path_resolution_output_keeps_spaces_in_paths() {
        let resolved =
            parse_path_resolution_output("ok ok /mnt/c/Users/me/My Documents/project\n", 1)
                .unwrap();
        assert_eq!(resolved[0].path, "/mnt/c/Users/me/My Documents/project");
    }

    #[test]
    fn parse_path_resolution_output_rejects_wrong_line_count() {
        assert!(parse_path_resolution_output("ok ok /a\n", 2).is_err());
        assert!(parse_path_resolution_output("ok ok /a\nok ok /b\n", 1).is_err());
    }

    #[test]
    fn parse_path_resolution_output_rejects_corrupted_lines() {
        assert!(parse_path_resolution_output("garbage\n", 1).is_err());
        assert!(parse_path_resolution_output("weird ok /a\n", 1).is_err());
        assert!(parse_path_resolution_output("ok weird /a\n", 1).is_err());
        assert!(parse_path_resolution_output("ok ok not-absolute\n", 1).is_err());
    }
}
