//! High-level public API: [`Image`], [`Vm`], [`VmConfig`], [`Error`].
//!
//! These types wrap the lower-level [`crate::vmm`] primitives
//! (`WarmPool`, `VmResources`, …) into a small, stable surface
//! for embedders: load an image, start a VM, talk to its guest,
//! stop. The lower-level types remain available under
//! `#[doc(hidden)]` for the CLI / router / bench crates that
//! pre-date the narrowing.

use std::collections::VecDeque;
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::process::ChildStdin;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};

use crate::assets::AssetPaths;
// VM-run types live in the macOS-gated VMM stack (until the KVM backend lands).
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::vmm::pool::{PoolClientError, WarmPool, WarmPoolError};
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::vmm::resources::VmResources;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::vmm::runner::RunOptions;

/// All errors the high-level API can return. Designed to be
/// `match`able: the variants name *what failed*, not which
/// internal type produced it.
///
/// Each variant carries a human-readable `msg` plus an optional
/// typed `source: Option<Box<dyn std::error::Error + Send + Sync>>`
/// so callers can downcast to the underlying error
/// (`io::Error`, `WarmPoolError`, `PoolClientError`, ...) when
/// they need typed handling. `std::error::Error::source()` walks
/// this chain so `?` propagation preserves it through downstream
/// `Box<dyn Error>` conversions.
///
/// `#[non_exhaustive]` so future versions can add variants without
/// breaking exhaustive matches in consumer code.
#[non_exhaustive]
pub enum Error {
    /// The image / snapshot couldn't be loaded — bad path, bad
    /// magic bytes, or version mismatch.
    Image {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// VM start / restore failed. Includes `WarmPool` setup errors,
    /// HVF entitlement issues, missing assets, and pool-spawn
    /// failures. Downcast `source` to the typed cause where
    /// applicable.
    Vm {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// The configured assets (kernel, init shim) couldn't be
    /// located. Set [`VmConfig::with_assets`] explicitly to
    /// override auto-discovery.
    Assets {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// I/O on a vsock socket / file. The original `io::Error` is
    /// the variant payload — match `Error::Io(e)` and inspect
    /// `e.kind()` for typed handling.
    Io(std::io::Error),
    /// Registry pull failed — image manifest fetch, layer download,
    /// or auth handshake. Surface message includes the registry
    /// HTTP status / response body where available.
    Network {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// [`PullPolicy::Never`] was set but no usable cache exists.
    /// Switch to [`PullPolicy::Missing`] (the default) to allow a
    /// pull, or pre-bake via the `supermachine` CLI.
    CacheMiss { msg: String },
    /// A cached snapshot was found but isn't loadable on this
    /// binary — runtime SHA mismatch, snapshot format version
    /// mismatch, or corrupt/missing layer files. The error message
    /// names the specific reason. With [`PullPolicy::Missing`] /
    /// [`PullPolicy::Always`] the library auto-rebakes; only
    /// [`PullPolicy::Never`] surfaces this.
    CacheInvalid { msg: String },
    /// The bake step itself failed — snapshot capture timed out,
    /// the workload didn't bind a port within the readiness window,
    /// or the worker exited mid-bake. See `bake.log` in the
    /// snapshot dir for details.
    Bake {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// Registry returned 404 / "manifest unknown" for the
    /// requested image+tag — typo, deleted upstream, or wrong
    /// registry. The `image` field holds the full `host/repo:tag`
    /// reference as the user passed it, so callers can do
    /// `match e { Error::ImageNotFound { image, .. } => ... }`
    /// without re-parsing the string.
    ImageNotFound {
        image: String,
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// Registry returned 401 / 403 — bad credentials, expired
    /// token, or repo requires login. Set creds via
    /// `~/.docker/config.json` or `--registry-auth USER:PASS`.
    RegistryAuth {
        image: String,
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// Registry HTTP request failed at the network layer — DNS
    /// failure, connection refused, TLS handshake error, timeout
    /// on the wire. Distinct from [`Error::ImageNotFound`] /
    /// [`Error::RegistryAuth`] which surface registry-side errors
    /// after a successful connection.
    RegistryUnreachable {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
    /// Pool has reached its `max` worker count and the
    /// `acquire_timeout` elapsed before any peer dropped its
    /// `PooledVm`. Increase `max`, increase `acquire_timeout`,
    /// or retry. Carries the elapsed timeout + max in the
    /// message for diagnostics.
    PoolExhausted { msg: String },
    /// The in-guest Linux kernel panicked during the bake's boot
    /// or init phase. Detected by tailing the worker's serial
    /// console (`bake.log`) for known fatal banners
    /// (`Kernel panic`, `Internal error: Oops`, `Unable to handle
    /// kernel ...`, `Attempted to kill the idle task`). On detection
    /// the worker child is killed so the bake doesn't hang for the
    /// full timeout. `first_line` is the matched banner; `stack` is
    /// the next ~40 lines of the panic trace pulled from `bake.log`.
    KernelPanic {
        first_line: String,
        stack: Vec<String>,
    },
}

// Constructor helpers that callers in this crate use. Keeping
// the public surface field-style (struct variants) means new
// fields are non-breaking; the callers below all funnel through
// these so we can evolve the construction shape later without
// touching every call site.
//
// Public API is the variants themselves; these are pub(crate).
impl Error {
    pub(crate) fn image_msg(msg: impl Into<String>) -> Self {
        Error::Image {
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn vm_msg(msg: impl Into<String>) -> Self {
        Error::Vm {
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn assets_msg(msg: impl Into<String>) -> Self {
        Error::Assets {
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn network_msg(msg: impl Into<String>) -> Self {
        Error::Network {
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn bake_msg(msg: impl Into<String>) -> Self {
        Error::Bake {
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn cache_miss(msg: impl Into<String>) -> Self {
        Error::CacheMiss { msg: msg.into() }
    }
    pub(crate) fn cache_invalid(msg: impl Into<String>) -> Self {
        Error::CacheInvalid { msg: msg.into() }
    }
    pub(crate) fn image_not_found(image: impl Into<String>, msg: impl Into<String>) -> Self {
        Error::ImageNotFound {
            image: image.into(),
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn registry_auth(image: impl Into<String>, msg: impl Into<String>) -> Self {
        Error::RegistryAuth {
            image: image.into(),
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn registry_unreachable(msg: impl Into<String>) -> Self {
        Error::RegistryUnreachable {
            msg: msg.into(),
            source: None,
        }
    }
    pub(crate) fn pool_exhausted(msg: impl Into<String>) -> Self {
        Error::PoolExhausted { msg: msg.into() }
    }
    pub(crate) fn kernel_panic(first_line: impl Into<String>, stack: Vec<String>) -> Self {
        Error::KernelPanic {
            first_line: first_line.into(),
            stack,
        }
    }

    /// True when the message looks like the agent-probe symptom
    /// of a multi-vCPU restore RCU-stall (probe timed out / EAGAIN
    /// in the underlying vsock read). Heuristic — used to
    /// downgrade probe failures to a warning when the snapshot
    /// was baked with vcpus > 1. Definitive stale-agent signals
    /// (the agent ack with an old protocol number) report a
    /// different message and are NOT covered.
    pub(crate) fn is_likely_multi_vcpu_restore_stall(&self) -> bool {
        let Error::Vm { msg, .. } = self else {
            return false;
        };
        // The probe's two failure modes that are indistinguishable
        // from a multi-vCPU RCU-stall:
        //   1. send_control_with_ack returns io::ErrorKind::Other
        //      with the underlying socket error string (most often
        //      "Resource temporarily unavailable" / EAGAIN, or a
        //      timeout phrase).
        //   2. The probe wrapper turned that into the
        //      "agent in this snapshot is from an older …" string;
        //      check for the embedded "(probe failed: …)".
        msg.contains("Resource temporarily unavailable")
            || msg.contains("os error 35")
            || msg.contains("os error 60") // ETIMEDOUT
            || msg.contains("timed out")
            || (msg.contains("probe failed") && !msg.contains("speaks protocol v"))
    }
}

impl std::fmt::Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Image { msg, source } => f
                .debug_struct("Image")
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::Vm { msg, source } => f
                .debug_struct("Vm")
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::Assets { msg, source } => f
                .debug_struct("Assets")
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::Io(e) => f.debug_tuple("Io").field(e).finish(),
            Error::Network { msg, source } => f
                .debug_struct("Network")
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::CacheMiss { msg } => f.debug_struct("CacheMiss").field("msg", msg).finish(),
            Error::CacheInvalid { msg } => {
                f.debug_struct("CacheInvalid").field("msg", msg).finish()
            }
            Error::Bake { msg, source } => f
                .debug_struct("Bake")
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::ImageNotFound { image, msg, source } => f
                .debug_struct("ImageNotFound")
                .field("image", image)
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::RegistryAuth { image, msg, source } => f
                .debug_struct("RegistryAuth")
                .field("image", image)
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::RegistryUnreachable { msg, source } => f
                .debug_struct("RegistryUnreachable")
                .field("msg", msg)
                .field("source", source)
                .finish(),
            Error::PoolExhausted { msg } => {
                f.debug_struct("PoolExhausted").field("msg", msg).finish()
            }
            Error::KernelPanic { first_line, stack } => f
                .debug_struct("KernelPanic")
                .field("first_line", first_line)
                .field("stack_lines", &stack.len())
                .finish(),
        }
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Image { msg, .. } => write!(f, "image: {msg}"),
            Error::Vm { msg, .. } => write!(f, "vm: {msg}"),
            Error::Assets { msg, .. } => write!(f, "assets: {msg}"),
            Error::Io(e) => write!(f, "io: {e}"),
            Error::Network { msg, .. } => write!(f, "network: {msg}"),
            Error::CacheMiss { msg } => write!(f, "cache miss: {msg}"),
            Error::CacheInvalid { msg } => write!(f, "cache invalid: {msg}"),
            Error::Bake { msg, .. } => write!(f, "bake: {msg}"),
            Error::ImageNotFound { image, msg, .. } => {
                write!(f, "image not found ({image}): {msg}")
            }
            Error::RegistryAuth { image, msg, .. } => {
                write!(f, "registry auth failed for {image}: {msg}")
            }
            Error::RegistryUnreachable { msg, .. } => {
                write!(f, "registry unreachable: {msg}")
            }
            Error::PoolExhausted { msg } => write!(f, "pool exhausted: {msg}"),
            Error::KernelPanic { first_line, stack } => {
                writeln!(f, "kernel panic during bake: {first_line}")?;
                for line in stack {
                    writeln!(f, "  {line}")?;
                }
                Ok(())
            }
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Image { source, .. }
            | Error::Vm { source, .. }
            | Error::Assets { source, .. }
            | Error::Network { source, .. }
            | Error::Bake { source, .. }
            | Error::ImageNotFound { source, .. }
            | Error::RegistryAuth { source, .. }
            | Error::RegistryUnreachable { source, .. } => source
                .as_ref()
                .map(|s| s.as_ref() as &(dyn std::error::Error + 'static)),
            Error::Io(e) => Some(e),
            Error::CacheMiss { .. }
            | Error::CacheInvalid { .. }
            | Error::PoolExhausted { .. }
            | Error::KernelPanic { .. } => None,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::Io(e)
    }
}

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
impl From<WarmPoolError> for Error {
    fn from(e: WarmPoolError) -> Self {
        Error::Vm {
            msg: e.to_string(),
            source: Some(Box::new(e)),
        }
    }
}

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
impl From<PoolClientError> for Error {
    fn from(e: PoolClientError) -> Self {
        Error::Vm {
            msg: e.to_string(),
            source: Some(Box::new(e)),
        }
    }
}

/// How [`Image::from_oci`] decides whether to talk to the registry
/// or use a locally-cached snapshot. Same semantics as Docker's
/// `--pull` flag.
///
/// **The default is [`PullPolicy::Missing`]** — use the cache if
/// it exists; pull only if absent. Right for pinned tags or digest
/// references. For `:latest`-style mutable tags use
/// [`PullPolicy::Always`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PullPolicy {
    /// Pull manifest from the registry every time; rebake if the
    /// digest changed since the last bake. Right for `:latest`-style
    /// mutable tags.
    Always,
    /// Use the cached snapshot if it exists locally and is valid.
    /// Don't talk to the registry at all unless the cache is
    /// missing or invalid. **The default.**
    Missing,
    /// Use the cache or fail. Never pull. Right for offline /
    /// air-gapped environments.
    Never,
}

impl Default for PullPolicy {
    fn default() -> Self {
        Self::Missing
    }
}

impl PullPolicy {
    /// String form the underlying `bake` pipeline accepts. Mirror
    /// of the CLI's `--pull` argument values.
    fn as_bake_str(self) -> &'static str {
        match self {
            Self::Always => "always",
            Self::Missing => "missing",
            Self::Never => "never",
        }
    }
}

/// A baked OCI image: its restore snapshot plus the metadata
/// describing which kernel + virtio-blk layers it needs. Cheap
/// to clone.
///
/// Two ways to construct one:
///
/// - [`Image::from_oci`] — pull (or reuse cache) from a registry,
///   bake into a snapshot, return the resulting image. The
///   high-level "I have an image reference" entry point.
/// - [`Image::from_snapshot`] — load an already-baked snapshot
///   directory directly. Useful when you want to keep snapshots
///   under your own management or share one across processes.
/// Linux/KVM boot artifacts an [`Image`] carries when its `metadata.json` has
/// `"backend": "kvm"`. The [`Vm::start`] Linux path boots these directly via
/// [`crate::kvm::run::LinuxVm`] (kernel + agent PID-1 initramfs, with the
/// optional `disk` attached as `/dev/vda`). The HVF `restore.snap` /
/// virtio-fs-layer model does not apply — a KVM image boots cold.
#[derive(Debug, Clone)]
pub(crate) struct KvmImageParts {
    /// Cold-boot kernel (bzImage). `None` for snapshot-restore images.
    pub(crate) kernel: Option<PathBuf>,
    /// Cold-boot PID-1 agent initramfs. `None` for snapshot-restore images.
    pub(crate) initrd: Option<PathBuf>,
    /// Optional rootfs disk attached as `/dev/vda` (cold boot only).
    pub(crate) disk: Option<PathBuf>,
    /// A `VmSnapshot` file (SMSNAP03). When set, the VM is RESTORED from it
    /// (CoW mmap, ~ms) instead of cold-booting from kernel+initrd — produced
    /// by [`Vm::snapshot`].
    pub(crate) snapshot: Option<PathBuf>,
}

/// Artifacts for a `"backend":"sentry"` image (the no-virt process-isolation
/// backend — see [`crate::sentry`]). The sentry has no guest kernel and no
/// disk image: it serves the guest's files straight from an extracted rootfs
/// DIRECTORY via `openat2(RESOLVE_IN_ROOT)`, so the only artifact is that dir
/// plus a flag for whether a workload-launch script was baked into it.
#[derive(Debug, Clone)]
pub(crate) struct SentryImageParts {
    /// The extracted, layer-merged rootfs directory (the bake's output; NOT a
    /// squashfs). `crate::sentry::run` confines every guest path beneath it.
    pub(crate) rootfs: PathBuf,
    /// Whether `/.supermachine/run-workload` was staged into `rootfs` (i.e. the
    /// OCI image declared an Entrypoint/Cmd). When false, the image has no
    /// command to launch and [`Image::run_sentry`] is a no-op success.
    pub(crate) has_workload: bool,
    /// The baked image's `User` (`metadata.json["user"]`), parsed into a numeric
    /// `(uid, gid)` pair iff it was a numeric `uid` or `uid:gid` form. The sentry
    /// drops the guest cell to this uid/gid before sealing (defense in depth — a
    /// hostile guest is host-unprivileged). `None` for `root`/empty/username-only
    /// `User` values (the sentry can't resolve names without a passwd lookup, so it
    /// leaves the guest at the sentry's own uid in that case).
    pub(crate) user: Option<(u32, u32)>,
    /// The baked image environment (`metadata.json["image_env"]`, the OCI config's
    /// `Env` parsed at bake time) as `(KEY, VALUE)` pairs. Used as the env BASE for
    /// every `exec` against the sentry: a per-request env overlays it (request keys
    /// win), so a command run via `Vm::exec` sees the image's `PATH`/`LANG`/etc.
    /// just as it would inside a booted VM.
    pub(crate) image_env: Vec<(String, String)>,
    /// The effective workload argv recorded at bake (`metadata.json["cmd"]`, or
    /// `["entrypoint"]` when only an Entrypoint was declared). For a warm-daemon
    /// image this is the argv [`crate::sentry::Pool::warm`] parks at the SENTINEL
    /// checkpoint. Empty when the base image declared no Cmd/Entrypoint.
    pub(crate) cmd: Vec<String>,
    /// Whether this image was baked for the warm-daemon pool backend
    /// (`Image::builder(..).with_warm_daemon()`). Selects the `SentryPool` backend
    /// variant at pool-build time. `false` for legacy/base/ExecServer images.
    pub(crate) warm_daemon: bool,
}

/// Artifacts for a `"backend":"runsc"` image. This is the external no-KVM
/// backend path backed by gVisor/runsc systrap checkpoint/restore. It starts
/// from the same extracted OCI rootfs model as sentry, but runtime state is a
/// runsc container checkpoint rather than a sentry in-process cell snapshot.
#[derive(Debug, Clone)]
pub(crate) struct RunscImageParts {
    /// The extracted, layer-merged rootfs directory used as the OCI bundle root.
    pub(crate) rootfs: PathBuf,
    /// Metadata memory size preserved for API parity and snapshot round-trips.
    pub(crate) memory_mib: u32,
    /// Metadata vCPU count preserved for API parity and snapshot round-trips.
    pub(crate) vcpus: u32,
    /// Whether `/.supermachine/run-workload` was staged into `rootfs`.
    pub(crate) has_workload: bool,
    /// Numeric `User` parsed from metadata when available.
    pub(crate) user: Option<(u32, u32)>,
    /// The baked image environment (`metadata.json["image_env"]`).
    pub(crate) image_env: Vec<(String, String)>,
    /// The effective workload argv recorded at bake.
    pub(crate) cmd: Vec<String>,
    /// Whether this image was requested as a warm-daemon image.
    pub(crate) warm_daemon: bool,
    /// Optional runsc checkpoint captured by a build warmup.
    pub(crate) checkpoint: Option<PathBuf>,
    /// Host directory mounts to expose as OCI bind mounts.
    pub(crate) mounts: Vec<crate::vmm::resources::MountSpec>,
    /// Guest path of the staged supermachine agent binary.
    pub(crate) agent_path: String,
    /// Env var the agent reads to choose Unix-socket transport.
    pub(crate) agent_unix_env: String,
}

/// Artifacts for a `"backend":"kboxlike"` image. This is the native Rust
/// LKL/kbox-inspired no-KVM backend under construction. It intentionally uses
/// the same extracted OCI rootfs shape as sentry/runsc while the execution
/// layer is brought up behind the metadata boundary.
#[derive(Debug, Clone)]
pub(crate) struct KboxlikeImageParts {
    /// The extracted, layer-merged rootfs directory.
    pub(crate) rootfs: PathBuf,
    /// Metadata memory size preserved for API parity and snapshot round-trips.
    pub(crate) memory_mib: u32,
    /// Metadata vCPU count preserved for API parity and snapshot round-trips.
    pub(crate) vcpus: u32,
    /// Whether `/.supermachine/run-workload` was staged into `rootfs`.
    pub(crate) has_workload: bool,
    /// Numeric `User` parsed from metadata when available.
    pub(crate) user: Option<(u32, u32)>,
    /// The baked image environment (`metadata.json["image_env"]`).
    pub(crate) image_env: Vec<(String, String)>,
    /// The effective workload argv recorded at bake.
    pub(crate) cmd: Vec<String>,
    /// Whether this image was requested as a warm-daemon image.
    pub(crate) warm_daemon: bool,
    /// Host directory mounts preserved for the future kboxlike runtime.
    pub(crate) mounts: Vec<crate::vmm::resources::MountSpec>,
    /// Writable volumes persisted in metadata. The kboxlike no-virt runtime
    /// maps these as directory-backed bind mounts, matching the sentry backend.
    pub(crate) volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
struct RunscRuntime {
    runsc_bin: PathBuf,
    runsc_root: PathBuf,
    network: String,
    build_netns: Option<RunscBuildNetwork>,
    container_id: String,
    bundle_dir: PathBuf,
    rootfs: PathBuf,
    parts: RunscImageParts,
    file_agent_exec: bool,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
struct RunscPortForwardTarget {
    runsc_bin: PathBuf,
    runsc_root: PathBuf,
    network: String,
    container_id: String,
    agent_path: String,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct RunscPoolInner {
    parts: RunscImageParts,
    vm_config: VmConfig,
    min: usize,
    max: usize,
    warm_spares: usize,
    runsc_bin: PathBuf,
    runsc_root: PathBuf,
    network: String,
    build_netns: Option<RunscBuildNetwork>,
    pool_dir: PathBuf,
    base_bundle_dir: PathBuf,
    base_rootfs: PathBuf,
    checkpoint_dir: Option<PathBuf>,
    admission: Admission,
    checked_out: std::sync::atomic::AtomicUsize,
    idle: Mutex<Vec<Vm>>,
    restore_on_release: bool,
    shut_down: std::sync::atomic::AtomicBool,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct KboxlikePoolInner {
    parts: KboxlikeImageParts,
    vm_config: VmConfig,
    build_root: Option<PathBuf>,
    min: usize,
    admission: Admission,
    idle: Mutex<Vec<Vm>>,
    restore_on_release: bool,
    refilling: std::sync::atomic::AtomicBool,
    shut_down: std::sync::atomic::AtomicBool,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
struct RunscBuildNetwork {
    name: String,
    host_veth: String,
    subnet: String,
    iface: String,
    old_ip_forward: Option<String>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RunscRuntime {
    fn cleanup(&self) {
        let mut kill = Command::new(&self.runsc_bin);
        crate::exec::add_runsc_global_flags_with_network(
            &mut kill,
            &self.runsc_root,
            &self.network,
        );
        let _ = kill
            .arg("kill")
            .arg(&self.container_id)
            .arg("KILL")
            .status();

        let mut delete = Command::new(&self.runsc_bin);
        crate::exec::add_runsc_global_flags_with_network(
            &mut delete,
            &self.runsc_root,
            &self.network,
        );
        let _ = delete
            .arg("delete")
            .arg("-force")
            .arg(&self.container_id)
            .status();

        let _ = Command::new("umount").arg("-l").arg(&self.rootfs).status();

        if let Some(dir) = self.bundle_dir.parent() {
            let _ = std::fs::remove_dir_all(dir);
        }
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for RunscRuntime {
    fn drop(&mut self) {
        self.cleanup();
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
struct KboxlikeRuntime {
    runtime_dir: PathBuf,
    source_root_host_pid: Option<i32>,
    restored_host_pids: Vec<i32>,
    rootfs: PathBuf,
    parts: KboxlikeImageParts,
    mounts: Vec<(PathBuf, String)>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
struct KboxlikePortForwardTarget {
    restored_host_pids: Vec<i32>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KboxlikeRuntime {
    fn cleanup(&mut self) {
        if let Some(pid) = self.source_root_host_pid.take() {
            crate::kboxlike::kill_and_reap_live_process_group(pid);
        }
        let mut pending: std::collections::BTreeSet<i32> =
            self.restored_host_pids.drain(..).collect();
        for pid in pending.iter().copied() {
            unsafe {
                libc::kill(pid, libc::SIGKILL);
            }
        }
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
        while !pending.is_empty() && std::time::Instant::now() <= deadline {
            let mut status = 0;
            let waited = unsafe { libc::waitpid(-1, &mut status, libc::__WALL | libc::WNOHANG) };
            if waited > 0 {
                pending.remove(&waited);
            } else if waited == 0 {
                std::thread::sleep(std::time::Duration::from_millis(1));
            } else {
                break;
            }
        }
        let _ = std::fs::remove_dir_all(&self.runtime_dir);
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for KboxlikeRuntime {
    fn drop(&mut self) {
        self.cleanup();
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for KboxlikePoolInner {
    fn drop(&mut self) {
        self.shut_down
            .store(true, std::sync::atomic::Ordering::SeqCst);
        self.idle.lock().unwrap_or_else(|e| e.into_inner()).clear();
        if let Some(dir) = self.build_root.take() {
            let _ = std::fs::remove_dir_all(dir);
        }
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_host_exe_path(parts: &KboxlikeImageParts, argv0: Option<&str>) -> String {
    let Some(argv0) = argv0.filter(|value| !value.is_empty()) else {
        return std::env::current_exe()
            .ok()
            .map(|path| path.to_string_lossy().into_owned())
            .unwrap_or_else(|| "supermachine".to_owned());
    };
    if let Some(guest_abs) = argv0.strip_prefix('/') {
        return parts.rootfs.join(guest_abs).to_string_lossy().into_owned();
    }
    argv0.to_owned()
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_timing_enabled() -> bool {
    matches!(
        std::env::var("SUPERMACHINE_RUNSC_TIMINGS").as_deref(),
        Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
    )
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_timing(label: &str, start: Instant) {
    if runsc_timing_enabled() {
        eprintln!(
            "supermachine(runsc timing): {label} {}ms",
            start.elapsed().as_millis()
        );
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_pool_spares(max: usize, restore_on_release: bool) -> usize {
    if !restore_on_release {
        return 0;
    }
    if let Ok(raw) = std::env::var("SUPERMACHINE_RUNSC_POOL_SPARES") {
        if let Ok(n) = raw.parse::<usize>() {
            return n;
        }
    }
    ((max + 1) / 2).clamp(1, 4).min(max.max(1))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RunscBuildNetwork {
    fn create() -> Result<Self, Error> {
        let iface = runsc_default_host_iface()?;
        let suffix = format!("{:x}", unique_suffix());
        let short = &suffix[..suffix.len().min(8)];
        let name = format!("sm{short}");
        let host_veth = format!("vh{short}");
        let guest_veth = format!("vg{short}");
        let octet = 32 + (std::process::id() % 160);
        let subnet = format!("10.231.{octet}.0/24");
        let host_ip = format!("10.231.{octet}.1");
        let guest_ip = format!("10.231.{octet}.2");
        let old_ip_forward = std::fs::read_to_string("/proc/sys/net/ipv4/ip_forward")
            .ok()
            .map(|s| s.trim().to_owned());

        let net = Self {
            name,
            host_veth,
            subnet,
            iface,
            old_ip_forward,
        };
        if let Err(e) = net.setup(&guest_veth, &host_ip, &guest_ip) {
            net.cleanup();
            return Err(e);
        }
        Ok(net)
    }

    fn path(&self) -> PathBuf {
        PathBuf::from(format!("/run/netns/{}", self.name))
    }

    fn setup(&self, guest_veth: &str, host_ip: &str, guest_ip: &str) -> Result<(), Error> {
        runsc_host_cmd("sysctl", &["-w", "net.ipv4.ip_forward=1"])?;
        runsc_host_cmd("ip", &["netns", "add", &self.name])?;
        runsc_host_cmd(
            "ip",
            &[
                "link",
                "add",
                &self.host_veth,
                "type",
                "veth",
                "peer",
                "name",
                guest_veth,
            ],
        )?;
        runsc_host_cmd("ip", &["link", "set", guest_veth, "netns", &self.name])?;
        let host_cidr = format!("{host_ip}/24");
        runsc_host_cmd("ip", &["addr", "add", &host_cidr, "dev", &self.host_veth])?;
        runsc_host_cmd("ip", &["link", "set", &self.host_veth, "up"])?;
        let guest_cidr = format!("{guest_ip}/24");
        runsc_host_cmd(
            "ip",
            &[
                "netns",
                "exec",
                &self.name,
                "ip",
                "addr",
                "add",
                &guest_cidr,
                "dev",
                guest_veth,
            ],
        )?;
        runsc_host_cmd(
            "ip",
            &["netns", "exec", &self.name, "ip", "link", "set", "lo", "up"],
        )?;
        runsc_host_cmd(
            "ip",
            &[
                "netns", "exec", &self.name, "ip", "link", "set", guest_veth, "up",
            ],
        )?;
        runsc_host_cmd(
            "ip",
            &[
                "netns", "exec", &self.name, "ip", "route", "add", "default", "via", host_ip,
            ],
        )?;
        runsc_host_cmd(
            "iptables",
            &[
                "-t",
                "nat",
                "-A",
                "POSTROUTING",
                "-s",
                &self.subnet,
                "-o",
                &self.iface,
                "-j",
                "MASQUERADE",
            ],
        )
    }

    fn cleanup(&self) {
        let _ = Command::new("iptables")
            .args([
                "-t",
                "nat",
                "-D",
                "POSTROUTING",
                "-s",
                &self.subnet,
                "-o",
                &self.iface,
                "-j",
                "MASQUERADE",
            ])
            .status();
        let _ = Command::new("ip")
            .args(["link", "del", &self.host_veth])
            .status();
        let _ = Command::new("ip")
            .args(["netns", "del", &self.name])
            .status();
        if let Some(old) = &self.old_ip_forward {
            let setting = format!("net.ipv4.ip_forward={old}");
            let _ = Command::new("sysctl").args(["-w", &setting]).status();
        }
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for RunscBuildNetwork {
    fn drop(&mut self) {
        self.cleanup();
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_host_cmd(program: &str, args: &[&str]) -> Result<(), Error> {
    let out = Command::new(program)
        .args(args)
        .output()
        .map_err(|e| Error::vm_msg(format!("{program} {}: {e}", args.join(" "))))?;
    if out.status.success() {
        Ok(())
    } else {
        Err(Error::vm_msg(format!(
            "{program} {} failed with status {:?}: {}",
            args.join(" "),
            out.status.code(),
            String::from_utf8_lossy(&out.stderr)
        )))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_default_host_iface() -> Result<String, Error> {
    let routes = std::fs::read_to_string("/proc/net/route").map_err(Error::Io)?;
    for line in routes.lines().skip(1) {
        let fields: Vec<_> = line.split_whitespace().collect();
        if fields.len() > 1 && fields[1] == "00000000" {
            return Ok(fields[0].to_owned());
        }
    }
    Err(Error::vm_msg(
        "runsc build network: host default route not found".to_owned(),
    ))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_network_for_config(config: &VmConfig) -> &'static str {
    if config.for_build {
        crate::exec::RUNSC_NETWORK_SANDBOX
    } else {
        crate::exec::RUNSC_NETWORK_NONE
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RunscPoolInner {
    fn build(
        image: &Image,
        config: &VmConfig,
        min: usize,
        max: usize,
        acquire_timeout: Option<Duration>,
        restore_on_release: bool,
    ) -> Result<Arc<Self>, Error> {
        let parts = image
            .runsc
            .as_ref()
            .ok_or_else(|| Error::vm_msg("RunscPoolInner::build on a non-runsc image"))?
            .clone();
        let runsc_bin = runsc_bin_path();
        let network = runsc_network_for_config(config).to_owned();
        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        std::fs::create_dir_all(&dir).map_err(Error::Io)?;
        let pool_dir = dir.join(format!(
            "supermachine-runsc-pool-{}-{}",
            std::process::id(),
            unique_suffix()
        ));
        let base_bundle_dir = pool_dir.join("base-bundle");
        let base_rootfs = base_bundle_dir.join("rootfs");
        let runsc_root = pool_dir.join("runsc-root");
        std::fs::create_dir_all(&runsc_root).map_err(Error::Io)?;
        let build_netns = if network == crate::exec::RUNSC_NETWORK_SANDBOX {
            Some(RunscBuildNetwork::create()?)
        } else {
            None
        };
        let netns_path = build_netns.as_ref().map(|net| net.path());
        prepare_runsc_bundle(
            &runsc_bin,
            &parts,
            &base_bundle_dir,
            &base_rootfs,
            &network,
            netns_path.as_deref(),
        )?;

        let checkpoint_dir = if network == crate::exec::RUNSC_NETWORK_SANDBOX {
            None
        } else {
            let checkpoint_dir = pool_dir.join("checkpoint");
            std::fs::create_dir_all(&checkpoint_dir).map_err(Error::Io)?;
            Some(checkpoint_dir)
        };

        if let (Some(source_checkpoint), Some(checkpoint_dir)) =
            (&parts.checkpoint, checkpoint_dir.as_ref())
        {
            if let Err(e) = copy_dir_recursive(source_checkpoint, &checkpoint_dir) {
                let _ = std::fs::remove_dir_all(&pool_dir);
                return Err(Error::vm_msg(format!(
                    "runsc checkpoint clone {} -> {}: {e}",
                    source_checkpoint.display(),
                    checkpoint_dir.display()
                )));
            }
        } else if let Some(checkpoint_dir) = checkpoint_dir.as_ref() {
            let warm_id = format!("sm-runsc-warm-{}-{}", std::process::id(), unique_suffix());
            let pid_file = pool_dir.join("warm.pid");
            if let Err(e) = runsc_run_detached(
                &runsc_bin,
                &runsc_root,
                &network,
                &base_bundle_dir,
                &pid_file,
                &warm_id,
            ) {
                let _ = std::fs::remove_dir_all(&pool_dir);
                return Err(e);
            }
            let checkpoint =
                runsc_checkpoint(&runsc_bin, &runsc_root, &network, &checkpoint_dir, &warm_id);
            let _ = runsc_delete_force(&runsc_bin, &runsc_root, &network, &warm_id);
            if let Err(e) = checkpoint {
                let _ = std::fs::remove_dir_all(&pool_dir);
                return Err(e);
            }
        }

        let max = max.max(1);
        let min = min.min(max);
        let warm_spares = if min > 0 {
            runsc_pool_spares(max, restore_on_release)
        } else {
            0
        };
        let inner = Arc::new(Self {
            parts,
            vm_config: config.clone(),
            min,
            max,
            warm_spares,
            runsc_bin,
            runsc_root,
            network,
            build_netns,
            pool_dir,
            base_bundle_dir,
            base_rootfs,
            checkpoint_dir,
            admission: Admission::new(max.saturating_add(warm_spares), acquire_timeout),
            checked_out: std::sync::atomic::AtomicUsize::new(0),
            idle: Mutex::new(Vec::new()),
            restore_on_release,
            shut_down: std::sync::atomic::AtomicBool::new(false),
        });
        inner.prewarm_to_min();
        Ok(inner)
    }

    fn warm_target(&self) -> usize {
        self.min.saturating_add(self.warm_spares)
    }

    fn try_checkout(&self) -> bool {
        use std::sync::atomic::Ordering::SeqCst;
        loop {
            let cur = self.checked_out.load(SeqCst);
            if cur >= self.max {
                return false;
            }
            if self
                .checked_out
                .compare_exchange(cur, cur + 1, SeqCst, SeqCst)
                .is_ok()
            {
                return true;
            }
        }
    }

    fn release_checkout(&self) {
        self.checked_out
            .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
        self.admission.notify();
    }

    fn acquire_vm(&self) -> Result<Vm, Error> {
        let deadline = self
            .admission
            .acquire_timeout
            .map(|d| std::time::Instant::now() + d);
        loop {
            if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
                return Err(Error::pool_exhausted("pool is shut down"));
            }
            if self.checked_out.load(std::sync::atomic::Ordering::SeqCst) < self.max {
                let mut idle = self.idle.lock().unwrap_or_else(|e| e.into_inner());
                if !idle.is_empty() && self.try_checkout() {
                    return Ok(idle.pop().expect("idle checked above"));
                }
            }
            if self.try_checkout() {
                if self.admission.try_reserve() {
                    return self.restore_one().inspect_err(|_| {
                        self.release_checkout();
                        self.admission.release();
                    });
                }
                self.release_checkout();
            }
            if !self.admission.wait_for_change(deadline)? {
                return Err(Error::pool_exhausted(format!(
                    "runsc pool saturated: all {} checkout slots in use and none freed within \
                     acquire_timeout",
                    self.max
                )));
            }
        }
    }

    fn restore_one(&self) -> Result<Vm, Error> {
        let restore_total = Instant::now();
        let runtime_dir = self.pool_dir.join(format!(
            "restore-{}-{}",
            std::process::id(),
            unique_suffix()
        ));
        let bundle_dir = runtime_dir.join("bundle");
        let rootfs = bundle_dir.join("rootfs");
        let phase = Instant::now();
        std::fs::create_dir_all(&bundle_dir).map_err(Error::Io)?;
        prepare_runsc_restore_rootfs(&self.base_rootfs, &runtime_dir, &rootfs)?;
        runsc_timing("restore.prepare_rootfs", phase);
        let netns_path = self.build_netns.as_ref().map(|net| net.path());
        let phase = Instant::now();
        copy_runsc_bundle_config(
            &self.base_bundle_dir,
            &bundle_dir,
            &self.parts,
            &rootfs,
            &self.network,
            netns_path.as_deref(),
        )?;
        runsc_timing("restore.bundle_config", phase);

        let container_id = format!(
            "sm-runsc-restored-{}-{}",
            std::process::id(),
            unique_suffix()
        );
        let pid_file = runtime_dir.join("container.pid");
        if let Some(checkpoint_dir) = &self.checkpoint_dir {
            let phase = Instant::now();
            if let Err(e) = runsc_restore_detached(
                &self.runsc_bin,
                &self.runsc_root,
                &self.network,
                &bundle_dir,
                checkpoint_dir,
                &pid_file,
                &container_id,
            ) {
                let _ = std::fs::remove_dir_all(&runtime_dir);
                return Err(e);
            }
            runsc_timing("restore.runsc_restore", phase);
        } else {
            let phase = Instant::now();
            if let Err(e) = runsc_run_detached(
                &self.runsc_bin,
                &self.runsc_root,
                &self.network,
                &bundle_dir,
                &pid_file,
                &container_id,
            ) {
                let _ = std::fs::remove_dir_all(&runtime_dir);
                return Err(e);
            }
            runsc_timing("restore.runsc_run", phase);
        }
        runsc_timing("restore.total", restore_total);

        Ok(Vm {
            running: None,
            sentry_exec: None,
            sentry_src_rootfs: None,
            vsock_mux_path: runtime_dir.join("runsc-unwired.sock"),
            vsock_exec_path: runtime_dir.join("runsc-exec-unwired.sock"),
            own_vsock_mux_dir: None,
            skip_cleanup: false,
            time_sync_stop: Arc::new(AtomicBool::new(false)),
            _sentry_mem_admission: None,
            sentry_warm_pool: None,
            sentry_unified_pool: None,
            runsc: Some(RunscRuntime {
                runsc_bin: self.runsc_bin.clone(),
                runsc_root: self.runsc_root.clone(),
                network: self.network.clone(),
                build_netns: None,
                container_id,
                bundle_dir,
                rootfs,
                parts: self.parts.clone(),
                file_agent_exec: false,
            }),
            kboxlike: None,
        })
    }

    fn prewarm_to_min(&self) {
        loop {
            let idle_len = self.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
            if idle_len >= self.warm_target() || !self.admission.try_reserve() {
                break;
            }
            match self.restore_one() {
                Ok(vm) => {
                    self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
                    self.admission.notify();
                }
                Err(e) => {
                    eprintln!("supermachine(runsc): prewarm restore failed: {e}");
                    self.admission.release();
                    break;
                }
            }
        }
    }

    fn release_vm(self: &Arc<Self>, vm: Vm) {
        if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
            self.release_checkout();
            drop(vm);
            self.admission.release();
            return;
        }
        if !self.restore_on_release {
            self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
            self.release_checkout();
            self.admission.notify();
            return;
        }

        self.release_checkout();
        let inner = Arc::clone(self);
        if let Err(e) = std::thread::Builder::new()
            .name("supermachine-runsc-refill".into())
            .spawn(move || inner.recycle_released_vm(vm))
        {
            eprintln!("supermachine(runsc): spawn refill thread failed: {e}");
            self.admission.release();
        }
    }

    fn recycle_released_vm(self: Arc<Self>, vm: Vm) {
        let total = Instant::now();
        drop(vm);
        runsc_timing("release.drop_dirty", total);
        if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
            self.admission.release();
            return;
        }
        let keep_warm =
            self.idle.lock().unwrap_or_else(|e| e.into_inner()).len() < self.warm_target();
        if keep_warm {
            match self.restore_one() {
                Ok(fresh) => {
                    self.idle
                        .lock()
                        .unwrap_or_else(|e| e.into_inner())
                        .push(fresh);
                    self.admission.notify();
                }
                Err(e) => {
                    eprintln!("supermachine(runsc): restore_on_release refill failed: {e}");
                    self.admission.release();
                }
            }
        } else {
            self.admission.release();
        }
        runsc_timing("release.total", total);
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for RunscPoolInner {
    fn drop(&mut self) {
        self.shut_down
            .store(true, std::sync::atomic::Ordering::SeqCst);
        self.admission.notify();
        let drained: Vec<Vm> =
            std::mem::take(&mut *self.idle.lock().unwrap_or_else(|e| e.into_inner()));
        for vm in drained {
            drop(vm);
        }
        let _ = std::fs::remove_dir_all(&self.pool_dir);
    }
}

/// Run-config for a sentry bake (Linux/x86_64): the [`OciImageBuilder`] fields
/// that the no-virt backend honors. The KVM/HVF backends carry these through
/// their own bake pipelines; the sentry path applies them in
/// [`Image::bake_sentry_with_config`].
///
/// Construct via [`SentryBakeConfig::default`] (the base image's own config) and
/// fill the fields the builder set. Not `Clone`/`Debug` because `warmup` is a
/// boxed `FnOnce`.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
pub struct SentryBakeConfig {
    /// Default memory recorded in image metadata; VmConfig can still override it.
    pub memory_mib: Option<u32>,
    /// Default vCPU count recorded in image metadata; VmConfig can still override it.
    pub vcpus: Option<u32>,
    /// Override the workload argv (`Entrypoint`/`Cmd`). `None` → the image's own.
    pub cmd: Option<Vec<String>>,
    /// Extra env merged OVER the image `Env` (builder wins on key collision).
    pub env: Vec<(String, String)>,
    /// Override the workload's working directory. `None` → the image's.
    pub working_dir: Option<String>,
    /// Override the recorded `user`. `None` → the image's.
    pub user: Option<String>,
    /// Files staged into the rootfs at bake time: `(host_path, guest_path)`.
    pub extra_files: Vec<(PathBuf, String)>,
    /// Mounts persisted in metadata: `(host_path, guest_tag, policy, guest_path)`.
    pub mounts: Vec<(
        PathBuf,
        String,
        crate::vmm::resources::SymlinkPolicy,
        String,
    )>,
    /// Writable volumes persisted in metadata: `(host_file, guest_path, size)`.
    pub volumes: Vec<(PathBuf, String, u64)>,
    /// Post-bake warmup run against a transient sentry [`Vm`]; its rootfs writes
    /// land in-place in the baked artifact.
    pub warmup: Option<Box<dyn FnOnce(&Vm) -> Result<(), Error> + Send>>,
    /// Bake the image for the warm-daemon pool backend (a persistent parked cell).
    /// Persisted to `metadata.json["warm_daemon"]` and read back into
    /// [`SentryImageParts::warm_daemon`].
    pub warm_daemon: bool,
    /// Stable warmup tag, persisted to `metadata.json["warmup_tag"]` for round-trip
    /// parity with the KVM/HVF bakes (the sentry path previously DROPPED it).
    pub warmup_tag: Option<String>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_sha256_bytes(bytes: &[u8]) -> String {
    let digest = ring::digest::digest(&ring::digest::SHA256, bytes);
    let mut out = String::with_capacity(64);
    for b in digest.as_ref() {
        use std::fmt::Write as _;
        let _ = write!(out, "{b:02x}");
    }
    out
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_sha256_file(path: &Path) -> Result<String, Error> {
    let mut f = std::fs::File::open(path).map_err(|e| {
        Error::bake_msg(format!(
            "hash sentry bake input file {}: {e}",
            path.display()
        ))
    })?;
    let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
    let mut buf = [0u8; 64 * 1024];
    loop {
        let n = f.read(&mut buf).map_err(|e| {
            Error::bake_msg(format!(
                "read sentry bake input file {}: {e}",
                path.display()
            ))
        })?;
        if n == 0 {
            break;
        }
        ctx.update(&buf[..n]);
    }
    let digest = ctx.finish();
    let mut out = String::with_capacity(64);
    for b in digest.as_ref() {
        use std::fmt::Write as _;
        let _ = write!(out, "{b:02x}");
    }
    Ok(out)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_file_mode(path: &Path) -> Result<u32, Error> {
    use std::os::unix::fs::PermissionsExt as _;
    Ok(std::fs::metadata(path)
        .map_err(|e| Error::bake_msg(format!("stat sentry bake input {}: {e}", path.display())))?
        .permissions()
        .mode()
        & 0o7777)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_bake_cache_key(image_ref: &str, config: &SentryBakeConfig) -> Result<String, Error> {
    let mut extra_files = Vec::with_capacity(config.extra_files.len());
    for (host, guest) in &config.extra_files {
        extra_files.push(serde_json::json!({
            "guest": guest,
            "mode": sentry_file_mode(host)?,
            "sha256": sentry_sha256_file(host)?,
        }));
    }
    let mounts: Vec<_> = config
        .mounts
        .iter()
        .map(|(host, tag, policy, guest)| {
            serde_json::json!({
                "host": host.to_string_lossy(),
                "tag": tag,
                "policy": format!("{policy:?}"),
                "guest": guest,
            })
        })
        .collect();
    let volumes: Vec<_> = config
        .volumes
        .iter()
        .map(|(host, guest, size)| {
            serde_json::json!({
                "host": host.to_string_lossy(),
                "guest": guest,
                "size": size,
            })
        })
        .collect();
    let input = serde_json::json!({
        "schema": 1,
        "image": image_ref,
        "memory_mib": config.memory_mib,
        "vcpus": config.vcpus,
        "cmd": &config.cmd,
        "env": &config.env,
        "working_dir": &config.working_dir,
        "user": &config.user,
        "extra_files": extra_files,
        "mounts": mounts,
        "volumes": volumes,
        "has_warmup": config.warmup.is_some(),
        "warmup_tag": &config.warmup_tag,
        "warm_daemon": config.warm_daemon,
    });
    let bytes = serde_json::to_vec(&input)
        .map_err(|e| Error::bake_msg(format!("encode sentry bake cache key: {e}")))?;
    Ok(sentry_sha256_bytes(&bytes))
}

#[derive(Debug, Clone)]
pub struct Image {
    snapshot_path: PathBuf,
    /// Default memory; can be overridden via [`VmConfig::with_memory_mib`].
    pub(crate) memory_mib: u32,
    /// Default vCPUs; can be overridden via [`VmConfig::with_vcpus`].
    pub(crate) vcpus: u32,
    /// 16-hex-prefix of the supermachine-worker SHA256 that baked
    /// this snapshot, parsed from `metadata.json["runtime_sha16"]`.
    /// Used to skip the agent-protocol probe when a fresh-bake
    /// invariant holds (snapshot baked by the same lib version
    /// the host is now running). `None` means metadata didn't
    /// record one — older snapshots, third-party bakes, etc; the
    /// probe runs unconditionally in that case.
    pub(crate) baker_runtime_sha16: Option<String>,
    /// `metadata.json["balloon_target_pages"]` — bake-time
    /// recommended balloon inflation target for idle workers.
    /// Plumbed through SpawnConfig → worker CLI → runner so each
    /// pool worker requests inflate after restore. None disables
    /// ballooning. Defaults to 75% of `memory_mib` in 4 KiB
    /// pages (set by bake.rs).
    pub(crate) balloon_target_pages: Option<u32>,
    /// TSI control-channel auth token (64-char lowercase hex).
    /// Generated at fresh bake, persisted in metadata.json,
    /// re-injected on every restore via the worker's
    /// `--tsi-token` flag. `None` on legacy snapshots (pre-0.6.0)
    /// — the muxer accepts all control DGRAMs in that case.
    /// See `kernel-build/patches/af-tsi/0014-*.patch` for the
    /// threat model.
    pub(crate) tsi_token: Option<String>,
    /// 0.7.65+ per-snapshot egress policy from
    /// `metadata.json["egress_policy"]` (e.g. `deny_private`,
    /// `allowlist:<cidrs>`, `denylist:<cidrs>`). Re-injected on the
    /// worker via `--egress-policy` so the muxer enforces it at TSI
    /// connect time. `None` (the common case) → `allow_all`. Previously
    /// only the router/worker CLI honored this; threading it here makes
    /// the high-level `Image::acquire` / `Pool` path enforce a baked
    /// snapshot's network policy too.
    pub(crate) egress_policy: Option<String>,
    /// 0.7.44+ pre-exec sync flag. `true` → init-oci is paused
    /// inside `read(0)` in the saved snapshot at the pre-exec
    /// pause point. spawn_one sets
    /// SUPERMACHINE_PRE_EXEC_SYNC_RESUME=1 on the worker so the
    /// `supermachine-pre-exec-resume` thread pushes the unblock
    /// byte after restore. `false` (legacy / pre-0.7.44) → init-oci
    /// used the 250 ms nanosleep at that point; no push needed.
    pub(crate) pre_exec_sync: bool,
    /// virtio-blk layer file paths in the order the bake step
    /// produced them. The microVM needs all of them attached at
    /// restore time (the OverlayFS in the guest is mounted on top).
    pub(crate) layers: Vec<PathBuf>,
    /// Optional per-image delta layer applied after `layers`.
    pub(crate) delta_squashfs: Option<PathBuf>,
    /// virtio-fs DAX mounts persisted in metadata.json. The bake
    /// captures the snapshot with these mounts wired into the FDT,
    /// so restore must re-create the same VirtioFs devices at the
    /// same MMIO addresses for the guest kernel to find them.
    pub(crate) mounts: Vec<crate::vmm::resources::MountSpec>,
    /// Writable virtio-blk volumes persisted in metadata.json as
    /// `[{host_file, guest_path, size_bytes, pristine?}, ...]`.
    /// The bake snapshots the guest BEFORE `mount_volumes()` runs
    /// (init-oci's `[SUPERMACHINE-INIT] heartbeat counter=1` marker
    /// fires pre-mount), so the snapshot itself doesn't carry the
    /// volume mount — init-oci re-runs `mount_volumes()` on each
    /// restore. For that re-mount to find a `/dev/vd<N>` device,
    /// the warm-restore worker MUST re-attach the volume via
    /// `--volume HOST:GUEST:SIZE`. Without this, the guest hangs
    /// on any ext4 read after restore. (Field-report bug fixed
    /// 0.7.30.)
    ///
    /// Tuple shape: (host_file, guest_path, size_bytes, pristine).
    /// `pristine` is `Some(path)` for 0.7.49+ snapshots and points
    /// at an immutable byte-identical copy of `host_file` captured
    /// at snapshot save time. Pools running with
    /// `restoreOnRelease=true` clone the pristine to a per-worker
    /// temp file on each spawn so the worker's writes never leak
    /// between acquires — keeps virtio-blk bytes in lock-step with
    /// the snapshot's captured ext4 RAM state. Pre-0.7.49 snapshots
    /// have `None`; the pool logs a one-line warning and falls
    /// through to the legacy behavior (worker uses `host_file`
    /// directly, writes persist).
    pub(crate) volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)>,
    /// Bundled kernel path, if the snapshot dir ships one alongside.
    /// Lets a self-contained bundle (e.g. `MyApp.app/Contents/
    /// Resources/<image>/kernel`) start a VM without requiring the
    /// embedder's host to have supermachine assets installed
    /// system-wide. `None` means [`Vm::start`] falls back to
    /// [`AssetPaths::discover`].
    pub(crate) bundled_kernel: Option<PathBuf>,
    /// Linux/KVM artifacts parsed from `metadata.json` when
    /// `"backend": "kvm"`: the bzImage, the PID-1 agent initramfs, and an
    /// optional rootfs disk (attached as `/dev/vda`). `None` for HVF
    /// snapshots. Consumed by the Linux [`Vm::start`].
    pub(crate) kvm: Option<KvmImageParts>,
    /// `"backend":"sentry"`: the extracted rootfs dir served by the no-virt
    /// process-isolation backend (see [`crate::sentry`]). `None` for HVF/KVM
    /// images. Run with [`Image::run_sentry`].
    pub(crate) sentry: Option<SentryImageParts>,
    /// `"backend":"runsc"`: the extracted rootfs dir served by the external
    /// gVisor/runsc no-KVM backend. `None` for HVF/KVM/sentry images.
    pub(crate) runsc: Option<RunscImageParts>,
    /// `"backend":"kboxlike"`: the extracted rootfs dir for the native Rust
    /// kbox/LKL-inspired no-KVM backend under construction.
    pub(crate) kboxlike: Option<KboxlikeImageParts>,
    /// Hidden warm pool, lazy-initialized on first
    /// [`Image::acquire`]. The pool holds a single long-lived
    /// `WarmPool` (in-process worker + the snapshot mmap'd in)
    /// behind a mutex; each `acquire` calls `restore` to reset
    /// the worker to clean snapshot state, returns a [`PooledVm`]
    /// that holds the lock, and re-enters the pool on `Drop`.
    /// Per-acquire cost is just the snapshot restore (~5 ms),
    /// not the full VM spawn (~50–100 ms).
    ///
    /// Wrapped in `Arc` so cloning an `Image` shares the same
    /// pool instance — useful when multiple parts of an app hold
    /// `Image` references but should share a single warm worker.
    pub(crate) hidden_pool: std::sync::OnceLock<Arc<HiddenPool>>,
    /// Warm handoff: the bake-time worker, kept alive after the
    /// pipelined bake completes (`Image::builder().with_warmup(…)`)
    /// so the FIRST `Pool::acquire()` can use it directly instead
    /// of paying ~50 ms spawn + ~5 ms restore for a fresh worker.
    /// Subsequent acquires fall through to the normal spawn-from-
    /// disk path.
    ///
    /// Atomic claim semantics: the consumer (`PoolBuilder::build` or
    /// `HiddenPool::ensure_min_workers`) calls
    /// `warm_baked_worker.lock().take()`. Only one party gets
    /// `Some`; everyone else gets `None` and falls through cleanly.
    /// This also guards against multi-pool-from-same-Image: only
    /// the first pool gets the warm worker.
    ///
    /// Drop semantics: if the Image is dropped without anyone
    /// claiming, `Drop for Image` (below) takes the value and
    /// shuts down the worker via QUIT (drains in-flight saves)
    /// + `child.wait()` + socks_dir cleanup. The bake driver
    /// already drained the bg base save before returning the
    /// BakedWorker, so QUIT is a fast (~10 ms) drain-noop in this
    /// case — no `.partial` leaks.
    pub(crate) warm_baked_worker: Arc<crate::bake::WarmStash>,
    /// Linux/KVM ambient warm pool, built lazily on the first `acquire`/
    /// `acquire_with` and shared across clones (Arc). Makes `image.acquire()`
    /// a CoW restore (~ms) instead of a cold boot (~1s) — the macOS
    /// `hidden_pool` equivalent for KVM. `Arc<Pool>` breaks the Image↔Pool cycle.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub(crate) kvm_pool: std::sync::OnceLock<Arc<Pool>>,
}

impl Image {
    /// Test-only introspection: is a warm-handoff worker currently
    /// stashed? Used by the warm-handoff integration tests to
    /// verify the bake actually populated the stash, and that the
    /// first `Pool::build()` consumed it. Not part of the public
    /// API contract — the field itself is `pub(crate)` and may
    /// move; this accessor is doc-hidden because it's a peephole
    /// for tests, not a feature.
    /// Test-only introspection: is this a Linux/KVM cold-boot image (kernel +
    /// initrd, no warm snapshot)? Cold-boot images go through
    /// `warm_snapshot_for_pool`'s persist+dedup path; snapshot-backed ones
    /// early-return. Used by the warm-persist test to skip cross-family
    /// assertions when the second image is already snapshot-backed. Not stable.
    #[doc(hidden)]
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn is_kvm_cold_boot(&self) -> bool {
        self.kvm
            .as_ref()
            .map(|k| k.snapshot.is_none())
            .unwrap_or(false)
    }
    /// True iff this image is backed by the no-virt sentry backend (vs KVM/HVF).
    /// Lets a caller (e.g. the CLI's snapshot cache) tell which backend a loaded
    /// snapshot was baked for, so a cache hit can be matched to the requested backend.
    pub fn is_sentry(&self) -> bool {
        self.sentry.is_some()
    }
    /// True iff this image is backed by the experimental runsc no-KVM backend.
    pub fn is_runsc(&self) -> bool {
        self.runsc.is_some()
    }
    /// True iff this image is backed by the experimental native kboxlike backend.
    #[doc(hidden)]
    pub fn is_kboxlike(&self) -> bool {
        self.kboxlike.is_some()
    }
    #[doc(hidden)]
    pub fn _warm_handoff_present(&self) -> bool {
        self.warm_baked_worker
            .inner
            .lock()
            .map(|g| g.is_some())
            .unwrap_or(false)
    }
    /// Test-only: PID of the stashed warm worker, or None. Used by
    /// the R1 integration test (Image-dropped-without-claim) to
    /// `kill -0` the pid after dropping the Image and confirm
    /// reaping. Not stable.
    #[doc(hidden)]
    pub fn _warm_handoff_pid(&self) -> Option<u32> {
        self.warm_baked_worker
            .inner
            .lock()
            .ok()
            .and_then(|g| g.as_ref().map(|bw| bw.child.id()))
    }
}

/// Internal state for the hidden subprocess pool an [`Image`]
/// manages for [`Image::acquire`] users. Spawns N
/// `supermachine-worker` subprocesses up front, each pre-restored
/// from the snapshot — so `acquire` is just "pop an idle worker
/// off the queue" (~1 ms) and N concurrent acquires really run N
/// VMs in parallel (each in its own subprocess, each its own
/// `hv_vm_create` singleton).
///
/// On `Drop`, kills every worker and unlinks every socket.
#[doc(hidden)]
pub struct HiddenPool {
    /// Per-worker state: idle queue + counts. Arc'd separately
    /// from the pool so housekeeping threads can hold it across
    /// condvar waits without keeping `HiddenPool` itself alive
    /// (otherwise the user-side drop never fires).
    state: Arc<Mutex<PoolState>>,
    /// Wakes `acquire()` callers blocked on an empty idle queue
    /// AND wakes housekeeping threads on shutdown. Signalled when
    /// a worker re-enters idle, when the pool is shutting down,
    /// or when a wait_timeout window in a housekeeper expires.
    available: Arc<Condvar>,
    /// Workers handed back from `PooledVm::drop` waiting to be
    /// restored to clean snapshot state before going back into
    /// the idle queue. The restorer thread drains this. Arc'd
    /// so the restorer can wait on the condvar without keeping
    /// `HiddenPool` alive.
    dirty: Option<Arc<Mutex<VecDeque<Worker>>>>,
    /// Wakes any restorer thread when a worker lands on dirty
    /// (or on shutdown).
    dirty_pending: Option<Arc<Condvar>>,
    /// Where each worker's vsock mux/exec sockets live.
    socks_dir: PathBuf,
    /// True once `Drop` has started; replenisher / restorer /
    /// janitor exit the next time around their wait loops. Arc'd
    /// so housekeeping threads can poll it after dropping their
    /// strong `HiddenPool` reference.
    shutting_down: Arc<AtomicBool>,
    /// Image-derived spawn config, copied so the pool is self-
    /// contained.
    spawn_cfg: Arc<SpawnConfig>,
    /// Static pool policy. Read from many threads; never mutated
    /// after construction so we can stash by value (no Mutex).
    policy: PoolPolicy,
}

/// Static policy for an auto-scaling pool. Set at builder time;
/// immutable thereafter.
#[derive(Debug, Clone, Copy)]
struct PoolPolicy {
    /// Always-warm baseline. The replenisher keeps `alive >= min`.
    /// `min == 0` means lazy: the first acquire spawns the first
    /// worker.
    min: usize,
    /// Hard concurrency cap. Acquire blocks (with timeout) when
    /// `alive == max`. `max == usize::MAX` means uncapped.
    max: usize,
    /// Idle workers above `min` that have sat unused for longer
    /// than this get evicted by the janitor. `Duration::MAX`
    /// disables eviction (fixed-size pool).
    idle_timeout: Duration,
    /// Caller's `acquire()` blocks at most this long when the
    /// pool is at `max` and no worker is idle. After that the
    /// call returns `Error::PoolExhausted`. `None` = block forever.
    acquire_timeout: Option<Duration>,
    /// When `true` (default), `PooledVm::drop` queues the worker
    /// for restoration via the supervisor RESTORE RPC before
    /// it goes back to idle. Each cycle starts with a clean
    /// snapshot-state guest. Costs ~3 ms per cycle (off the
    /// critical path if the pool has a buddy slot).
    ///
    /// When `false`, drop pushes the worker DIRECTLY back to
    /// idle without restoring. The next acquire gets the same
    /// guest in whatever state the previous user left it. The
    /// per-cycle restore cost vanishes, AND the guest's page
    /// cache stays warm — for workloads like rustc that re-read
    /// the same sysroot/deps every invocation, this is a HUGE
    /// win (300 ms cold compile → ~50–100 ms warm-cache compile).
    ///
    /// Caveats: workloads must be tolerant of leftover state
    /// in `/tmp` etc. The integrator's pattern (write_file
    /// `main.rs`, run `rustc -o /tmp/m && /tmp/m`) is safe — the
    /// `&&` short-circuits on compile failure, and outputs are
    /// always overwritten. Pair with periodic full pool drain
    /// + rebuild if you need bounded resource accumulation over
    /// long runs.
    restore_on_release: bool,
}

impl Default for PoolPolicy {
    fn default() -> Self {
        Self {
            min: 0,
            max: 64,
            idle_timeout: Duration::from_secs(60),
            acquire_timeout: Some(Duration::from_secs(60)),
            restore_on_release: true,
        }
    }
}

/// Per-Pool internal state under one mutex. Held only briefly
/// during acquire/release; the long-running work (spawn /
/// restore / kill) happens outside the lock.
struct PoolState {
    /// Idle workers ready for the next `acquire()`. **LIFO** —
    /// the most recently used worker has the hottest host page
    /// cache (squashfs layer pages, kernel mmap pages), so it
    /// pays the cheapest restore on the next acquire. Both ops
    /// are at the back of the `Vec`.
    idle: Vec<IdleEntry>,
    /// Total workers in the pool right now: idle + currently
    /// checked-out + currently being spawned/restored. The
    /// auto-grow path bumps this *before* spawning so a
    /// concurrent `acquire` can see "we already promised someone
    /// a worker, don't double-spawn."
    alive: usize,
    /// Currently-blocked acquire callers (for stats /
    /// observability). Incremented at the wait site, decremented
    /// when the wait returns.
    waiting: usize,
    /// Cumulative count of workers the watchdog reaped because their
    /// process had died while sitting idle (never popped, so the
    /// acquire-time `try_wait` never saw them). Pure observability.
    reaped: u64,
}

/// An idle worker plus the timestamp it returned to the queue,
/// used by the janitor to evict above-`min` workers that have
/// sat unused longer than `idle_timeout`.
struct IdleEntry {
    worker: Worker,
    last_used: Instant,
}

/// One spawned `supermachine-worker` subprocess + its vsock
/// socket paths + the lib-side end of the supervisor control
/// socket.
///
/// Workers are launched in `--pool-worker` mode (see
/// `bin/worker.rs`): each one connects back to a unix socket the
/// lib listens on, accepts text-line commands (`RESTORE <path>`,
/// `QUIT`), and writes `DONE …` after each restore. This lets
/// the same worker process serve many `acquire`/`drop` cycles
/// — each cycle is a snapshot restore (~3 ms) instead of a full
/// process spawn (~10 ms) plus a kill.
///
/// The control socket is bidirectional: lib writes commands,
/// worker writes responses. We hold separate read/write halves
/// behind a `Mutex` so the restorer thread (which sends RESTORE
/// during release-handling) doesn't race with the QUIT path
/// during pool teardown.
struct Worker {
    child: Child,
    vsock_mux_path: PathBuf,
    vsock_exec_path: PathBuf,
    /// Path of the unix listener the lib is using to talk to
    /// this worker. Cleaned up on drop alongside the vsock paths.
    control_path: PathBuf,
    /// Live control connection. Writer side; reads happen on a
    /// `BufReader` we construct on demand for the restore round-
    /// trip. Wrapped in `Mutex` so concurrent code paths
    /// (RESTORE during release vs. QUIT during shutdown) are
    /// linearized cleanly.
    control: Arc<Mutex<ControlChannel>>,
    /// The snapshot file this worker was last restored from.
    /// Used as the `base` hint on cycle-snapshot RPCs so the
    /// runner can use APFS clonefile + diff pwrite instead of
    /// the plain streaming sync save (~3× speedup on the
    /// cycle-snapshot path).
    last_restore_path: PathBuf,
    /// Guest memory (MiB) the worker was started with. Used by
    /// the cycle-restore path to gate the post-restore
    /// `drop_vfs_caches` RPC behind the 512-MiB memory threshold
    /// (see `drop_vfs_caches_via_agent`).
    memory_mib: u32,
    /// 0.7.44+ — true when the snapshot has virtio-fs mounts OR
    /// virtio-blk volumes (any host-backed write surface). The
    /// cycle-restore path checks this to skip the VFS-drop RPC
    /// when there's nothing host-backed to invalidate. Pure-image
    /// snapshots (no mounts, no volumes) save ~5-15 ms / cycle.
    has_mounts: bool,
    /// 0.7.44+ — vcpu count this worker was started with. Used by
    /// the cycle-restore path to gate the post-restore smpark_unpark
    /// RPC (only needed when vcpus > 1; secondaries don't exist
    /// otherwise).
    vcpus: u32,
    /// 0.7.49+ — per-worker virtio-blk temp files (clonefile of
    /// the snapshot's `volumes/<i>.pristine`). 0.7.50+: non-empty
    /// whenever the snapshot has pristines, regardless of
    /// `policy.restore_on_release` (previously gated on
    /// `restore_on_release=true`). Unlinked on Worker drop so
    /// /tmp stays clean. Empty only for no-pristine snapshots
    /// (pre-0.7.49 bakes; workers use the host file directly).
    volume_temp_files: Vec<PathBuf>,
    /// 0.7.67+ — guest mountpoints of this worker's virtio-blk
    /// volumes (e.g. `/data`). init-oci runs `mount_volumes()`
    /// AFTER the snapshot/exec-ready marker (the snapshot is captured
    /// before the volume mount so each restore re-mounts fresh host
    /// bytes — see `SpawnConfig.volumes`). On restore the guest
    /// therefore resumes with the agent already accepting execs but
    /// `/data` not yet mounted — a TOCTOU window where the caller's
    /// first exec reads an empty volume. The post-restore readiness
    /// gate (`wait_for_volumes_mounted`) polls `/proc/mounts` for
    /// these paths before the worker is handed out. Empty for
    /// no-volume snapshots (the gate is skipped).
    volume_guest_paths: Vec<String>,
    /// 0.7.49+ — true when this worker has per-worker COW volume
    /// temps. The pool's release path uses this to decide
    /// kill+respawn (volume-bearing `restore_on_release=true`
    /// pools) vs. RESTORE-recycle / push-to-idle (everything
    /// else): RESTORE can't reset the worker's open virtio-blk
    /// fds back to pristine bytes.
    ///
    /// 0.7.50+: also true for `restore_on_release=false` pools
    /// (the COW gate moved upstream — every spawn gets a temp).
    /// In skip-restore mode the worker is reused across acquires
    /// within the process, so the temp's writes persist within
    /// the process — exactly matching the prior skip-restore
    /// semantics for volume mutations. The cross-process leak is
    /// blocked because each new process spawns a worker with a
    /// fresh COW clone.
    has_cow_volumes: bool,
    /// 0.7.63+ — memory admission reservation, held for this worker's
    /// whole lifetime and returned on drop. `spawn_one` charges it
    /// against the process-wide budget BEFORE forking (blocking if the
    /// budget is full) so concurrent spawns can't overcommit host RAM
    /// into jetsam/OOM; warm-handoff workers charge non-blockingly
    /// (their RAM is already spent). See [`crate::memory_admission`].
    _admission: crate::memory_admission::AdmissionGuard,
}

/// Stats returned from a successful subprocess snapshot RPC.
struct SnapshotStats {
    bytes_written: u64,
    capture_us: u64,
    save_us: u64,
}

/// Lib-side bookkeeping for the `--pool-worker` text protocol.
struct ControlChannel {
    /// Buffered reader so we can read line-at-a-time without
    /// over-consuming bytes.
    reader: std::io::BufReader<std::os::unix::net::UnixStream>,
    /// Direct write half — `BufReader` borrows the reader half.
    writer: std::os::unix::net::UnixStream,
}

impl ControlChannel {
    fn send_line(&mut self, line: &str) -> std::io::Result<()> {
        use std::io::Write;
        self.writer.write_all(line.as_bytes())?;
        if !line.ends_with('\n') {
            self.writer.write_all(b"\n")?;
        }
        self.writer.flush()
    }

    fn read_line(&mut self) -> std::io::Result<String> {
        use std::io::BufRead;
        // Transparently skip `SAVE_DONE <path>` / `SAVE_FAIL <path> ...`
        // notifications. The worker's bg async-save thread emits those
        // on the same supervisor channel as the request/response
        // protocol — they're orthogonal "I finished a save you asked
        // me to start earlier" announcements, never a response to the
        // current request. If we returned one to a caller expecting
        // (say) DONE_SNAPSHOT, the line-oriented parse would mis-match.
        //
        // The bake pipeline has its own copy of this filter
        // (`read_supervisor_line_skip_save_notifications` in bake.rs)
        // because the bake driver owns the BufReader directly; this
        // version is for the `ControlChannel`-wrapped pooled path.
        loop {
            let mut buf = String::new();
            let n = self.reader.read_line(&mut buf)?;
            if n == 0 {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::UnexpectedEof,
                    "worker control socket closed",
                ));
            }
            let trimmed = buf.trim_start();
            if trimmed.starts_with("SAVE_DONE ") || trimmed.starts_with("SAVE_FAIL ") {
                tracing::debug!(line = %buf.trim_end(), "skipping orthogonal bg-save notification");
                continue;
            }
            // 0.7.43+ kernel-boot cache marker is normally consumed
            // by the bake driver's kcache-WRITE orchestrator in
            // `bake.rs::run_native_supermachine_bake_pipelined`.
            // This control channel (ControlChannel, the pooled-VM
            // path) doesn't enter the kcache window so won't see
            // the marker in normal operation. Keep the skip as
            // defense-in-depth: if a future code path leaks the
            // marker through here, drop silently rather than
            // confusing the protocol.
            if trimmed.starts_with("MARKER_KCACHE_READY") {
                tracing::debug!("skipping MARKER_KCACHE_READY notification");
                continue;
            }
            return Ok(buf);
        }
    }
}

/// Convert a `bake::BakedWorker` (handle handed back from the
/// pipelined-bake driver when `keep_alive=true`) into a regular
/// `Worker` that the `HiddenPool` can use as an idle entry.
///
/// This is a pure handle-transfer — no I/O, no liveness check
/// (caller did the `try_wait` first). The resulting Worker has
/// `last_restore_path` set to the warm snapshot path, so any
/// subsequent cycle SNAPSHOT RPCs use it as the diff-via-clone
/// `base=` hint, same as a normal pool worker that just restored.
#[cfg(target_os = "macos")]
fn warm_baked_to_worker(
    bw: crate::bake::BakedWorker,
    memory_mib: u32,
    has_mounts: bool,
    volume_guest_paths: Vec<String>,
    vcpus: u32,
    use_once: bool,
) -> Worker {
    // Already-running bake-time worker: account its footprint so later
    // blocking spawns see it, but don't gate (the RAM is already
    // committed — blocking here would be pointless). It's live, so
    // charge what it MEASURES (+ the standard margin via
    // learned_charge_mib) rather than its guest-RAM cap — a cap charge
    // for a CoW-resident worker overstates by GiBs and starves the
    // pool's parallel prewarm spawns of budget.
    let admission = crate::memory_admission::charge({
        let overhead = crate::memory_admission::WORKER_OVERHEAD_MIB;
        let observed = crate::memory_admission::phys_footprint_mib(bw.child.id()).unwrap_or(0);
        learned_charge_mib(memory_mib, observed, overhead)
    });
    Worker {
        child: bw.child,
        vsock_mux_path: bw.vsock_mux_path,
        vsock_exec_path: bw.vsock_exec_path,
        control_path: bw.control_path,
        control: Arc::new(Mutex::new(ControlChannel {
            reader: std::io::BufReader::new(bw.control_reader),
            writer: bw.control_writer,
        })),
        last_restore_path: bw.last_restore_path,
        memory_mib,
        has_mounts,
        vcpus,
        // Empty: the warm-handoff worker was launched at bake-time
        // and has its volume fds pointing at the user's host file
        // — not at a COW temp. Pool's `release` path uses
        // `has_cow_volumes` to decide kill-on-release.
        //
        // For warm-handoff workers in a `restore_on_release=true`
        // pool with pristines, `use_once=true` flips the flag
        // synthetically so release kills the worker after one
        // cycle; the replenisher then spawns a fresh worker that
        // DOES go through the COW path. The user's host file is
        // mutated by that ONE cycle but the pristine is unaffected
        // so subsequent COW spawns get clean bytes.
        //
        // For `restore_on_release=false`, `use_once=false` and the
        // warm-handoff worker is reused across acquires within
        // the process. It writes to the user's host file, not a
        // COW. The cross-process safety still holds: a separate
        // process opens via `Image::from_snapshot` (no warm
        // handoff), spawn_one always COWs from pristine, and
        // pristine never changes — so process N+1 sees bake-time
        // bytes regardless of what process N's warm-handoff
        // worker wrote.
        volume_temp_files: Vec::new(),
        // Stored for the worker's *cycle* restores: the first warm
        // handoff already has volumes mounted (booted at bake time),
        // but every later send_restore resumes pre-mount_volumes and
        // must re-gate on them.
        volume_guest_paths,
        has_cow_volumes: use_once,
        _admission: admission,
    }
}

/// Block until every `guest_paths` mountpoint is present in the guest's
/// `/proc/mounts`, or a bounded budget elapses.
///
/// init-oci runs `mount_volumes()` AFTER the snapshot/exec-ready marker
/// (the snapshot is captured before the volume mount so each restore
/// re-mounts against current host bytes — see `SpawnConfig.volumes`).
/// On restore the guest resumes with the agent already accepting execs
/// but the virtio-blk volumes not yet mounted: a TOCTOU window where the
/// caller's first exec would read an empty `/data`. `/proc/mounts` is the
/// deterministic mount-done signal; we poll it via the agent — which also
/// proves the worker can fork+exec (the same liveness signal as the
/// `/bin/true` probe). Returns `Err` if the volumes never mount within the
/// budget (a real failure) so the worker is respawned rather than silently
/// served with an unmounted volume.
#[cfg(target_os = "macos")]
fn wait_for_volumes_mounted(exec_path: &Path, guest_paths: &[String]) -> Result<(), Error> {
    let t0 = std::time::Instant::now();
    let budget = std::time::Duration::from_secs(3);
    loop {
        let out = crate::exec::ExecBuilder::new(exec_path.to_path_buf())
            .argv(["cat", "/proc/mounts"])
            .timeout(std::time::Duration::from_secs(3))
            .output()
            .map_err(|e| {
                Error::vm_msg(format!(
                    "post-restore volume-readiness exec failed in {:?}: {e} — \
                     worker will be respawned",
                    t0.elapsed()
                ))
            })?;
        if out.status.code() == Some(0) {
            let mounts = String::from_utf8_lossy(&out.stdout);
            // /proc/mounts lines are "<dev> <mountpoint> <fstype> …";
            // the 2nd whitespace field is the mountpoint.
            let all_mounted = guest_paths.iter().all(|gp| {
                mounts
                    .lines()
                    .any(|l| l.split(' ').nth(1) == Some(gp.as_str()))
            });
            if all_mounted {
                return Ok(());
            }
        }
        if t0.elapsed() >= budget {
            return Err(Error::vm_msg(format!(
                "post-restore volumes not mounted within {budget:?}: {guest_paths:?} \
                 absent from /proc/mounts — worker will be respawned"
            )));
        }
        std::thread::sleep(std::time::Duration::from_millis(10));
    }
}

impl Worker {
    /// Send a `RESTORE <path>` command and block until the
    /// worker writes `DONE …`. Returns `Ok` on success, `Err`
    /// if the protocol broke (worker crashed, socket closed,
    /// malformed response). Caller treats `Err` as "worker is
    /// unusable" and respawns.
    fn send_restore(&self, snapshot_path: &Path) -> Result<(), Error> {
        let path_str = snapshot_path
            .to_str()
            .ok_or_else(|| Error::vm_msg("snapshot path is not valid UTF-8".to_owned()))?;
        let mut ctl = self
            .control
            .lock()
            .map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
        ctl.send_line(&format!("RESTORE {path_str}"))
            .map_err(Error::Io)?;
        let line = ctl.read_line().map_err(Error::Io)?;
        if !line.starts_with("DONE") {
            return Err(Error::vm_msg(format!(
                "worker RESTORE: expected DONE response, got: {}",
                line.trim()
            )));
        }

        // 0.7.44+ host-direct smpark_unpark BEFORE we drop the
        // control mutex. Sets the unpark_signal byte directly via
        // the worker's mapped host RAM, BEFORE the agent RPC below
        // sends the SGI/IPI. Belt-and-suspenders: if the IPI path
        // is slow under load, the secondaries still wake on the
        // next vtimer tick (≤10 ms) and see signal=1.
        //
        // ERR_SMPARK_UNPARK on older smpark.ko or single-vCPU is
        // harmless — we just continue.
        if self.vcpus > 1 {
            ctl.send_line("SMPARK_UNPARK_DIRECT").map_err(Error::Io)?;
            let _ = ctl.read_line().map_err(Error::Io)?;
        }

        // Drop the agent's mutex before issuing the agent RPCs
        // below — they open their own AF_VSOCK connections on the
        // exec path, independent of the control channel.
        drop(ctl);

        // 0.7.44+ post-cycle-restore agent reachability +
        // secondary unpark + liveness verification.
        //
        // The supervisor's DONE only confirms restore_snapshot
        // completed — it does NOT guarantee the worker is ready
        // to handle a user exec. Two races + one verification:
        //
        //   1. vCPU 0 resume-window race: when DONE is written,
        //      the dispatch thread is about to loop back to
        //      hv_vcpu_run but hasn't actually entered it yet.
        //      Host's next acquire+exec can land a vsock REQUEST
        //      before vCPU 0 is executing — muxer raises an IRQ
        //      that's sometimes lost.
        //   2. Secondary-parked race: secondaries are in WFI
        //      inside smpark_park_routine (snapshot was captured
        //      with them parked). After cycle-restore, they need
        //      to be unparked — initial spawn_one does this via
        //      smpark_unpark CONTROL RPC, but the cycle-restore
        //      path historically bypassed it. Even with the
        //      unpark RPC, Apple silicon HVF's SGI delivery has
        //      variable latency (30-50 ms under load): if a user
        //      exec's fork() child (which resets affinity via
        //      sched_setaffinity(0)) lands in this window, it
        //      can be scheduled onto a still-parked secondary
        //      and block in WFI forever — exec hangs 5 s + 2 s,
        //      returns exit=-1 stdout="".
        //
        // Fix: 3 steps to address both races, then a liveness
        // exec to VERIFY the worker is actually ready:
        //
        //   1. PROBE: vsock-exec CONTROL ack-roundtrip with retry
        //      (10 × 200 ms). Forces muxer to kick the guest,
        //      wakes vCPU 0 from resume-window WFI. Typical
        //      convergence: 1 attempt.
        //   2. SMPARK_UNPARK CONTROL RPC: agent ioctl's
        //      /dev/smpark to write unpark_signal byte +
        //      smp_call_function_many to send SGIs. Wakes
        //      secondaries from WFI. (SMPARK_UNPARK_DIRECT on
        //      the supervisor socket above ALSO writes the
        //      signal byte — belt-and-suspenders for slow IPI
        //      paths since secondaries also wake on vtimer when
        //      they re-check the byte.)
        //   3. LIVENESS EXEC: run `/bin/true` through the agent.
        //      This exercises the EXACT path that user execs
        //      use — fork+exec+affinity-reset+wait. If it
        //      returns within timeout, the worker is FULLY
        //      ALIVE. If it hangs/fails, return Err so the
        //      restorer shuts down this worker and the
        //      replenisher spawns a fresh one. Replaces the
        //      brittle "sleep N ms" approach (50 ms gave ~95%
        //      pass, 200 ms gave ~78% — longer sleep exposed
        //      MORE races, so sleeping isn't the answer).
        //
        // Repro of the bug this fix addresses:
        // python:3.12-alpine pool, 1024 MiB, 2 vcpus,
        // restoreOnRelease, no warmup — T2/T3's exec failed
        // intermittently (~10-50% depending on system load).
        // With this fix: 100 % pass on 400+ iter stress tests
        // including under load average 11.83.
        //
        // Env: SUPERMACHINE_POST_RESTORE_PROBE=0 disables probe
        // + smpark_unpark steps;
        // SUPERMACHINE_POST_RESTORE_LIVENESS=0 disables the
        // liveness exec. For embedders who do their own warmup
        // and don't want the per-cycle overhead.
        let probe_enabled = std::env::var("SUPERMACHINE_POST_RESTORE_PROBE")
            .map(|v| v != "0" && v != "false")
            .unwrap_or(true);
        if probe_enabled {
            let per_attempt = std::time::Duration::from_millis(200);
            let max_attempts = 10;
            for attempt in 0..max_attempts {
                let body = serde_json::json!({ "action": "probe" });
                match crate::exec::send_control_with_ack(
                    &self.vsock_exec_path,
                    &body,
                    Some(per_attempt),
                ) {
                    Ok(_) => break,
                    Err(_) if attempt + 1 < max_attempts => {}
                    Err(e) => {
                        return Err(Error::vm_msg(format!(
                            "post-restore agent probe failed after {max_attempts} attempts: {e}"
                        )));
                    }
                }
            }
            if self.vcpus > 1 {
                let body = serde_json::json!({ "action": "smpark_unpark" });
                let _ = crate::exec::send_control_with_ack(
                    &self.vsock_exec_path,
                    &body,
                    Some(std::time::Duration::from_millis(500)),
                );
            }
            // 0.7.44+ liveness exec.
            //
            // Sleep-based "wait for secondaries to wake" was unreliable:
            // 100 ms gave ~95% pass, 200 ms gave ~78% (longer sleep
            // exposed other races). The fundamental problem is sleep
            // is a guess; we want PROOF that the worker can run a
            // fork+exec workload.
            //
            // Solution: do a real /bin/true exec inside send_restore.
            // This forks the agent's per-conn handler, which forks a
            // child, which resets affinity (the path that triggers
            // "child blocks on parked secondary"), and execs /bin/true.
            // If the exec returns within timeout, the worker is FULLY
            // alive — vCPU 0 + secondaries + scheduler + agent fork
            // all working. If the exec hangs/fails, return Err so the
            // worker is shut down + replaced by the replenisher.
            //
            // Cost: one full exec round-trip (~5-15 ms typical) on
            // every cycle-restore. Replaces the 100 ms sleep.
            //
            // Env: SUPERMACHINE_POST_RESTORE_LIVENESS=0 to disable.
            let liveness_enabled = std::env::var("SUPERMACHINE_POST_RESTORE_LIVENESS")
                .map(|v| v != "0" && v != "false")
                .unwrap_or(true);
            if liveness_enabled {
                let liveness_t0 = std::time::Instant::now();
                let exec_res = crate::exec::ExecBuilder::new(self.vsock_exec_path.clone())
                    .argv(["/bin/true"])
                    .timeout(std::time::Duration::from_secs(3))
                    .output();
                match exec_res {
                    Ok(out) if out.status.code() == Some(0) => {
                        // Worker is fully alive — vCPU 0 + secondaries
                        // + scheduler + agent fork path all working.
                    }
                    Ok(out) => {
                        return Err(Error::vm_msg(format!(
                            "post-restore liveness exec returned non-zero in {:?}: exit={:?} \
                             stderr={:?} — worker will be respawned",
                            liveness_t0.elapsed(),
                            out.status.code(),
                            String::from_utf8_lossy(&out.stderr)
                                .chars()
                                .take(200)
                                .collect::<String>(),
                        )));
                    }
                    Err(e) => {
                        return Err(Error::vm_msg(format!(
                            "post-restore liveness exec failed in {:?}: {e} — worker will be respawned",
                            liveness_t0.elapsed(),
                        )));
                    }
                }
            }
        }

        // 0.7.67+ — volume-mount readiness gate (mirrors spawn_one).
        // Cycle-restore resumes the guest BEFORE init-oci re-runs
        // mount_volumes(), so without this a post-restore exec could
        // read an unmounted /data. Runs regardless of the liveness env
        // (it's a correctness gate, not just a liveness probe) and
        // also proves the worker can fork+exec. Err => caller treats
        // the worker as unusable and respawns it.
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        if !self.volume_guest_paths.is_empty() {
            wait_for_volumes_mounted(&self.vsock_exec_path, &self.volume_guest_paths)?;
        }

        // Cycle-restore reinstates RAM from the snapshot — that
        // includes the guest VFS page/dentry/inode caches as they
        // were at capture time. Without dropping, subsequent reads
        // on `mounts:`/`volumes:` paths can return stale bytes
        // when the host backing file changed post-capture. See
        // `drop_vfs_caches_via_agent` for the full story.
        //
        // 0.7.44+ probe-skip: when this snapshot has no virtio-fs
        // mounts (Worker.has_mounts=false) there's nothing
        // host-backed in the pagecache, so the drop is a no-op.
        // Skip the RPC to shave ~5-15 ms per cycle-restore on the
        // common Image.build-from-Docker, no-extra-mounts case.
        if self.has_mounts {
            let _ = drop_vfs_caches_via_agent(&self.vsock_exec_path, self.memory_mib);
        }
        // 0.7.51+ wall-clock sync. Unlike drop_vfs_caches, this
        // is NOT gated on the presence of host-backed surfaces —
        // every cycle-restore needs to advance the guest's
        // CLOCK_REALTIME from snapshot-capture-time to "now",
        // regardless of mounts/volumes. The vtimer state keeps
        // CLOCK_MONOTONIC ticking correctly across restore (which
        // is what tokio/timer-wheels need), but the wall-clock
        // offset is captured + restored verbatim — so without
        // this RPC `Date.now()` reports bake-time post-restore.
        // Order: cache-drop first (so re-LOOKUPs see fresh host
        // state), then time-sync. Both are best-effort and
        // independent — neither failure blocks the other.
        let _ = sync_time_via_agent(&self.vsock_exec_path);
        Ok(())
    }

    /// Send `SNAPSHOT <out_path>` and block on the response.
    /// The worker pauses the guest, captures snapshot, writes to
    /// `out_path`, then writes either `DONE_SNAPSHOT
    /// bytes_written=N capture_us=… save_us=…` or
    /// `ERR_SNAPSHOT <reason>` on the supervisor socket. We
    /// parse the response and surface it as `Result`.
    fn send_snapshot(&self, out_path: &Path) -> Result<SnapshotStats, Error> {
        let path_str = out_path
            .to_str()
            .ok_or_else(|| Error::vm_msg("snapshot path is not valid UTF-8".to_owned()))?;
        let mut ctl = self
            .control
            .lock()
            .map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
        ctl.send_line(&format!("SNAPSHOT {path_str}"))
            .map_err(Error::Io)?;
        let line = ctl.read_line().map_err(Error::Io)?;
        if let Some(rest) = line.strip_prefix("DONE_SNAPSHOT") {
            let mut stats = SnapshotStats {
                bytes_written: 0,
                capture_us: 0,
                save_us: 0,
            };
            for kv in rest.split_ascii_whitespace() {
                if let Some(v) = kv.strip_prefix("bytes_written=") {
                    stats.bytes_written = v.parse().unwrap_or(0);
                } else if let Some(v) = kv.strip_prefix("capture_us=") {
                    stats.capture_us = v.parse().unwrap_or(0);
                } else if let Some(v) = kv.strip_prefix("save_us=") {
                    stats.save_us = v.parse().unwrap_or(0);
                }
            }
            Ok(stats)
        } else if let Some(rest) = line.strip_prefix("ERR_SNAPSHOT ") {
            Err(Error::vm_msg(format!(
                "worker SNAPSHOT failed: {}",
                rest.trim()
            )))
        } else {
            Err(Error::vm_msg(format!(
                "worker SNAPSHOT: unexpected response: {}",
                line.trim()
            )))
        }
    }

    /// Differential snapshot variant. Sends
    /// `SNAPSHOT <out_path> base=<base_path>`. The worker, if it
    /// has a matching in-flight async save to `base_path` (i.e.
    /// the snapshot is still in memory from a recent
    /// SNAPSHOT_ASYNC), uses APFS `clonefile` + diff `pwrite` to
    /// land the warm snapshot — usually ~10x faster than the
    /// plain streaming sync path.
    ///
    /// Falls back to the plain streaming sync save inside the
    /// runner on any of: no matching in-flight save, clonefile
    /// EXDEV (different filesystems), warm meta overflows base's
    /// ram_offset slack. Caller never sees this distinction; the
    /// returned `SnapshotStats` is well-formed in both cases.
    ///
    /// Used by the pipelined bake flow's warm capture.
    fn send_snapshot_with_base(
        &self,
        out_path: &Path,
        base_path: &Path,
        inline: bool,
    ) -> Result<SnapshotStats, Error> {
        let path_str = out_path
            .to_str()
            .ok_or_else(|| Error::vm_msg("snapshot out path is not valid UTF-8".to_owned()))?;
        let base_str = base_path
            .to_str()
            .ok_or_else(|| Error::vm_msg("snapshot base path is not valid UTF-8".to_owned()))?;
        let mut ctl = self
            .control
            .lock()
            .map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
        // `inline` makes the worker wait for the diff save to land before
        // replying, so the caller can read the file immediately (the builder's
        // per-layer chain) — while still retaining the base in memory.
        let line = if inline {
            format!("SNAPSHOT {path_str} base={base_str} inline")
        } else {
            format!("SNAPSHOT {path_str} base={base_str}")
        };
        ctl.send_line(&line).map_err(Error::Io)?;
        let line = ctl.read_line().map_err(Error::Io)?;
        if let Some(rest) = line.strip_prefix("DONE_SNAPSHOT") {
            let mut stats = SnapshotStats {
                bytes_written: 0,
                capture_us: 0,
                save_us: 0,
            };
            for kv in rest.split_ascii_whitespace() {
                if let Some(v) = kv.strip_prefix("bytes_written=") {
                    stats.bytes_written = v.parse().unwrap_or(0);
                } else if let Some(v) = kv.strip_prefix("capture_us=") {
                    stats.capture_us = v.parse().unwrap_or(0);
                } else if let Some(v) = kv.strip_prefix("save_us=") {
                    stats.save_us = v.parse().unwrap_or(0);
                }
            }
            Ok(stats)
        } else if let Some(rest) = line.strip_prefix("ERR_SNAPSHOT ") {
            Err(Error::vm_msg(format!(
                "worker SNAPSHOT (with base) failed: {}",
                rest.trim()
            )))
        } else {
            Err(Error::vm_msg(format!(
                "worker SNAPSHOT (with base): unexpected response: {}",
                line.trim()
            )))
        }
    }

    /// Async-save variant of [`Worker::send_snapshot`]. Sends
    /// `SNAPSHOT_ASYNC <out_path>`; the worker pauses, captures
    /// into a compact in-memory buffer, kicks off a background
    /// save thread, and returns `DONE_SNAPSHOT_ASYNC` immediately.
    /// The on-disk file appears asynchronously — drain via
    /// [`Worker::shutdown`] (which sends QUIT and waits for the
    /// worker process to exit) before relying on the file existing.
    ///
    /// Used by the pipelined bake flow.
    #[allow(dead_code)]
    fn send_snapshot_async(&self, out_path: &Path) -> Result<SnapshotStats, Error> {
        let path_str = out_path
            .to_str()
            .ok_or_else(|| Error::vm_msg("snapshot path is not valid UTF-8".to_owned()))?;
        let mut ctl = self
            .control
            .lock()
            .map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
        ctl.send_line(&format!("SNAPSHOT_ASYNC {path_str}"))
            .map_err(Error::Io)?;
        let line = ctl.read_line().map_err(Error::Io)?;
        if let Some(rest) = line.strip_prefix("DONE_SNAPSHOT_ASYNC") {
            let mut stats = SnapshotStats {
                bytes_written: 0,
                capture_us: 0,
                save_us: 0,
            };
            for kv in rest.split_ascii_whitespace() {
                if let Some(v) = kv.strip_prefix("bytes_written=") {
                    stats.bytes_written = v.parse().unwrap_or(0);
                } else if let Some(v) = kv.strip_prefix("capture_us=") {
                    stats.capture_us = v.parse().unwrap_or(0);
                } else if let Some(v) = kv.strip_prefix("save_us=") {
                    stats.save_us = v.parse().unwrap_or(0);
                }
            }
            Ok(stats)
        } else if let Some(rest) = line.strip_prefix("ERR_SNAPSHOT ") {
            Err(Error::vm_msg(format!(
                "worker SNAPSHOT_ASYNC failed: {}",
                rest.trim()
            )))
        } else {
            Err(Error::vm_msg(format!(
                "worker SNAPSHOT_ASYNC: unexpected response: {}",
                line.trim()
            )))
        }
    }

    /// Best-effort `smpark_park` CONTROL RPC to the in-guest agent.
    /// Drives all secondary vCPUs into a known parked-WFI state via
    /// `ioctl(/dev/smpark, PARK)`. Returns `Ok(true)` on success,
    /// `Ok(false)` if the module isn't available (single-vCPU bake
    /// or smpark.ko not loaded), `Err` only on transport failure.
    /// Caller treats `Ok(false)` as "skip park, fall back to the
    /// existing rendezvous-only path".
    #[cfg_attr(
        not(all(target_os = "macos", target_arch = "aarch64")),
        allow(dead_code)
    )]
    fn send_smpark_park(&self) -> Result<bool, Error> {
        let body = serde_json::json!({ "action": "smpark_park" });
        match crate::exec::send_control_with_ack(
            &self.vsock_exec_path,
            &body,
            Some(Duration::from_secs(5)),
        ) {
            Ok(_) => Ok(true),
            Err(e) => {
                // Agent reports `ok=false` for "module not loaded /
                // /dev/smpark not available" — surfaces as
                // io::ErrorKind::Other with the agent's message.
                // Treat any transport-level failure as "skip park"
                // so callers don't break the snapshot path on the
                // 1-vCPU common case (where smpark is a no-op the
                // module reports as such).
                tracing::debug!(error = %e, "smpark_park unavailable; skipping");
                Ok(false)
            }
        }
    }

    /// Best-effort `smpark_unpark` CONTROL RPC. Wakes the parked
    /// secondaries. Same fallback semantics as
    /// [`Worker::send_smpark_park`] — returns `Ok(false)` if the
    /// module isn't there.
    #[cfg_attr(
        not(all(target_os = "macos", target_arch = "aarch64")),
        allow(dead_code)
    )]
    fn send_smpark_unpark(&self) -> Result<bool, Error> {
        let body = serde_json::json!({ "action": "smpark_unpark" });
        match crate::exec::send_control_with_ack(
            &self.vsock_exec_path,
            &body,
            Some(Duration::from_secs(5)),
        ) {
            Ok(_) => Ok(true),
            Err(e) => {
                tracing::debug!(error = %e, "smpark_unpark unavailable; skipping");
                Ok(false)
            }
        }
    }

    /// Best-effort QUIT. Tries the supervisor protocol first —
    /// gives the worker a chance to flush state cleanly — then
    /// falls back to SIGKILL if the worker doesn't exit within
    /// the grace window.
    fn shutdown(&mut self) {
        // Send QUIT — best effort, ignore failures (worker may
        // already be dead, mutex may be poisoned).
        if let Ok(mut ctl) = self.control.lock() {
            let _ = ctl.send_line("QUIT");
        }
        // Give the worker ~100 ms to exit cleanly. Most exits
        // happen in <10 ms; the upper bound is for slow restore-
        // cleanup paths.
        let deadline = Instant::now() + Duration::from_millis(100);
        loop {
            match self.child.try_wait() {
                Ok(Some(_)) => break,
                Ok(None) if Instant::now() < deadline => {
                    std::thread::sleep(Duration::from_millis(2));
                }
                _ => {
                    let _ = self.child.kill();
                    let _ = self.child.wait();
                    break;
                }
            }
        }
        let _ = std::fs::remove_file(&self.vsock_mux_path);
        let _ = std::fs::remove_file(&self.vsock_exec_path);
        let _ = std::fs::remove_file(&self.control_path);
        let mut h = self.vsock_mux_path.clone();
        h.set_extension("handoff");
        let _ = std::fs::remove_file(&h);
        // 0.7.49+ COW volume temps. Worker is dead; the kernel
        // already closed the fds, so unlink here is safe (no fd
        // leaks). Empty vec for restore_on_release=false / no-
        // pristine snapshots.
        for path in self.volume_temp_files.drain(..) {
            let _ = std::fs::remove_file(&path);
        }
    }
}

/// Resolved + reusable spawn config for one Image's pool.
struct SpawnConfig {
    /// High-water-mark of the resident footprint (phys_footprint, MiB)
    /// observed across this pool's workers — measured at spawn (idle,
    /// post-restore) and at release (post-workload). `0` until the first
    /// measurement. Admission charges the MEASURED footprint (+margin)
    /// instead of the `--memory` cap, which is usually a big
    /// over-estimate for CoW restores — see `learned_charge_mib`.
    /// Shared across spawns via the enclosing `Arc<SpawnConfig>`.
    observed_footprint_mib: std::sync::atomic::AtomicU64,
    worker_bin: PathBuf,
    snapshot_path: PathBuf,
    layers: Vec<PathBuf>,
    delta_squashfs: Option<PathBuf>,
    /// virtio-fs mounts to re-create on the worker side. Snapshot's
    /// metadata.json `mounts` field, propagated through `Image`.
    mounts: Vec<crate::vmm::resources::MountSpec>,
    /// virtio-blk volumes to re-attach on the worker side. Snapshot's
    /// metadata.json `volumes` field, propagated through `Image`.
    /// Each entry is `(host_file, guest_path, size_bytes, pristine)`
    /// and is passed to the worker as `--volume HOST:GUEST:SIZE`.
    ///
    /// `pristine` (0.7.49+) is the bake-time captured copy of
    /// `host_file`. 0.7.50+: when `pristine.is_some()`, spawn_one
    /// clones the pristine to a per-process temp file under /tmp
    /// and substitutes that path for `host_file` in the `--volume`
    /// arg — regardless of `policy.restore_on_release`. The
    /// Worker tracks the temp files for cleanup on drop / pool
    /// shutdown. This blocks the cross-process leak where
    /// `restore_on_release=false` workers used to write straight
    /// to the user's host file and diverge it from the snapshot's
    /// in-RAM ext4 state.
    ///
    /// CRITICAL for warm-restore correctness: init-oci's snapshot
    /// marker fires BEFORE `mount_volumes()`, so the snapshot doesn't
    /// carry the volume mount. Each restore re-runs `mount_volumes()`
    /// which scans `/dev/vd*`. If the warm-restore worker doesn't
    /// re-attach the volumes here, the device count comes up short,
    /// the ext4 mount references a non-existent device, and reads
    /// hang on the restored guest. (Field-report bug fixed 0.7.30.)
    volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)>,
    memory_mib: u32,
    vcpus: u32,
    socks_dir: PathBuf,
    /// Identifier folded into the socket file names. Just for
    /// readability when looking at /tmp.
    name_prefix: String,
    /// Honored when waiting for a freshly spawned worker's vsock
    /// socket to appear.
    spawn_timeout: Duration,
    /// `metadata.json["runtime_sha16"]` of the snapshot being
    /// restored. When this matches the current `worker_bin`'s
    /// SHA16, we know the in-guest agent shipped with this
    /// snapshot is the same agent the lib expects, so the
    /// agent-protocol probe can be skipped. ~25 ms saved per
    /// acquire.
    baker_runtime_sha16: Option<String>,
    /// `metadata.json["balloon_target_pages"]` from the snapshot
    /// — number of 4 KiB pages the host asks the guest to
    /// inflate via virtio-balloon after restore. Drops idle
    /// worker RSS from `~memory_mib` to ~25% of memory_mib on
    /// rust:1-slim and similar workloads. Plumbed through to
    /// the worker binary as `--balloon-target-pages N`.
    balloon_target_pages: Option<u32>,
    /// `metadata.json["tsi_token"]` from the snapshot — 64-char
    /// lowercase hex, encoding the 32-byte secret baked into the
    /// guest kernel's BSS at fresh bake. Re-injected on every
    /// restore so the muxer's `--tsi-token` matches what the
    /// frozen kernel already has. Absent on pre-0.6.0 snapshots
    /// (legacy unauthenticated mode).
    tsi_token: Option<String>,
    /// `metadata.json["egress_policy"]` — per-snapshot network egress
    /// policy, passed to the worker as `--egress-policy` so the muxer
    /// enforces it at TSI connect time. `None` → `allow_all`.
    egress_policy: Option<String>,
    /// 0.7.44+ `metadata.json["pre_exec_sync"]`. When true,
    /// spawn_one sets `SUPERMACHINE_PRE_EXEC_SYNC_RESUME=1` in
    /// the restored worker's env so its
    /// `supermachine-pre-exec-resume` thread pushes the 'P\n'
    /// unblock byte after restore. Required for pre_exec_sync
    /// snapshots — the saved guest is blocked inside `read(0)`
    /// and never wakes without the push.
    pre_exec_sync: bool,
    /// 0.7.49+ — `PoolPolicy::restore_on_release` propagated here
    /// for spawn-time volume COW. When true AND a volume's
    /// `pristine` is `Some`, spawn_one clones the pristine to a
    /// per-worker temp file under /tmp and substitutes the temp
    /// path for the user's host file in the `--volume` arg. Keeps
    /// per-cycle volume state in lock-step with the snapshot's
    /// captured ext4 RAM state — the user's host file never gets
    /// the worker's mid-life writes. Worker drop unlinks the temp.
    restore_on_release: bool,
}

/// Admission charge for one worker: the pool's measured resident
/// footprint high-water-mark + 25% margin (+ overhead), never above the
/// configured cap. Unmeasured (`observed == 0`, i.e. the pool's first
/// spawn) charges the conservative full cap.
///
/// Why charge by observation instead of the cap: a restored VM's
/// `--memory` is fixed at bake time, but a CoW restore actually commits
/// a small fraction of it (e.g. ~288 MiB resident for an 8 GiB-cap
/// browser image). Charging the cap made every pool spawn past
/// `budget/cap` sit out the FULL 60 s admission timeout before the
/// forward-progress override force-admitted it — pure dead time that
/// turned a ~2 s pool spin-up into ~60 s per extra worker. The charge
/// is a prediction, not a guarantee, so the margin + the budget's 20%
/// host headroom + the host memory-pressure backstop (spawns pause
/// while the host is genuinely tight) carry the overcommit risk; the
/// cap ceiling means we never charge MORE than the worst case. There
/// is deliberately no half-cap floor: the floor is what kept the
/// 8 GiB-cap/288 MiB-resident shape stalling, and footprint GROWTH is
/// fed back via the high-water-mark (later spawns charge more as the
/// fleet's real usage grows).
fn learned_charge_mib(memory_mib: u32, observed_mib: u64, overhead: u64) -> u64 {
    let cap = memory_mib as u64 + overhead;
    if observed_mib == 0 {
        return cap;
    }
    let margined = observed_mib + observed_mib / 4 + overhead; // +25%
    margined.min(cap)
}

/// MiB to reserve from the process-wide memory-admission budget for a no-virt
/// **sentry** unit (Linux/x86_64). Unlike the KVM worker — one subprocess whose
/// whole guest RAM is committed up front — the sentry forks a fresh cgroup-capped
/// cell PER concurrent exec, so the resident worst case scales with the number of
/// concurrent cells, each bounded by its `memory.max` (= `mem_mib`) plus the same
/// fixed host-side overhead the worker charges. A one-shot `Vm::start_sentry`
/// passes `cells = 1`; a `SentryPool` with concurrency cap `max` passes
/// `cells = max` so the pool reserves its WHOLE worst-case footprint at build —
/// the analogue of the KVM pool's per-VM charge summed over its admission cap,
/// which is what stops an unbounded sentry pool from OOMing the host.
///
/// `mem_mib == 0` (the explicitly-uncapped / no-baked-memory case) charges only
/// the overhead per cell — we still want SOME accounting, but with no cgroup
/// ceiling there is no worst case to bound against, so we don't fabricate one.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_admission_charge_mib(mem_mib: u64, cells: u64) -> u64 {
    let per_cell = mem_mib.saturating_add(crate::memory_admission::WORKER_OVERHEAD_MIB);
    per_cell.saturating_mul(cells.max(1))
}

/// The cgroup-`memory.max` value (MiB) a sentry cell is bounded to for `image`
/// under `config` — the same derivation [`Image::sentry_sandbox_with`] uses to
/// set the cell's `memory.max`. `0` when the sandbox is explicitly uncapped (no
/// `memory.max` set), so the admission charge degrades to overhead-only.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_cell_mem_mib(image: &Image, config: &VmConfig) -> u64 {
    if config.sentry_uncapped {
        return 0;
    }
    config.memory_mib.unwrap_or(image.memory_mib) as u64
}

#[cfg(test)]
mod learned_charge_tests {
    use super::learned_charge_mib;
    const OH: u64 = 64;

    #[test]
    fn unmeasured_charges_full_cap() {
        // No observation yet → conservative full cap.
        assert_eq!(learned_charge_mib(512, 0, OH), 512 + OH);
    }

    #[test]
    fn measured_charges_observed_plus_margin() {
        // 300 MiB observed → 300 + 25% (75) + OH = 439.
        assert_eq!(learned_charge_mib(512, 300, OH), 375 + OH);
    }

    #[test]
    fn never_exceeds_the_cap() {
        // Observed at/above the cap → clamped to the conservative cap;
        // the learned charge never exceeds the worst case.
        assert_eq!(learned_charge_mib(512, 1000, OH), 512 + OH);
    }

    #[test]
    fn cow_restored_pool_fits_under_budget_without_timeouts() {
        // The field shape that motivated learned charging: 8 GiB-cap
        // VMs whose CoW restores commit ~288 MiB. The old cap charge
        // (8256+64) packed only 2 under a 19660 MiB budget — workers
        // 3+ each burned the full 60 s admission timeout. The learned
        // charge must fit a pool of 8 once the first worker's
        // footprint is observed.
        let charge = learned_charge_mib(8256, 288, OH);
        assert_eq!(charge, 288 + 72 + OH);
        assert!(
            charge * 8 < 19660,
            "8 CoW-restored workers must fit the integrator's budget (charge {charge} MiB)"
        );
    }
}

#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod sentry_admission_tests {
    use super::sentry_admission_charge_mib;
    const OH: u64 = crate::memory_admission::WORKER_OVERHEAD_MIB; // 64

    #[test]
    fn one_shot_charges_one_capped_cell_plus_overhead() {
        // Vm::start_sentry reserves exactly one cell's worst case.
        assert_eq!(sentry_admission_charge_mib(512, 1), 512 + OH);
    }

    #[test]
    fn pool_charge_scales_with_concurrency_cap() {
        // The helper composes N live cells' worst-case footprint. Pool acquire
        // charges one cell at a time; callers can still use the helper to reason
        // about a saturated pool.
        assert_eq!(sentry_admission_charge_mib(512, 4), 4 * (512 + OH));
        // Strictly grows with the cell count.
        assert!(sentry_admission_charge_mib(512, 8) > sentry_admission_charge_mib(512, 4));
    }

    #[test]
    fn uncapped_cell_charges_overhead_only() {
        // mem_mib == 0 (sentry_uncapped / no baked memory): no cgroup ceiling, so
        // no worst case to bound — charge only the per-cell overhead, never a
        // fabricated cap.
        assert_eq!(sentry_admission_charge_mib(0, 1), OH);
        assert_eq!(sentry_admission_charge_mib(0, 4), 4 * OH);
    }

    #[test]
    fn zero_cells_clamps_to_one() {
        // A pool's `max` is clamped to >=1 by `Admission::new`; mirror that here
        // so a degenerate `max == 0` still reserves a real (non-zero) cell.
        assert_eq!(sentry_admission_charge_mib(512, 0), 512 + OH);
    }

    #[test]
    fn over_budget_pool_reserves_more_than_the_budget() {
        // THE OOM-prevention contract: a pool whose worst-case footprint exceeds
        // the admission budget asks the accountant for MORE than the budget — so
        // `memory_admission::admit_with` back-pressures it (blocks until a release
        // or the admission timeout) instead of letting unbounded cell spawn
        // overcommit host RAM into OOM. Here: 32 cells × (512 + 64) = 18432 MiB
        // against a 4096-MiB budget.
        let budget_mib = 4096u64;
        let pool_charge = sentry_admission_charge_mib(512, 32);
        assert!(
            pool_charge > budget_mib,
            "an over-budget pool must reserve > the budget so admission gates it \
             (charge {pool_charge} MiB vs budget {budget_mib} MiB)"
        );
        // A pool that DOES fit (4 cells = 2304 MiB) charges within budget, so it
        // admits without back-pressure — the gate only bites the oversized pool.
        assert!(sentry_admission_charge_mib(512, 4) <= budget_mib);
    }
}

impl SpawnConfig {
    /// MiB to reserve from the memory-admission budget for a new worker:
    /// the pool's MEASURED footprint high-water-mark + a 25% margin,
    /// never above the configured cap (+ overhead). See
    /// [`learned_charge_mib`] for why observation, not the guest
    /// ceiling, is the right charge for CoW-restored pool VMs.
    ///
    /// Before anything is measured, the expectation is the
    /// restore-typical baseline, NOT the cap: every `spawn_one` is a
    /// CoW restore whose spawn-time resident cost is a few hundred MiB
    /// regardless of the guest ceiling (an idle alpine restore measures
    /// single-digit MiB of phys_footprint; the integrator's browser
    /// image, ~288 MiB). The ceiling is only approached as the workload
    /// dirties pages — captured later by the spawn/release re-measures
    /// feeding the HWM, with the budget's 20% headroom + the host
    /// memory-pressure backstop carrying the prediction error. Charging
    /// the cap pre-measurement re-creates the cold-prewarm
    /// chicken-and-egg: all parallel min-spawns ask for the ceiling,
    /// blow the budget, and serialize on the 60 s admission timeout
    /// with nobody running yet to BE measured.
    fn admission_charge_mib(&self) -> u64 {
        /// Pre-measurement expectation for one CoW restore (pre-margin).
        const UNMEASURED_RESTORE_BASELINE_MIB: u64 = 256;
        let overhead = crate::memory_admission::WORKER_OVERHEAD_MIB;
        let observed = self
            .observed_footprint_mib
            .load(std::sync::atomic::Ordering::Relaxed);
        let effective = if observed == 0 {
            UNMEASURED_RESTORE_BASELINE_MIB
        } else {
            observed
        };
        learned_charge_mib(self.memory_mib, effective, overhead)
    }

    /// Spawn ONE worker subprocess in `--pool-worker` mode and
    /// return it ready to serve. The worker handles its initial
    /// snapshot restore on boot and writes a `DONE …` line on
    /// the supervisor control socket once that completes — we
    /// block on that line so callers can treat the returned
    /// `Worker` as fully restored.
    fn spawn_one(&self) -> Result<Worker, Error> {
        use std::os::unix::net::UnixListener;

        // 64-bit hex suffix from `unique_suffix()` (nanos +
        // monotonic counter). Distinct across parallel spawns;
        // collision-free in practice.
        let suffix = unique_suffix();
        let vsock_mux_path = self
            .socks_dir
            .join(format!("{}-{:016x}.sock", self.name_prefix, suffix));
        let vsock_exec_path = {
            let mut p = vsock_mux_path.clone();
            let mut name = p.file_name().unwrap().to_owned();
            name.push("-exec");
            p.set_file_name(name);
            p
        };
        let control_path = {
            let mut p = vsock_mux_path.clone();
            let mut name = p.file_name().unwrap().to_owned();
            name.push("-ctl");
            p.set_file_name(name);
            p
        };
        let _ = std::fs::remove_file(&vsock_mux_path);
        let _ = std::fs::remove_file(&vsock_exec_path);
        let _ = std::fs::remove_file(&control_path);

        // Lib listens on the control socket BEFORE spawning so
        // the worker's `connect()` always finds it.
        let ctl_listener = UnixListener::bind(&control_path).map_err(|e| {
            Error::vm_msg(format!(
                "bind control socket {}: {e}",
                control_path.display()
            ))
        })?;

        // ATOMIC-RENAME SENTINEL POLL: when the Image was returned
        // from an always-pipelined-skip-warm `.build()`, the bg
        // save of `restore.snap` may still be in flight. The worker
        // would fail-fast on `--restore-from` pointing at a missing
        // file. `save_compact_to_file` writes to `<path>.partial`
        // and atomic-renames to `<path>` on completion, so file
        // existence ↔ save complete.
        //
        // Bound at `spawn_timeout` (default 30s; configurable via
        // `VmConfig::restore_timeout`). Steady-state save lands in
        // ~50–200 ms on typical hardware; the cap is well over the
        // p99.99. On timeout, surface a clear error rather than
        // letting the worker fail with a confusing "no such file"
        // message.
        //
        // No-op when the file already exists (the common case for
        // workers spawned after the first acquire, or for Images
        // loaded via `Image::from_snapshot`).
        if !self.snapshot_path.is_file() {
            let poll_t0 = Instant::now();
            let poll_deadline = poll_t0 + self.spawn_timeout;
            let mut backoff = Duration::from_millis(2);
            loop {
                if self.snapshot_path.is_file() {
                    if crate::trace::enabled("timings") {
                        eprintln!(
                            "[spawn_one] waited {:?} for snapshot file to land at {}",
                            poll_t0.elapsed(),
                            self.snapshot_path.display(),
                        );
                    }
                    break;
                }
                if Instant::now() > poll_deadline {
                    let _ = std::fs::remove_file(&control_path);
                    return Err(Error::vm_msg(format!(
                        "worker spawn: snapshot file {} did not appear within {:?} \
                         (bg save in flight from a pipelined bake — increase \
                         `VmConfig::restore_timeout` if your disk is slow)",
                        self.snapshot_path.display(),
                        self.spawn_timeout
                    )));
                }
                std::thread::sleep(backoff);
                backoff = (backoff * 2).min(Duration::from_millis(50));
            }
        }

        let mut cmd = Command::new(&self.worker_bin);
        for layer in &self.layers {
            cmd.arg("--virtio-blk").arg(layer);
        }
        if let Some(delta) = &self.delta_squashfs {
            cmd.arg("--virtio-blk").arg(delta);
        }
        // virtio-fs mounts persisted in the snapshot metadata. The
        // worker re-constructs the VirtioFs devices at the same MMIO
        // slots so the guest's baked-in FDT entries find them.
        // Wire encoding mirrors the bake-time form:
        // `HOST:TAG:GUEST_PATH[:POLICY]`.
        for m in &self.mounts {
            cmd.arg("--mount")
                .arg(format!("{}:{}:{}", m.host_path, m.guest_tag, m.guest_path));
        }
        // virtio-blk volumes persisted in the snapshot metadata.
        // Same critical replay as `mounts`: the FDT baked into the
        // snapshot expects these virtio-blk devices to be present
        // at restore, and init-oci's `mount_volumes()` re-runs
        // post-restore against `/dev/vd<N>`. Without this loop, the
        // post-restore guest mounts ext4 against a non-existent
        // device and reads hang.
        //
        // Wire encoding mirrors the bake-time form:
        // `HOST_FILE:GUEST_PATH:SIZE_BYTES`. Worker's CLI parser
        // builds a `VolumeSpec` from each, attaches a `virtio-blk`
        // device, and orders volumes after `layers + delta` so the
        // guest sees `/dev/vd<n+1>..` (same ordering the bake-time
        // worker used, keeping init-oci's index → device mapping
        // stable across bake and restore).
        //
        // 0.7.50+ COW-on-spawn (was 0.7.49 + restoreOnRelease=true
        // only; broadened to always-on when pristine is available):
        // every spawn clones the pristine to a per-process temp
        // file. The worker's writes land on the temp; the user's
        // host file stays at bake-time content forever.
        //
        // Why always-on, not just for `restoreOnRelease=true`:
        //
        //   * `restoreOnRelease=false` pools share one worker
        //     across acquires within ONE process — writes still
        //     persist within that process (same worker → same COW
        //     temp). The skip-restore semantics are unchanged.
        //   * Across PROCESSES, each new pool gets a fresh COW
        //     clone from the bake-time pristine. Process N's
        //     writes never leak into process N+1's view.
        //
        // Pre-0.7.50 with `restoreOnRelease=false`, the worker
        // wrote directly to the user's host file. The on-disk
        // bytes drifted from the snapshot's in-RAM ext4 state on
        // every cycle. The next process re-opened the same host
        // file with the same in-RAM snapshot and got divergence:
        // EUCLEAN on lstat for files the prior process touched
        // (vite's `.vite/deps`), or silent leakage of bake-foreign
        // files visible only via disk-bypassing probes (the
        // shape this test asserts).
        //
        // On release of a worker with a COW temp:
        //   * `restoreOnRelease=true`: release path at 2503 kills
        //     the worker (RESTORE can't reset open virtio-blk
        //     fds), replenisher spawns a fresh COW-backed worker.
        //   * `restoreOnRelease=false`: release path returns
        //     early at 2481 (push to idle), worker is reused
        //     within the process. The COW temp lives until the
        //     worker dies (Pool drop / shutdown). See
        //     `cow_volume_for_spawn` for the temp path scheme and
        //     `Worker::shutdown` for unlink.
        let cow_volumes_enabled = self.volumes.iter().any(|(_, _, _, p)| p.is_some());
        let mut volume_temp_files: Vec<PathBuf> = Vec::new();
        for (i, (host_file, guest_path, size_bytes, pristine)) in self.volumes.iter().enumerate() {
            let effective_host = if cow_volumes_enabled {
                match pristine.as_ref() {
                    Some(pristine_path) => {
                        match cow_volume_for_spawn(pristine_path, i, suffix) {
                            Ok(temp) => {
                                volume_temp_files.push(temp.clone());
                                temp
                            }
                            Err(e) => {
                                // Clean up any temps we already
                                // made — never strand them in /tmp.
                                for p in &volume_temp_files {
                                    let _ = std::fs::remove_file(p);
                                }
                                let _ = std::fs::remove_file(&control_path);
                                return Err(Error::vm_msg(format!(
                                    "spawn_one: COW volume {}: {e}",
                                    pristine_path.display()
                                )));
                            }
                        }
                    }
                    None => {
                        // Snapshot pre-dates the pristine fix.
                        // One-line warn (rate-limited globally so
                        // multi-volume pools don't spam) then
                        // fall back to legacy behavior.
                        warn_legacy_volume_without_pristine_once(host_file);
                        host_file.clone()
                    }
                }
            } else {
                host_file.clone()
            };
            cmd.arg("--volume").arg(format!(
                "{}:{}:{}",
                effective_host.display(),
                guest_path,
                size_bytes
            ));
        }
        cmd.arg("--memory").arg(self.memory_mib.to_string());
        cmd.arg("--vcpus").arg(self.vcpus.to_string());
        cmd.arg("--restore-from").arg(&self.snapshot_path);
        cmd.arg("--cow-restore");
        cmd.arg("--vsock-mux").arg(&vsock_mux_path);
        cmd.arg("--vsock-exec").arg(&vsock_exec_path);
        cmd.arg("--pool-worker").arg(&control_path);
        // 0.7.44+ pre-compute host IPv6 routing in the lib (cached
        // process-wide) and pass to the worker as an env var. The
        // worker used to run its own probe at startup — ~1-3 ms
        // bind+connect+getsockname on every spawn_one. Identical
        // semantics; the worker still falls back to its local probe
        // if the env var isn't set (direct worker invocation).
        // `host_ipv6_route` (not `cached_host_ipv6_route`): an explicit
        // SUPERMACHINE_HOST_IPV6 set on THIS process must win over the
        // probe, matching the worker's own precedence — otherwise an
        // operator override would be silently stomped on every spawn.
        cmd.env(
            "SUPERMACHINE_HOST_IPV6",
            if crate::utils::net::host_ipv6_route() {
                "1"
            } else {
                "0"
            },
        );
        if let Some(pages) = self.balloon_target_pages {
            cmd.arg("--balloon-target-pages").arg(pages.to_string());
        }
        // TSI control-channel auth: re-inject the per-snapshot
        // token so the muxer's expected-prefix matches what the
        // frozen kernel will send on every control DGRAM. See
        // `kernel-build/patches/af-tsi/0014-*.patch`.
        if let Some(hex) = self.tsi_token.as_deref() {
            cmd.arg("--tsi-token").arg(hex);
        }
        // Per-snapshot egress policy: the muxer enforces it at TSI
        // connect time (one-time, never per-packet). Set once at spawn;
        // the worker's process-global policy persists across this
        // worker's cycle-restores. `None` → worker default (allow_all).
        if let Some(policy) = self.egress_policy.as_deref() {
            cmd.arg("--egress-policy").arg(policy);
        }
        // 0.7.44+ pre-exec sync resume. Snapshot was taken with
        // init-oci paused inside `read(0)`. Tell the worker to
        // start its supermachine-pre-exec-resume thread, which
        // pushes 'P\n' shortly after restore so init-oci unblocks.
        // Without this, the restored guest hangs in the read for
        // 30 s, then the SIGALRM fires and the bake/exec continues
        // (slowly + with a "timeout" log line that's confusing).
        if self.pre_exec_sync {
            cmd.env("SUPERMACHINE_PRE_EXEC_SYNC_RESUME", "1");
        }
        // Quiet by default — embedders don't want VM kernel logs
        // on their stdout. SUPERMACHINE_WORKER_LOG=1 to opt in.
        let log_to_stdio = std::env::var("SUPERMACHINE_WORKER_LOG")
            .map(|v| v == "1" || v == "true")
            .unwrap_or(false);
        // Pipe stdout+stderr to a file we can inspect later when the
        // user opts in via SUPERMACHINE_WORKER_LOG_FILE=<path>. A `%`
        // in the path is expanded to THIS worker's unique hex suffix
        // (the same id used for its sockets), so a pool of concurrent
        // workers writes separate, correlatable logs — e.g.
        // `/tmp/sm-worker-%.log` → `/tmp/sm-worker-1a2b...f.log`. This
        // makes a post-mortem (e.g. tracing an "agent closed before
        // EXIT" back to one worker's muxer diagnostics) tractable.
        // Without a `%`, all workers append to the one named file
        // (legacy behaviour; lines from different workers interleave).
        let log_file_path = std::env::var("SUPERMACHINE_WORKER_LOG_FILE").ok().map(|p| {
            if p.contains('%') {
                p.replace('%', &format!("{suffix:016x}"))
            } else {
                p
            }
        });
        if let Some(path) = log_file_path.as_deref() {
            let f = std::fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open(path)
                .ok();
            if let Some(f) = f {
                let f2 = f.try_clone().ok();
                cmd.stdout(Stdio::from(f));
                if let Some(f2) = f2 {
                    cmd.stderr(Stdio::from(f2));
                } else {
                    cmd.stderr(Stdio::null());
                }
            }
        } else if !log_to_stdio {
            cmd.stdout(Stdio::null()).stderr(Stdio::null());
        }

        // Memory admission: reserve this worker's footprint against the
        // process-wide budget BEFORE forking. If too many workers are
        // already live this BLOCKS until one is released, rather than
        // overcommitting host RAM into macOS jetsam/OOM. The reservation
        // rides in the returned `Worker` and is released on drop. Declared
        // before `SpawnCleanup` so an early return kills the child first,
        // then releases the budget. Disabled with
        // SUPERMACHINE_MEMORY_BUDGET_MIB=0.
        //
        // The charge is the pool's MEASURED footprint high-water-mark +
        // margin (capped at the configured guest RAM + overhead; the
        // first spawn charges the cap) — see admission_charge_mib. The
        // host memory-pressure backstop pauses spawns while the host is
        // actually tight, so the learned-charge overcommit can't drive
        // jetsam; it's a no-op sysctl when pressure is normal.
        crate::memory_admission::await_pressure_relief();
        // admit_with: the charge is re-computed while blocked, so a cold
        // pool's parallel prewarm doesn't serialize on the timeout — the
        // first sibling to finish restoring records the real footprint
        // (notify_waiters below) and the blocked spawns re-ask with the
        // learned charge instead of their original cap-sized request.
        let admission_guard = crate::memory_admission::admit_with(|| self.admission_charge_mib());

        // Boot-rate gate: cap how many spawns BOOT concurrently (vCPU
        // threads + restore are thread/CPU-heavy; a storm can hit EAGAIN).
        // Acquired AFTER the memory admit (consistent global lock order, no
        // AB-BA) and held only as a local for the duration of this boot —
        // it is NOT moved into `Worker`, so it releases when `spawn_one`
        // returns. That caps concurrent boots, never running VMs, so VM
        // density and the acquire/exec hot paths are untouched. On an early
        // return it drops here (frees the slot). Disabled with
        // SUPERMACHINE_MAX_CONCURRENT_SPAWNS=0.
        let _spawn_permit = crate::spawn_concurrency::acquire();

        let __t0 = Instant::now();
        let child = cmd.spawn().map_err(|e| {
            Error::vm_msg(format!("spawn worker {}: {e}", self.worker_bin.display()))
        })?;
        let __t_spawned = __t0.elapsed();

        // RAII cleanup for a partially-spawned worker. Until ownership
        // passes to the returned `Worker` on success, ANY early return
        // (or panic) below must kill the orphaned worker process and
        // unlink its COW volume clones + control socket. Without this a
        // failed handshake (stale agent, restore stall, control-accept
        // timeout) leaks a live `supermachine-worker` process plus up
        // to GiB of `/tmp/supermachine-vol-*.img` per attempt — only
        // swept on the NEXT pool init, not in-run. `child` is otherwise
        // untouched between here and the success `Ok(Worker)`.
        struct SpawnCleanup {
            child: Option<std::process::Child>,
            cleanup_paths: Vec<PathBuf>,
            armed: bool,
        }
        impl Drop for SpawnCleanup {
            fn drop(&mut self) {
                if !self.armed {
                    return;
                }
                if let Some(mut c) = self.child.take() {
                    let _ = c.kill();
                    let _ = c.wait();
                }
                for p in &self.cleanup_paths {
                    let _ = std::fs::remove_file(p);
                }
            }
        }
        let mut spawn_cleanup = {
            let mut cleanup_paths = volume_temp_files.clone();
            cleanup_paths.push(control_path.clone());
            SpawnCleanup {
                child: Some(child),
                cleanup_paths,
                armed: true,
            }
        };

        // Worker connects to ctl_listener once it's far enough
        // through main() to call UnixStream::connect — well
        // before any expensive work. We just accept and read the
        // initial DONE line, which the worker writes after its
        // snapshot restore completes. That means callers get a
        // worker that's truly ready to serve, no further polling.
        ctl_listener
            .set_nonblocking(true)
            .map_err(|e| Error::vm_msg(format!("set control listener nonblocking: {e}")))?;
        let deadline = Instant::now() + self.spawn_timeout;
        let mut backoff = Duration::from_millis(1);
        let stream = loop {
            match ctl_listener.accept() {
                Ok((s, _)) => break s,
                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
                    if Instant::now() > deadline {
                        let _ = std::fs::remove_file(&control_path);
                        return Err(Error::vm_msg(format!(
                            "worker spawn: control connect did not arrive within {:?}",
                            self.spawn_timeout
                        )));
                    }
                    std::thread::sleep(backoff);
                    backoff = (backoff * 2).min(Duration::from_millis(10));
                }
                Err(e) => {
                    let _ = std::fs::remove_file(&control_path);
                    return Err(Error::vm_msg(format!("worker spawn: control accept: {e}")));
                }
            }
        };
        // Switch back to blocking — the rest is line-oriented and
        // synchronous.
        stream
            .set_nonblocking(false)
            .map_err(|e| Error::vm_msg(format!("set control stream blocking: {e}")))?;
        let writer = stream
            .try_clone()
            .map_err(|e| Error::vm_msg(format!("clone control stream: {e}")))?;
        let mut control = ControlChannel {
            reader: std::io::BufReader::new(stream),
            writer,
        };

        // The supervisor protocol opens with a "READY\n" line
        // from the worker (it writes that immediately after
        // connecting, before doing any HVF setup). We then send
        // RESTORE <snap_path>, the worker boots+restores, and
        // writes back DONE us=… host_port=… …
        let ready = match control.read_line() {
            Ok(l) => l,
            Err(e) => {
                let _ = std::fs::remove_file(&control_path);
                return Err(Error::vm_msg(format!("worker spawn: read READY: {e}")));
            }
        };
        if ready.trim() != "READY" {
            let _ = std::fs::remove_file(&control_path);
            return Err(Error::vm_msg(format!(
                "worker spawn: expected READY, got: {}",
                ready.trim()
            )));
        }
        let snap_path_str = self.snapshot_path.to_string_lossy().to_string();
        if let Err(e) = control.send_line(&format!("RESTORE {snap_path_str}")) {
            let _ = std::fs::remove_file(&control_path);
            return Err(Error::vm_msg(format!(
                "worker spawn: send initial RESTORE: {e}"
            )));
        }
        let done = match control.read_line() {
            Ok(l) => l,
            Err(e) => {
                let _ = std::fs::remove_file(&control_path);
                return Err(Error::vm_msg(format!(
                    "worker spawn: read initial DONE: {e}"
                )));
            }
        };
        if !done.starts_with("DONE") {
            let _ = std::fs::remove_file(&control_path);
            return Err(Error::vm_msg(format!(
                "worker spawn: expected initial DONE, got: {}",
                done.trim()
            )));
        }
        if crate::trace::enabled("timings") {
            eprintln!(
                "[spawn_one] spawn={:?} accept_to_done={:?} total={:?}",
                __t_spawned,
                __t0.elapsed() - __t_spawned,
                __t0.elapsed()
            );
        }

        // Probe the in-guest agent's wire-protocol version.
        // Catches the failure mode where a snapshot was baked
        // against a previous supermachine release whose agent
        // doesn't speak the current protocol — without this,
        // unknown JSON fields (stage_files / chain) silently
        // hit serde's `#[serde(default)]` ignore-unknown path
        // and the host sees "exec ran with exit 0" but the
        // staged file was never written.
        //
        // Fast path: skip the probe when the snapshot's recorded
        // `runtime_sha16` matches the current worker binary's
        // SHA16. By construction the in-guest agent shipped at
        // bake time matches what the lib expects — the probe
        // would always succeed, just round-trip for nothing.
        // ~25 ms saved per acquire on the hot path.
        //
        // Multi-vCPU caveat: HVF can't round-trip ICH_LR_EL2
        // (GIC List Registers) cleanly across snapshot/restore,
        // so a multi-vCPU restored guest sometimes RCU-stalls
        // on boot — see docs/design/multi-vcpu-snapshot-
        // intermittency-2026-04-27.md and the deferral note. In
        // that state the agent never responds and the probe
        // times out, masquerading as a stale-agent error.
        // For vcpus > 1 we downgrade probe timeouts to a soft
        // warning instead of failing spawn — the workload's
        // first exec() will surface the real error if the
        // guest is genuinely hung. Definitive stale-agent
        // signals (probe ack with old protocol number) still
        // hard-fail.
        //
        // Post-restore smpark_unpark. The snapshot file was
        // captured with all secondaries parked in WFI inside
        // smpark_park_routine (so HVF's lossy ICH/redistributor
        // round-trip lands on a byte-identical state). On
        // restore, those secondaries come back STILL in the
        // park spin loop with `unpark_signal=0`. Without this
        // RPC, they sit in WFI forever and any task scheduled on
        // them stalls — manifests as `__skb_wait_for_more_packets`
        // hangs on rosettad's UNIX-DGRAM socket when the first
        // amd64 binary tries to start.
        //
        // The agent itself runs on vCPU 0 (the boot vCPU, never
        // parked) so the vsock RPC works pre-unpark. The unpark
        // routine on vCPU 0 sends an IPI to each secondary which
        // wakes them from WFI; they re-check `unpark_signal`,
        // see 1, exit the spin loop, re-enable IRQs, and return
        // from the SGI handler back to whatever the kernel
        // scheduler was doing pre-park (typically the idle loop).
        //
        // Best-effort: agent reports `ok=false` when the kernel
        // module isn't loaded (single-vCPU bake or pre-smpark
        // snapshot) — that surfaces as transport `Ok(_)` here
        // since the agent did ack the request, just with
        // `ok=false`. Either way we ignore the result and let
        // probe_agent_protocol below tell us if the guest is
        // genuinely alive.
        //
        // Skip entirely for single-vCPU — no secondaries to
        // unpark, the RPC is pure overhead (~1-3 ms vsock
        // round-trip). The integrator's `vm.exec()` hot path
        // doesn't need this.
        if self.vcpus > 1 {
            let body = serde_json::json!({ "action": "smpark_unpark" });
            match crate::exec::send_control_with_ack(
                &vsock_exec_path,
                &body,
                Some(Duration::from_secs(2)),
            ) {
                Ok(_) => {}
                Err(e) => {
                    tracing::debug!(
                        error = %e,
                        "post-restore smpark_unpark RPC error (continuing)"
                    );
                }
            }
        }

        // Drop the guest's VFS caches so the first
        // virtio-fs/virtio-blk access after this cold restore
        // re-LOOKUPs through the daemon / re-reads from disk
        // instead of returning stale bytes the snapshot captured
        // alongside RAM. Single-vCPU and multi-vCPU both need
        // this — orthogonal to smpark.
        //
        // 0.7.44+ probe-skip: only safe to skip when there are NO
        // virtio-fs mounts AND no virtio-blk volumes. Both surfaces
        // can have host-side state that drifts between bake and
        // restore (host file changes for mounts; out-of-band volume
        // writes via Vm::stage_files or external dd for volumes).
        // Layers stay read-only and content-addressable, so they
        // don't need invalidation.
        //
        // Saves ~5-15 ms vsock RPC on the dominant
        // Image.build-from-Docker no-mounts no-volumes case.
        if !self.mounts.is_empty() || !self.volumes.is_empty() {
            let _ = drop_vfs_caches_via_agent(&vsock_exec_path, self.memory_mib);
        }
        // 0.7.51+ wall-clock sync. See the matching call in
        // `Worker::send_restore` for the rationale: even pure-image
        // snapshots with no mounts need this — Date.now() in the
        // guest reads CLOCK_REALTIME, which was frozen at snapshot
        // capture and stays frozen across restore (the vtimer
        // state covers CLOCK_MONOTONIC but not the wall-clock
        // offset). Fixes JWT iat/exp, OAuth, TLS cert validation,
        // etc. against external authorities.
        let _ = sync_time_via_agent(&vsock_exec_path);

        let probe_skip = self
            .baker_runtime_sha16
            .as_deref()
            .and_then(|stored| {
                current_worker_sha16(&self.worker_bin).map(|current| stored == current.as_str())
            })
            .unwrap_or(false);
        if !probe_skip {
            if let Err(e) = probe_agent_protocol(&vsock_exec_path, self.vcpus) {
                if self.vcpus > 1 && e.is_likely_multi_vcpu_restore_stall() {
                    eprintln!(
                        "supermachine: WARNING multi-vCPU ({}vCPU) snapshot agent probe \
                         timed out — guest may be RCU-stalled after restore. \
                         Continuing (multi-vCPU is unsupported per design); \
                         workload exec will surface a real error if the guest is hung.",
                        self.vcpus
                    );
                } else {
                    let _ = std::fs::remove_file(&control_path);
                    return Err(e);
                }
            }
        }

        // 0.7.67+ — volume-mount readiness gate. init-oci runs
        // mount_volumes() AFTER the snapshot/exec-ready marker, so a
        // freshly-restored guest can accept execs before /data is
        // mounted. Block until every volume appears in /proc/mounts so
        // the caller's first exec can't read an unmounted volume (the
        // snapshot_combinations "T2" empty-/data race). On failure we
        // return before disarming `spawn_cleanup`, so its armed guard
        // tears down the child — exactly like the probe failure above.
        if !self.volumes.is_empty() {
            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
            let gps: Vec<String> = self
                .volumes
                .iter()
                .map(|(_, gp, _, _)| gp.clone())
                .collect();
            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
            if let Err(e) = wait_for_volumes_mounted(&vsock_exec_path, &gps) {
                let _ = std::fs::remove_file(&control_path);
                return Err(e);
            }
        }

        // Spawn succeeded end-to-end: take the child back from the
        // cleanup guard and disarm it (the COW temps + control socket
        // now live for the Worker's lifetime).
        spawn_cleanup.armed = false;
        let child = spawn_cleanup
            .child
            .take()
            .expect("spawn_cleanup holds the child until success");
        // Fold this freshly-restored worker's resident footprint into
        // the pool's high-water-mark. Per-spawn (off the hot path).
        // This is the feedback that lets every LATER spawn charge the
        // measured footprint instead of the guest ceiling — see
        // admission_charge_mib.
        if let Some(fp) = crate::memory_admission::phys_footprint_mib(child.id()) {
            let prev = self
                .observed_footprint_mib
                .fetch_max(fp, std::sync::atomic::Ordering::Relaxed);
            if fp > prev {
                eprintln!(
                    "[admission] observed worker footprint {fp} MiB (cap {} MiB) — \
                     new high-water-mark",
                    self.memory_mib
                );
            }
            if prev == 0 {
                // First measurement for this pool: spawns blocked on the
                // conservative cap charge can now re-ask with the learned
                // one — wake them instead of letting them sit out the
                // 60 s admission timeout.
                crate::memory_admission::notify_waiters();
            }
        }
        Ok(Worker {
            child,
            vsock_mux_path,
            vsock_exec_path,
            control_path,
            control: Arc::new(Mutex::new(control)),
            last_restore_path: self.snapshot_path.clone(),
            memory_mib: self.memory_mib,
            has_mounts: !self.mounts.is_empty() || !self.volumes.is_empty(),
            volume_guest_paths: self
                .volumes
                .iter()
                .map(|(_, gp, _, _)| gp.clone())
                .collect(),
            vcpus: self.vcpus,
            has_cow_volumes: !volume_temp_files.is_empty(),
            volume_temp_files,
            _admission: admission_guard,
        })
    }
}

/// Clone the pristine to a per-worker
/// `/tmp/supermachine-vol-<pid>-<suffix>-<index>.img`.
///
/// `pid` ties the file back to the process that owns it (used by
/// crash-recovery sweep to safely unlink only files belonging to
/// dead processes). `suffix` is the same `unique_suffix()` used
/// for the worker's socket paths — gives us per-spawn uniqueness
/// without a separate counter.
///
/// Uses the bake crate's `cow_or_copy` helper: APFS clonefile is
/// metadata-only (~µs), so per-spawn cost is negligible on the
/// dominant macOS-APFS path. Falls back to a full `std::fs::copy`
/// on non-APFS filesystems.
///
/// Caller MUST track the returned path on the spawned Worker so
/// the temp gets unlinked when the worker exits.
fn cow_volume_for_spawn(pristine: &Path, index: usize, suffix: u64) -> std::io::Result<PathBuf> {
    let pid = std::process::id();
    let dst =
        std::env::temp_dir().join(format!("supermachine-vol-{pid}-{suffix:016x}-{index}.img"));
    crate::bake::cow_or_copy(pristine, &dst)?;
    Ok(dst)
}

/// Process-wide once-per-process latch around `sweep_stranded_volume_temps`.
/// Pool builds can fire concurrently; only the first one pays the
/// /tmp readdir cost.
fn sweep_volume_temps_once() {
    static SWEPT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
    if SWEPT.get().is_some() {
        return;
    }
    sweep_stranded_volume_temps();
    let _ = SWEPT.set(());
}

/// Best-effort sweep of stranded COW volume temp files in /tmp.
/// Identifies files matching `supermachine-vol-<pid>-*.img` and
/// unlinks them if the owning pid no longer exists. Used by pool
/// construction (one shot per Image-pool init) so a crash in a
/// previous process doesn't leave 4 GiB+ files lying around.
///
/// Live processes are detected via `kill(pid, 0)` — same probe
/// used elsewhere in the codebase for liveness checks. ESRCH ⇒
/// dead, unlink. EPERM ⇒ alive but owned by someone else, skip.
/// On macOS POSIX semantics this is safe even for a pid that has
/// been recycled to a different process (worst case we keep the
/// file; we never unlink a live process's file by mistake).
fn sweep_stranded_volume_temps() {
    let tmp = std::env::temp_dir();
    let entries = match std::fs::read_dir(&tmp) {
        Ok(e) => e,
        Err(_) => return,
    };
    let me = std::process::id();
    for entry in entries.flatten() {
        let fname = entry.file_name();
        let Some(name) = fname.to_str() else {
            continue;
        };
        // Match supermachine-vol-<pid>-...img
        let Some(rest) = name.strip_prefix("supermachine-vol-") else {
            continue;
        };
        if !name.ends_with(".img") {
            continue;
        }
        let Some(dash) = rest.find('-') else {
            continue;
        };
        let pid_str = &rest[..dash];
        let Ok(pid) = pid_str.parse::<i32>() else {
            continue;
        };
        // Never sweep our own files — they belong to live workers
        // that haven't released yet.
        if pid as u32 == me {
            continue;
        }
        // Liveness probe. kill(pid, 0) returns 0 if pid is alive
        // and we have permission to signal it, -1 with errno set
        // otherwise.
        let alive = unsafe { libc::kill(pid, 0) };
        if alive == 0 {
            // Still live, leave it.
            continue;
        }
        let err = std::io::Error::last_os_error();
        // ESRCH: no such pid → safe to unlink. EPERM: pid exists
        // but not signalable by us → conservative skip.
        if err.raw_os_error() != Some(libc::ESRCH) {
            continue;
        }
        let _ = std::fs::remove_file(entry.path());
    }
}

/// Best-effort one-shot warning when a snapshot lacks `pristine`
/// files. Pre-0.7.49 snapshots — fix requires a re-bake. Globally
/// rate-limited so a multi-volume snapshot only logs once total.
fn warn_legacy_volume_without_pristine_once(host_file: &Path) {
    static WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
    if WARNED.get().is_some() {
        return;
    }
    let _ = WARNED.set(());
    eprintln!(
        "supermachine: WARNING snapshot lacks pristine volume bytes (host_file={}); \
         workers will write directly to the host file, leaking state across \
         processes and across `restore_on_release=true` acquires — re-bake \
         under 0.7.49+ for per-spawn COW reset semantics",
        host_file.display()
    );
}

/// Minimum agent wire-protocol version this lib understands.
/// MUST match `AGENT_PROTOCOL` in
/// `crates/supermachine-guest-agent/src/main.rs`. Bump in
/// lockstep when the protocol gains a feature the lib expects
/// the agent to support (e.g. stage_files, chain).
///
/// Version log:
///   1: pre-stage_file. ExecRequest = {argv, env, cwd, tty,
///      cols, rows}. CONTROL actions: signal, write_file,
///      read_file.
///   2: + stage_files + chain on ExecRequest. + probe CONTROL.
///   3: + sync_time CONTROL. The host pushes wall-clock on every
///      restore so the guest's CLOCK_REALTIME doesn't stay frozen
///      at bake-time (which breaks JWT iat/exp, OAuth, TLS cert
///      date validation, etc.). Old snapshots warn via the
///      `probe_agent_protocol` mismatch message — rebake to pick
///      up the new agent.
const HOST_AGENT_PROTOCOL_MIN: u32 = 3;

/// Returns a 16-hex prefix of the SHA-256 of the worker binary at
/// `worker_bin`, cached per-binary-path. Used by the spawn-time
/// probe-skip fast path to compare against the snapshot's stored
/// `runtime_sha16`. Caching is keyed on `(path, file_size,
/// mtime_ns)` so a re-installed worker (same path, new contents)
/// invalidates correctly.
fn current_worker_sha16(worker_bin: &Path) -> Option<String> {
    use std::sync::Mutex;
    static CACHE: Mutex<Option<(PathBuf, u64, u128, String)>> = Mutex::new(None);

    let meta = std::fs::metadata(worker_bin).ok()?;
    let len = meta.len();
    let mtime_ns = meta
        .modified()
        .ok()?
        .duration_since(std::time::UNIX_EPOCH)
        .ok()?
        .as_nanos();

    if let Ok(g) = CACHE.lock() {
        if let Some((p, l, m, sha)) = g.as_ref() {
            if p == worker_bin && *l == len && *m == mtime_ns {
                return Some(sha.clone());
            }
        }
    }

    // 0.7.44+ in-process SHA-256 via ring (same algorithm + same
    // digest format as `bake::sha256_file`, so the comparison still
    // matches metadata.json's `runtime_sha16` written by the bake).
    // ~10× faster than the pre-0.7.44 `shasum` fork (~15 ms of
    // fork+execve overhead saved on first call; subsequent calls
    // hit this function's pre-existing per-binary-path cache and
    // pay nothing).
    use ring::digest::{Context, SHA256};
    use std::io::Read;
    let mut f = std::fs::File::open(worker_bin).ok()?;
    let mut ctx = Context::new(&SHA256);
    let mut buf = [0u8; 64 * 1024];
    loop {
        let n = f.read(&mut buf).ok()?;
        if n == 0 {
            break;
        }
        ctx.update(&buf[..n]);
    }
    let d = ctx.finish();
    let mut digest = String::with_capacity(64);
    for b in d.as_ref() {
        use std::fmt::Write;
        let _ = write!(digest, "{:02x}", b);
    }
    let sha16 = digest[..digest.len().min(16)].to_owned();

    if let Ok(mut g) = CACHE.lock() {
        *g = Some((worker_bin.to_path_buf(), len, mtime_ns, sha16.clone()));
    }
    Some(sha16)
}

/// Tell the in-guest agent to drop the kernel's VFS caches
/// (page + dentry + inode) via `echo 3 > /proc/sys/vm/drop_caches`.
///
/// Why we do this on every restore: a snapshot captures the
/// guest kernel's RAM verbatim — including the VFS caches. After
/// a restore the guest's view of `mounts:` (virtio-fs) and
/// `volumes:` (virtio-blk + ext4) is whatever the kernel cached
/// at capture time. If the host bind file or volume backing file
/// changed between capture and restore (typical: developer
/// edits a `dist/` file in their workspace mount; or
/// `restoreOnRelease: true` cycled and a different exec wrote to
/// the volume between snapshots), the post-restore first read
/// returns the OLD bytes. The FUSE entry/attr validity windows
/// (60 s) are measured in guest jiffies, which are also frozen
/// at snapshot, so the cache stays "fresh" by guest reckoning
/// long after it's actually stale.
///
/// Dropping the caches forces the guest to re-LOOKUP via the
/// virtio-fs daemon (which reports current host attrs) and to
/// re-read ext4 inode/page state from the underlying block
/// device — both of which see post-snapshot host changes
/// correctly.
///
/// Best-effort and silent: the RPC failing (agent missing,
/// vsock wedged, mode-0 opt-out) does NOT abort the restore;
/// the workload may still execute correctly against the stale
/// caches in many cases, and a hard-fail here would be more
/// disruptive than the staleness it prevents.
///
/// Opt-out: `SUPERMACHINE_VFS_AUTO_INVALIDATE=0` skips the RPC.
/// Useful for throughput-sensitive workloads that read mostly
/// from snapshot-stable paths and don't want to pay the
/// ~few-ms drop on every restore.
///
/// Memory note: `drop_caches=3` drops pagecache too, which
/// forces re-reads of the agent's own executable pages and any
/// virtio-blk/ext4 metadata blocks. On a memory-tight guest
/// (alpine 256 MiB after balloon inflate ~ 96 MiB usable), those
/// re-reads can OOM the agent. Production guests using
/// `mounts:`/`volumes:` are typically ≥512 MiB; the OOM is
/// observable only on the smallest test shapes. If you see
/// guest stderr with `Out of memory: Killed process … (supermachine-ag)`
/// after a restore, bump `memoryMib` to ≥512 or set
/// `SUPERMACHINE_VFS_AUTO_INVALIDATE=0`.
///
/// Diagnostic: set `SUPERMACHINE_VFS_TRACE=1` to print each
/// RPC's outcome + latency to host stderr.
fn drop_vfs_caches_via_agent(vsock_exec_path: &Path, memory_mib: u32) -> bool {
    // `SUPERMACHINE_VFS_AUTO_INVALIDATE` accepts:
    //   "0"     — skip the drop (kill-switch for stale-but-fast reads)
    //   "force" — drop even on memoryMib < 512 (accept OOM risk)
    //   anything else / unset — default (drop when memoryMib >= 512)
    //
    // 0.7.36's separate `SUPERMACHINE_VFS_AUTO_INVALIDATE_FORCE=1`
    // was redundant — collapsed here.
    let setting = std::env::var("SUPERMACHINE_VFS_AUTO_INVALIDATE").ok();
    let s = setting.as_deref();
    if s == Some("0") {
        return false;
    }
    let force = s == Some("force") || s == Some("FORCE");
    // Memory gate. `echo 3 > /proc/sys/vm/drop_caches` forces
    // re-reads of the agent's executable pages + virtio-blk/ext4
    // metadata blocks. The agent's pages are mlock'd (in
    // `crates/supermachine-guest-agent/src/main.rs::main`), but the
    // re-read still allocates pagecache for the metadata blocks.
    // On tight-memory guests (≤256 MiB), the post-restore balloon
    // inflate (≈62.5% reclaim per resources.rs) leaves the agent
    // with ≈96 MiB headroom, and the drop's re-reads push the
    // guest into either an OOM-kill of the agent OR (if the agent
    // is OOM-immune) an unrecoverable kernel panic
    // ("System is deadlocked on memory"). Neither is recoverable
    // from the host.
    //
    // 512 MiB threshold: empirically the floor where the agent's
    // exec path doesn't tip over after drop_caches=3.  Smaller
    // guests trade the cache-staleness fix for not crashing — the
    // operator can either bump memoryMib, or explicitly opt in via
    // `SUPERMACHINE_VFS_AUTO_INVALIDATE=force`.
    if memory_mib < 512 && !force {
        if crate::trace::enabled("vfs") {
            eprintln!(
                "[drop_vfs_caches] SKIP memory_mib={memory_mib} < 512 (set \
                 SUPERMACHINE_VFS_AUTO_INVALIDATE=force to override)"
            );
        }
        return false;
    }
    let body = serde_json::json!({ "action": "drop_vfs_caches" });
    let t0 = Instant::now();
    let trace = crate::trace::enabled("vfs");
    match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(Duration::from_secs(5))) {
        Ok(ack) => {
            if trace {
                eprintln!(
                    "[drop_vfs_caches] OK in {} ms ack={ack:?}",
                    t0.elapsed().as_millis()
                );
            }
            true
        }
        Err(e) => {
            if trace {
                eprintln!(
                    "[drop_vfs_caches] FAIL in {} ms ({e})",
                    t0.elapsed().as_millis()
                );
            }
            tracing::debug!(
                error = %e,
                "drop_vfs_caches RPC error (continuing with stale caches)"
            );
            false
        }
    }
}

/// Push the host's current wall-clock to the guest so its
/// CLOCK_REALTIME advances with host time across restores.
/// See the agent's `SyncTime` doc-comment for the full
/// rationale: the snapshot captures Linux's `xtime` verbatim,
/// so without this RPC `Date.now()` in the guest reports
/// bake-time hours after the host has moved on — breaking JWT
/// iat/exp, OAuth token freshness, TLS NotBefore/NotAfter,
/// etc. CLOCK_MONOTONIC is deliberately untouched (the
/// captured vtimer state preserves it correctly across
/// restore, which is what tokio's timer wheel, in-flight
/// futures, and TCP retransmit timers need).
///
/// Best-effort: if the RPC fails (agent missing, snapshot
/// baked against pre-v3 agent that doesn't understand the
/// action, CAP_SYS_TIME denied) we log via the existing
/// trace channel and continue. Workloads that don't validate
/// timestamps against external authorities are unaffected;
/// ones that do (JWT, TLS) will hit auth failures, which is
/// exactly what they'd see today.
///
/// Opt-out: `SUPERMACHINE_TIME_AUTO_SYNC=0` skips the call.
/// Useful for tests that intentionally exercise frozen-time
/// scenarios.
///
/// Diagnostic: set `SUPERMACHINE_TRACE=time` (or `all`) to
/// print each RPC's outcome + latency to host stderr.
fn sync_time_via_agent(vsock_exec_path: &Path) -> bool {
    if std::env::var("SUPERMACHINE_TIME_AUTO_SYNC").as_deref() == Ok("0") {
        return false;
    }
    let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
        Ok(d) => d.as_nanos(),
        Err(_) => return false,
    };
    let host_unix_ns = u64::try_from(now).unwrap_or(u64::MAX);
    let body = serde_json::json!({
        "action": "sync_time",
        "host_unix_ns": host_unix_ns,
    });
    let t0 = Instant::now();
    let trace = crate::trace::enabled("time");
    match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(Duration::from_secs(5))) {
        Ok(ack) => {
            if trace {
                eprintln!(
                    "[sync_time] OK in {} ms ack={ack:?}",
                    t0.elapsed().as_millis()
                );
            }
            true
        }
        Err(e) => {
            if trace {
                eprintln!("[sync_time] FAIL in {} ms ({e})", t0.elapsed().as_millis());
            }
            tracing::debug!(
                error = %e,
                "sync_time RPC error (continuing with frozen wall-clock)"
            );
            false
        }
    }
}

/// Best-effort guest-wide writeback flush (`sync(2)` in the guest)
/// via the agent's `sync` CONTROL action. Returns `true` when the
/// agent ack'd the flush.
///
/// Sent before EVERY snapshot capture. The capture freezes guest
/// RAM atomically, but host-persistent writable backing — virtio-blk
/// volume files, writeback-cached virtio-fs host files — only has
/// the writes the guest already flushed; anything still dirty in the
/// guest page cache is missing from the host file. The captured
/// guest itself would flush those pages after a restore, but the
/// backing file outlives this VM: a rebake or a fresh VM attaching
/// the same volume reads the torn host file directly. Field case:
/// a warmup's `npm install` left a package's source map empty in
/// the volume backing file; the next bake saw the package dir
/// present, skipped the reinstall, and the torn file persisted
/// through every subsequent run.
///
/// Must be sent while vCPUs still run normally — in particular
/// BEFORE `smpark_park` (writeback kworkers may be scheduled on
/// secondaries, which park freezes in WFI).
///
/// Timeout is generous (60 s): the flush is bounded by dirty-data
/// volume, and a post-warmup capture is exactly when there can be
/// hundreds of MiB of it. Failure (old agent without the action,
/// transport error) is logged and the capture proceeds — same
/// semantics those callers already use for park/drop-caches.
pub(crate) fn guest_sync_via_agent(vsock_exec_path: &Path) -> bool {
    let body = serde_json::json!({ "action": "sync" });
    let t0 = Instant::now();
    match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(Duration::from_secs(60)))
    {
        Ok(_) => {
            if crate::trace::enabled("phases") {
                eprintln!("[guest_sync] OK in {} ms", t0.elapsed().as_millis());
            }
            true
        }
        Err(e) => {
            eprintln!(
                "supermachine: guest sync before snapshot capture unavailable \
                 ({e}); volume writes still dirty in the guest may be missing \
                 from the host backing file (rebake to refresh the in-guest agent)"
            );
            false
        }
    }
}

/// First-contact handshake with an in-guest agent. Sends a
/// `probe` CONTROL action; verifies the returned protocol
/// version is at least `HOST_AGENT_PROTOCOL_MIN`. Mismatch →
/// surface a typed `Error` that tells the caller exactly what
/// to do (rebake the snapshot).
///
/// `vcpus` is the snapshot's vCPU count. For vcpus > 1 we use a
/// short timeout (1 s) — a multi-vCPU restored guest either
/// answers within milliseconds or has RCU-stalled and is never
/// going to answer; waiting the full 10 s only delays the
/// downgrade-to-warning path in the spawn caller.
fn probe_agent_protocol(vsock_exec_path: &Path, vcpus: u32) -> Result<(), Error> {
    let timeout = if vcpus > 1 {
        Duration::from_secs(1)
    } else {
        Duration::from_secs(10)
    };
    let body = serde_json::json!({ "action": "probe" });
    let ack = match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(timeout)) {
        Ok(a) => a,
        Err(e) => {
            // Old agents (pre-protocol-2) don't know "probe"
            // and return ack with ok=false; send_control_with_ack
            // surfaces that as an io::Error with the agent's
            // error message. Any error here = stale agent.
            return Err(Error::vm_msg(format!(
                "agent in this snapshot is from an older supermachine release \
                 (probe failed: {e}). Rebake the snapshot to pick up the new \
                 agent: rm -rf the snapshot dir and re-run your bake (e.g. \
                 `supermachine pull <image> --name <name>`)."
            )));
        }
    };
    let proto = ack.get("protocol").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
    if proto < HOST_AGENT_PROTOCOL_MIN {
        return Err(Error::vm_msg(format!(
            "agent in this snapshot speaks protocol v{proto} but this \
             supermachine library expects v{HOST_AGENT_PROTOCOL_MIN}+. The \
             snapshot was baked against a previous release; rebake to pick up \
             the new agent (rm -rf the snapshot dir and re-run your bake)."
        )));
    }
    Ok(())
}

impl std::fmt::Debug for HiddenPool {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = self.state.lock().ok();
        f.debug_struct("HiddenPool")
            .field("socks_dir", &self.socks_dir)
            .field("alive", &s.as_ref().map(|s| s.alive).unwrap_or(usize::MAX))
            .field(
                "idle",
                &s.as_ref().map(|s| s.idle.len()).unwrap_or(usize::MAX),
            )
            .finish()
    }
}

impl HiddenPool {
    /// Explicit shutdown — same effect as dropping the pool, but
    /// callable by name from the high-level API. Sets the
    /// `shutting_down` flag, wakes every `acquire()` caller blocked
    /// on the condvar (they observe the flag and return
    /// `Error::vm_msg("pool is shutting down")`), drains the idle
    /// + dirty queues, and shuts each worker down (QUIT → SIGKILL).
    ///
    /// Idempotent. Subsequent calls are no-ops once the queues are
    /// drained and the flag is set.
    fn shutdown_pool(&self) {
        self.shutting_down.store(true, Ordering::SeqCst);
        self.available.notify_all();
        if let Some(c) = self.dirty_pending.as_ref() {
            c.notify_all();
        }
        if let Ok(mut s) = self.state.lock() {
            while let Some(mut e) = s.idle.pop() {
                e.worker.shutdown();
                s.alive = s.alive.saturating_sub(1);
            }
            if let Some(d) = self.dirty.as_ref() {
                if let Ok(mut q) = d.lock() {
                    while let Some(mut w) = q.pop_front() {
                        w.shutdown();
                        s.alive = s.alive.saturating_sub(1);
                    }
                }
            }
        }
    }
}

impl Drop for HiddenPool {
    fn drop(&mut self) {
        // Signal housekeeping threads to exit. They hold their
        // own clones of the wait-state Arcs (`PoolWaitHandles`),
        // not `Arc<HiddenPool>`, so they don't keep us alive
        // and this drop is allowed to fire as soon as the user-
        // side `Pool` + `Image` references go away. Each thread
        // re-checks `shutting_down` within ~100 ms of its next
        // condvar wait timeout.
        self.shutdown_pool();
        // Best-effort cleanup of the per-pool socks dir.
        let _ = std::fs::remove_dir_all(&self.socks_dir);
    }
}

impl HiddenPool {
    /// Acquire a worker. Three paths:
    ///
    /// 1. **Idle hit (hot path).** A worker is in the idle queue
    ///    — pop and return. ~µs.
    /// 2. **Auto-grow.** No idle worker, but `alive < max`. Bump
    ///    the alive count (reserves a slot before we drop the
    ///    lock), spawn a fresh worker outside the lock, return
    ///    it. ~10-15 ms cold spawn cost.
    /// 3. **Wait.** No idle worker, `alive == max`. Block on the
    ///    `available` condvar with `acquire_timeout`. On timeout
    ///    return `Error::PoolExhausted`.
    fn acquire(&self) -> Result<Worker, Error> {
        let acquire_t0 = Instant::now();
        let acquire_timeout = self.policy.acquire_timeout;
        let mut state = self
            .state
            .lock()
            .map_err(|_| Error::vm_msg("pool mutex poisoned".to_owned()))?;
        loop {
            // Liveness check at pop time. Dead workers are a real
            // failure mode with `restore_on_release(false)`: the
            // worker is returned to idle on Vm drop WITHOUT a cycle
            // restore, so if its process died (HVF crash, SIGKILL,
            // jetsam, etc.) we'd hand the caller a doomed handle
            // whose first `vm.exec` fails with "Connection refused"
            // on the closed vsock socket. Try_wait is non-blocking
            // and cheap; discarding the dead entry + decrementing
            // alive lets the auto-grow path below spawn a fresh
            // worker on the same acquire call.
            //
            // `restore_on_release(true)` workers normally can't
            // reach this branch alive-but-dead because the
            // restorer thread already RESTOREs them on drop (a
            // failed restore would have removed them from the
            // pool). Belt-and-suspenders: same check applies.
            while let Some(mut entry) = state.idle.pop() {
                match entry.worker.child.try_wait() {
                    Ok(None) => {
                        // 0.7.51+ skip-restore wall-clock refresh.
                        // restore_on_release=true workers come off
                        // the dirty queue via send_restore, which
                        // already calls sync_time. restore_on_release
                        // =false workers go straight back to idle on
                        // release without a RESTORE round-trip — so
                        // their CLOCK_REALTIME keeps drifting from
                        // the host's. If the worker has sat idle
                        // for more than a second, push the host's
                        // wall-clock in.
                        //
                        // 1 s threshold: avoids per-acquire RPC
                        // overhead in tight-loop throughput
                        // workloads (where idle time between
                        // acquires is sub-ms and drift is
                        // negligible), while still keeping a
                        // worker-reused-after-an-hour case accurate.
                        // The first-acquire-after-spawn case is
                        // already covered by spawn_one's
                        // sync_time_via_agent call.
                        if !self.policy.restore_on_release {
                            let idle_for = entry.last_used.elapsed();
                            if idle_for > Duration::from_secs(1) {
                                // Drop the lock before the vsock RPC
                                // so we don't block other acquirers
                                // (per-pool global mutex).
                                let exec_path = entry.worker.vsock_exec_path.clone();
                                drop(state);
                                let _ = sync_time_via_agent(&exec_path);
                                return Ok(entry.worker);
                            }
                        }
                        return Ok(entry.worker); // alive
                    }
                    Ok(Some(_status)) => {
                        // Already exited. Decrement alive so the
                        // auto-grow branch (or replenisher) replaces
                        // it. The worker handle's Drop will tidy
                        // sockets without trying to send QUIT to a
                        // dead process.
                        state.alive = state.alive.saturating_sub(1);
                        continue; // try next idle entry
                    }
                    Err(_) => {
                        // Stat failed (e.g. permission, OS weirdness).
                        // Treat as dead — same handling.
                        state.alive = state.alive.saturating_sub(1);
                        continue;
                    }
                }
            }
            if self.shutting_down.load(Ordering::SeqCst) {
                return Err(Error::vm_msg("pool is shutting down".to_owned()));
            }
            // Auto-grow: spawn a worker if we're under the cap.
            // Reserve the alive slot inside the lock so concurrent
            // acquires don't both spawn past max.
            if state.alive < self.policy.max {
                state.alive += 1;
                drop(state);
                let spawned = self.spawn_cfg.spawn_one();
                match spawned {
                    Ok(w) => return Ok(w),
                    Err(e) => {
                        // Roll back the reservation so the pool
                        // doesn't permanently lose the slot.
                        if let Ok(mut s) = self.state.lock() {
                            s.alive = s.alive.saturating_sub(1);
                        }
                        // Wake any other waiters so they can retry
                        // (they may not need to spawn — restorer
                        // could push to idle in the meantime).
                        self.available.notify_all();
                        return Err(e);
                    }
                }
            }
            // At max: wait on condvar with timeout (or forever).
            // If we've already exceeded the budget, return now —
            // don't bother re-entering wait.
            if let Some(total) = acquire_timeout {
                if acquire_t0.elapsed() >= total {
                    return Err(Error::pool_exhausted(format!(
                        "acquire timed out after {total:?}; pool at max ({})",
                        self.policy.max
                    )));
                }
            }
            state.waiting += 1;
            let (new_state, timed_out) = match acquire_timeout {
                None => match self.available.wait(state) {
                    Ok(s) => (s, false),
                    Err(_) => return Err(Error::vm_msg("pool condvar poisoned".to_owned())),
                },
                Some(total) => {
                    let remaining = total.saturating_sub(acquire_t0.elapsed());
                    match self.available.wait_timeout(state, remaining) {
                        Ok((s, r)) => (s, r.timed_out()),
                        Err(_) => return Err(Error::vm_msg("pool condvar poisoned".to_owned())),
                    }
                }
            };
            state = new_state;
            state.waiting = state.waiting.saturating_sub(1);
            if timed_out {
                return Err(Error::pool_exhausted(format!(
                    "acquire timed out after {:?}; pool at max ({})",
                    acquire_timeout.unwrap_or_default(),
                    self.policy.max
                )));
            }
        }
    }

    /// `PooledVm::drop` calls this with the now-dirty worker.
    /// Pushes onto the dirty queue and signals the restorer
    /// thread, which sends a RESTORE command to reset the
    /// worker's guest state and then returns it to the idle
    /// queue. Drop returns to the user immediately — the ~3 ms
    /// restore happens off the user's thread.
    ///
    /// Returns the worker to the dirty queue rather than
    /// killing it. The same supervisor-mode worker process
    /// serves many cycles; we only kill on shutdown or on a
    /// genuine RESTORE protocol failure (then the replenisher
    /// spawns a fresh worker to keep the pool at target size).
    fn release(&self, worker: Worker) {
        // Post-workload footprint re-measure: the spawn-time measurement
        // sees only the idle CoW-restore footprint (single-digit MiB —
        // clean file-backed mmap pages don't count toward
        // phys_footprint). The workload's dirty set shows up HERE, at
        // hand-back, and folding it into the pool's high-water-mark is
        // what makes the learned admission charge GROW with real usage
        // instead of admitting forever at the idle-restore size. One
        // proc_pid_rusage on the (already lock-taking) release path.
        if let Some(fp) = crate::memory_admission::phys_footprint_mib(worker.child.id()) {
            let prev = self
                .spawn_cfg
                .observed_footprint_mib
                .fetch_max(fp, std::sync::atomic::Ordering::Relaxed);
            if fp > prev {
                eprintln!(
                    "[admission] observed worker footprint {fp} MiB at release \
                     (cap {} MiB) — new high-water-mark",
                    self.spawn_cfg.memory_mib
                );
            }
        }
        // Skip-restore mode: push directly to idle without
        // queueing for the restorer. Per-cycle cost vanishes
        // and the guest's page cache stays warm across uses.
        // Caller opted in via `PoolBuilder::restore_on_release(false)`.
        if !self.policy.restore_on_release {
            if let Ok(mut s) = self.state.lock() {
                s.idle.push(IdleEntry {
                    worker,
                    last_used: Instant::now(),
                });
                self.available.notify_all();
            }
            return;
        }
        // 0.7.49+ COW-volume cycle: kill + respawn instead of
        // RESTORE. RESTORE only rewinds RAM; the worker's open
        // virtio-blk fds keep pointing at temp files that contain
        // the just-finished cycle's writes. Killing the worker
        // unlinks the temp (via shutdown() → drain
        // volume_temp_files), the replenisher spawns a fresh
        // worker which clones a new pristine → temp pair, and the
        // next acquire sees clean ext4 bytes that match the
        // snapshot's captured ext4 RAM state. ~50 ms spawn cost
        // happens off the user's thread (release returns
        // immediately); the next acquire blocks only if
        // replenisher hasn't finished by then.
        //
        // Note: this branch is reachable ONLY under
        // `restore_on_release=true` — the skip-restore early
        // return above (line 2530) covers `=false`, so a worker
        // with `has_cow_volumes=true` and `restore_on_release=false`
        // is reused within the process (same worker → same COW
        // temp → writes persist for that process). The
        // cross-process safety lives in `spawn_one` (every spawn
        // gets a fresh COW from pristine), not here.
        if worker.has_cow_volumes {
            let mut w = worker;
            w.shutdown();
            if let Ok(mut s) = self.state.lock() {
                s.alive = s.alive.saturating_sub(1);
            }
            self.available.notify_all();
            return;
        }
        if let Some(d) = self.dirty.as_ref() {
            if let Ok(mut q) = d.lock() {
                q.push_back(worker);
            }
            if let Some(c) = self.dirty_pending.as_ref() {
                c.notify_all();
            }
        } else {
            // Defensive fallback: if for some reason the dirty
            // queue isn't wired up (shouldn't happen with the
            // current init path), fall back to the old "kill +
            // ask replenisher" behaviour so we never strand a
            // worker.
            let mut w = worker;
            w.shutdown();
            if let Ok(mut s) = self.state.lock() {
                s.alive = s.alive.saturating_sub(1);
            }
            self.available.notify_all();
        }
    }
}

impl Image {
    /// Load an image from the on-disk artifacts produced by
    /// `supermachine run IMAGE`. The argument can be either:
    ///
    /// - The directory containing `metadata.json` and `restore.snap`
    ///   (typical: `~/.local/supermachine-snapshots/<name>/`).
    /// - The `restore.snap` file itself; we read `metadata.json`
    ///   from its parent dir.
    ///
    /// ```sh
    /// supermachine run nginx:1.27-alpine --detach && supermachine run --stop
    /// # snapshot dir: ~/.local/supermachine-snapshots/nginx_1_27-alpine/
    /// ```
    ///
    /// On disk, that directory contains:
    ///
    /// ```text
    /// metadata.json    # layers, memory, vcpus, etc.
    /// restore.snap     # captured VM state (CoW-mappable)
    /// delta.squashfs   # writable overlay layer (optional)
    /// ```
    pub fn from_snapshot(path: impl Into<PathBuf>) -> Result<Self, Error> {
        Self::from_snapshot_inner(path.into(), false)
    }

    /// Like [`Self::from_snapshot`] but tolerates the snapshot
    /// `restore.snap` file being absent — used by the
    /// always-pipelined plain-`build()` path where the bg
    /// `save_compact_to_file` may still be in flight when
    /// `build()` returns. The Image is constructed from
    /// `metadata.json` (which IS written synchronously) and the
    /// stashed warm `BakedWorker` covers the first acquire from
    /// in-memory state. Subsequent `Pool::spawn_one` calls poll
    /// for `snapshot_path.is_file()` before invoking the worker —
    /// `save_compact_to_file` writes to `<path>.partial` and
    /// atomic-renames, so file existence ↔ save complete.
    pub(crate) fn from_snapshot_pending(path: impl Into<PathBuf>) -> Result<Self, Error> {
        Self::from_snapshot_inner(path.into(), true)
    }

    fn from_snapshot_inner(path: PathBuf, allow_pending: bool) -> Result<Self, Error> {
        // Resolve to a (snapshot_path, metadata_path) pair. Under
        // `allow_pending`, the path may be a directory whose
        // `restore.snap` doesn't exist yet — accept that and let
        // spawn_one poll for the file later.
        let (snapshot_path, metadata_path) = if path.is_dir() {
            (path.join("restore.snap"), path.join("metadata.json"))
        } else if path.is_file() {
            let parent = path.parent().ok_or_else(|| {
                Error::image_msg(format!(
                    "snapshot path has no parent dir: {}",
                    path.display()
                ))
            })?;
            (path.clone(), parent.join("metadata.json"))
        } else if path
            .file_name()
            .map(|n| n == "restore.snap")
            .unwrap_or(false)
            && path
                .parent()
                .map(|p| p.join("metadata.json").is_file())
                .unwrap_or(false)
        {
            // A `<dir>/restore.snap` whose file is LEGITIMATELY ABSENT — a sentry
            // layer (FS-backed, no RAM dump) or a KVM cold-boot image. `snapshot_path()`
            // returns this conventional `restore.snap` path for EVERY backend, so the
            // round-trip `from_snapshot(img.snapshot_path())` (used across the builder,
            // e.g. executor `build_stage`) must reconstruct the image even when no
            // restore.snap exists. Resolve metadata from the sibling and let the backend
            // gate below decide whether the file is actually required (HVF: yes).
            let parent = path.parent().expect("checked parent has metadata.json");
            (path.clone(), parent.join("metadata.json"))
        } else if allow_pending {
            // Path doesn't exist as either dir or file. With
            // pending we still expect the directory at minimum
            // (metadata.json must live there).
            return Err(Error::image_msg(format!(
                "snapshot path not found: {}",
                path.display()
            )));
        } else {
            return Err(Error::image_msg(format!(
                "snapshot path not found: {}",
                path.display()
            )));
        };

        // metadata.json is required for every backend; read it first so we know
        // the backend before deciding whether a `restore.snap` is mandatory.
        if !metadata_path.is_file() {
            return Err(Error::image_msg(format!(
                "metadata.json not found alongside snapshot at {}",
                metadata_path.display()
            )));
        }

        let meta_text = std::fs::read_to_string(&metadata_path)
            .map_err(|e| Error::image_msg(format!("read {}: {e}", metadata_path.display())))?;
        let meta: serde_json::Value = serde_json::from_str(&meta_text)
            .map_err(|e| Error::image_msg(format!("parse {}: {e}", metadata_path.display())))?;

        // Backend selects the runtime artifact model. HVF restores from a
        // `restore.snap`; KVM ("backend":"kvm") boots COLD from a kernel +
        // agent initramfs (+ optional rootfs disk), so it has no snapshot file.
        let backend = meta
            .get("backend")
            .and_then(|v| v.as_str())
            .unwrap_or("hvf")
            .to_string();
        // HVF restores from `restore.snap`; KVM cold-boots from kernel+initrd,
        // while sentry/runsc/kboxlike serve from an extracted rootfs dir — none
        // of those require an HVF-style snapshot file here.
        if !allow_pending
            && backend != "kvm"
            && backend != "sentry"
            && backend != "runsc"
            && backend != "kboxlike"
            && !snapshot_path.is_file()
        {
            return Err(Error::image_msg(format!(
                "snapshot file not found: {}",
                snapshot_path.display()
            )));
        }

        // Snapshots baked under a previous binary version are REFUSED, not
        // warned about: the pinned init/kernel/agent inside the snapshot can
        // be arbitrarily incompatible with current binaries, and the failure
        // shape is vicious — a field report had a 0.7.74 dockerfile-layer
        // snapshot restore fine under 0.7.80 binaries and run with ZERO
        // egress (DNS + direct-IP silently dead). A hard error here turns
        // that into an actionable message, and every cache-or-bake caller
        // (`build_hvf`, `from_oci_to_dir`, `warm_snapshot_for_pool`, the
        // dockerfile short-circuit, the builder's layer-cache prefix scan)
        // treats a from_snapshot error as a cache MISS and rebakes
        // automatically. Indeterminate version info (bundles, custom kernel
        // paths) stays permissive — we can't reason about skew there.
        if let Some(baked) = snapshot_baked_under_other_version(&meta) {
            let current = env!("CARGO_PKG_VERSION");
            return Err(Error::cache_invalid(format!(
                "snapshot at {} was baked under v{baked}; current binaries are \
                 v{current}. Restoring across versions can produce a silently \
                 broken VM (e.g. no egress). Re-bake: delete the snapshot dir \
                 and re-run the build/pull that produced it.",
                snapshot_path.display()
            )));
        }

        let memory_mib = meta
            .get("memory_mib")
            .and_then(|v| v.as_u64())
            .map(|v| v as u32)
            .unwrap_or(256);
        let vcpus = meta
            .get("vcpus")
            .and_then(|v| v.as_u64())
            .map(|v| v as u32)
            .unwrap_or(1);

        // metadata.json paths may be absolute (default for native
        // bakes that store paths under ~/.local/...) or relative
        // (used by `supermachine bundle --image NAME`, which writes
        // a self-contained dir with `./layers/<sha>.squashfs` style
        // entries). Resolve relative paths against the metadata
        // dir so a bundle works after `cp -r` to a different host.
        let metadata_dir = metadata_path
            .parent()
            .map(Path::to_path_buf)
            .unwrap_or_else(|| PathBuf::from("."));
        let resolve_path = |s: &str| -> PathBuf {
            let p = PathBuf::from(s);
            if p.is_absolute() {
                p
            } else {
                metadata_dir.join(p)
            }
        };

        let layers: Vec<PathBuf> = meta
            .get("layers")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|x| x.as_str().map(resolve_path))
                    .collect()
            })
            .unwrap_or_default();
        let delta_squashfs = meta
            .get("delta_squashfs")
            .and_then(|v| v.as_str())
            .map(resolve_path);

        // Validate that referenced layer files actually exist on disk
        // BEFORE returning `Ok(Self)`. Without this, a warm-tag snapshot
        // whose metadata points at layer files in a *sibling* base
        // snapshot dir (`<base>` vs `<base>__warm__<tag>`) survives a
        // user `rm -rf <base>` and looks loadable to the cache-hit
        // check upstream — but the actual restore fails partway
        // through with `open block device …: No such file or
        // directory` and the worker control socket closes mid-protocol
        // ("read initial DONE: worker control socket closed"). The
        // confusing error mode reproduces exactly when
        // `bake_timing.test.ts` deletes the base dir while a sibling
        // `__warm__<tag>` dir still references it.
        //
        // Returning Err here makes the upstream `PullPolicy::Missing`
        // arm fall through to re-bake instead of trying to restore an
        // inconsistent snapshot. `allow_pending` callers (where
        // restore.snap doesn't exist yet) skip this check because the
        // bake is still in flight and the layer files may not have
        // been materialized yet.
        if !allow_pending {
            for layer in &layers {
                if !layer.is_file() {
                    return Err(Error::image_msg(format!(
                        "snapshot at {} references missing layer file {}: a sibling \
                         snapshot dir was likely deleted (e.g. the base dir of a \
                         `__warm__<tag>` variant). Delete this dir too and re-bake.",
                        snapshot_path.display(),
                        layer.display()
                    )));
                }
            }
            if let Some(delta) = &delta_squashfs {
                if !delta.is_file() {
                    return Err(Error::image_msg(format!(
                        "snapshot at {} references missing delta squashfs {}: a \
                         sibling snapshot dir was likely deleted (e.g. the base dir \
                         of a `__warm__<tag>` variant). Delete this dir too and re-bake.",
                        snapshot_path.display(),
                        delta.display()
                    )));
                }
            }
        }

        // virtio-fs mounts: persisted by the bake driver as
        // `metadata.mounts = [{host_path, guest_tag, guest_path[,
        // symlinks]}, ...]`. `guest_path` is required — snapshots
        // baked by 0.7.27 or earlier (no auto-mount API) are
        // rejected with an actionable error here rather than
        // silently restoring a broken mount tree.
        let mounts: Vec<crate::vmm::resources::MountSpec> = meta
            .get("mounts")
            .and_then(|v| v.as_array())
            .map(|arr| -> Result<Vec<_>, Error> {
                arr.iter()
                    .filter_map(|x| {
                        let host = x.get("host_path")?.as_str()?;
                        let tag = x.get("guest_tag")?.as_str()?;
                        let guest_path = match x.get("guest_path").and_then(|v| v.as_str()) {
                            Some(p) => p,
                            None => {
                                return Some(Err(Error::bake_msg(format!(
                                    "snapshot mount entry for tag `{tag}` has no \
                                     `guest_path` — this snapshot was baked by \
                                     supermachine 0.7.27 or earlier, which used a \
                                     legacy non-auto-mount API. Re-bake with \
                                     `MountSpec::new(host, tag, guest_path)` to \
                                     pin a guest mount point."
                                ))));
                            }
                        };
                        let policy = x
                            .get("symlinks")
                            .and_then(|v| v.as_str())
                            .and_then(parse_symlink_policy)
                            .unwrap_or_default();
                        let m = crate::vmm::resources::MountSpec::new(host, tag, guest_path)
                            .with_symlinks(policy);
                        Some(Ok(m))
                    })
                    .collect()
            })
            .transpose()?
            .unwrap_or_default();

        // Validate that each recorded mount's host directory still exists,
        // same contract as the layer/delta checks above. Without this, a
        // cached snapshot whose mount dir was deleted (typical for a
        // tmpdir-mounting test or a moved project) looks loadable to the
        // warm cache-hit probe upstream, but the spawned worker dies
        // during restore with `open virtio-fs mount …: No such file or
        // directory` on ITS stderr and the embedder only sees the opaque
        // "read initial DONE: worker control socket closed". Erroring
        // here instead (a) gives the actionable cause, and (b) turns the
        // `.build()` cache-hit probe into a cache MISS so the image is
        // re-baked against the caller's current mount config.
        if !allow_pending {
            for m in &mounts {
                if !Path::new(&m.host_path).is_dir() {
                    return Err(Error::image_msg(format!(
                        "snapshot at {} records virtio-fs mount tag `{}` from host \
                         dir {} which no longer exists. Restore the directory, or \
                         delete the snapshot dir to re-bake with the current mounts.",
                        snapshot_path.display(),
                        m.guest_tag,
                        m.host_path,
                    )));
                }
            }
        }

        // virtio-blk volumes: persisted by the bake driver as
        // `metadata.volumes = [{host_file, guest_path, size_bytes,
        // pristine?}, ...]`. host_file/guest_path/size_bytes are
        // required for the warm-restore worker to replay
        // `--volume HOST:GUEST:SIZE`. Pre-0.7.30 snapshots may be
        // missing `size_bytes`; fall back to the default cap then
        // so older snapshots still warm-restore (with a slightly
        // inflated sparse-size hint, which is harmless because the
        // host file already exists with its actual size on disk).
        //
        // 0.7.49+ snapshots also carry a `pristine` path pointing at
        // a captured-at-bake-time copy of `host_file`. 0.7.50+: when
        // present, the pool clones the pristine to a per-process
        // temp file on each spawn regardless of
        // `restoreOnRelease` — every worker writes to its own COW
        // and the user's host file is never mutated by the pool
        // (only by the baker). Missing field (pre-0.7.49) → None;
        // pool falls back to legacy behavior (workers share the
        // user's host file) and logs a one-line warning the first
        // time it's used.
        let volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)> = meta
            .get("volumes")
            .and_then(|v| v.as_array())
            .map(|arr| -> Vec<(PathBuf, String, u64, Option<PathBuf>)> {
                arr.iter()
                    .filter_map(|x| {
                        let host_file = x.get("host_file")?.as_str()?;
                        let guest_path = x.get("guest_path")?.as_str()?;
                        let size_bytes = x
                            .get("size_bytes")
                            .and_then(|v| v.as_u64())
                            .unwrap_or(crate::vmm::resources::VolumeSpec::DEFAULT_SIZE_BYTES);
                        let pristine = x
                            .get("pristine")
                            .and_then(|v| v.as_str())
                            .map(PathBuf::from)
                            .filter(|p| p.is_file());
                        Some((
                            PathBuf::from(host_file),
                            guest_path.to_owned(),
                            size_bytes,
                            pristine,
                        ))
                    })
                    .collect()
            })
            .unwrap_or_default();

        // Bundled kernel discovery: a self-contained bundle puts
        // the kernel image next to the snapshot. Prefer that over
        // host-wide AssetPaths so a shipped `.app` doesn't depend
        // on the user having supermachine installed.
        let bundled_kernel = {
            let cand = metadata_dir.join("kernel");
            if cand.is_file() {
                Some(cand)
            } else {
                None
            }
        };

        let baker_runtime_sha16 = meta
            .get("runtime_sha16")
            .and_then(|v| v.as_str())
            .map(|s| s.to_owned());

        let balloon_target_pages = meta
            .get("balloon_target_pages")
            .and_then(|v| v.as_u64())
            .and_then(|n| u32::try_from(n).ok())
            .filter(|n| *n > 0);

        // metadata.json["tsi_token"]: 64 lowercase hex chars iff
        // present. Pre-0.6.0 snapshots don't have this field;
        // restore in legacy unauthenticated mode (the kernel
        // baked at the same version also lacks the cmdline arg).
        let tsi_token = meta
            .get("tsi_token")
            .and_then(|v| v.as_str())
            .filter(|s| s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()))
            .map(|s| s.to_ascii_lowercase());

        // metadata.json["egress_policy"]: optional per-snapshot network
        // policy string (allow_all / deny_private / allowlist:<cidrs> /
        // denylist:<cidrs>). Threaded to the worker so the muxer enforces
        // it at TSI connect time. Absent → allow_all (worker default).
        let egress_policy = meta
            .get("egress_policy")
            .and_then(|v| v.as_str())
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty());

        // 0.7.44+ pre-exec sync. true → snapshot was captured with
        // init-oci paused inside `read(0)` at the pre-exec pause
        // point. spawn_one() will set
        // SUPERMACHINE_PRE_EXEC_SYNC_RESUME=1 in the worker env so
        // the resume thread pushes the unblock byte after restore.
        // Absent / false / pre-0.7.44 snapshots use the legacy
        // nanosleep path which doesn't need a host push.
        let pre_exec_sync = meta
            .get("pre_exec_sync")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);

        // Linux/KVM artifacts (only when "backend":"kvm"). kernel + agent
        // initramfs are required; disk (rootfs as /dev/vda) is optional.
        let kvm = if backend == "kvm" {
            let kernel = meta
                .get("kvm_kernel")
                .and_then(|v| v.as_str())
                .map(resolve_path);
            let initrd = meta
                .get("kvm_initrd")
                .and_then(|v| v.as_str())
                .map(resolve_path);
            let disk = meta
                .get("kvm_disk")
                .and_then(|v| v.as_str())
                .map(resolve_path);
            let snapshot = meta
                .get("kvm_snapshot")
                .and_then(|v| v.as_str())
                .map(resolve_path);
            // A KVM image either cold-boots (kernel + initrd) or restores from a
            // snapshot — require one of those shapes.
            if snapshot.is_none() && (kernel.is_none() || initrd.is_none()) {
                return Err(Error::image_msg(
                    "metadata.json \"backend\":\"kvm\" needs either \"kvm_snapshot\", \
                     or both \"kvm_kernel\" and \"kvm_initrd\""
                        .to_owned(),
                ));
            }
            Some(KvmImageParts {
                kernel,
                initrd,
                disk,
                snapshot,
            })
        } else {
            None
        };

        // Sentry artifacts (only when "backend":"sentry"): the extracted rootfs
        // dir + a workload flag. No kernel/disk/snapshot — the sentry serves
        // files from the dir directly.
        let sentry = if backend == "sentry" {
            let rootfs = meta
                .get("sentry_rootfs")
                .and_then(|v| v.as_str())
                .map(resolve_path)
                .ok_or_else(|| {
                    Error::image_msg(
                        "metadata.json \"backend\":\"sentry\" needs \"sentry_rootfs\"".to_owned(),
                    )
                })?;
            if !allow_pending && !rootfs.is_dir() {
                return Err(Error::image_msg(format!(
                    "sentry rootfs dir not found: {}. Delete the snapshot dir and re-bake.",
                    rootfs.display()
                )));
            }
            let has_workload = meta
                .get("sentry_has_workload")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            // metadata.json["user"]: the OCI config `User` (recorded at bake by
            // `bake_sentry_from_ref`). Parse only the numeric `uid` / `uid:gid`
            // forms — a username (no passwd lookup in the sentry) or `root`/`0` /
            // empty leaves the guest at the sentry's own uid (no drop).
            let user = meta
                .get("user")
                .and_then(|v| v.as_str())
                .and_then(parse_numeric_user);
            // metadata.json["image_env"]: the OCI config `Env` (KEY→VALUE object)
            // recorded at bake. Becomes the exec env base (see SentryImageParts).
            let image_env = meta
                .get("image_env")
                .and_then(|v| v.as_object())
                .map(|o| {
                    o.iter()
                        .filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
                        .collect()
                })
                .unwrap_or_default();
            // metadata.json["cmd"]: the effective workload argv recorded at bake
            // (bake_sentry_with_config). Fall back to ["entrypoint"] when only an
            // Entrypoint was declared (a warm-daemon image relying on the base
            // image's Entrypoint must still surface its argv to Pool::warm).
            // Absent on legacy/base bakes -> empty (the warm-daemon backend gates
            // on `warm_daemon && !cmd.is_empty()`).
            let str_array = |v: &serde_json::Value| -> Vec<String> {
                v.as_array()
                    .map(|a| {
                        a.iter()
                            .filter_map(|s| s.as_str().map(str::to_owned))
                            .collect()
                    })
                    .unwrap_or_default()
            };
            let cmd = meta
                .get("cmd")
                .map(str_array)
                .filter(|c| !c.is_empty())
                .or_else(|| meta.get("entrypoint").map(str_array))
                .unwrap_or_default();
            // metadata.json["warm_daemon"]: default false so legacy/base/KVM/HVF
            // metadata without the key still loads.
            let warm_daemon = meta
                .get("warm_daemon")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            Some(SentryImageParts {
                rootfs,
                has_workload,
                user,
                image_env,
                cmd,
                warm_daemon,
            })
        } else {
            None
        };

        let runsc = if backend == "runsc" {
            let rootfs = meta
                .get("runsc_rootfs")
                .or_else(|| meta.get("sentry_rootfs"))
                .and_then(|v| v.as_str())
                .map(resolve_path)
                .ok_or_else(|| {
                    Error::image_msg(
                        "metadata.json \"backend\":\"runsc\" needs \"runsc_rootfs\"".to_owned(),
                    )
                })?;
            if !allow_pending && !rootfs.is_dir() {
                return Err(Error::image_msg(format!(
                    "runsc rootfs dir not found: {}. Delete the snapshot dir and re-bake.",
                    rootfs.display()
                )));
            }
            let has_workload = meta
                .get("runsc_has_workload")
                .or_else(|| meta.get("sentry_has_workload"))
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let user = meta
                .get("user")
                .and_then(|v| v.as_str())
                .and_then(parse_numeric_user);
            let image_env = meta
                .get("image_env")
                .and_then(|v| v.as_object())
                .map(|o| {
                    o.iter()
                        .filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
                        .collect()
                })
                .unwrap_or_default();
            let str_array = |v: &serde_json::Value| -> Vec<String> {
                v.as_array()
                    .map(|a| {
                        a.iter()
                            .filter_map(|s| s.as_str().map(str::to_owned))
                            .collect()
                    })
                    .unwrap_or_default()
            };
            let cmd = meta
                .get("cmd")
                .map(str_array)
                .filter(|c| !c.is_empty())
                .or_else(|| meta.get("entrypoint").map(str_array))
                .unwrap_or_default();
            let warm_daemon = meta
                .get("warm_daemon")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let checkpoint = meta
                .get("runsc_checkpoint")
                .and_then(|v| v.as_str())
                .map(resolve_path);
            let agent_path = meta
                .get("runsc_agent_path")
                .and_then(|v| v.as_str())
                .unwrap_or("/.supermachine/supermachine-agent")
                .to_owned();
            let agent_unix_env = meta
                .get("runsc_agent_unix_env")
                .and_then(|v| v.as_str())
                .unwrap_or("SUPERMACHINE_AGENT_UNIX_SOCKET")
                .to_owned();
            Some(RunscImageParts {
                rootfs,
                memory_mib,
                vcpus,
                has_workload,
                user,
                image_env,
                cmd,
                warm_daemon,
                checkpoint,
                mounts: mounts.clone(),
                agent_path,
                agent_unix_env,
            })
        } else {
            None
        };

        let kboxlike = if backend == "kboxlike" {
            let rootfs = meta
                .get("kboxlike_rootfs")
                .or_else(|| meta.get("sentry_rootfs"))
                .and_then(|v| v.as_str())
                .map(resolve_path)
                .ok_or_else(|| {
                    Error::image_msg(
                        "metadata.json \"backend\":\"kboxlike\" needs \"kboxlike_rootfs\""
                            .to_owned(),
                    )
                })?;
            if !allow_pending && !rootfs.is_dir() {
                return Err(Error::image_msg(format!(
                    "kboxlike rootfs dir not found: {}. Delete the snapshot dir and re-bake.",
                    rootfs.display()
                )));
            }
            let has_workload = meta
                .get("kboxlike_has_workload")
                .or_else(|| meta.get("sentry_has_workload"))
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let user = meta
                .get("user")
                .and_then(|v| v.as_str())
                .and_then(parse_numeric_user);
            let image_env = meta
                .get("image_env")
                .and_then(|v| v.as_object())
                .map(|o| {
                    o.iter()
                        .filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
                        .collect()
                })
                .unwrap_or_default();
            let str_array = |v: &serde_json::Value| -> Vec<String> {
                v.as_array()
                    .map(|a| {
                        a.iter()
                            .filter_map(|s| s.as_str().map(str::to_owned))
                            .collect()
                    })
                    .unwrap_or_default()
            };
            let cmd = meta
                .get("cmd")
                .map(str_array)
                .filter(|c| !c.is_empty())
                .or_else(|| meta.get("entrypoint").map(str_array))
                .unwrap_or_default();
            let warm_daemon = meta
                .get("warm_daemon")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            Some(KboxlikeImageParts {
                rootfs,
                memory_mib,
                vcpus,
                has_workload,
                user,
                image_env,
                cmd,
                warm_daemon,
                mounts: mounts.clone(),
                volumes: volumes.clone(),
            })
        } else {
            None
        };

        Ok(Self {
            snapshot_path,
            memory_mib,
            vcpus,
            baker_runtime_sha16,
            balloon_target_pages,
            tsi_token,
            egress_policy,
            pre_exec_sync,
            layers,
            delta_squashfs,
            mounts,
            volumes,
            bundled_kernel,
            kvm,
            sentry,
            runsc,
            kboxlike,
            hidden_pool: std::sync::OnceLock::new(),
            warm_baked_worker: Arc::new(crate::bake::WarmStash::new(None)),
            #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
            kvm_pool: std::sync::OnceLock::new(),
        })
    }

    /// Pull and bake an image from a registry reference, returning
    /// the loadable [`Image`]. Equivalent to running
    /// `supermachine run <image_ref> --no-detach` from a Rust app,
    /// minus the daemon — you get the [`Image`] back, then call
    /// [`Vm::start`] yourself.
    ///
    /// Uses [`PullPolicy::Missing`] (cache-first) by default. For
    /// other policies, see [`Image::from_oci_with_policy`].
    ///
    /// ```no_run
    /// # use supermachine::{Image, Vm, VmConfig};
    /// let image = Image::from_oci("nginx:1.27-alpine")?;
    /// let vm = Vm::start(&image, &VmConfig::new())?;
    /// # let _ = vm; Ok::<(), supermachine::Error>(())
    /// ```
    pub fn from_oci(image_ref: &str) -> Result<Self, Error> {
        Self::from_oci_with_policy(image_ref, PullPolicy::default())
    }

    /// As [`Image::from_oci`] but with an explicit [`PullPolicy`].
    /// See [`PullPolicy`] for the cache + registry interaction
    /// table.
    pub fn from_oci_with_policy(image_ref: &str, policy: PullPolicy) -> Result<Self, Error> {
        let snapshots_dir = default_snapshots_dir();
        Self::from_oci_to_dir(image_ref, policy, &snapshots_dir, None)
    }

    /// Most explicit constructor: pull/bake into a specific
    /// snapshots directory, with an optional explicit name.
    /// Lets you keep multiple "supermachine snapshot stores"
    /// (e.g. per-project), or pin a snapshot under a name that
    /// differs from the image-derived default.
    pub fn from_oci_to_dir(
        image_ref: &str,
        policy: PullPolicy,
        snapshots_dir: &Path,
        name: Option<&str>,
    ) -> Result<Self, Error> {
        // 1. Compute where the cached snapshot would live and
        //    short-circuit on hit (Missing) or miss (Never).
        let derived = name
            .map(|s| s.to_owned())
            .unwrap_or_else(|| crate::bake::snapshot_name_for_image(image_ref));
        let snap_dir = snapshots_dir.join(&derived);
        let cache_loadable = Self::from_snapshot(&snap_dir).is_ok();
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        let cache_loadable = {
            let mut cache_loadable = cache_loadable;
            if cache_loadable {
                let expected = linux_default_backend();
                let cached = snap_dir_backend(&snap_dir);
                if cached.as_deref() != Some(expected) {
                    if matches!(policy, PullPolicy::Never) {
                        return Err(Error::cache_invalid(format!(
                            "cached snapshot at {} was baked for backend {:?}, but this Linux \
                             build resolves to backend {expected:?} (PullPolicy::Never won't \
                             auto-rebake)",
                            snap_dir.display(),
                            cached
                        )));
                    }
                    eprintln!(
                        "supermachine: snapshot at {} was baked for backend {:?}; current Linux \
                         backend is {expected:?}. Auto-rebaking.",
                        snap_dir.display(),
                        cached
                    );
                    invalidate_stale_snapshot_tree(&snap_dir);
                    cache_loadable = false;
                }
            }
            cache_loadable
        };
        // Version-skew check for Missing/Always: if the cached
        // snapshot was baked under a different binary version,
        // treat as cache-miss and invalidate so the bake below
        // produces a clean tree. Never auto-rebakes on
        // PullPolicy::Never — the operator opted into "fail
        // rather than re-bake."
        let cache_loadable = if cache_loadable && !matches!(policy, PullPolicy::Never) {
            if let Some(baked) = snap_dir_baked_under_other_version(&snap_dir) {
                let current = env!("CARGO_PKG_VERSION");
                eprintln!(
                    "supermachine: snapshot at {} was baked under v{baked}; \
                     current binaries are v{current}. Auto-rebaking.",
                    snap_dir.display()
                );
                invalidate_stale_snapshot_tree(&snap_dir);
                false
            } else {
                true
            }
        } else {
            cache_loadable
        };

        match policy {
            PullPolicy::Never => {
                if cache_loadable {
                    return Self::from_snapshot(&snap_dir);
                }
                let restore_snap = snap_dir.join("restore.snap");
                if restore_snap.is_file() {
                    return Err(Error::cache_invalid(format!(
                        "snapshot present at {} but not loadable on this binary; \
                         rebake required (PullPolicy::Never won't auto-rebake)",
                        snap_dir.display()
                    )));
                }
                return Err(Error::cache_miss(format!(
                    "no cached snapshot for {image_ref} at {} (PullPolicy::Never)",
                    snap_dir.display()
                )));
            }
            PullPolicy::Missing if cache_loadable => {
                return Self::from_snapshot(&snap_dir);
            }
            // Missing+invalid OR Always: fall through to bake.
            _ => {}
        }

        // 2. Bake (cache-miss path). The two backends bake differently:
        //
        //  - KVM (Linux/x86_64): bake the OCI ref straight into the snapshot dir
        //    via the self-contained KVM pipeline (registry pull → rootfs
        //    squashfs → agent initramfs, kernel/busybox/agent from the bundled
        //    supermachine-kernel). No external worker process.
        //
        //  - HVF (macOS/aarch64): the existing run_push worker pipeline (pull →
        //    squashfs → boot worker once → capture snapshot).
        //
        // Both leave a loadable `"backend":...` snapshot under `snap_dir`, so
        // the cache-hit branch above and Image::from_snapshot are shared.
        //  - Sentry (Linux/x86_64, NO KVM): when hardware virt is unavailable
        //    (nested guest / CI / customer sandbox), bake for the no-virt
        //    process-isolation backend instead, so the image still runs. The
        //    `runsc` is the Linux default for now, because it is the current
        //    green no-KVM parity backend. `SUPERMACHINE_BACKEND`
        //    (`runsc` | `kvm` | `sentry` | `kboxlike`) can still force another
        //    backend for testing or KVM/kboxlike bring-up.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        {
            match linux_default_backend() {
                "runsc" => return Self::bake_runsc_from_ref(image_ref, &snap_dir),
                "sentry" => return Self::bake_sentry_from_ref(image_ref, &snap_dir),
                "kboxlike" => return Self::bake_kboxlike_from_ref(image_ref, &snap_dir),
                "kvm" => return Self::bake_kvm_auto(image_ref, &snap_dir),
                other => {
                    return Err(Error::bake_msg(format!(
                        "unsupported SUPERMACHINE_BACKEND={other:?}; expected runsc, kvm, sentry, kboxlike, or auto"
                    )));
                }
            }
        }

        #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
        {
            let root = repo_root_for_bake()?;
            let request = crate::bake::BakeRequest {
                image: image_ref.to_owned(),
                name: name.map(|s| s.to_owned()),
                runtime: "supermachine".to_owned(),
                guest_port: 80,
                memory_mib: 256,
                vcpus: 1,
                pull_policy: policy.as_bake_str().to_owned(),
                snapshots_dir: snapshots_dir.to_path_buf(),
                cmd_override: None,
                extra_args: Vec::new(),
                platform: "linux/arm64".to_owned(),
            };
            let bake_t0 = std::time::Instant::now();
            crate::bake::run_push(&request, bake_t0, &root)
                .map_err(|e| map_bake_error(&request.image, e))?;

            // 3. Load the freshly-baked snapshot.
            Self::from_snapshot(&snap_dir)
        }
    }

    /// Builder for configurable bakes — env vars, cmd override,
    /// custom memory / port, custom snapshot name.
    ///
    /// ```no_run
    /// # use supermachine::Image;
    /// let image = Image::builder("nginx:1.27-alpine")
    ///     .with_name("nginx-prod")
    ///     .with_memory_mib(512)
    ///     .with_env("FOO", "bar")
    ///     .with_cmd(["nginx", "-g", "daemon off;"])
    ///     .build()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    ///
    /// The builder produces a different snapshot for each
    /// configuration — bake-time inputs are part of the snapshot
    /// fingerprint. Reuse a name across configurations and the
    /// previous snapshot is invalidated; pick distinct names if
    /// you need both side-by-side.
    pub fn builder(image_ref: impl Into<String>) -> OciImageBuilder {
        OciImageBuilder::new(image_ref)
    }

    /// Get an [`Image`] for `name`, baking it from `image_ref`
    /// only if a compatible snapshot doesn't already exist.
    ///
    /// This is the right call for app startup. The first run
    /// bakes (one-time cost: the registry pull + snapshot build,
    /// e.g. ~12 s for `rust:1-slim`); subsequent runs see the
    /// cached snapshot and return in microseconds. After a
    /// `cargo update` that bumped the supermachine version, the
    /// cached snapshot's bake-key no longer matches the current
    /// worker binary, and `ensure_baked` rebakes automatically —
    /// no shell scripts, no manual `rm -rf snapshots/`.
    ///
    /// `configure` is a builder closure: chain
    /// [`OciImageBuilder`] methods like `with_memory_mib`,
    /// `with_cmd`, `with_env` to customize the bake. Pass
    /// `|b| b` for defaults.
    ///
    /// ```no_run
    /// use std::time::Duration;
    /// use supermachine::{Image, VmConfig};
    ///
    /// // Bake once on first run, reuse forever after — including
    /// // across supermachine version upgrades.
    /// let image = Image::ensure_baked("rust_1_slim", "rust:1-slim", |b| {
    ///     b.with_memory_mib(2048)
    /// })?;
    /// // Configure pool: 5 always-warm, scale to 50 under burst.
    /// let pool = image.pool().min(5).max(50).build()?;
    ///
    /// // Per-task path:
    /// let vm = pool.acquire()?;
    /// vm.write_file("/tmp/main.rs", b"fn main() { println!(\"hi\"); }")?;
    /// let out = vm.exec_builder()
    ///     .argv(["sh", "-c", "rustc /tmp/main.rs -o /tmp/m && /tmp/m"])
    ///     .timeout(Duration::from_secs(30))
    ///     .output()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn ensure_baked<F>(
        name: impl Into<String>,
        image_ref: impl Into<String>,
        configure: F,
    ) -> Result<Image, Error>
    where
        F: FnOnce(OciImageBuilder) -> OciImageBuilder,
    {
        let builder = configure(OciImageBuilder::new(image_ref).with_name(name));
        builder.build()
    }

    /// Path to the snapshot file backing this image.
    pub fn snapshot_path(&self) -> &Path {
        &self.snapshot_path
    }

    /// Memory the snapshot was baked with. [`Vm::start`] uses
    /// this if [`VmConfig::with_memory_mib`] isn't set.
    pub fn memory_mib(&self) -> u32 {
        self.memory_mib
    }

    /// vCPUs the snapshot was baked with.
    pub fn vcpus(&self) -> u32 {
        self.vcpus
    }

    /// Test-only accessor for `balloon_target_pages` — the
    /// integration tests assert the metadata round-trip. Hidden
    /// from rustdoc; not part of the stable surface.
    #[doc(hidden)]
    pub fn balloon_target_pages_for_test(&self) -> Option<u32> {
        self.balloon_target_pages
    }

    /// Start a one-shot microVM from this image. Equivalent to
    /// [`Vm::start(self, config)`][Vm::start] but reads more
    /// naturally at the call site:
    ///
    /// ```no_run
    /// # use supermachine::{Image, VmConfig};
    /// let image = Image::from_snapshot("path/to/snapshot")?;
    /// let vm = image.start(&VmConfig::new())?;
    /// // ... use vm ...
    /// vm.stop()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    ///
    /// Use [`Image::acquire`] instead if you want a `PooledVm`
    /// that returns to a (hidden) pool on `Drop` for cheaper
    /// reuse — typical for evaluation harnesses, CI verifiers,
    /// or any code that runs many short-lived VMs of the same
    /// image back-to-back.
    #[cfg(any(
        all(target_os = "macos", target_arch = "aarch64"),
        all(target_os = "linux", target_arch = "x86_64")
    ))]
    pub fn start(&self, config: &VmConfig) -> Result<Vm, Error> {
        Vm::start(self, config)
    }

    /// Bake a KVM-bootable [`Image`] from OCI layer tarballs (Linux/x86_64).
    ///
    /// Merges `layer_tars` (bottom-up, OCI whiteouts applied) into one rootfs
    /// squashfs attached as `/dev/vda`, and writes a `"backend":"kvm"` snapshot
    /// dir under `dest_dir` referencing `kernel` (bzImage) and `agent_initrd`
    /// (the PID-1 exec agent). The result loads + runs via [`Image::start`] /
    /// [`Image::acquire`].
    ///
    /// Remaining KVM-bake pieces (not yet automated here): OCI-ref → layer tars
    /// (registry pull / `docker save` extraction) and a self-contained agent
    /// initramfs. This consumes already-materialized layer tars + a prebuilt
    /// agent initramfs.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kvm(
        layer_tars: &[PathBuf],
        kernel: &Path,
        agent_initrd: &Path,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        Self::bake_kvm_with_workload(layer_tars, kernel, agent_initrd, None, dest_dir)
    }

    /// Like [`bake_kvm`](Image::bake_kvm) but also bakes the OCI workload-launch
    /// script (`Entrypoint`/`Cmd`/`Env`/`WorkingDir` → `sh`) into the rootfs at
    /// `/.supermachine/run-workload`, so the guest starts the image's command
    /// (nginx, a server, …) and not just the agent. `bake_kvm_from_ref` derives
    /// the script from the pulled image config; `bake_kvm` passes `None` (the
    /// agent-only path, for prebuilt layer tars with no resolvable config).
    pub fn bake_kvm_with_workload(
        layer_tars: &[PathBuf],
        kernel: &Path,
        agent_initrd: &Path,
        workload_script: Option<&str>,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        let dest = dest_dir.into();
        std::fs::create_dir_all(&dest).map_err(Error::Io)?;
        let rootfs = dest.join("rootfs.squashfs");
        let stage = dest.join("rootfs-stage");
        crate::bake::build_kvm_rootfs_squashfs(layer_tars, &stage, &rootfs, workload_script)
            .map_err(Error::bake_msg)?;
        let _ = std::fs::remove_dir_all(&stage);
        // Physical footprint (rootfs + kernel + initrd) so `supermachine images`
        // can show a SIZE without a separate HVF-style `snapshot_physical_bytes`
        // probe; `baked_at` (RFC3339) matches the HVF metadata so `images` sorts
        // newest-first across backends.
        let phys = std::fs::metadata(&rootfs).map(|m| m.len()).unwrap_or(0)
            + std::fs::metadata(kernel).map(|m| m.len()).unwrap_or(0)
            + std::fs::metadata(agent_initrd)
                .map(|m| m.len())
                .unwrap_or(0);
        let metadata = serde_json::json!({
            "backend": "kvm",
            "kvm_kernel": kernel.to_string_lossy(),
            "kvm_initrd": agent_initrd.to_string_lossy(),
            "kvm_disk": rootfs.to_string_lossy(),
            "memory_mib": 512,
            "vcpus": 1,
            // Bake-time balloon inflation target (~75% of memory in 4 KiB pages),
            // so a restored KVM VM auto-inflates idle RAM like HVF.
            "balloon_target_pages": crate::bake::compute_balloon_target_pages(512),
            "baked_at": chrono_rfc3339_now(),
            "snapshot_physical_bytes": phys,
        });
        std::fs::write(
            dest.join("metadata.json"),
            serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?,
        )
        .map_err(Error::Io)?;
        Image::from_snapshot(&dest)
    }

    /// Bake a KVM-bootable [`Image`] directly from an OCI image reference
    /// (Linux/x86_64) — e.g. `"alpine:3.20"` or `"docker.io/library/busybox"`.
    ///
    /// Pulls the image (registry-direct by default — no Docker daemon; honors
    /// `SUPERMACHINE_IMAGE_SOURCE` + `oci-layout:` / `oci-archive:` refs),
    /// resolves its amd64 layers, and delegates to [`Image::bake_kvm`]. Still
    /// takes a prebuilt `agent_initrd` (the self-contained agent cpio is the
    /// remaining bake piece).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kvm_from_ref(
        image_ref: &str,
        kernel: &Path,
        agent_initrd: &Path,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        let dest = dest_dir.into();
        std::fs::create_dir_all(&dest).map_err(Error::Io)?;
        let pull_work = dest.join("oci-pull");
        let t_pull = std::time::Instant::now();
        let (layers, workload, oci_config) =
            crate::bake::pull_oci_layers(image_ref, "amd64", &pull_work)
                .map_err(Error::bake_msg)?;
        let pull_ms = t_pull.elapsed().as_millis();
        let t_build = std::time::Instant::now();
        let img = Self::bake_kvm_with_workload(
            &layers,
            kernel,
            agent_initrd,
            workload.as_deref(),
            dest.clone(),
        )?;
        if crate::trace::enabled("run") {
            eprintln!(
                "supermachine: kvm bake phases pull_oci_ms={} build_rootfs_ms={}",
                pull_ms,
                t_build.elapsed().as_millis()
            );
        }
        // Record the original image ref into metadata.json so `supermachine images`
        // shows the IMAGE column (the bake itself only knows resolved layers) —
        // plus the image's RUNTIME CONFIG. The HVF bake persists the config;
        // the KVM bake silently didn't, so anything reading it back from a
        // cached snapshot got nothing: the in-VM builder's FROM-base env
        // inheritance (builder #7) was a no-op on linux while working on HVF
        // (caught by integration_builder_base_env on the box), and the #122
        // version gate stayed permissive forever for KVM bakes (no
        // baked_by_version stamp).
        let meta_path = dest.join("metadata.json");
        if let Ok(text) = std::fs::read_to_string(&meta_path) {
            if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&text) {
                if let Some(obj) = json.as_object_mut() {
                    obj.insert("image".to_string(), serde_json::json!(image_ref));
                    obj.insert(
                        "baked_by_version".to_string(),
                        serde_json::json!(env!("CARGO_PKG_VERSION")),
                    );
                    if let Some(cfg) = &oci_config {
                        // Env arrives as OCI ["K=V", …]; persist as the same
                        // `image_env` object shape the HVF bake records (what
                        // builder::base_image_env reads).
                        let mut env_obj = serde_json::Map::new();
                        for kv in cfg
                            .get("Env")
                            .and_then(|v| v.as_array())
                            .into_iter()
                            .flatten()
                            .filter_map(|v| v.as_str())
                        {
                            if let Some(eq) = kv.find('=') {
                                env_obj
                                    .insert(kv[..eq].to_string(), serde_json::json!(&kv[eq + 1..]));
                            }
                        }
                        obj.insert("image_env".to_string(), serde_json::Value::Object(env_obj));
                        for (oci_key, meta_key) in [
                            ("Cmd", "cmd"),
                            ("Entrypoint", "entrypoint"),
                            ("WorkingDir", "working_dir"),
                            ("User", "user"),
                        ] {
                            if let Some(v) = cfg.get(oci_key) {
                                if !v.is_null() {
                                    obj.insert(meta_key.to_string(), v.clone());
                                }
                            }
                        }
                    }
                    if let Ok(pretty) = serde_json::to_string_pretty(&json) {
                        let _ = std::fs::write(&meta_path, pretty);
                    }
                }
            }
        }
        Ok(img)
    }

    /// Bake a [`Image`] for the **sentry** backend directly from an OCI image
    /// reference (Linux/x86_64) — the no-virt path, for hosts where
    /// [`Vm::kvm_usable`] is false (nested guests, CI, customer sandboxes).
    ///
    /// Pulls the image (same registry-direct puller as [`bake_kvm_from_ref`]),
    /// merges its amd64 layers into an extracted rootfs DIRECTORY under
    /// `dest_dir/rootfs` (no kernel, no squashfs — the sentry serves files from
    /// the dir), bakes the OCI workload-launch script into it, and writes a
    /// `"backend":"sentry"` `metadata.json`. Run the result with
    /// [`Image::run_sentry`].
    ///
    /// This is the no-config entry point (the image's own Entrypoint/Cmd/Env);
    /// the [`OciImageBuilder`] routes through [`Self::bake_sentry_with_config`]
    /// to apply `with_cmd` / `with_env` / `with_extra_file` / `with_mount` /
    /// `with_volume` / `with_warmup`.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_sentry_from_ref(
        image_ref: &str,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        Self::bake_sentry_with_config(image_ref, dest_dir, SentryBakeConfig::default())
    }

    /// Bake a [`Image`] for the experimental **runsc** backend directly from an
    /// OCI image reference (Linux/x86_64). This intentionally shares the OCI
    /// extraction/rootfs materialization path with sentry, but publishes
    /// `"backend":"runsc"` metadata so the runtime dispatch can be implemented
    /// independently around gVisor/runsc checkpoint/restore.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_runsc_from_ref(
        image_ref: &str,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        Self::bake_runsc_with_config(image_ref, dest_dir, SentryBakeConfig::default())
    }

    /// Bake an [`Image`] for the experimental native **kboxlike** backend
    /// directly from an OCI image reference (Linux/x86_64). This is a metadata
    /// and rootfs materialization step only; runtime/warmup support lands with
    /// the native execution layer.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kboxlike_from_ref(
        image_ref: &str,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        Self::bake_kboxlike_with_config(image_ref, dest_dir, SentryBakeConfig::default())
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_runsc_with_config(
        image_ref: &str,
        dest_dir: impl Into<PathBuf>,
        mut config: SentryBakeConfig,
    ) -> Result<Image, Error> {
        let runsc_bake_key = sentry_bake_cache_key(image_ref, &config)?;
        let warmup = config.warmup.take();
        // The sentry rootfs materialization step is reused only to produce the
        // extracted filesystem and metadata. runsc warmup is handled below as a
        // real runsc checkpoint so live daemons/process state survive.
        config.warmup = None;
        let dest = dest_dir.into();
        let _ = Self::bake_sentry_with_config(image_ref, &dest, config)?;
        let meta_path = dest.join("metadata.json");
        let meta_text = std::fs::read_to_string(&meta_path)
            .map_err(|e| Error::bake_msg(format!("read {}: {e}", meta_path.display())))?;
        let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
            .map_err(|e| Error::bake_msg(format!("parse {}: {e}", meta_path.display())))?;
        if let Some(obj) = meta.as_object_mut() {
            obj.insert("backend".to_owned(), serde_json::json!("runsc"));
            if let Some(rootfs) = obj.remove("sentry_rootfs") {
                obj.insert("runsc_rootfs".to_owned(), rootfs);
            }
            if let Some(has_workload) = obj.remove("sentry_has_workload") {
                obj.insert("runsc_has_workload".to_owned(), has_workload);
            }
            if let Some(key) = obj.get("sentry_bake_key").cloned() {
                obj.insert("runsc_bake_key".to_owned(), key);
            }
            obj.insert(
                "runsc_bake_key".to_owned(),
                serde_json::json!(runsc_bake_key),
            );
        }
        let rootfs = meta
            .get("runsc_rootfs")
            .and_then(|v| v.as_str())
            .map(PathBuf::from)
            .ok_or_else(|| Error::bake_msg("runsc metadata missing runsc_rootfs".to_owned()))?;
        let agent_path = rootfs.join(".supermachine/supermachine-agent");
        if let Some(parent) = agent_path.parent() {
            std::fs::create_dir_all(parent).map_err(Error::Io)?;
        }
        if let Some(agent_override) = std::env::var_os("SUPERMACHINE_RUNSC_AGENT_PATH")
            .or_else(|| std::env::var_os("SUPERMACHINE_X86_AGENT_PATH"))
        {
            std::fs::copy(&agent_override, &agent_path).map_err(|e| {
                Error::bake_msg(format!(
                    "stage runsc agent {} -> {}: {e}",
                    PathBuf::from(agent_override).display(),
                    agent_path.display()
                ))
            })?;
            set_executable(&agent_path).map_err(Error::Io)?;
        } else {
            supermachine_kernel::extract_supermachine_agent_to(&agent_path).map_err(Error::Io)?;
        }
        if let Some(obj) = meta.as_object_mut() {
            obj.insert(
                "runsc_agent_path".to_owned(),
                serde_json::json!("/.supermachine/supermachine-agent"),
            );
            obj.insert(
                "runsc_agent_unix_env".to_owned(),
                serde_json::json!("SUPERMACHINE_AGENT_UNIX_SOCKET"),
            );
        }
        let meta_out = serde_json::to_string_pretty(&meta)
            .map_err(|e| Error::bake_msg(format!("runsc metadata serialize: {e}")))?;
        std::fs::write(&meta_path, meta_out).map_err(Error::Io)?;
        if let Some(warmup) = warmup {
            let image = Image::from_snapshot(&dest)?;
            runsc_warmup_checkpoint(&image, &dest, warmup)?;
        }
        Image::from_snapshot(&dest)
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kboxlike_with_config(
        image_ref: &str,
        dest_dir: impl Into<PathBuf>,
        config: SentryBakeConfig,
    ) -> Result<Image, Error> {
        let kboxlike_bake_key = sentry_bake_cache_key(image_ref, &config)?;
        if config.warm_daemon {
            return Err(Error::bake_msg(
                "kboxlike warm-daemon images are not implemented yet; use runsc or kvm".to_owned(),
            ));
        }
        let dest = dest_dir.into();
        let _ = Self::bake_sentry_with_config(image_ref, &dest, config)?;
        let meta_path = dest.join("metadata.json");
        let meta_text = std::fs::read_to_string(&meta_path)
            .map_err(|e| Error::bake_msg(format!("read {}: {e}", meta_path.display())))?;
        let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
            .map_err(|e| Error::bake_msg(format!("parse {}: {e}", meta_path.display())))?;
        if let Some(obj) = meta.as_object_mut() {
            obj.insert("backend".to_owned(), serde_json::json!("kboxlike"));
            if let Some(rootfs) = obj.remove("sentry_rootfs") {
                obj.insert("kboxlike_rootfs".to_owned(), rootfs);
            }
            if let Some(has_workload) = obj.remove("sentry_has_workload") {
                obj.insert("kboxlike_has_workload".to_owned(), has_workload);
            }
            obj.insert(
                "kboxlike_bake_key".to_owned(),
                serde_json::json!(kboxlike_bake_key),
            );
        }
        let meta_out = serde_json::to_string_pretty(&meta)
            .map_err(|e| Error::bake_msg(format!("kboxlike metadata serialize: {e}")))?;
        std::fs::write(&meta_path, meta_out).map_err(Error::Io)?;
        let restore_snap = dest.join("restore.snap");
        if !restore_snap.is_file() {
            std::fs::write(&restore_snap, b"kboxlike-rootfs\n").map_err(Error::Io)?;
        }
        Image::from_snapshot(&dest)
    }

    /// Bake a sentry image applying an [`OciImageBuilder`]'s run-config
    /// (Linux/x86_64). The config-aware counterpart of
    /// [`Self::bake_sentry_from_ref`] — the builder's `cmd` / `env` /
    /// `working_dir` / `user` / `extra_files` / `mounts` / `volumes` / `warmup`
    /// are mapped into the baked rootfs + metadata, mirroring what the KVM/HVF
    /// bakes do for their backends:
    ///
    ///   - **cmd/env/working_dir** override the `/.supermachine/run-workload`
    ///     script (env merged OVER the image Env) AND the recorded metadata
    ///     `cmd`/`entrypoint`/`image_env`/`working_dir`/`user`.
    ///   - **extra_files** are written into the rootfs at bake time, confined
    ///     (their guest paths are joined under the rootfs, `..`/absolute
    ///     escapes rejected).
    ///   - **mounts/volumes** are persisted in `metadata.json` (the sentry has
    ///     no KVM-volume tail; this is how the mapping survives a reload).
    ///   - **warmup** is run after the rootfs is built, against a transient
    ///     sentry [`Vm`] over the freshly-baked image — its filesystem writes
    ///     land IN-PLACE in the rootfs (the artifact), so the warm state is
    ///     captured without an extra snapshot.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_sentry_with_config(
        image_ref: &str,
        dest_dir: impl Into<PathBuf>,
        config: SentryBakeConfig,
    ) -> Result<Image, Error> {
        let dest = dest_dir.into();
        let sentry_bake_key = sentry_bake_cache_key(image_ref, &config)?;
        std::fs::create_dir_all(&dest).map_err(Error::Io)?;
        let pull_work = dest.join("oci-pull");
        let (layers, image_workload, oci_config) =
            crate::bake::pull_oci_layers(image_ref, "amd64", &pull_work)
                .map_err(Error::bake_msg)?;

        // Effective env = image Env merged UNDER the builder's env (builder
        // wins on a key collision), recorded as the `image_env` object the
        // KVM/HVF bakes use and exported by the workload script.
        let mut env_pairs: Vec<(String, String)> = Vec::new();
        if let Some(cfg) = &oci_config {
            for kv in cfg
                .get("Env")
                .and_then(|v| v.as_array())
                .into_iter()
                .flatten()
                .filter_map(|v| v.as_str())
            {
                if let Some(eq) = kv.find('=') {
                    env_pairs.push((kv[..eq].to_string(), kv[eq + 1..].to_string()));
                }
            }
        }
        for (k, v) in &config.env {
            // Builder override: drop any earlier (image) value for this key,
            // then append so the builder's value wins and dedups.
            env_pairs.retain(|(ek, _)| ek != k);
            env_pairs.push((k.clone(), v.clone()));
        }

        // Effective workdir/user: builder override, else image config.
        let image_workdir = oci_config
            .as_ref()
            .and_then(|c| c.get("WorkingDir"))
            .and_then(|v| v.as_str())
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string());
        let effective_workdir = config
            .working_dir
            .clone()
            .filter(|s| !s.is_empty())
            .or(image_workdir);
        let image_user = oci_config
            .as_ref()
            .and_then(|c| c.get("User"))
            .and_then(|v| v.as_str())
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string());
        let effective_user = config.user.clone().filter(|s| !s.is_empty()).or(image_user);

        // Effective workload script. When the builder overrides cmd (or env, in
        // which case the exported env must change), render the script from the
        // explicit argv + merged env + workdir; otherwise reuse the image's own
        // script from `pull_oci_layers`.
        let workload: Option<String> = if let Some(cmd) = &config.cmd {
            crate::bake::sentry_workload_script(cmd, &env_pairs, effective_workdir.as_deref())
        } else if !config.env.is_empty() || effective_workdir.is_some() {
            // No cmd override, but env/workdir changed: rebuild from the image's
            // own argv (Entrypoint ++ Cmd) so the new env/workdir take effect.
            let mut argv: Vec<String> = Vec::new();
            if let Some(cfg) = &oci_config {
                for v in cfg
                    .get("Entrypoint")
                    .and_then(|v| v.as_array())
                    .into_iter()
                    .flatten()
                    .filter_map(|v| v.as_str())
                {
                    argv.push(v.to_string());
                }
                for v in cfg
                    .get("Cmd")
                    .and_then(|v| v.as_array())
                    .into_iter()
                    .flatten()
                    .filter_map(|v| v.as_str())
                {
                    argv.push(v.to_string());
                }
            }
            if argv.is_empty() {
                // Image declared no command; keep the image's script (None).
                image_workload.clone()
            } else {
                crate::bake::sentry_workload_script(&argv, &env_pairs, effective_workdir.as_deref())
            }
        } else {
            image_workload.clone()
        };

        let rootfs = dest.join("rootfs");
        crate::bake::build_sentry_rootfs(&layers, &rootfs, workload.as_deref())
            .map_err(Error::bake_msg)?;
        // The rootfs dir IS the artifact; the OCI pull scratch isn't needed.
        let _ = std::fs::remove_dir_all(&pull_work);

        // Stage extra files into the rootfs, CONFINED: each guest path is joined
        // under the rootfs (rejecting absolute / `..` escapes), parent dirs are
        // created, and the host file is copied in. This is the sentry analogue
        // of the KVM bake's `--extra-file` delta-layer staging.
        for (host, guest) in &config.extra_files {
            let rel = guest.trim_start_matches('/');
            let target = confined_rootfs_join(&rootfs, rel).ok_or_else(|| {
                Error::bake_msg(format!(
                    "extra-file guest path escapes the rootfs: {guest:?}"
                ))
            })?;
            if let Some(parent) = target.parent() {
                std::fs::create_dir_all(parent).map_err(Error::Io)?;
            }
            std::fs::copy(host, &target).map_err(|e| {
                Error::bake_msg(format!(
                    "stage extra-file {} -> {}: {e}",
                    host.display(),
                    target.display()
                ))
            })?;
        }

        let phys = dir_size_bytes(&rootfs);
        let mut metadata = serde_json::json!({
            "backend": "sentry",
            "sentry_rootfs": rootfs.to_string_lossy(),
            "sentry_has_workload": workload.is_some(),
            "memory_mib": config.memory_mib.unwrap_or(512),
            "vcpus": config.vcpus.unwrap_or(1),
            "image": image_ref,
            "sentry_bake_key": sentry_bake_key,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
            "baked_at": chrono_rfc3339_now(),
            "snapshot_physical_bytes": phys,
        });
        // Persist the EFFECTIVE runtime config (Env/Cmd/Entrypoint/WorkingDir/
        // User) in the same shape the KVM/HVF bakes record, so the in-VM
        // builder's FROM-base env inheritance + `supermachine images` see it.
        // The builder's overrides win over the image's recorded config.
        if let Some(obj) = metadata.as_object_mut() {
            let mut env_obj = serde_json::Map::new();
            for (k, v) in &env_pairs {
                env_obj.insert(k.clone(), serde_json::json!(v));
            }
            obj.insert("image_env".to_string(), serde_json::Value::Object(env_obj));
            // Record argv: a builder cmd override goes in `cmd` (entrypoint
            // cleared so cmd is authoritative); otherwise carry the image's
            // recorded Cmd/Entrypoint forward.
            if let Some(cmd) = &config.cmd {
                obj.insert("cmd".to_string(), serde_json::json!(cmd));
                obj.remove("entrypoint");
            } else if let Some(cfg) = &oci_config {
                for (oci_key, meta_key) in [("Cmd", "cmd"), ("Entrypoint", "entrypoint")] {
                    if let Some(v) = cfg.get(oci_key) {
                        if !v.is_null() {
                            obj.insert(meta_key.to_string(), v.clone());
                        }
                    }
                }
            }
            if let Some(dir) = &effective_workdir {
                obj.insert("working_dir".to_string(), serde_json::json!(dir));
            }
            if let Some(user) = &effective_user {
                obj.insert("user".to_string(), serde_json::json!(user));
            }
            // Warm-daemon pool intent + warmup tag (round-trip parity with KVM/HVF;
            // the sentry path previously dropped warmup_tag). `warm_daemon` is ALWAYS
            // written so the reader's default (false) and the persisted value agree;
            // `warmup_tag` is omitted when None (reader defaults None). The daemon
            // argv itself rides the `cmd`/`entrypoint` keys written above.
            obj.insert(
                "warm_daemon".to_string(),
                serde_json::json!(config.warm_daemon),
            );
            if let Some(tag) = &config.warmup_tag {
                obj.insert("warmup_tag".to_string(), serde_json::json!(tag));
            }
        }
        // Persist mounts/volumes so a reload re-attaches them (the sentry has no
        // KVM persist_kvm_builder_volumes tail). Shapes match what
        // `Image::from_snapshot` parses (mounts: {host_path, guest_tag,
        // guest_path[, symlinks]}; volumes: {host_file, guest_path, size_bytes}).
        if let Some(obj) = metadata.as_object_mut() {
            if !config.mounts.is_empty() {
                let arr: Vec<serde_json::Value> = config
                    .mounts
                    .iter()
                    .map(|(host, tag, policy, guest_path)| {
                        let mut m = serde_json::Map::new();
                        m.insert(
                            "host_path".to_string(),
                            serde_json::json!(host.to_string_lossy()),
                        );
                        m.insert("guest_tag".to_string(), serde_json::json!(tag));
                        m.insert("guest_path".to_string(), serde_json::json!(guest_path));
                        // Opaque is the default the parser assumes; only record a
                        // non-default policy (matches the HVF `--mount` encoding).
                        match policy {
                            crate::vmm::resources::SymlinkPolicy::Opaque => {}
                            crate::vmm::resources::SymlinkPolicy::Deny => {
                                m.insert("symlinks".to_string(), serde_json::json!("deny"));
                            }
                            crate::vmm::resources::SymlinkPolicy::Follow => {
                                m.insert("symlinks".to_string(), serde_json::json!("follow"));
                            }
                        }
                        serde_json::Value::Object(m)
                    })
                    .collect();
                obj.insert("mounts".to_string(), serde_json::json!(arr));
            }
            if !config.volumes.is_empty() {
                let arr: Vec<serde_json::Value> = config
                    .volumes
                    .iter()
                    .map(|(host, guest, size)| {
                        serde_json::json!({
                            "host_file": host.to_string_lossy(),
                            "guest_path": guest,
                            "size_bytes": size,
                        })
                    })
                    .collect();
                obj.insert("volumes".to_string(), serde_json::json!(arr));
            }
        }
        // Atomic publish (a peer's from_snapshot / verify cell may read `dest`
        // concurrently — see persist_run_config). Unique tmp (threads share pid)
        // then rename(2) so the live file is never observed half-written.
        {
            static SENTRY_META_TMP_SEQ: std::sync::atomic::AtomicU64 =
                std::sync::atomic::AtomicU64::new(0);
            let meta_out = serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?;
            let seq = SENTRY_META_TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            let tmp = dest.join(format!(".metadata.json.tmp.{}.{seq}", std::process::id()));
            std::fs::write(&tmp, meta_out).map_err(Error::Io)?;
            if let Err(e) = std::fs::rename(&tmp, dest.join("metadata.json")) {
                let _ = std::fs::remove_file(&tmp);
                return Err(Error::Io(e));
            }
        }

        // Warmup: start a transient sentry supervisor over the freshly-baked image
        // and run the warmup closure against it. The cells share the rootfs DIR,
        // so filesystem writes land in-place. Unlike the old filesystem-only
        // path, preserve background children during this bake-time warmup and C4
        // capture them into `restore.snap`/`mem.blob`, matching KVM/HVF warmups
        // that leave live daemons/listeners ready at restore time.
        let img = Image::from_snapshot(&dest)?;
        if let Some(warmup) = config.warmup {
            let parts = img
                .sentry
                .as_ref()
                .ok_or_else(|| Error::vm_msg("sentry warmup image missing sentry metadata"))?;
            let warm_sock = std::env::temp_dir().join(format!(
                ".supermachine-sentry-warmup-{}-{}.sock",
                std::process::id(),
                unique_suffix()
            ));
            let sandbox = img
                .sentry_sandbox_with(&VmConfig::new())
                .map_err(|e| Error::vm_msg(format!("sentry warmup sandbox: {e}")))?
                .without_netns()
                .with_preserve_stragglers(true);
            let pool = sandbox
                .pool_with_exec(warm_sock.clone())
                .map_err(|e| Error::vm_msg(format!("sentry warmup pool: {e}")))?;
            let deadline = std::time::Instant::now() + Duration::from_secs(15);
            while !warm_sock.exists() {
                if std::time::Instant::now() >= deadline {
                    pool.stop();
                    return Err(Error::vm_msg(
                        "sentry warmup exec socket never appeared (supervisor bind failed)",
                    ));
                }
                std::thread::sleep(Duration::from_millis(5));
            }
            if parts.has_workload && parts.rootfs.join(".supermachine/run-workload").is_file() {
                let wrapped = vec![
                    "/.supermachine/sm-checkpoint".to_string(),
                    "/bin/sh".to_string(),
                    "/.supermachine/run-workload".to_string(),
                ];
                if pool.warm(wrapped.iter().cloned()).is_ok() {
                    if let Ok(wpid) = pool.acquire_running() {
                        let run_dir = parts.rootfs.join("run");
                        let _ = std::fs::create_dir_all(&run_dir);
                        let _ = std::fs::write(
                            run_dir.join("supermachine-workload.pid"),
                            format!("{wpid}\n"),
                        );
                    }
                }
            }
            let pool = Arc::new(pool);
            let vm = Vm {
                running: None,
                sentry_exec: None,
                sentry_src_rootfs: Some(parts.rootfs.clone()),
                vsock_mux_path: warm_sock.clone(),
                vsock_exec_path: warm_sock,
                own_vsock_mux_dir: None,
                skip_cleanup: false,
                time_sync_stop: Arc::new(AtomicBool::new(false)),
                _sentry_mem_admission: None,
                sentry_warm_pool: None,
                sentry_unified_pool: Some(Arc::clone(&pool)),
                runsc: None,
                kboxlike: None,
            };
            let res = warmup(&vm);
            let snap_res = if res.is_ok() {
                match pool.snapshot(&dest) {
                    Ok(()) => Ok(()),
                    Err(e) if e.raw_os_error() == Some(libc::EINVAL) => Ok(()),
                    Err(e) => Err(Error::vm_msg(format!("sentry warmup C4 snapshot: {e}"))),
                }
            } else {
                Ok(())
            };
            let _ = vm.stop();
            res?;
            snap_res?;
            // Re-load so the returned Image reflects warm filesystem and any C4
            // live-state sidecars (and re-validates the rootfs is intact).
            return Image::from_snapshot(&dest);
        }
        Ok(img)
    }

    /// Run this sentry image's baked workload to completion, confined to its
    /// rootfs, and return the workload's exit code (Linux/x86_64).
    ///
    /// This is the no-virt counterpart of cold-booting a KVM image: it launches
    /// the image's Entrypoint/Cmd (via the rootfs's `/bin/sh` running the baked
    /// `/.supermachine/run-workload` script, which exports the image Env and
    /// `cd`s into WorkingDir) under [`crate::sentry::run`]. Blocks until the
    /// workload exits — the sentry has no long-lived exec channel yet (live
    /// `exec`/`expose_tcp` into a running sentry workload is a later milestone),
    /// so this is the current run surface, not [`Vm::start`].
    ///
    /// Errors if the image isn't a sentry image. If the image declares no
    /// command (`has_workload == false`), returns `Ok(0)` — nothing to launch.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn run_sentry(&self) -> Result<i32, Error> {
        let parts = self.sentry.as_ref().ok_or_else(|| {
            Error::vm_msg(
                "Image is not a sentry image (metadata.json needs \"backend\":\"sentry\"); \
                 bake with Image::bake_sentry_from_ref."
                    .to_owned(),
            )
        })?;
        if !parts.has_workload {
            return Ok(0);
        }
        // `/bin/sh` runs the baked script; its `#!/.supermachine/busybox sh`
        // first line is an ordinary comment to /bin/sh, so the same script the
        // KVM bake stages works unchanged. The script sets the image Env + cwd
        // and execs the workload. PATH resolution + env are the shell's job.
        crate::sentry::run(
            std::path::Path::new("/bin/sh"),
            &["/.supermachine/run-workload".to_string()],
            Some(&parts.rootfs),
        )
        .map_err(|e| Error::vm_msg(format!("sentry run: {e}")))
    }

    /// As [`run_sentry`](Image::run_sentry), but also returns the structured
    /// [`SentryError`](crate::sentry::SentryError) reason when the workload ended
    /// other than a plain exit — so a caller can tell a **sandbox-policy kill**
    /// (e.g. a forbidden-syscall `SeccompViolation`, which would otherwise just be
    /// an ambiguous exit `159`) apart from the workload's own non-zero status. The
    /// integer code is identical to [`run_sentry`](Image::run_sentry).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn run_sentry_classified(
        &self,
    ) -> Result<(i32, Option<crate::sentry::SentryError>), Error> {
        let parts = self.sentry.as_ref().ok_or_else(|| {
            Error::vm_msg(
                "Image is not a sentry image (metadata.json needs \"backend\":\"sentry\"); \
                 bake with Image::bake_sentry_from_ref."
                    .to_owned(),
            )
        })?;
        if !parts.has_workload {
            return Ok((0, None));
        }
        crate::sentry::run_classified(
            std::path::Path::new("/bin/sh"),
            &["/.supermachine/run-workload".to_string()],
            Some(&parts.rootfs),
        )
        .map_err(|e| Error::vm_msg(format!("sentry run: {e}")))
    }

    /// Open a handle-based [`Sandbox`](crate::sentry::Sandbox) on this sentry
    /// image's rootfs (Linux/x86_64). Unlike [`run_sentry`](Image::run_sentry)
    /// (which runs the workload to completion), a `Sandbox` lets the image's
    /// workload run in the background ([`start_workload`](crate::sentry::Sandbox::start_workload))
    /// while you [`exec`](crate::sentry::Sandbox::exec) additional confined
    /// commands against the same rootfs — the no-virt analogue of `docker exec`
    /// (filesystem-shared; PID/net-namespace sharing is the later M5 work).
    ///
    /// Errors if the image isn't a sentry image.
    ///
    /// This is the no-config entry point: it builds the sandbox with the image's
    /// own baked defaults (memory/vCPU-derived caps, a default `pids.max`, the
    /// baked `User` uid-drop, the baked `Env`, the recorded egress policy). To
    /// override the caps from a [`VmConfig`] (e.g. a smaller memory cap, a
    /// different pids cap, or an uncapped run), use
    /// [`sentry_sandbox_with`](Image::sentry_sandbox_with).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn sentry_sandbox(&self) -> Result<crate::sentry::Sandbox, Error> {
        self.sentry_sandbox_with(&VmConfig::new())
    }

    /// Build a fully-configured [`Sandbox`](crate::sentry::Sandbox) on this sentry
    /// image's rootfs, applying BOTH the image's baked metadata AND `config`
    /// (Linux/x86_64). This is the ONE place that builds + configures the sandbox
    /// for the whole sentry surface (`Vm::start`'s sentry path, the pool, the
    /// handle API), so a single tenant's guest is host-safe by default:
    ///
    /// - **memory** — `cgroup-v2 memory.max` = `config.with_memory_mib` if set,
    ///   else the image's baked `memory_mib` (MiB → bytes). Over-budget guest
    ///   trees are OOM-killed; the host + neighbors are protected.
    /// - **CPU** — `cgroup-v2 cpu.max` = `vcpus * 100ms` per `100ms`
    ///   (`config.with_vcpus` if set, else the image's baked `vcpus`), i.e. that
    ///   many cores' worth of CPU.
    /// - **pids** — `cgroup-v2 pids.max` = `config.with_sentry_pids_max` if set,
    ///   else `SENTRY_DEFAULT_PIDS_MAX` (fork-bomb protection).
    /// - **uid-drop** — the baked image `User` (numeric `uid`/`uid:gid`) drops the
    ///   guest cell to that unprivileged host uid/gid before sealing.
    /// - **env** — the baked image `Env` seeds every cell (the per-`exec` env
    ///   overlays it).
    /// - **egress** — the recorded `egress_policy` is enforced on every outbound
    ///   TCP.
    ///
    /// [`with_sentry_uncapped`](VmConfig::with_sentry_uncapped) disables the three
    /// cgroup caps (trusted single-tenant tooling only). Errors if the image isn't
    /// a sentry image.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn sentry_sandbox_with(&self, config: &VmConfig) -> Result<crate::sentry::Sandbox, Error> {
        let parts = self.sentry.as_ref().ok_or_else(|| {
            Error::vm_msg(
                "Image is not a sentry image (metadata.json needs \"backend\":\"sentry\"); \
                 bake with Image::bake_sentry_from_ref."
                    .to_owned(),
            )
        })?;
        let mut sandbox = crate::sentry::Sandbox::new(parts.rootfs.clone());
        // Honor the image's recorded egress policy (metadata.json["egress_policy"],
        // e.g. deny_private) — the supervisor enforces it on every outbound TCP.
        if let Some(policy) = &self.egress_policy {
            sandbox = sandbox.with_egress(policy.clone());
        }
        // Drop the guest cell to an unprivileged host uid/gid — defense in depth, and
        // the CROSS-TENANT barrier: distinct uids per tenant make the kernel deny
        // every cross-tenant process_vm / ptrace / signal / same-uid file access, so
        // one tenant's cell cannot reach another's even at the same supervisor uid.
        // `config.sentry_uid` (per-INSTANCE, set by the orchestration to a UNIQUE uid
        // per tenant) overrides the image's baked `User`; falling back to the baked
        // User, then to no drop (the sentry's own uid). A username / root / empty
        // image User was already filtered to None by parse_numeric_user.
        if let Some((uid, gid)) = config.sentry_uid.or(parts.user) {
            sandbox = sandbox.with_uid(uid, gid);
        }
        // Seed the baked image Env as the exec env base (the per-request env
        // overlays it), and inherit host proxy/CA knobs when the image did not
        // already define them. Dockerfile RUN layers for kboxlike currently use
        // sentry as the mature layer executor, so this keeps build-step HTTPS
        // behavior aligned with kboxlike runtime guests under MITM proxies.
        let mut image_env = parts.image_env.clone();
        crate::kboxlike::merge_host_proxy_env_if_absent(&mut image_env);
        // Skip when empty so the sentry's built-in PATH/PWD/LANG fallback still
        // applies.
        if !image_env.is_empty() {
            sandbox = sandbox.with_env(image_env);
        }
        // Baked virtio-fs mounts persisted in image metadata. The KVM/HVF restore
        // path replays these into the worker; sentry has no virtio-fs device, so
        // attach them as the same confined bind mounts used for per-run config.
        for m in &self.mounts {
            sandbox = sandbox.with_mount(
                std::path::PathBuf::from(&m.guest_path),
                std::path::PathBuf::from(&m.host_path),
                m.read_only,
            );
        }
        // Writable volumes. KVM/HVF attach a host FILE as a virtio-blk device; the
        // sentry has no block layer, so a volume is a persistent host DIRECTORY
        // bind-mounted at the guest path (served by the same confined-dirfd
        // machinery as any mount). The data lives in the host dir, so it survives
        // snapshot (it's external to the rootfs copy) and is re-attached on restore
        // (the volume list round-trips through metadata.json → `self.volumes`),
        // giving the same "volume-backed cache survives snapshot+resume" contract
        // the KVM/HVF backends provide.
        for (host_path, guest_path, _size, _pristine) in &self.volumes {
            std::fs::create_dir_all(host_path).map_err(|e| {
                Error::vm_msg(format!(
                    "sentry volume backing dir {}: {e}",
                    host_path.display()
                ))
            })?;
            sandbox = sandbox.with_mount(guest_path.clone(), host_path.clone(), false);
        }
        // Resource caps: bound the guest cell tree so one tenant can't exhaust the
        // host. Off only when explicitly uncapped (trusted single-tenant tooling).
        if !config.sentry_uncapped {
            // memory.max: the VmConfig override, else the image's baked default.
            let mem_mib = config.memory_mib.unwrap_or(self.memory_mib) as u64;
            if mem_mib > 0 {
                sandbox = sandbox.with_memory_limit(mem_mib * 1024 * 1024);
            }
            // cpu.max: `vcpus` cores' worth of CPU (quota = vcpus * period).
            let vcpus = config.vcpus.unwrap_or(self.vcpus).max(1) as u64;
            sandbox = sandbox.with_cpu_limit(vcpus * SENTRY_CPU_PERIOD_US, SENTRY_CPU_PERIOD_US);
            // pids.max: the VmConfig override, else the sentry default.
            let pids = config.sentry_pids_max.unwrap_or(SENTRY_DEFAULT_PIDS_MAX);
            sandbox = sandbox.with_pids_limit(pids);
        }
        // Bounded-execution watchdog — THE RELEASE GATE. This is the single place
        // that builds + configures the Sandbox for the whole sentry surface (both
        // `Vm::start`'s sentry path AND `SentryPool::build` route through here), so
        // applying the timeout HERE bounds production by default: a fork+signal-
        // trapping RUN (or the deferred C1 reentrancy hang) can never hang the
        // caller forever — it becomes a SIGKILL + `SentryError::Timeout` (137).
        // Independent of the cgroup caps so it applies even under `sentry_uncapped`.
        // `None` → the generous 10-min default; `Some(ZERO)` opts out (unlimited).
        // Bound only the TENANT-FACING exec surface by default. A build pool's RUN
        // steps are the operator's OWN (trusted) Dockerfile commands — a legitimate
        // compile can run many minutes — so the generous production ceiling does NOT
        // apply to `for_build` pools; they stay unbounded (timeout 0 ⇒ the plain
        // `waitpid` wait, byte-identical to the pre-watchdog build path) unless the
        // operator opts in with an explicit `with_sentry_exec_timeout`. (Reserving the
        // pidfd+poll watchdog for non-build execs also keeps the concurrent-build
        // path — two builds cloning the same base, the `task_96ba9a6d` rootfs race —
        // on its original, validated timing.)
        let exec_timeout = match config.sentry_exec_timeout {
            Some(explicit) => explicit, // operator opt-in: honor for ANY sandbox
            None if config.for_build => Duration::ZERO, // trusted build: unbounded by default
            None => SENTRY_DEFAULT_EXEC_TIMEOUT, // production default ceiling
        };
        sandbox = sandbox.with_exec_timeout(exec_timeout);
        // virtio-fs mounts (config.virtiofs) → confined bind-mounts. The sentry has
        // no FUSE-over-virtio; each MountSpec becomes an `openat2(RESOLVE_IN_ROOT)`
        // bind of `host_path` at `guest_path`, honoring the spec's read_only flag.
        // The `guest_tag` (a virtio-fs device tag) and `symlinks` policy have no
        // analogue in the sentry's confinement and are dropped (documented in the
        // workstream risks). The host dir must already exist (the FUSE backend
        // would have required the same).
        for m in &config.virtiofs {
            sandbox = sandbox.with_mount(
                std::path::PathBuf::from(&m.guest_path),
                std::path::PathBuf::from(&m.host_path),
                m.read_only,
            );
        }
        // Data volumes (config.volumes) → read-write host-dir binds. The sentry has
        // no block layer, so a "volume" is just a confined bind of the host backing
        // DIRECTORY at `guest_path`; `size_bytes` is NOT enforced (no quota — see
        // the workstream risks). Ensure the host dir exists (create it like the KVM
        // path lazily creates a backing file) so first-run volumes are writable.
        for v in &config.volumes {
            let host_dir = std::path::PathBuf::from(&v.host_path);
            std::fs::create_dir_all(&host_dir).map_err(Error::Io)?;
            sandbox = sandbox.with_mount(std::path::PathBuf::from(&v.guest_path), host_dir, false);
        }
        Ok(sandbox)
    }

    /// Start a PERSISTENT-supervisor [`Pool`](crate::sentry::Pool) on this sentry
    /// image's rootfs (Linux/x86_64): one long-lived supervisor (rootfs dirfd +
    /// rings + servicers set up once) that forks a fresh cell per
    /// [`exec`](crate::sentry::Pool::exec) and fork-from-warm
    /// [`acquire`](crate::sentry::Pool::acquire) instance, amortizing the per-exec
    /// setup. The image's recorded egress policy is enforced on every cell. To
    /// configure mounts / cgroup limits / uid-drop, build the pool from a
    /// configured [`Sandbox`](crate::sentry::Sandbox):
    /// `image.sentry_sandbox()?.with_memory_limit(..).pool()`. Errors if the image
    /// isn't a sentry image.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn sentry_pool(&self) -> Result<crate::sentry::Pool, Error> {
        // Inherit the image's egress policy (and any future config) by routing
        // through the same configured Sandbox the handle API builds.
        self.sentry_sandbox()?
            .pool()
            .map_err(|e| Error::vm_msg(format!("sentry pool: {e}")))
    }

    /// Bake a KVM-bootable [`Image`] from an OCI reference with **zero asset
    /// paths** (Linux/x86_64) — the fully hands-off entry point:
    ///
    /// ```no_run
    /// let img = supermachine::Image::bake_kvm_auto("alpine", "/tmp/alpine-vm")?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    ///
    /// The guest kernel, `busybox`, and in-VM agent are all sourced from the
    /// bundled `supermachine-kernel` crate (its x86_64 sub-crate, selected by
    /// `target_arch`), so the caller never points at a kernel build or builds
    /// an initramfs. Internally it extracts the bundled minimal module-free
    /// bzImage, assembles the agent initramfs in-process from the bundled
    /// busybox + agent (no kernel modules — vsock/squashfs/overlay are built
    /// in), then delegates to [`bake_kvm_from_ref`](Image::bake_kvm_from_ref).
    /// All artifacts land under `dest_dir`, so the snapshot is self-contained.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kvm_auto(image_ref: &str, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
        let dest = dest_dir.into();
        std::fs::create_dir_all(&dest).map_err(Error::Io)?;
        let t_assets = std::time::Instant::now();
        let (kernel, initrd) = Self::extract_bundled_kvm_boot_assets(&dest)?;
        if crate::trace::enabled("run") {
            eprintln!(
                "supermachine: kvm boot-assets extract_ms={}",
                t_assets.elapsed().as_millis()
            );
        }
        Self::bake_kvm_from_ref(image_ref, &kernel, &initrd, dest)
    }

    /// Extract the bundled, hands-off KVM boot assets into `dest`: the minimal
    /// module-free x86_64 guest kernel (`dest/kernel`) and an in-process-assembled
    /// agent initramfs (`dest/agent.cpio`, from the bundled busybox + agent — the
    /// kernel has virtio-vsock/squashfs/overlayfs built in, MODULES=n, so no
    /// modules are staged). Shared by [`bake_kvm_auto`](Image::bake_kvm_auto) and
    /// [`bake_kvm_from_squashfs_auto`](Image::bake_kvm_from_squashfs_auto) so both
    /// hands-off bake entry points source boot assets the one same way.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn extract_bundled_kvm_boot_assets(dest: &Path) -> Result<(PathBuf, PathBuf), Error> {
        let kernel = dest.join("kernel");
        // `SUPERMACHINE_KVM_KERNEL=/path/to/bzImage` overrides the bundled guest
        // kernel — used to validate experimental kernels (e.g. the virtio-fs/DAX
        // build) without re-bundling. Falls back to the bundled kernel if unset.
        match std::env::var_os("SUPERMACHINE_KVM_KERNEL") {
            Some(p) if std::path::Path::new(&p).is_file() => {
                std::fs::copy(&p, &kernel).map_err(Error::Io)?;
            }
            _ => supermachine_kernel::extract_kernel_to(&kernel).map_err(Error::Io)?,
        }
        let assets = dest.join("agent-assets");
        std::fs::create_dir_all(&assets).map_err(Error::Io)?;
        let agent_bin = assets.join("supermachine-agent");
        let busybox_bin = assets.join("busybox");
        supermachine_kernel::extract_supermachine_agent_to(&agent_bin).map_err(Error::Io)?;
        supermachine_kernel::extract_busybox_to(&busybox_bin).map_err(Error::Io)?;
        let initrd = dest.join("agent.cpio");
        crate::bake::build_kvm_agent_initramfs(&agent_bin, &busybox_bin, &[], &initrd)
            .map_err(Error::bake_msg)?;
        let _ = std::fs::remove_dir_all(&assets);
        Ok((kernel, initrd))
    }

    /// Wrap an already-built read-only `rootfs.squashfs` as a KVM-bootable
    /// [`Image`] — the same rootfs format [`bake_kvm`](Image::bake_kvm) produces,
    /// but with **no layer re-extraction**. This is the re-import side of the
    /// in-VM builder's RAM-density loop: a built image is flattened to a shared
    /// read-only squashfs by [`commit_squashfs`](crate::builder::commit_squashfs),
    /// then booted back with an EMPTY tmpfs overlay upper — so the build's files
    /// live in the squashfs (page-cache shared CoW across every booted VM) instead
    /// of each VM's private tmpfs RAM. The squashfs is placed at `dest/rootfs.squashfs`
    /// (copied in only if it isn't already there — `commit_squashfs` can write it
    /// straight into `dest`, making this copy-free).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kvm_from_squashfs(
        squashfs: &Path,
        kernel: &Path,
        agent_initrd: &Path,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        let dest = dest_dir.into();
        std::fs::create_dir_all(&dest).map_err(Error::Io)?;
        let rootfs = dest.join("rootfs.squashfs");
        if squashfs != rootfs {
            std::fs::copy(squashfs, &rootfs).map_err(Error::Io)?;
        }
        let metadata = serde_json::json!({
            "backend": "kvm",
            "kvm_kernel": kernel.to_string_lossy(),
            "kvm_initrd": agent_initrd.to_string_lossy(),
            "kvm_disk": rootfs.to_string_lossy(),
            "memory_mib": 512,
            "vcpus": 1,
            "balloon_target_pages": crate::bake::compute_balloon_target_pages(512),
        });
        std::fs::write(
            dest.join("metadata.json"),
            serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?,
        )
        .map_err(Error::Io)?;
        Image::from_snapshot(&dest)
    }

    /// Hands-off variant of [`bake_kvm_from_squashfs`](Image::bake_kvm_from_squashfs):
    /// source the guest kernel + agent initramfs from the bundled crate assets
    /// (like [`bake_kvm_auto`](Image::bake_kvm_auto)), so the caller only supplies
    /// the committed squashfs + a destination.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn bake_kvm_from_squashfs_auto(
        squashfs: &Path,
        dest_dir: impl Into<PathBuf>,
    ) -> Result<Image, Error> {
        let dest = dest_dir.into();
        std::fs::create_dir_all(&dest).map_err(Error::Io)?;
        let (kernel, initrd) = Self::extract_bundled_kvm_boot_assets(&dest)?;
        Self::bake_kvm_from_squashfs(squashfs, &kernel, &initrd, dest)
    }

    /// Assemble a self-contained KVM agent initramfs (Linux/x86_64) in-process,
    /// for use as the `agent_initrd` of [`bake_kvm`](Image::bake_kvm) /
    /// [`bake_kvm_from_ref`](Image::bake_kvm_from_ref).
    ///
    /// Builds a `newc` cpio whose PID-1 init mounts `/proc` + `/dev`, `insmod`s
    /// `ordered_modules` (`.ko` or `.ko.zst`, in order), mounts the rootfs disk
    /// (`/dev/vda`) at `/mnt`, and execs the agent — no external `cpio`/busybox
    /// build step. (A vsock-built-in kernel would drop the module list.)
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn build_kvm_initramfs(
        agent_bin: &Path,
        busybox_bin: &Path,
        ordered_modules: &[PathBuf],
        out_cpio: &Path,
    ) -> Result<(), Error> {
        crate::bake::build_kvm_agent_initramfs(agent_bin, busybox_bin, ordered_modules, out_cpio)
            .map_err(Error::bake_msg)
    }

    /// Acquire a microVM from this image's hidden pool. Returns
    /// a [`PooledVm`] which `Deref`s to [`Vm`] and returns to
    /// the pool on `Drop`. Use this for the common
    /// "spin up a VM, do one task, throw it away, do another"
    /// loop — the pool keeps re-restoring from the same snapshot
    /// behind the scenes so per-iteration cost stays at the
    /// snapshot-restore floor (~5 ms on Apple Silicon).
    ///
    /// ```no_run
    /// # use supermachine::{Image, VmConfig};
    /// # use std::time::Duration;
    /// let image = Image::from_snapshot("path/to/rust-slim")?;
    /// for src in ["fn main() {}", "fn main() { panic!() }"] {
    ///     let vm = image.acquire()?;
    ///     vm.write_file("/tmp/main.rs", src.as_bytes())?;
    ///     let out = vm.exec_builder()
    ///         .argv(["sh", "-c", "rustc /tmp/main.rs -o /tmp/m && /tmp/m"])
    ///         .timeout(Duration::from_secs(30))
    ///         .output()?;
    ///     println!("status={:?} out={:?}", out.status.code(), out.stdout);
    ///     // vm dropped here — returned to pool, restored from snapshot
    /// }
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    ///
    /// ## Pool sizing
    ///
    /// `Image::acquire` uses an ambient pool with default policy
    /// (`min=0`, `max=64`, `idle_timeout=60s`,
    /// `acquire_timeout=60s`). For an explicit policy use
    /// [`Image::pool`] to build a [`Pool`] and call
    /// `pool.acquire()` instead.
    ///
    /// Per-acquire cost is the snapshot restore (~3 ms on Apple
    /// Silicon) when an idle worker is available; cold spawn
    /// (lazy-grow path) is ~15 ms. The pool auto-grows up to
    /// `max` under burst and auto-evicts above-`min` workers
    /// after they sit idle for `idle_timeout`.
    #[cfg(any(
        all(target_os = "macos", target_arch = "aarch64"),
        all(target_os = "linux", target_arch = "x86_64")
    ))]
    pub fn acquire(&self) -> Result<PooledVm<'_>, Error> {
        self.acquire_with(&VmConfig::new())
    }

    /// Like [`Image::acquire`] but with an explicit
    /// [`VmConfig`] (overrides for memory, vCPUs, asset paths,
    /// **pool size**, etc.). The config is honored on **first**
    /// acquire — when the pool is built. Subsequent acquires
    /// reuse the existing pool regardless of `config`. This is
    /// fine for most use cases; create a fresh `Image` if you
    /// need a different config without restarting your app.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn acquire_with(&self, config: &VmConfig) -> Result<PooledVm<'_>, Error> {
        let _span = tracing::info_span!(
            "supermachine.acquire",
            memory_mib = self.memory_mib,
            vcpus = self.vcpus,
        )
        .entered();
        let pool_arc = self.ensure_default_pool(config)?;
        let worker = pool_arc.acquire()?;
        let vm = Vm {
            pool: None,
            vsock_mux_path: worker.vsock_mux_path.clone(),
            vsock_exec_path: worker.vsock_exec_path.clone(),
            own_vsock_mux_dir: None,
            skip_cleanup: true,
            // Populate image_meta from the source Image so
            // `Vm::snapshot` can build metadata.json. The
            // capture itself runs over the worker subprocess's
            // supervisor protocol — see `Vm::snapshot`'s pool-
            // worker dispatch path. No HVF-entitlement
            // requirement on the calling binary.
            image_meta: Some(Arc::new(ImageMeta {
                memory_mib: config.memory_mib.unwrap_or(self.memory_mib),
                vcpus: config.vcpus.unwrap_or(self.vcpus),
                layers: self.layers.clone(),
                delta_squashfs: self.delta_squashfs.clone(),
                baker_runtime_sha16: self.baker_runtime_sha16.clone(),
                tsi_token: self.tsi_token.clone(),
                egress_policy: self.egress_policy.clone(),
                pre_exec_sync: self.pre_exec_sync,
            })),
        };
        Ok(PooledVm {
            vm: Some(vm),
            worker: Some(worker),
            pool_arc: Arc::clone(pool_arc),
            _image: std::marker::PhantomData,
        })
    }

    /// Linux/KVM `Image::acquire_with`: acquires from an ambient warm pool built
    /// lazily on first use (boot → snapshot once), so each acquire is a CoW
    /// restore (~ms) instead of a cold boot (~1s). The pool is cached on the
    /// Image (shared across clones); the first call's `config` fixes its policy/
    /// memory — for per-acquire control use an explicit [`Image::pool`]. Falls
    /// back to a cold [`Vm::start`] if warming fails.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn acquire_with(&self, config: &VmConfig) -> Result<PooledVm<'_>, Error> {
        // No-virt sentry image: an ambient pool over ONE shared, always-warm
        // exec server (forks a fresh sealed cell per exec). Cached in the same
        // `kvm_pool` OnceLock the KVM ambient pool uses; warmed once.
        if self.sentry.is_some() {
            if self.kvm_pool.get().is_none() {
                let pol = PoolPolicy::default();
                if let Ok(sp) = SentryPool::build(
                    self,
                    config,
                    pol.max,
                    pol.acquire_timeout,
                    pol.restore_on_release,
                ) {
                    let _ = self.kvm_pool.set(Arc::new(Pool {
                        kvm: None,
                        sentry: Some(sp),
                        runsc: None,
                        kboxlike: None,
                    }));
                }
            }
            if let Some(pool) = self.kvm_pool.get() {
                return pool.acquire();
            }
            // Server build failed — cold per-acquire fallback (still correct).
            let vm = Vm::start(self, config)?;
            return Ok(PooledVm {
                vm: Some(vm),
                pool: None,
                sentry_pool: None,
                runsc_pool: None,
                kboxlike_pool: None,
                sentry_mem_admission: None,
                sentry_running_pid: None,
                sentry_running_pool: None,
                sentry_stop_running_pool_on_drop: false,
                sentry_touched: AtomicBool::new(false),
                _image: std::marker::PhantomData,
            });
        }
        if self.runsc.is_some() {
            if self.kvm_pool.get().is_none() {
                let pol = PoolPolicy::default();
                if let Ok(rp) = RunscPoolInner::build(
                    self,
                    config,
                    pol.min,
                    pol.max,
                    pol.acquire_timeout,
                    pol.restore_on_release,
                ) {
                    let _ = self.kvm_pool.set(Arc::new(Pool {
                        kvm: None,
                        sentry: None,
                        runsc: Some(rp),
                        kboxlike: None,
                    }));
                }
            }
            if let Some(pool) = self.kvm_pool.get() {
                return pool.acquire();
            }
            // Pool build failed — cold per-acquire fallback (still correct).
            let vm = Vm::start(self, config)?;
            return Ok(PooledVm {
                vm: Some(vm),
                pool: None,
                sentry_pool: None,
                runsc_pool: None,
                kboxlike_pool: None,
                sentry_mem_admission: None,
                sentry_running_pid: None,
                sentry_running_pool: None,
                sentry_stop_running_pool_on_drop: false,
                sentry_touched: AtomicBool::new(false),
                _image: std::marker::PhantomData,
            });
        }
        if self.kboxlike.is_some() {
            if self.kvm_pool.get().is_none() {
                let pol = PoolPolicy::default();
                if let Ok(kp) = KboxlikePoolInner::build(
                    self,
                    config,
                    pol.min,
                    pol.max,
                    pol.acquire_timeout,
                    pol.restore_on_release,
                ) {
                    let _ = self.kvm_pool.set(Arc::new(Pool {
                        kvm: None,
                        sentry: None,
                        runsc: None,
                        kboxlike: Some(kp),
                    }));
                }
            }
            if let Some(pool) = self.kvm_pool.get() {
                return pool.acquire();
            }
            let vm = Vm::start(self, config)?;
            return Ok(PooledVm {
                vm: Some(vm),
                pool: None,
                sentry_pool: None,
                runsc_pool: None,
                kboxlike_pool: None,
                sentry_mem_admission: None,
                sentry_running_pid: None,
                sentry_running_pool: None,
                sentry_stop_running_pool_on_drop: false,
                sentry_touched: AtomicBool::new(false),
                _image: std::marker::PhantomData,
            });
        }
        if self.kvm_pool.get().is_none() {
            // Warm once + cache. A lost race just wastes one warm-up; the first
            // value stored in the OnceLock wins and both threads use it. The
            // ambient pool uses default policy (min=0 → no idle pre-warm; each
            // acquire is a ~ms CoW restore). Use an explicit Image::pool for a
            // min>0 always-warm idle set.
            if let Ok(warm) = warm_snapshot_for_pool(self, config) {
                let pol = PoolPolicy::default();
                let inner = Arc::new(KvmPoolInner {
                    image: warm,
                    vm_config: config.clone(),
                    min: 0,
                    idle: std::sync::Mutex::new(Vec::new()),
                    admission: Admission::new(pol.max, pol.acquire_timeout),
                    restore_on_release: pol.restore_on_release,
                    refilling: std::sync::atomic::AtomicBool::new(false),
                    shut_down: std::sync::atomic::AtomicBool::new(false),
                });
                let _ = self.kvm_pool.set(Arc::new(Pool {
                    kvm: Some(inner),
                    sentry: None,
                    runsc: None,
                    kboxlike: None,
                }));
            }
        }
        if let Some(pool) = self.kvm_pool.get() {
            return pool.acquire();
        }
        // Warming failed (e.g. agent never came up) — cold start as a fallback.
        let vm = Vm::start(self, config)?;
        Ok(PooledVm {
            vm: Some(vm),
            pool: None,
            sentry_pool: None,
            runsc_pool: None,
            kboxlike_pool: None,
            sentry_mem_admission: None,
            sentry_running_pid: None,
            sentry_running_pool: None,
            sentry_stop_running_pool_on_drop: false,
            sentry_touched: AtomicBool::new(false),
            _image: std::marker::PhantomData,
        })
    }

    /// Configure an explicit pool against this image. Use when
    /// you want auto-scaling or fine control over min/max/idle/
    /// acquire timeouts; for the simple case
    /// `image.acquire()` already manages an ambient default-
    /// policy pool for you.
    ///
    /// ```no_run
    /// # use std::time::Duration;
    /// # use supermachine::Image;
    /// let image = Image::ensure_baked("rust_warm", "rust:1-slim", |b| b)?;
    /// // Auto-scale 5..=50, evict idle workers after 60s,
    /// // fail acquire if pool stays at max for >10s:
    /// let pool = image.pool()
    ///     .min(5)
    ///     .max(50)
    ///     .idle_timeout(Duration::from_secs(60))
    ///     .acquire_timeout(Duration::from_secs(10))
    ///     .build()?;
    /// let vm = pool.acquire()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn pool(&self) -> PoolBuilder<'_> {
        PoolBuilder {
            image: self,
            policy: PoolPolicy::default(),
            vm_config: VmConfig::new(),
        }
    }

    /// Build a fresh `HiddenPool` against `policy`, store it in
    /// `self.hidden_pool`, return the borrowed Arc.
    /// Build a fresh standalone `HiddenPool`. Caller decides
    /// what to do with the Arc — `acquire()` parks it in the
    /// per-Image `hidden_pool` OnceLock; `pool().build()`
    /// returns it as a `Pool` to the caller.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    fn build_pool_arc(
        &self,
        config: &VmConfig,
        policy: PoolPolicy,
    ) -> Result<Arc<HiddenPool>, Error> {
        // 0.7.49+ best-effort sweep of stranded COW volume temps in
        // /tmp left by previously-crashed processes. Idempotent + per-
        // process (a OnceLock latches the first call). Cheap when /tmp
        // is clean; bounded by /tmp entry count on a busy host.
        sweep_volume_temps_once();
        // Find supermachine-worker. Tries env override, sibling-
        // of-current-exe (cargo install layout), dev-tree
        // target/release (workspace).
        #[cfg(target_os = "macos")]
        let worker_bin = {
            // Three resolution modes:
            //
            //   1. `SUPERMACHINE_WORKER_BIN` set:
            //      Explicit user override. Use the path AS-IS — no
            //      copy, no codesign mutation. Devs running a custom
            //      worker against the library expect this.
            //
            //   2. `SUPERMACHINE_WORKER_BIN_BUNDLED` set:
            //      The npm JS shim (`npm/supermachine-core/index.js`)
            //      sets this to `@supermachine/core-darwin-arm64/
            //      supermachine-worker`. We treat it as a SOURCE and
            //      copy to the per-version user-data dir, signing
            //      the COPY there. Critical for npm consumers: the
            //      node_modules path is never codesign-rewritten, so
            //      npm's integrity model isn't violated and the
            //      stuck-`.cstemp`-with-uchg recovery loop that
            //      wedged 0.7.27 installs can't happen on
            //      node_modules.
            //
            //   3. Neither set:
            //      Fall back to `codesign::locate_worker_bin`'s
            //      sibling-of-current-exe + ~/.cargo/bin + $PATH
            //      walk. The found path is then copied to user-dir
            //      and signed there, same as mode 2.
            if let Some(p) = std::env::var_os("SUPERMACHINE_WORKER_BIN") {
                let p = PathBuf::from(p);
                if !p.is_file() {
                    return Err(Error::assets_msg(format!(
                        "SUPERMACHINE_WORKER_BIN={} does not exist or is not a file",
                        p.display()
                    )));
                }
                p
            } else {
                let source =
                    if let Some(bundled) = std::env::var_os("SUPERMACHINE_WORKER_BIN_BUNDLED") {
                        let p = PathBuf::from(bundled);
                        if !p.is_file() {
                            return Err(Error::assets_msg(format!(
                            "SUPERMACHINE_WORKER_BIN_BUNDLED={} does not exist or is not a file",
                            p.display()
                        )));
                        }
                        p
                    } else {
                        crate::codesign::locate_worker_bin().ok_or_else(|| {
                            Error::assets_msg(
                                "supermachine-worker binary not found (looked for sibling of \
                             current_exe and target/release/supermachine-worker). Set \
                             SUPERMACHINE_WORKER_BIN if you have it elsewhere."
                                    .to_owned(),
                            )
                        })?
                    };
                crate::assets::ensure_worker_in_user_dir(&source).map_err(Error::assets_msg)?
            }
        };
        #[cfg(not(target_os = "macos"))]
        let worker_bin: PathBuf = std::env::var_os("SUPERMACHINE_WORKER_BIN")
            .map(PathBuf::from)
            .ok_or_else(|| {
                Error::assets_msg("SUPERMACHINE_WORKER_BIN must be set on this platform".to_owned())
            })?;
        // HARD: verify the worker binary matches the library
        // version. The supervisor protocol evolves between
        // releases; a stale ~/.cargo/bin/supermachine-worker from
        // an older `cargo install supermachine` deadlocks
        // pipelined-bake silently otherwise. See
        // codesign::verify_worker_version for the diagnostic
        // signature and the upgrade hint we surface to the user.
        #[cfg(target_os = "macos")]
        {
            crate::codesign::verify_worker_version(&worker_bin).map_err(Error::vm_msg)?;
            // Ensure the user-dir worker carries the HVF entitlement.
            // We used to silently ignore the result here on the
            // theory that `hv_vm_create` would surface its own
            // error if signing failed. In practice that lost the
            // actual diagnostic (`HV_DENIED` is opaque; users
            // couldn't tell whether codesign failed, the
            // entitlements plist was wrong, the binary was already
            // signed but with a different entitlement, …) and the
            // autopilot's stuck-`.cstemp` recovery loop went
            // undetected for entire sessions. Surface the codesign
            // failure with its exact stderr + a recovery hint
            // instead of letting the downstream bake fail with a
            // cryptic supervisor-closed message.
            //
            // Idempotent — a successful sign caches via a per-version
            // sentinel and subsequent calls return immediately.
            //
            // Signing happens on the user-dir copy (see
            // ensure_worker_in_user_dir above), so node_modules is
            // never touched.
            if let Err(e) = crate::codesign::ensure_worker_signed(&worker_bin) {
                return Err(Error::vm_msg(format!(
                    "supermachine-worker codesign failed: {e}"
                )));
            }
        }

        // Unix socket paths are capped at 104 bytes on macOS
        // (SUN_LEN). Default to /tmp instead of $TMPDIR, which on
        // macOS resolves to /var/folders/.../T/ and burns ~50
        // characters before we even start. `/tmp/supermachine-
        // pool-<pid>-<suffix>/` leaves room for a meaningful
        // socket name underneath.
        let socks_dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => PathBuf::from(format!(
                "/tmp/supermachine-pool-{}-{:x}",
                std::process::id(),
                unique_suffix(),
            )),
        };
        std::fs::create_dir_all(&socks_dir).map_err(Error::Io)?;

        // Sweep `~/.cache/supermachine/rosetta-aot/` for AOT cache
        // orphans before the worker spawns. Two classes get evicted:
        //
        //   1. **Zero-byte `<x>.aotcache` files.** Migration cleanup
        //      for caches built with supermachine ≤ 0.7.25, before
        //      the FUSE-layer `.partial`-then-rename publish protocol
        //      (`fn create` / `fn release` in posix.rs) closed the
        //      orphan window. A 0-byte file from a pre-fix lifetime
        //      will still trigger the rosetta interpreter's
        //      `aot_hdr->segment_count <= kMaxSegments` assertion
        //      (ImageInfo.cpp:71 — pread of an empty file returns 0
        //      and stack-garbage at offset 8 is parsed as
        //      segment_count) or SIGBUS-on-mmap-past-EOF on the
        //      first amd64 exec that hashes to that build-id.
        //   2. **`.<x>.aotcache.partial` leftovers.** Crash recovery
        //      for the new publish protocol. If a prior VM was
        //      SIGKILLed or the host crashed between FUSE_CREATE and
        //      FUSE_RELEASE, the partial file is on disk and would
        //      block a fresh `O_CREAT|O_EXCL` open of the same name.
        //      posix.rs's create-time retry handles this inline, but
        //      cleaning up at acquire keeps the cache dir tidy and
        //      avoids EEXIST surprises in narrower acquire-time
        //      windows.
        //
        // Cheap: one readdir + one stat per entry, runs in <5 ms on
        // a fully populated cache. Silently no-op when the cache
        // dir doesn't exist (no `rosettad-cache` mount in this
        // image).
        if let Some(home) = std::env::var_os("HOME") {
            let cache_dir = PathBuf::from(home)
                .join(".cache")
                .join("supermachine")
                .join("rosetta-aot");
            if cache_dir.is_dir() {
                let _ = scrub_aotcache_orphans(&cache_dir);
            }
        }

        let memory_mib = config.memory_mib.unwrap_or(self.memory_mib);
        let vcpus = config.vcpus.unwrap_or(self.vcpus);
        let spawn_timeout = config
            .restore_timeout
            .unwrap_or_else(|| Duration::from_secs(30));
        // Same SUN_LEN concern: use a small token instead of the
        // full snapshot dir name. Per-pool counter would be even
        // shorter; for now an 8-char hash is enough.
        let name_prefix = "w".to_owned();
        let spawn_cfg = Arc::new(SpawnConfig {
            observed_footprint_mib: std::sync::atomic::AtomicU64::new(0),
            worker_bin,
            snapshot_path: self.snapshot_path.clone(),
            layers: self.layers.clone(),
            delta_squashfs: self.delta_squashfs.clone(),
            mounts: self.mounts.clone(),
            volumes: self.volumes.clone(),
            memory_mib,
            vcpus,
            socks_dir: socks_dir.clone(),
            name_prefix,
            spawn_timeout,
            baker_runtime_sha16: self.baker_runtime_sha16.clone(),
            balloon_target_pages: self.balloon_target_pages,
            tsi_token: self.tsi_token.clone(),
            egress_policy: self.egress_policy.clone(),
            pre_exec_sync: self.pre_exec_sync,
            restore_on_release: policy.restore_on_release,
        });
        // Spawn `min` workers in parallel up front so the pool
        // is ready to serve immediately. `max - min` more can be
        // spawned later by the auto-grow path in `acquire`.
        //
        // WARM HANDOFF: claim the bake-time worker (if any) and
        // use it as one of the initial idle entries — saves spawn
        // (~50 ms) + restore (~5 ms) for the FIRST acquire. This
        // is the per-Image atomic claim: only the first
        // PoolBuilder::build() to run on a given Image gets the
        // warm worker; concurrent / subsequent calls take None
        // and spawn fresh as today.
        //
        // If the warm worker's child has already died (e.g. user
        // kept the Image around so long the worker exited on its
        // own, or HVF returned an error), we fall through cleanly
        // to spawn-from-disk.
        let claimed_warm = self.warm_baked_worker.take().and_then(|mut bw| {
            // Liveness check — if the child reaped itself, ditch it.
            match bw.child.try_wait() {
                Ok(None) => Some(bw), // still running, claim it
                Ok(Some(_)) => None,  // exited, fall through
                Err(_) => None,       // EBADF or similar
            }
        });
        let initial = policy.min;
        let mut idle: Vec<IdleEntry> = Vec::with_capacity(initial.max(1));
        let extra_warm: usize;
        if let Some(bw) = claimed_warm {
            // 0.7.49+ COW-volume use-once flag. When the pool is
            // configured for restore_on_release AND the snapshot
            // ships pristines, the warm-handoff worker's volume
            // fds point at the user's host file (no COW). Mark it
            // use-once so the first release kills it; the
            // replenisher will then spawn a fresh COW-backed worker.
            let warm_use_once = policy.restore_on_release
                && spawn_cfg.volumes.iter().any(|(_, _, _, p)| p.is_some());
            let worker = warm_baked_to_worker(
                bw,
                spawn_cfg.memory_mib,
                !spawn_cfg.mounts.is_empty() || !spawn_cfg.volumes.is_empty(),
                spawn_cfg
                    .volumes
                    .iter()
                    .map(|(_, gp, _, _)| gp.clone())
                    .collect(),
                spawn_cfg.vcpus,
                warm_use_once,
            );
            // Even though this worker never went through a
            // RESTORE — it's the live bake worker reused — its
            // VFS caches still reflect "what the host looked
            // like during bake". If the user edited bound files
            // between Image.build and Pool.acquire, those reads
            // would return stale bytes. Drop now to force
            // re-LOOKUP on first access.
            //
            // 0.7.44+ probe-skip: same logic as `spawn_one`'s
            // restore path — drop only when the snapshot has a
            // host-backed surface (mounts or volumes). Pure-image
            // snapshots save ~5-15 ms.
            if !spawn_cfg.mounts.is_empty() || !spawn_cfg.volumes.is_empty() {
                let _ = drop_vfs_caches_via_agent(&worker.vsock_exec_path, spawn_cfg.memory_mib);
            }
            // 0.7.51+ wall-clock sync. The warm-handoff worker
            // was launched at bake time; if the snapshot has
            // sat on disk for hours/days before this pool build
            // claimed it, the in-guest CLOCK_REALTIME is just
            // as stale as on a cold restore. Same RPC fixes it.
            let _ = sync_time_via_agent(&worker.vsock_exec_path);
            // Deliberately NOT seeded into the pool's footprint
            // high-water-mark: this worker COLD-BOOTED and ran the bake,
            // so its resident footprint is near the guest ceiling
            // (measured ~2.2 GiB for a 2 GiB cap) — wildly
            // unrepresentative of the CoW restores the rest of the pool
            // spawns (~MiBs dirty). Seeding it would poison the learned
            // admission charge up to the cap and stall the prewarm. Its
            // own admission charge (inside warm_baked_to_worker) does
            // use its real measurement — that part is honest.
            idle.push(IdleEntry {
                worker,
                last_used: Instant::now(),
            });
            extra_warm = 1;
        } else {
            extra_warm = 0;
        }
        // We need `initial` idle entries total. The warm worker (if
        // claimed) covers one slot; spawn (initial - 1) fresh ones.
        // For min=0 with a warm worker, we still keep the warm one
        // around (it's effectively a free min=1).
        let to_spawn = initial.saturating_sub(extra_warm);
        if to_spawn == 1 {
            idle.push(IdleEntry {
                worker: spawn_cfg.spawn_one()?,
                last_used: Instant::now(),
            });
        } else if to_spawn > 1 {
            let mut handles = Vec::with_capacity(to_spawn);
            for _ in 0..to_spawn {
                let cfg = Arc::clone(&spawn_cfg);
                handles.push(std::thread::spawn(move || cfg.spawn_one()));
            }
            for h in handles {
                let w = h
                    .join()
                    .map_err(|_| Error::vm_msg("pool spawn thread panicked".to_owned()))??;
                idle.push(IdleEntry {
                    worker: w,
                    last_used: Instant::now(),
                });
            }
        }
        // Pool's `alive` accounting: total idle count (incl. warm
        // handoff entry).
        let initial = idle.len();
        let pool = Arc::new(HiddenPool {
            state: Arc::new(Mutex::new(PoolState {
                idle,
                alive: initial,
                waiting: 0,
                reaped: 0,
            })),
            available: Arc::new(Condvar::new()),
            dirty: Some(Arc::new(Mutex::new(VecDeque::new()))),
            dirty_pending: Some(Arc::new(Condvar::new())),
            socks_dir,
            shutting_down: Arc::new(AtomicBool::new(false)),
            spawn_cfg: Arc::clone(&spawn_cfg),
            policy,
        });
        // Spawn the housekeeping threads: replenisher (handles
        // genuine worker death + maintains `min`), restorer
        // (recycles dirty workers via RESTORE), janitor (evicts
        // idle-too-long workers above `min` to free RAM). All
        // detach naturally when the pool Arc drops via
        // `Weak::upgrade` failure.
        let wait_handles = pool.wait_handles();
        let h_replenish = {
            let h = wait_handles.clone();
            std::thread::Builder::new()
                .name("supermachine-pool-replenish".into())
                .spawn(move || replenisher_loop(h))
                .map_err(|e| Error::vm_msg(format!("spawn replenisher thread: {e}")))?
        };
        // Restorer threads only run when `restore_on_release`
        // is on. With it off, drop pushes workers straight to
        // idle and the restorer would have nothing to do.
        let mut handles = vec![h_replenish];
        if policy.restore_on_release {
            // Multi-restorer: scale the recycle thread count
            // with pool size. Each RESTORE RPC is ~3 ms in-
            // place; under bursty drop patterns a single
            // restorer becomes the serialization point and
            // shows up as a phantom delay on the *next* acquire
            // (acquire pops the idle queue, restorer's still
            // chewing through dirty, idle is empty → user
            // blocks ~3 ms × queue depth).
            //
            // Formula: ⌈max/2⌉ clamped [1, 4], capped at `max`.
            // 1 restorer for max=2, 3 for max=5, 4 for max=8+.
            let restorer_count = ((policy.max + 1) / 2).clamp(1, 4).min(policy.max.max(1));
            for _ in 0..restorer_count {
                let h = wait_handles.clone();
                let h_restore = std::thread::Builder::new()
                    .name("supermachine-pool-restore".into())
                    .spawn(move || restorer_loop(h))
                    .map_err(|e| Error::vm_msg(format!("spawn restorer thread: {e}")))?;
                handles.push(h_restore);
            }
        }
        // Janitor: only spawn if eviction is enabled. Saves a
        // sleeping thread for fixed-size pools.
        if pool.policy.idle_timeout != Duration::MAX {
            let h = wait_handles.clone();
            let h_janitor = std::thread::Builder::new()
                .name("supermachine-pool-janitor".into())
                .spawn(move || janitor_loop(h))
                .map_err(|e| Error::vm_msg(format!("spawn janitor thread: {e}")))?;
            handles.push(h_janitor);
        }
        // Detach the handles. Threads exit on their own when
        // `shutting_down` is set in `HiddenPool::drop`; we don't
        // need to join them — they hold no `Arc<HiddenPool>` and
        // don't block the user-side drop from firing.
        drop(handles);
        Ok(pool)
    }

    /// Initialise (once) the per-Image default pool used by
    /// `image.acquire()`. Lazy: subsequent calls return the
    /// same pool regardless of `config`. Distinct from
    /// `pool().build()` — that always returns a fresh pool.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    fn ensure_default_pool(&self, config: &VmConfig) -> Result<&Arc<HiddenPool>, Error> {
        if let Some(p) = self.hidden_pool.get() {
            return Ok(p);
        }
        let pool = self.build_pool_arc(config, PoolPolicy::default())?;
        // Race-friendly: if another thread set this in parallel,
        // the new pool is dropped (and its housekeeping threads
        // exit on the dropped Arc's strong-count → 0). The
        // observable result is "either pool wins, all subsequent
        // calls see the same one".
        let _ = self.hidden_pool.set(pool);
        Ok(self
            .hidden_pool
            .get()
            .expect("hidden pool just initialized"))
    }
}

/// Bundle of clonable handles into the pool's wait state. Each
/// housekeeping thread owns one of these; no one holds a strong
/// `Arc<HiddenPool>` across a condvar wait, so user-side `Pool`
/// + `Image` drops fire immediately and the threads exit on
/// `shutting_down` after their next short timed wait.
#[derive(Clone)]
struct PoolWaitHandles {
    state: Arc<Mutex<PoolState>>,
    available: Arc<Condvar>,
    dirty: Option<Arc<Mutex<VecDeque<Worker>>>>,
    dirty_pending: Option<Arc<Condvar>>,
    shutting_down: Arc<AtomicBool>,
    spawn_cfg: Arc<SpawnConfig>,
    policy: PoolPolicy,
}

impl HiddenPool {
    fn wait_handles(&self) -> PoolWaitHandles {
        PoolWaitHandles {
            state: Arc::clone(&self.state),
            available: Arc::clone(&self.available),
            dirty: self.dirty.as_ref().map(Arc::clone),
            dirty_pending: self.dirty_pending.as_ref().map(Arc::clone),
            shutting_down: Arc::clone(&self.shutting_down),
            spawn_cfg: Arc::clone(&self.spawn_cfg),
            policy: self.policy,
        }
    }
}

/// Restorer thread: drain the dirty queue, send RESTORE on each
/// worker's supervisor control socket to reset guest state to
/// the snapshot, then push back to idle. This is the steady-
/// state recycle path — replaces the old "kill + respawn fresh"
/// loop. Saves ~10 ms per cycle (no fork+exec+dyld+restore;
/// just an in-place restore).
fn restorer_loop(h: PoolWaitHandles) {
    let (Some(dirty), Some(pending)) = (h.dirty.as_ref(), h.dirty_pending.as_ref()) else {
        return;
    };
    loop {
        if h.shutting_down.load(Ordering::SeqCst) {
            return;
        }
        // Wait for a dirty worker (or shutdown). Bounded wait so
        // we re-check shutting_down even if no notify_all arrives.
        let mut worker = {
            let mut q = match dirty.lock() {
                Ok(q) => q,
                Err(_) => return,
            };
            loop {
                if h.shutting_down.load(Ordering::SeqCst) {
                    return;
                }
                if let Some(w) = q.pop_front() {
                    break w;
                }
                q = match pending.wait_timeout(q, Duration::from_millis(100)) {
                    Ok((g, _)) => g,
                    Err(_) => return,
                };
            }
        };

        let snap_path = h.spawn_cfg.snapshot_path.clone();
        match worker.send_restore(&snap_path) {
            Ok(()) => {
                if let Ok(mut s) = h.state.lock() {
                    s.idle.push(IdleEntry {
                        worker,
                        last_used: Instant::now(),
                    });
                    h.available.notify_all();
                }
            }
            Err(_) => {
                // Restore protocol broke — kill this worker so the
                // replenisher spawns a fresh replacement.
                worker.shutdown();
                if let Ok(mut s) = h.state.lock() {
                    s.alive = s.alive.saturating_sub(1);
                }
                h.available.notify_all();
            }
        }
    }
}

/// Replenisher: maintains `alive >= min`. Spawns workers when
/// below the floor (worker died, was evicted into a regrow gap,
/// etc.). Sleeps on `available` otherwise.
fn replenisher_loop(h: PoolWaitHandles) {
    loop {
        if h.shutting_down.load(Ordering::SeqCst) {
            return;
        }
        let need_more = {
            let s = match h.state.lock() {
                Ok(s) => s,
                Err(_) => return,
            };
            s.alive < h.policy.min
        };
        if !need_more {
            let s = match h.state.lock() {
                Ok(s) => s,
                Err(_) => return,
            };
            // Bounded wait — see PoolWaitHandles docs for why.
            let _ = h.available.wait_timeout(s, Duration::from_millis(100));
            continue;
        }
        // Reserve a slot before spawning so a concurrent
        // acquire's auto-grow doesn't double-spawn.
        if let Ok(mut s) = h.state.lock() {
            if s.alive >= h.policy.min {
                continue;
            }
            s.alive += 1;
        }
        match h.spawn_cfg.spawn_one() {
            Ok(w) => {
                if let Ok(mut s) = h.state.lock() {
                    s.idle.push(IdleEntry {
                        worker: w,
                        last_used: Instant::now(),
                    });
                    h.available.notify_all();
                }
            }
            Err(_) => {
                if let Ok(mut s) = h.state.lock() {
                    s.alive = s.alive.saturating_sub(1);
                }
                std::thread::sleep(Duration::from_millis(500));
            }
        }
    }
}

/// Janitor: every `idle_timeout / 4`, walk the idle queue and
/// shut down workers above `min` that have been idle longer than
/// `idle_timeout`. Frees host RAM during quiet periods.
fn janitor_loop(h: PoolWaitHandles) {
    let timeout = h.policy.idle_timeout;
    let min = h.policy.min;
    if timeout == Duration::MAX {
        return;
    }
    // Cap each individual wait so we re-check shutting_down often
    // enough to drop within ~100 ms of pool teardown.
    let tick = (timeout / 4).max(Duration::from_millis(100));
    let wait_unit = Duration::from_millis(100).min(tick);
    loop {
        if h.shutting_down.load(Ordering::SeqCst) {
            return;
        }
        // Collect workers to evict under the lock; shut down
        // outside it so we don't hold up acquires.
        let mut to_evict: Vec<Worker> = Vec::new();
        let mut reaped_now = 0u64;
        if let Ok(mut s) = h.state.lock() {
            let now = Instant::now();
            // Watchdog reap: a worker that died WHILE IDLE (and was never
            // popped) would otherwise sit as a phantom `alive` count
            // forever — acquire's `try_wait` only catches it on pop, and
            // a low-traffic pool may never pop it. Sweep the whole idle
            // queue for dead processes and reap them, INDEPENDENT of `min`
            // (a dead worker is never useful; the replenisher backfills).
            // `try_wait` is a cheap non-blocking waitpid; the actual
            // teardown runs outside the lock below.
            let mut i = 0;
            while i < s.idle.len() {
                let dead = match s.idle[i].worker.child.try_wait() {
                    Ok(Some(_)) => true, // process exited
                    Ok(None) => false,   // alive
                    Err(_) => true,      // can't stat → treat as dead
                };
                if dead {
                    let entry = s.idle.remove(i);
                    s.alive = s.alive.saturating_sub(1);
                    s.reaped += 1;
                    reaped_now += 1;
                    to_evict.push(entry.worker);
                } else {
                    i += 1;
                }
            }
            // Walk from the front (oldest entries) and evict
            // while we're above min and the entry is stale. Stop
            // as soon as we hit a fresh one — LIFO ordering means
            // older entries pile up at the front.
            while s.alive > min && !s.idle.is_empty() {
                let oldest = &s.idle[0];
                if now.duration_since(oldest.last_used) < timeout {
                    break;
                }
                let entry = s.idle.remove(0);
                s.alive -= 1;
                to_evict.push(entry.worker);
            }
        }
        if reaped_now > 0 {
            // A freed slot means the replenisher should backfill toward
            // `min`; wake it (and any blocked acquirer) so recovery is
            // prompt rather than waiting for the next tick.
            h.available.notify_all();
            eprintln!(
                "[pool-watchdog] reaped {reaped_now} dead idle worker(s) — pool will respawn \
                 toward min"
            );
        }
        for mut w in to_evict {
            w.shutdown();
        }
        // Sleep up to `tick` total in `wait_unit` slices so we
        // observe shutting_down promptly.
        let mut remaining = tick;
        while remaining > Duration::ZERO && !h.shutting_down.load(Ordering::SeqCst) {
            let chunk = remaining.min(wait_unit);
            if let Ok(s) = h.state.lock() {
                let _ = h.available.wait_timeout(s, chunk);
            }
            remaining = remaining.saturating_sub(chunk);
        }
    }
}

/// A [`Vm`] checked out of an [`Image`]'s hidden pool. `Deref`s
/// to `Vm`, so every method on `Vm` is callable. On `Drop` the
/// VM returns to the pool — the next [`Image::acquire`] gets
/// a freshly snapshot-restored worker in ~5 ms.
///
/// Bound to the `Image`'s lifetime so the pool can't outlive
/// its owner. Acquires currently serialize on the pool's single
/// worker; concurrent acquires from one process block until the
/// previous PooledVm is dropped.
pub struct PooledVm<'a> {
    vm: Option<Vm>,
    /// Worker subprocess we checked out. On Drop, returned to
    /// the pool (which kills + replenishes). macOS/HVF only — the
    /// Linux/KVM pool runs VMs in-process (no worker subprocess).
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    worker: Option<Worker>,
    /// Keeps the pool alive for the lifetime of this PooledVm.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pool_arc: Arc<HiddenPool>,
    /// Linux/KVM: the pool to return to on Drop (discard the dirty VM + refill
    /// the idle queue). `None` for a poolless cold-start fallback.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pool: Option<Arc<KvmPoolInner>>,
    /// Linux no-virt sentry: the shared-exec-server pool whose admission slot
    /// this checkout holds; released on Drop. `None` for KVM/cold-start.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry_pool: Option<Arc<SentryPool>>,
    /// Linux/runsc: checkpoint-backed pool whose admission slot this restored
    /// container holds. Dropping the inner Vm deletes the runsc container.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    runsc_pool: Option<Arc<RunscPoolInner>>,
    /// Linux/kboxlike: native no-KVM process-tree pool. Dropping the inner Vm
    /// kills the restored tree, unless dirty warm reuse is explicitly enabled.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    kboxlike_pool: Option<Arc<KboxlikePoolInner>>,
    /// Linux no-virt sentry: per-checkout memory-admission reservation. Sentry
    /// cells are forked on demand, so charging live checkouts matches KVM's
    /// worker-lifetime accounting better than reserving the pool's whole `max`
    /// at build time.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry_mem_admission: Option<crate::memory_admission::AdmissionGuard>,
    /// WarmDaemon pool: the detached daemon pid forked at acquire
    /// (`Pool::acquire_running`). Released (SIGKILL+reap via `Pool::release`) in
    /// Drop BEFORE the admission slot frees. `None` for the ExecServer pooled path /
    /// KVM / cold-start.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry_running_pid: Option<i32>,
    /// WarmDaemon/live-C4 pool that owns `sentry_running_pid` and serves this
    /// checkout's exec/snapshot route. Shared for warm-zygote pools; per-checkout
    /// for live-tree C4 restores.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry_running_pool: Option<Arc<crate::sentry::Pool>>,
    /// True for per-checkout live-tree C4 supervisors; stop them on Drop after
    /// releasing the restored root pid.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry_stop_running_pool_on_drop: bool,
    /// Linux no-virt sentry: set the first time this checkout is dereferenced
    /// into a `Vm`. Untouched checkouts have not run guest code or host-side file
    /// APIs, so `restore_on_release(true)` can skip the rootfs reset fast path.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry_touched: AtomicBool,
    _image: std::marker::PhantomData<&'a Image>,
}

impl std::ops::Deref for PooledVm<'_> {
    type Target = Vm;
    fn deref(&self) -> &Vm {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if self.sentry_pool.is_some() {
            self.sentry_touched.store(true, Ordering::Relaxed);
        }
        // Invariant: vm is `Some` until Drop runs.
        self.vm.as_ref().expect("PooledVm used after drop")
    }
}

impl std::ops::DerefMut for PooledVm<'_> {
    fn deref_mut(&mut self) -> &mut Vm {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if self.sentry_pool.is_some() {
            self.sentry_touched.store(true, Ordering::Relaxed);
        }
        self.vm.as_mut().expect("PooledVm used after drop")
    }
}

impl PooledVm<'_> {
    /// Capture a live snapshot of this pooled VM, non-consuming, so the builder
    /// can snapshot one checked-out machine repeatedly across layers. KVM
    /// delegates to [`Vm::snapshot_live`].
    ///
    /// **Sentry backend:** a pooled sentry `Vm` shares the pool supervisor
    /// (`running` and `sentry_src_rootfs` are both `None`), so it cannot use the
    /// KVM `RunningVm` path. It copies the served rootfs into `dest_dir/rootfs`,
    /// then asks the supervisor to capture any owned live process tree, memory,
    /// loopback, fd, cwd, and proc metadata into `restore.snap`/`mem.blob`.
    /// Capture at a quiescent point, matching the KVM/HVF live-snapshot contract.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn snapshot(&self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
        // Sentry-backed pooled VM: copy the served rootfs and append supervisor
        // C4 state. The pool keeps serving — non-consuming.
        if let Some(sp) = &self.sentry_pool {
            let dest_dir = dest_dir.into();
            let image = write_sentry_snapshot_dir_inner(
                &sp.rootfs,
                &dest_dir,
                "Vm::snapshot_live",
                true,
                Some(sp.mem_mib as u32),
                Some(sp.vcpus),
            )?;
            if let Some(pool) = &self.sentry_running_pool {
                pool.snapshot(&dest_dir)
                    .map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}")))?;
            } else {
                sp.capture_live_state_into(&dest_dir)?;
            }
            return Ok(image);
        }
        if self.runsc_pool.is_some() {
            let dest_dir = dest_dir.into();
            let vm = self
                .vm
                .as_ref()
                .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
            let runsc = vm.runsc.as_ref().ok_or_else(|| {
                Error::vm_msg("PooledVm::snapshot: runsc pool checkout has no runtime".to_owned())
            })?;
            return write_runsc_snapshot_dir(runsc, &dest_dir, "Vm::snapshot_live", true);
        }
        if self
            .kboxlike_pool
            .as_ref()
            .map(|pool| pool.vm_config.for_build)
            .unwrap_or(false)
        {
            let dest_dir = dest_dir.into();
            let vm = self
                .vm
                .as_ref()
                .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
            let kboxlike = vm.kboxlike.as_ref().ok_or_else(|| {
                Error::vm_msg(
                    "PooledVm::snapshot: kboxlike pool checkout has no runtime".to_owned(),
                )
            })?;
            return write_kboxlike_snapshot_dir(kboxlike, &dest_dir, "Vm::snapshot_live");
        }
        self.vm
            .as_ref()
            .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?
            .snapshot_live(dest_dir)
    }

    /// Live differential snapshot against `base_path` (a full snapshot's
    /// `restore.snap`) — non-consuming. Delegates to [`Vm::snapshot_diff_live`].
    ///
    /// **Sentry backend:** C4 state is not page-diffed yet, so this degrades to a
    /// full rootfs plus full owned-state capture, ignoring `base_path`. The
    /// returned layer dir is a complete, self-contained, loadable sentry image
    /// rather than a space-saving delta.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn snapshot_diff(
        &self,
        dest_dir: impl Into<PathBuf>,
        base_path: &Path,
    ) -> Result<Image, Error> {
        if let Some(sp) = &self.sentry_pool {
            // No page-level diff for sentry C4 yet: full rootfs + state capture.
            let _ = base_path;
            let dest_dir = dest_dir.into();
            let image = write_sentry_snapshot_dir_inner(
                &sp.rootfs,
                &dest_dir,
                "Vm::snapshot_live",
                true,
                Some(sp.mem_mib as u32),
                Some(sp.vcpus),
            )?;
            if let Some(pool) = &self.sentry_running_pool {
                pool.snapshot(&dest_dir)
                    .map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}")))?;
            } else {
                sp.capture_live_state_into(&dest_dir)?;
            }
            return Ok(image);
        }
        if self.runsc_pool.is_some() {
            let _ = base_path;
            let dest_dir = dest_dir.into();
            let vm = self
                .vm
                .as_ref()
                .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
            let runsc = vm.runsc.as_ref().ok_or_else(|| {
                Error::vm_msg(
                    "PooledVm::snapshot_diff: runsc pool checkout has no runtime".to_owned(),
                )
            })?;
            return write_runsc_snapshot_dir(runsc, &dest_dir, "Vm::snapshot_live", true);
        }
        if self
            .kboxlike_pool
            .as_ref()
            .map(|pool| pool.vm_config.for_build)
            .unwrap_or(false)
        {
            let _ = base_path;
            let dest_dir = dest_dir.into();
            let vm = self
                .vm
                .as_ref()
                .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
            let kboxlike = vm.kboxlike.as_ref().ok_or_else(|| {
                Error::vm_msg(
                    "PooledVm::snapshot_diff: kboxlike pool checkout has no runtime".to_owned(),
                )
            })?;
            return write_kboxlike_snapshot_dir(kboxlike, &dest_dir, "Vm::snapshot_live");
        }
        self.vm
            .as_ref()
            .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?
            .snapshot_diff_live(dest_dir, base_path)
    }

    /// Capture a snapshot of this VM's current state and return
    /// a new [`Image`] pointing at it. Equivalent to
    /// [`Vm::snapshot`] but works on the subprocess-pool path
    /// — the entitled worker subprocess does the HVF capture
    /// over the supervisor RPC, so the calling binary doesn't
    /// need the `com.apple.security.hypervisor` entitlement.
    ///
    /// Borrows `self` rather than consuming, so you can
    /// continue using the VM after capturing — handy for the
    /// "warm up + snapshot + keep working" pattern. The guest
    /// is paused for the duration of the capture (typically
    /// 10s of ms; bounded by disk write time for large RAM).
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn snapshot(&self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
        self.snapshot_with_opt_base(dest_dir, None)
    }

    /// Differential snapshot against `base_path` (another snapshot's
    /// `restore.snap`). The worker clonefiles the base + writes only the pages
    /// that changed, and (because the base is kept in memory) a *chain* of
    /// these — the in-VM builder's per-layer snapshots — each diffs against the
    /// previous in memory. `inline` so the file is on disk when this returns.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn snapshot_diff(
        &self,
        dest_dir: impl Into<PathBuf>,
        base_path: &Path,
    ) -> Result<Image, Error> {
        self.snapshot_with_opt_base(dest_dir, Some(base_path))
    }

    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    fn snapshot_with_opt_base(
        &self,
        dest_dir: impl Into<PathBuf>,
        base_path: Option<&Path>,
    ) -> Result<Image, Error> {
        let dest_dir = dest_dir.into();
        let _span = tracing::info_span!(
            "supermachine.snapshot",
            dest_dir = %dest_dir.display(),
        )
        .entered();
        let worker = self
            .worker
            .as_ref()
            .ok_or_else(|| Error::vm_msg("PooledVm: no worker (already dropped?)".to_owned()))?;
        let vm = self
            .vm
            .as_ref()
            .ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
        let meta = vm.image_meta.clone().ok_or_else(|| {
            Error::vm_msg(
                "PooledVm::snapshot: image metadata missing (acquire from a 0.3.8+ Image)"
                    .to_owned(),
            )
        })?;
        std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
        let snap_path = dest_dir.join("restore.snap");
        // Flush guest dirty pages to host-persistent backing files
        // (virtio-blk volumes, writeback-cached virtio-fs) so the
        // capture doesn't leave them torn on the host — see
        // `guest_sync_via_agent`. Before park: parked secondaries
        // can't run writeback kworkers.
        let _ = guest_sync_via_agent(&worker.vsock_exec_path);
        // Multi-vCPU: park secondaries via smpark.ko before the
        // capture rendezvous. The agent ioctls /dev/smpark, which
        // broadcasts an IPI to all secondaries; each one drains
        // local LRs, masks IRQs, and spins in WFI. The captured
        // per-vCPU state is then byte-identical-trivial across
        // secondaries — HVF can round-trip THAT, fixing the
        // "restored guest RCU-stalls / NULL-derefs in interrupt
        // context" failure class. Best-effort: if smpark.ko isn't
        // loaded (older snapshots, single-vCPU bake), the agent
        // returns ok=false and we fall through to the existing
        // rendezvous-only capture path.
        let parked = if meta.vcpus > 1 {
            worker.send_smpark_park()?
        } else {
            false
        };
        // The diff-via-clone path with `last_restore_path` as
        // base would in principle win 200+ ms here, but the
        // runner needs base in memory and lazy-loading from
        // disk costs ~700 ms on first call. Without warm-cache
        // hits across pool cycles, that's net-negative for
        // single-shot snapshots. We use the plain streaming
        // save (parallel sparse pwrite, ~290 ms on 2 GiB).
        //
        // SUPERMACHINE_DIFF_CYCLE_SNAPSHOT was a pre-0.7.38
        // opt-in to the diff path; no production caller used it
        // and the diff path requires base-in-memory which the
        // pool's cycle-snapshot flow doesn't guarantee. Deleted.
        let snap_result = match base_path {
            Some(base) => worker.send_snapshot_with_base(&snap_path, base, true),
            None => worker.send_snapshot(&snap_path),
        };
        // Always unpark, even on snapshot failure — otherwise
        // secondaries stay stuck in WFI and the next acquire of
        // this VM is a brick. Best-effort: if park failed in the
        // first place, unpark is also a no-op.
        if parked {
            let _ = worker.send_smpark_unpark()?;
        }
        let _stats = snap_result?;
        let metadata = serde_json::json!({
            "backend": "hvf",
            "memory_mib": meta.memory_mib,
            "vcpus": meta.vcpus,
            "layers": meta
                .layers
                .iter()
                .map(|p| p.to_string_lossy().to_string())
                .collect::<Vec<_>>(),
            "delta_squashfs": meta
                .delta_squashfs
                .as_ref()
                .map(|p| p.to_string_lossy().to_string()),
            "snapshot_base": snap_path.to_string_lossy().to_string(),
            "runtime_sha16": meta.baker_runtime_sha16,
            "tsi_token": meta.tsi_token,
            "egress_policy": meta.egress_policy,
            "pre_exec_sync": meta.pre_exec_sync,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
            "baked_at": chrono_rfc3339_now(),
            "source": "PooledVm::snapshot",
        });
        std::fs::write(
            dest_dir.join("metadata.json"),
            serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
        )
        .map_err(Error::Io)?;
        Image::from_snapshot(&dest_dir)
    }
}

impl Drop for PooledVm<'_> {
    fn drop(&mut self) {
        // macOS: the inner Vm's Drop is a no-op (skip_cleanup=true; the worker
        // owns the sockets). Hand the worker back to the pool, which kills it
        // (snapshot state may be dirty) and replenishes to target N.
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        {
            let _ = self.vm.take();
            if let Some(worker) = self.worker.take() {
                self.pool_arc.release(worker);
            }
        }
        // Linux no-virt sentry: drop the cheap Vm (skip_cleanup → no socket
        // unlink, no server teardown — the SentryPool owns the shared server)
        // and release the admission slot so a blocked acquirer can proceed.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(sp) = self.sentry_pool.take() {
            let touched = self.sentry_touched.load(Ordering::Relaxed);
            let had_running_checkout = self.sentry_running_pid.is_some();
            // WarmDaemon: SIGKILL+reap the detached daemon (frees its ring slot + host
            // fds) BEFORE freeing the admission slot — so a blocked acquirer doesn't
            // fork into a still-occupied ring. Best-effort (Drop can't ?-propagate);
            // Pool::release is idempotent for an unknown pid.
            if let (Some(pid), Some(pool)) = (
                self.sentry_running_pid.take(),
                self.sentry_running_pool.take(),
            ) {
                let _ = pool.release(pid);
                let _ = self.vm.take();
                if self.sentry_stop_running_pool_on_drop {
                    pool.stop();
                }
            } else {
                let _ = self.vm.take();
            }
            let shutting_down = sp.shut_down.load(std::sync::atomic::Ordering::SeqCst);
            if !shutting_down && (touched || had_running_checkout) {
                if let Err(e) = sp.restore_rootfs_from_baseline() {
                    eprintln!(
                        "supermachine: sentry rootfs restore_on_release reset failed ({e}); \
                         next acquire may observe dirty filesystem state"
                    );
                }
            }
            let _ = self.sentry_mem_admission.take();
            sp.admission.release();
            sp.cleanup_if_shutdown_idle();
            return;
        }
        // Linux/runsc: drop the restored container and release its admission slot.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(pool) = self.runsc_pool.take() {
            if let Some(vm) = self.vm.take() {
                pool.release_vm(vm);
            } else {
                pool.release_checkout();
                pool.admission.release();
            }
            return;
        }
        // Linux/kboxlike: drop or dirty-reuse the restored process tree and
        // release its admission slot.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(pool) = self.kboxlike_pool.take() {
            if let Some(vm) = self.vm.take() {
                pool.release_vm(vm);
            } else {
                pool.admission.release();
            }
            return;
        }
        // Linux/KVM.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(pool) = self.pool.take() {
            let Some(vm) = self.vm.take() else {
                return;
            };
            // Keep the VM warm (return ALIVE to idle for reuse) only up to the
            // `min` always-warm floor; beyond min, tear it down + free its slot.
            // This bounds idle at min when load subsides (so `min=0` drains to
            // zero — no leak, no janitor needed) while a `min=N` pool keeps N
            // VMs hot for full warm reuse. Concurrent releases may transiently
            // settle idle a little above min, still bounded by `max` — benign,
            // and those extras are warm-reusable.
            let warm_slot = {
                let idle = pool.idle.lock().unwrap_or_else(|e| e.into_inner());
                idle.len() < pool.min
            };
            if !warm_slot {
                // Above the warm floor: stop the VM (inner Vm Drop) + free slot.
                drop(vm);
                pool.admission.release();
                return;
            }
            // Within the warm floor: return the VM ALIVE to idle for reuse,
            // keeping its admission slot (it IS the replenishment, so no refill).
            //   - restore_on_release=true (default): in-place reset to the
            //     snapshot baseline FIRST (RAM + CPU + intc/clock + serial +
            //     device state) for clean per-cycle isolation — far cheaper than
            //     teardown + fresh restore since the KVM VM/vCPUs/threads/RAM all
            //     persist. On reset failure, tear down (never reuse a half-reset
            //     VM). Mirrors HVF's restore_on_release(true) in-place RESTORE.
            //   - restore_on_release=false (opt-in): reuse as-is — fastest, but
            //     the guest carries state across cycles (trusted/idempotent only).
            if pool.restore_on_release {
                if let Err(e) = vm.reset_to_snapshot() {
                    eprintln!(
                        "supermachine: KVM in-place reset failed ({e}); \
                         tearing down this VM instead of reusing it"
                    );
                    drop(vm);
                    pool.admission.release();
                    // We lost a within-floor warm VM; background-restore a fresh
                    // one to keep the idle set at `min`.
                    pool.refill_async();
                    return;
                }
                // The reset rewound guest RAM — including the kernel
                // timekeeper — to the warm BASELINE, so the guest wall
                // clock is now however far in the past the baseline
                // capture is. Re-anchor it exactly like the restore
                // path does (the 7a contract: monotonic survives,
                // REALTIME tracks the host); without this, a pool
                // serving traffic hours after its bake hands out VMs
                // whose TLS/signature validation lives in the past.
                // Caught by wall_clock_tracks_host_across_pool_reset_
                // cycles (drift == idle time between cycles).
                let _ = sync_time_via_agent(&vm.vsock_exec_path);
            }
            pool.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
            pool.admission.notify(); // wake an acquirer blocked at the cap
        } else {
            // Poolless cold-start fallback: just stop the VM.
            let _ = self.vm.take();
        }
    }
}

/// Configurable bake of an OCI image. Built via [`Image::builder`];
/// terminate with [`OciImageBuilder::build`] to produce an
/// [`Image`].
/// Builder for an explicitly configured worker pool. Started
/// with [`Image::pool`]; terminated with [`PoolBuilder::build`].
///
/// Defaults: `min=0`, `max=64`, `idle_timeout=60s`,
/// `acquire_timeout=60s` — i.e. lazy spawn, auto-evict, fail-
/// noisily on saturation. Override any of these with the
/// chainable setters below.
pub struct PoolBuilder<'a> {
    image: &'a Image,
    policy: PoolPolicy,
    /// Runtime overrides applied at pool-build time. Memory / vCPU
    /// changes here don't re-bake the snapshot — they override what
    /// the worker subprocess advertises to HVF at restore. Lazily
    /// committed (CoW page-fault), so a larger memory_mib than
    /// what was baked just raises the guest-visible ceiling without
    /// committing host pages.
    vm_config: VmConfig,
}

impl PoolBuilder<'_> {
    /// Always-warm baseline. The pool keeps at least this many
    /// workers alive — even if everyone drops their `PooledVm`,
    /// the next `acquire` finds these waiting in idle. Default 0
    /// (lazy-spawn on first acquire).
    pub fn min(mut self, n: usize) -> Self {
        self.policy.min = n;
        self
    }

    /// Hard concurrency cap. `acquire` blocks (with timeout) when
    /// `max` peers are checked out simultaneously. Default 64;
    /// raise for large multi-tenant fleets. Pass `usize::MAX` for
    /// effectively unbounded.
    pub fn max(mut self, n: usize) -> Self {
        self.policy.max = n.max(1);
        self
    }

    /// Idle workers above `min` that have been unused for longer
    /// than this get killed by the janitor — frees host RAM
    /// during quiet periods. Default 60 s. Pass `Duration::MAX`
    /// to disable eviction (fixed-size pool with no churn).
    pub fn idle_timeout(mut self, d: Duration) -> Self {
        self.policy.idle_timeout = d;
        self
    }

    /// Caller's `acquire` blocks at most this long when the pool
    /// is at `max` and no worker is idle. After that the call
    /// returns [`Error::PoolExhausted`]. Default 60 s. Pass `None`
    /// (via this method's twin if added) to block forever; pass
    /// `Duration::ZERO` to fail-fast.
    pub fn acquire_timeout(mut self, d: Duration) -> Self {
        self.policy.acquire_timeout = Some(d);
        self
    }

    /// Disable the acquire timeout — `acquire` blocks forever
    /// when at `max`. Useful for batch workloads where you'd
    /// rather wait than fail. Equivalent to passing `None`
    /// internally.
    pub fn no_acquire_timeout(mut self) -> Self {
        self.policy.acquire_timeout = None;
        self
    }

    /// Skip the per-cycle snapshot RESTORE on `PooledVm::drop`.
    /// Workers go straight back to the idle queue carrying
    /// whatever guest state the previous user left.
    ///
    /// Default `true`: every acquire starts from a clean
    /// snapshot-state guest. Pay ~3 ms restore per cycle (off
    /// the user's critical path with a buddy slot).
    ///
    /// `false`: opt into "warm worker reuse" — guest page cache
    /// stays warm across cycles. For workloads that re-read the
    /// same files every invocation (rustc + sysroot, python +
    /// stdlib, node + node_modules), this is a 3-6× speedup
    /// because the second compile/import doesn't re-fault
    /// pages from the squashfs layer.
    ///
    /// Safe when the workload always overwrites its own outputs
    /// (e.g. `rustc -o /tmp/m && /tmp/m` overwrites both source
    /// and binary on every cycle). Unsafe if the workload trusts
    /// `/tmp` to be clean or accumulates files unboundedly —
    /// the guest's RAM+fs accumulates state forever in this mode,
    /// so pair with periodic pool rebuild for long runs.
    pub fn restore_on_release(mut self, on: bool) -> Self {
        self.policy.restore_on_release = on;
        self
    }

    /// Mark this pool as backing a Dockerfile BUILD stage.
    /// `on = true` makes no-KVM rootfs backends serve the stage's `RUN` steps from
    /// a PER-BUILD PRIVATE clone of the warm rootfs — so each build/stage mutates
    /// its own throwaway filesystem (per-build/per-stage isolation + failed-RUN
    /// rollback + no cross-stage collision, all discarded on pool Drop) — and
    /// suppresses the base image's baked-workload auto-start (a build base must
    /// not run its `CMD` while `RUN` steps execute). Sentry uses its shared exec
    /// server; kboxlike uses its rootfs/chroot exec path. An explicit flag (NOT
    /// `restore_on_release(false)`, which a non-build caller legitimately sets);
    /// the KVM/HVF backends ignore it (they isolate per-layer via a tmpfs
    /// overlay-upper). Off by default.
    pub fn for_build(mut self, on: bool) -> Self {
        self.vm_config = std::mem::take(&mut self.vm_config).with_for_build(on);
        self
    }

    /// Per-acquire restore timeout. Forwarded to the inner
    /// [`VmConfig::with_restore_timeout`] used to spawn pool
    /// workers. Default 30 s (set at spawn time inside the pool).
    /// Bump for slow disks or large RAM snapshots; restore time
    /// scales roughly linearly with snapshot size.
    pub fn with_restore_timeout(mut self, timeout: Duration) -> Self {
        self.vm_config = std::mem::take(&mut self.vm_config).with_restore_timeout(timeout);
        self
    }

    /// Override the baked image's memory ceiling for this pool's
    /// workers. Pure runtime override — doesn't re-bake the
    /// snapshot. Pages are lazy-committed (CoW page-fault), so
    /// raising the ceiling beyond what was baked doesn't increase
    /// host commit unless the guest actually writes to the new
    /// pages.
    pub fn with_memory_mib(mut self, mib: u32) -> Self {
        self.vm_config = std::mem::take(&mut self.vm_config).with_memory_mib(mib);
        self
    }

    /// Bound how long any single `exec` in this pool may run before the supervisor
    /// SIGKILLs it and reports `SentryError::Timeout` (exit `137`) — sentry backend
    /// only. Forwarded to [`VmConfig::with_sentry_exec_timeout`]. UNSET = the
    /// generous default ceiling (10 min) so a wedged exec can't hang the pool
    /// forever; `Duration::ZERO` runs UNLIMITED.
    pub fn with_sentry_exec_timeout(mut self, dur: Duration) -> Self {
        self.vm_config = std::mem::take(&mut self.vm_config).with_sentry_exec_timeout(dur);
        self
    }

    /// Override where this pool places host-side communication sockets and, for
    /// the sentry backend, the private runtime/baseline rootfs copies owned by
    /// the pool. Mainly useful for lifecycle tests and embedders that want all
    /// transient pool state under a known parent.
    pub fn with_vsock_mux_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.vm_config = std::mem::take(&mut self.vm_config).with_vsock_mux_dir(dir);
        self
    }

    /// Override the baked image's vCPU count for this pool's
    /// workers. Pure runtime override.
    pub fn with_vcpus(mut self, vcpus: u32) -> Self {
        self.vm_config = std::mem::take(&mut self.vm_config).with_vcpus(vcpus);
        self
    }

    /// Build the pool against this image's snapshot. Spawns
    /// the initial `min` workers in parallel, starts the
    /// housekeeping threads (replenisher / restorer / janitor),
    /// returns a [`Pool`] handle.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn build(self) -> Result<Pool, Error> {
        let image = self.image;
        // Normalize: min must not exceed max — otherwise the
        // pre-spawn loop in build_hidden_pool would create more
        // workers than the auto-grow ceiling allows, and the
        // `alive ≤ max` invariant breaks.
        let mut policy = self.policy;
        if policy.min > policy.max {
            policy.min = policy.max;
        }
        let arc = image.build_pool_arc(&self.vm_config, policy)?;
        // Try to install this pool as the per-Image default so
        // subsequent `image.acquire()` / `image.acquire_with()`
        // calls go through the same configured pool. If the
        // OnceLock is already set (the user called acquire()
        // before pool().build()), we error rather than silently
        // returning a config-less default — that's the bug the
        // 0.4.1 docs accidentally created.
        match image.hidden_pool.set(Arc::clone(&arc)) {
            Ok(()) => Ok(Pool { inner: arc }),
            Err(_) => Err(Error::vm_msg(
                "this Image already has a Pool — pool() can only be called \
                 once per Image handle.\n\n\
                 To run two independent pools off the same snapshot, load the \
                 Image twice via Image.fromSnapshot:\n\n  \
                 const a = await Image.fromSnapshot(snapshotPath);\n  \
                 const b = await Image.fromSnapshot(snapshotPath);\n  \
                 const poolA = await a.pool(...);\n  \
                 const poolB = await b.pool(...);\n\n\
                 If you called image.acquire() before image.pool(), the \
                 Image has been bound to a default pool whose policy can't \
                 be changed in place — either reorder the calls or use \
                 fromSnapshot() to get a fresh handle."
                    .to_owned(),
            )),
        }
    }

    /// Build the pool (Linux/KVM). Warms the source image into a snapshot once
    /// (boot → agent-ready → capture), so every [`Pool::acquire`] is a CoW
    /// snapshot restore (~ms) instead of a cold boot (~1 s). If the image is
    /// already snapshot-backed it's used as-is.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn build(self) -> Result<Pool, Error> {
        // Parity with macOS/KVM, checked for BOTH backends: if the image is
        // already bound to a default pool (the user called image.acquire() before
        // pool().build() — both the sentry and KVM acquire paths latch the
        // `kvm_pool` OnceLock), error rather than silently returning a second,
        // unrelated pool whose policy the default-acquire path ignores.
        if self.image.kvm_pool.get().is_some() {
            return Err(Error::vm_msg(
                "this Image already has a default Pool — image.acquire() was \
                 called before pool().build(), binding a default pool whose \
                 policy can't be changed in place. Reorder so pool().build() \
                 runs first, or load a fresh handle via Image::from_snapshot."
                    .to_owned(),
            ));
        }
        // No-virt sentry image: build a pool over one shared, always-warm exec
        // server. `max` bounds concurrent checkouts; `min` is a no-op (the
        // server IS the warm set — there are no idle instances to pre-restore).
        if self.image.sentry.is_some() {
            let sp = SentryPool::build(
                self.image,
                &self.vm_config,
                self.policy.max,
                self.policy.acquire_timeout,
                self.policy.restore_on_release,
            )?;
            return Ok(Pool {
                kvm: None,
                sentry: Some(sp),
                runsc: None,
                kboxlike: None,
            });
        }
        if self.image.runsc.is_some() {
            let min = self.policy.min.min(self.policy.max.max(1));
            let rp = RunscPoolInner::build(
                self.image,
                &self.vm_config,
                min,
                self.policy.max,
                self.policy.acquire_timeout,
                self.policy.restore_on_release,
            )?;
            return Ok(Pool {
                kvm: None,
                sentry: None,
                runsc: Some(rp),
                kboxlike: None,
            });
        }
        if self.image.kboxlike.is_some() {
            let min = self.policy.min.min(self.policy.max.max(1));
            let kp = KboxlikePoolInner::build(
                self.image,
                &self.vm_config,
                min,
                self.policy.max,
                self.policy.acquire_timeout,
                self.policy.restore_on_release,
            )?;
            return Ok(Pool {
                kvm: None,
                sentry: None,
                runsc: None,
                kboxlike: Some(kp),
            });
        }
        let warm = warm_snapshot_for_pool(self.image, &self.vm_config)?;
        // min must not exceed max, or the pre-warm loop would over-spawn beyond
        // the admission cap.
        let min = self.policy.min.min(self.policy.max.max(1));
        let inner = Arc::new(KvmPoolInner {
            image: warm,
            vm_config: self.vm_config,
            min,
            idle: std::sync::Mutex::new(Vec::new()),
            admission: Admission::new(self.policy.max, self.policy.acquire_timeout),
            restore_on_release: self.policy.restore_on_release,
            refilling: std::sync::atomic::AtomicBool::new(false),
            shut_down: std::sync::atomic::AtomicBool::new(false),
        });
        // Eager pre-warm to `min` idle VMs SYNCHRONOUSLY, so the returned pool's
        // stats() immediately reflect the configured baseline (no-op when
        // min == 0 — lazy pools stay lazy).
        inner.prewarm_to_min();
        Ok(Pool {
            kvm: Some(inner),
            sentry: None,
            runsc: None,
            kboxlike: None,
        })
    }
}

/// Prepare KVM data volumes: for each [`VolumeSpec`], ensure the host backing
/// file exists (created sparse to `size_bytes` and formatted ext4 on first use,
/// reused otherwise so data persists), and return the per-volume attach specs
/// for [`crate::kvm::run::LinuxVmConfig`]. Formatting is done host-side with
/// the in-process `crate::ext4` synthesizer so the guest init only has to
/// mount — no host e2fsprogs needed.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_kvm_volumes(
    specs: &[crate::vmm::resources::VolumeSpec],
) -> Result<Vec<crate::kvm::run::VolumeAttach>, Error> {
    let mut out = Vec::with_capacity(specs.len());
    for spec in specs {
        let path = std::path::Path::new(&spec.host_path);
        let fresh = !path.exists();
        if fresh {
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent).map_err(Error::Io)?;
            }
            // Synthesize an empty, journal-less ext4 in-process (sparse
            // file; cleans up after itself on failure). No host mke2fs
            // needed — see `crate::ext4` for the format choices.
            crate::ext4::make_empty_ext4(path, spec.size_bytes)
                .map_err(|e| Error::vm_msg(format!("format volume {}: {e}", spec.host_path)))?;
        }
        out.push(crate::kvm::run::VolumeAttach {
            path: spec.host_path.clone(),
            size: spec.size_bytes,
            mount: spec.guest_path.clone(),
        });
    }
    Ok(out)
}

/// Join a guest-relative path under `rootfs`, rejecting any component that would
/// escape it (an absolute path or a `..`/root component). Used to stage
/// `with_extra_file` payloads into a sentry rootfs at bake time without a
/// traversal surface — the sentry analogue of the KVM bake's confined
/// delta-layer staging. Returns `None` on a hostile path.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn confined_rootfs_join(rootfs: &Path, rel: &str) -> Option<PathBuf> {
    let p = Path::new(rel);
    let escapes = p.is_absolute()
        || p.components().any(|c| {
            matches!(
                c,
                std::path::Component::ParentDir | std::path::Component::RootDir
            )
        });
    if escapes || rel.is_empty() {
        return None;
    }
    Some(rootfs.join(p))
}

/// Persist the OciImageBuilder's data volumes into a baked KVM snapshot's
/// `metadata.json` (the `volumes: [{host_file, guest_path, size_bytes}]` array
/// that [`Image::from_snapshot`] parses into `Image::volumes`). On every start,
/// `Vm::start` merges these baked volumes into `prepare_kvm_volumes` — parity
/// with the HVF backend's bake-time `with_volume`.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn persist_kvm_builder_volumes(
    snap_dir: &Path,
    volumes: &[(PathBuf, String, u64)],
) -> Result<(), Error> {
    let meta_path = snap_dir.join("metadata.json");
    let text = std::fs::read_to_string(&meta_path).map_err(Error::Io)?;
    let mut meta: serde_json::Value = serde_json::from_str(&text)
        .map_err(|e| Error::vm_msg(format!("parse {}: {e}", meta_path.display())))?;
    let arr: Vec<serde_json::Value> = volumes
        .iter()
        .map(|(host, guest, size)| {
            serde_json::json!({
                "host_file": host.to_string_lossy(),
                "guest_path": guest,
                "size_bytes": size,
            })
        })
        .collect();
    if let Some(obj) = meta.as_object_mut() {
        obj.insert("volumes".into(), serde_json::json!(arr));
    }
    std::fs::write(
        &meta_path,
        serde_json::to_string_pretty(&meta)
            .map_err(|e| Error::vm_msg(format!("serialize metadata: {e}")))?,
    )
    .map_err(Error::Io)?;
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kvm_bake_key_matches(snap_dir: &Path, expected: &str) -> bool {
    std::fs::read_to_string(snap_dir.join("metadata.json"))
        .ok()
        .and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
        .and_then(|meta| {
            meta.get("kvm_bake_key")
                .and_then(|v| v.as_str())
                .map(str::to_owned)
        })
        .as_deref()
        == Some(expected)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn builder_mount_specs(
    mounts: &[(
        PathBuf,
        String,
        crate::vmm::resources::SymlinkPolicy,
        String,
    )],
) -> Vec<crate::vmm::resources::MountSpec> {
    mounts
        .iter()
        .map(|(host, tag, policy, guest_path)| {
            crate::vmm::resources::MountSpec::new(
                host.to_string_lossy().to_string(),
                tag.clone(),
                guest_path.clone(),
            )
            .with_symlinks(policy.clone())
        })
        .collect()
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn effective_builder_workload(
    image_workload: Option<String>,
    oci_config: Option<&serde_json::Value>,
    cmd: Option<&Vec<String>>,
    envs: &[(String, String)],
) -> (
    Option<String>,
    Vec<(String, String)>,
    Option<String>,
    Option<String>,
) {
    let mut env_pairs: Vec<(String, String)> = Vec::new();
    if let Some(cfg) = oci_config {
        for kv in cfg
            .get("Env")
            .and_then(|v| v.as_array())
            .into_iter()
            .flatten()
            .filter_map(|v| v.as_str())
        {
            if let Some(eq) = kv.find('=') {
                env_pairs.push((kv[..eq].to_string(), kv[eq + 1..].to_string()));
            }
        }
    }
    for (k, v) in envs {
        env_pairs.retain(|(ek, _)| ek != k);
        env_pairs.push((k.clone(), v.clone()));
    }

    let effective_workdir = oci_config
        .and_then(|c| c.get("WorkingDir"))
        .and_then(|v| v.as_str())
        .filter(|s| !s.is_empty())
        .map(str::to_owned);
    let effective_user = oci_config
        .and_then(|c| c.get("User"))
        .and_then(|v| v.as_str())
        .filter(|s| !s.is_empty())
        .map(str::to_owned);

    let workload = if let Some(cmd) = cmd {
        crate::bake::sentry_workload_script(cmd, &env_pairs, effective_workdir.as_deref())
    } else if !envs.is_empty() || effective_workdir.is_some() {
        let mut argv: Vec<String> = Vec::new();
        if let Some(cfg) = oci_config {
            for v in cfg
                .get("Entrypoint")
                .and_then(|v| v.as_array())
                .into_iter()
                .flatten()
                .filter_map(|v| v.as_str())
            {
                argv.push(v.to_string());
            }
            for v in cfg
                .get("Cmd")
                .and_then(|v| v.as_array())
                .into_iter()
                .flatten()
                .filter_map(|v| v.as_str())
            {
                argv.push(v.to_string());
            }
        }
        if argv.is_empty() {
            image_workload
        } else {
            crate::bake::sentry_workload_script(&argv, &env_pairs, effective_workdir.as_deref())
        }
    } else {
        image_workload
    };

    (workload, env_pairs, effective_workdir, effective_user)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn mount_specs_metadata(mounts: &[crate::vmm::resources::MountSpec]) -> Vec<serde_json::Value> {
    mounts
        .iter()
        .map(|m| {
            let mut obj = serde_json::Map::new();
            obj.insert("host_path".to_string(), serde_json::json!(m.host_path));
            obj.insert("guest_tag".to_string(), serde_json::json!(m.guest_tag));
            obj.insert("guest_path".to_string(), serde_json::json!(m.guest_path));
            match m.symlinks {
                crate::vmm::resources::SymlinkPolicy::Opaque => {}
                crate::vmm::resources::SymlinkPolicy::Deny => {
                    obj.insert("symlinks".to_string(), serde_json::json!("deny"));
                }
                crate::vmm::resources::SymlinkPolicy::Follow => {
                    obj.insert("symlinks".to_string(), serde_json::json!("follow"));
                }
            }
            serde_json::Value::Object(obj)
        })
        .collect()
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn volume_specs_metadata(volumes: &[(PathBuf, String, u64)]) -> Vec<serde_json::Value> {
    volumes
        .iter()
        .map(|(host, guest, size)| {
            serde_json::json!({
                "host_file": host.to_string_lossy(),
                "guest_path": guest,
                "size_bytes": size,
            })
        })
        .collect()
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn insert_kvm_builder_run_config(
    obj: &mut serde_json::Map<String, serde_json::Value>,
    image_ref: &str,
    kvm_bake_key: &str,
    env_pairs: &[(String, String)],
    cmd: Option<&Vec<String>>,
    oci_config: Option<&serde_json::Value>,
    effective_workdir: Option<&str>,
    effective_user: Option<&str>,
    warm_daemon: bool,
    warmup_tag: Option<&str>,
    mounts: &[crate::vmm::resources::MountSpec],
    volumes: &[(PathBuf, String, u64)],
) {
    obj.insert("image".to_string(), serde_json::json!(image_ref));
    obj.insert("kvm_bake_key".to_string(), serde_json::json!(kvm_bake_key));
    obj.insert(
        "baked_by_version".to_string(),
        serde_json::json!(env!("CARGO_PKG_VERSION")),
    );
    let mut env_obj = serde_json::Map::new();
    for (k, v) in env_pairs {
        env_obj.insert(k.clone(), serde_json::json!(v));
    }
    obj.insert("image_env".to_string(), serde_json::Value::Object(env_obj));
    if let Some(cmd) = cmd {
        obj.insert("cmd".to_string(), serde_json::json!(cmd));
        obj.remove("entrypoint");
    } else if let Some(cfg) = oci_config {
        for (oci_key, meta_key) in [("Cmd", "cmd"), ("Entrypoint", "entrypoint")] {
            if let Some(v) = cfg.get(oci_key) {
                if !v.is_null() {
                    obj.insert(meta_key.to_string(), v.clone());
                }
            }
        }
    }
    if let Some(dir) = effective_workdir {
        obj.insert("working_dir".to_string(), serde_json::json!(dir));
    }
    if let Some(user) = effective_user {
        obj.insert("user".to_string(), serde_json::json!(user));
    }
    obj.insert("warm_daemon".to_string(), serde_json::json!(warm_daemon));
    if let Some(tag) = warmup_tag {
        obj.insert("warmup_tag".to_string(), serde_json::json!(tag));
    }
    if !mounts.is_empty() {
        obj.insert(
            "mounts".to_string(),
            serde_json::Value::Array(mount_specs_metadata(mounts)),
        );
    }
    if !volumes.is_empty() {
        obj.insert(
            "volumes".to_string(),
            serde_json::Value::Array(volume_specs_metadata(volumes)),
        );
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[allow(clippy::too_many_arguments)]
fn write_kvm_cold_builder_metadata(
    dest_dir: &Path,
    image_ref: &str,
    kvm_bake_key: &str,
    kernel: &Path,
    initrd: &Path,
    rootfs: &Path,
    memory_mib: u32,
    vcpus: u32,
    physical_bytes: u64,
    env_pairs: &[(String, String)],
    cmd: Option<&Vec<String>>,
    oci_config: Option<&serde_json::Value>,
    effective_workdir: Option<&str>,
    effective_user: Option<&str>,
    warm_daemon: bool,
    warmup_tag: Option<&str>,
    mounts: &[crate::vmm::resources::MountSpec],
    volumes: &[(PathBuf, String, u64)],
) -> Result<(), Error> {
    let mut metadata = serde_json::json!({
        "backend": "kvm",
        "kvm_kernel": kernel.to_string_lossy(),
        "kvm_initrd": initrd.to_string_lossy(),
        "kvm_disk": rootfs.to_string_lossy(),
        "memory_mib": memory_mib,
        "vcpus": vcpus,
        "balloon_target_pages": crate::bake::compute_balloon_target_pages(memory_mib),
        "baked_at": chrono_rfc3339_now(),
        "snapshot_physical_bytes": physical_bytes,
    });
    if let Some(obj) = metadata.as_object_mut() {
        insert_kvm_builder_run_config(
            obj,
            image_ref,
            kvm_bake_key,
            env_pairs,
            cmd,
            oci_config,
            effective_workdir,
            effective_user,
            warm_daemon,
            warmup_tag,
            mounts,
            volumes,
        );
    }
    std::fs::write(
        dest_dir.join("metadata.json"),
        serde_json::to_string_pretty(&metadata)
            .map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?,
    )
    .map_err(Error::Io)?;
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[allow(clippy::too_many_arguments)]
fn persist_kvm_builder_run_config(
    snap_dir: &Path,
    image_ref: &str,
    kvm_bake_key: &str,
    env_pairs: &[(String, String)],
    cmd: Option<&Vec<String>>,
    oci_config: Option<&serde_json::Value>,
    effective_workdir: Option<&str>,
    effective_user: Option<&str>,
    warm_daemon: bool,
    warmup_tag: Option<&str>,
    mounts: &[crate::vmm::resources::MountSpec],
    volumes: &[(PathBuf, String, u64)],
) -> Result<(), Error> {
    let meta_path = snap_dir.join("metadata.json");
    let text = std::fs::read_to_string(&meta_path).map_err(Error::Io)?;
    let mut meta: serde_json::Value = serde_json::from_str(&text)
        .map_err(|e| Error::vm_msg(format!("parse {}: {e}", meta_path.display())))?;
    let obj = meta
        .as_object_mut()
        .ok_or_else(|| Error::vm_msg("KVM metadata is not an object".to_owned()))?;
    insert_kvm_builder_run_config(
        obj,
        image_ref,
        kvm_bake_key,
        env_pairs,
        cmd,
        oci_config,
        effective_workdir,
        effective_user,
        warm_daemon,
        warmup_tag,
        mounts,
        volumes,
    );
    std::fs::write(
        &meta_path,
        serde_json::to_string_pretty(&meta)
            .map_err(|e| Error::vm_msg(format!("serialize metadata: {e}")))?,
    )
    .map_err(Error::Io)?;
    Ok(())
}

/// Write a `"backend":"kvm"` metadata.json next to a captured snapshot so
/// [`Image::from_snapshot`] can load it. `snap_rel` is the snapshot filename
/// RELATIVE to `dest_dir` (resolved dir-relative on load, so a cloned dir stays
/// loadable). `base` set → a differential snapshot (records the base ref).
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_kvm_snapshot_metadata(
    dest_dir: &Path,
    snap_rel: &str,
    base: Option<&Path>,
    mem_size: usize,
    num_cpus: u8,
) -> Result<(), Error> {
    let mut meta = serde_json::json!({
        "backend": "kvm",
        "kvm_snapshot": snap_rel,
        "memory_mib": (mem_size >> 20) as u32,
        "vcpus": num_cpus as u32,
        "baked_at": chrono_rfc3339_now(),
        "source": "Vm::snapshot_live",
    });
    if let Some(b) = base {
        meta["kvm_snapshot_base"] = serde_json::json!(b.to_string_lossy());
        meta["source"] = serde_json::json!("Vm::snapshot_diff_live");
    }
    std::fs::write(
        dest_dir.join("metadata.json"),
        serde_json::to_string_pretty(&meta)
            .map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
    )
    .map_err(Error::Io)?;
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kvm_effective_vcpus(requested: u32, context: &str) -> u32 {
    let requested = requested.max(1);
    if requested > 1 {
        eprintln!(
            "supermachine(KVM): {context} requested {requested} vCPUs, but Linux/x86_64 KVM \
             SMP is not enabled in this backend yet; using 1 vCPU"
        );
        1
    } else {
        requested
    }
}

/// Warm an image into a snapshot-backed [`Image`] for the pool's fast restore
/// path (Linux/KVM). A snapshot-backed image is returned unchanged; a cold-boot
/// image is booted once, the agent comes up, then it is captured to a PERSISTENT
/// warm snapshot in the shared snapshots dir, keyed by (cold-image family,
/// memory, vcpus, worker-binary identity). Restoring from the result is a CoW
/// mmap (~ms).
///
/// Persistence is the macOS warm-snapshot model brought to KVM: the warm
/// snapshot is reused across PROCESSES (a second pool/process for the same image
/// skips boot + the 3.5 s warmup entirely — a `cache hit`), and co-located warm
/// snapshots are memory-deduped against a shared golden base so concurrent VMs
/// of different images share the golden's RAM page cache (see
/// `dedup_warm_against_golden` + `docs/design/linux-memory-dedup-2026-06-08.md`).
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn warm_snapshot_for_pool(image: &Image, config: &VmConfig) -> Result<Image, Error> {
    // Already snapshot-backed (e.g. produced by Vm::snapshot): nothing to do.
    if image
        .kvm
        .as_ref()
        .and_then(|k| k.snapshot.as_ref())
        .is_some()
    {
        return Ok(image.clone());
    }
    let trace = crate::trace::enabled("pool");

    // Effective resources the VM will boot with (config overrides image
    // defaults) — part of the cache key so a 512m/1v warm is never reused for a
    // 1g/4v request.
    let mem = config.memory_mib.unwrap_or(image.memory_mib);
    let vcpus = kvm_effective_vcpus(
        config.vcpus.unwrap_or(image.vcpus),
        "warm_snapshot_for_pool",
    );

    let snapshots_dir = default_snapshots_dir();
    let family = warm_family_key(image);
    // Worker-binary fingerprint: a rebuilt/reinstalled worker (new SHA) yields a
    // new key, so a stale warm snapshot baked by an incompatible worker is never
    // reused — it just re-warms under the new key.
    let worker_fp = warm_worker_fingerprint();
    let warm_name = format!("{family}__kvmwarm__{mem}m{vcpus}v__{worker_fp}");
    let warm_dir = snapshots_dir.join(&warm_name);

    // Fast path: a valid cached warm snapshot exists → reuse (CoW restore, ~ms;
    // no boot, no warmup).
    if let Ok(img) = Image::from_snapshot(&warm_dir) {
        if trace {
            eprintln!("[warm-pool] cache hit: {}", warm_dir.display());
        }
        return Ok(img);
    }

    // Serialize same-key warmers (across processes) so we boot+warm exactly once.
    let _lock = WarmDirLock::acquire(&snapshots_dir, &warm_name);
    // Re-check under the lock — another process may have just produced it.
    if let Ok(img) = Image::from_snapshot(&warm_dir) {
        if trace {
            eprintln!("[warm-pool] cache hit (post-lock): {}", warm_dir.display());
        }
        return Ok(img);
    }

    // Cold warm: boot once and wait until the in-VM agent can service a
    // CONTROL sync. A fixed sleep is not enough on slow nested KVM: the kernel
    // can RCU-stall for tens of seconds, and capturing anyway preserves a
    // mid-boot / half-wired AF_VSOCK state that later accepts connections but
    // never completes exec requests.
    if trace {
        eprintln!(
            "[warm-pool] cold warm: boot + warmup -> {}",
            warm_dir.display()
        );
    }
    let vm = Vm::start(image, config)?;
    let sync_deadline = std::time::Instant::now() + std::time::Duration::from_secs(180);
    loop {
        if guest_sync_via_agent(&vm.vsock_exec_path) {
            break;
        }
        if std::time::Instant::now() >= sync_deadline {
            let _ = vm.stop();
            return Err(Error::vm_msg(
                "KVM warm-pool capture refused: guest agent did not answer sync before deadline"
                    .to_owned(),
            ));
        }
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    // Capture into a sibling `.partial` dir, finalize metadata + dedup, then
    // atomically rename into place so a concurrent reader never sees a
    // half-written warm dir.
    let partial = snapshots_dir.join(format!("{warm_name}.partial.{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&partial);
    // Consumes the boot VM (capture quiesces + tears down its vCPUs).
    let _ = vm.snapshot(&partial)?;
    finalize_warm_snapshot_metadata(&partial, &family, mem, &worker_fp)?;
    // Cross-family memory dedup vs a shared golden (best-effort; on any failure
    // the warm snapshot stays a full, still-correct snapshot).
    dedup_warm_against_golden(&snapshots_dir, &partial, mem, vcpus, &worker_fp, trace);

    // Atomic publish. If the target appeared meanwhile (a racing process), keep
    // theirs and drop ours.
    match std::fs::rename(&partial, &warm_dir) {
        Ok(()) => {}
        Err(_) if warm_dir.is_dir() => {
            let _ = std::fs::remove_dir_all(&partial);
        }
        Err(e) => {
            let _ = std::fs::remove_dir_all(&partial);
            return Err(Error::Io(e));
        }
    }
    // Best-effort GC of warm artifacts left by a previous worker binary (now
    // unreachable — the cache key only matches the current worker fingerprint).
    prune_stale_warm_artifacts(&snapshots_dir, &worker_fp);
    Image::from_snapshot(&warm_dir)
}

/// Garbage-collect persistent warm artifacts keyed by a DIFFERENT worker
/// fingerprint than `current_fp`. A rebuilt/reinstalled worker shifts the cache
/// key, so prior `<family>__kvmwarm__…__<old-fp>` dirs and their
/// `.kvmgolden/…__<old-fp>.snap` bases are never reused again — pure garbage.
/// We only touch STALE-fp artifacts (never the current fleet), and only past a
/// grace window, so a concurrent old-binary process mid-warm isn't disturbed
/// (and on Linux an unlinked file an old process still has `mmap`'d survives
/// until unmap anyway). Best-effort; all errors ignored.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prune_stale_warm_artifacts(snapshots_dir: &Path, current_fp: &str) {
    let grace = std::time::Duration::from_secs(60);
    let now = std::time::SystemTime::now();
    let old_enough = |p: &Path| -> bool {
        std::fs::metadata(p)
            .and_then(|m| m.modified())
            .map(|mt| now.duration_since(mt).map(|d| d >= grace).unwrap_or(false))
            .unwrap_or(false)
    };
    // The worker fingerprint is the final `__`-separated segment of the name.

    // 1. Stale warm family dirs.
    if let Ok(rd) = std::fs::read_dir(snapshots_dir) {
        for entry in rd.flatten() {
            let p = entry.path();
            let Some(name) = p.file_name().and_then(|s| s.to_str()) else {
                continue;
            };
            if name.contains("__kvmwarm__")
                && name.rsplit("__").next() != Some(current_fp)
                && p.is_dir()
                && old_enough(&p)
            {
                let _ = std::fs::remove_dir_all(&p);
            }
        }
    }
    // 2. Stale goldens (their only referrers were the stale warm dirs above).
    let golden_dir = snapshots_dir.join(".kvmgolden");
    if let Ok(rd) = std::fs::read_dir(&golden_dir) {
        for entry in rd.flatten() {
            let p = entry.path();
            if p.extension().and_then(|s| s.to_str()) != Some("snap") {
                continue;
            }
            let stale = p
                .file_stem()
                .and_then(|s| s.to_str())
                .and_then(|s| s.rsplit("__").next())
                .map(|fp| fp != current_fp)
                .unwrap_or(false);
            if stale && old_enough(&p) {
                let _ = std::fs::remove_file(&p);
            }
        }
    }
}

#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod warm_gc_tests {
    use super::*;

    fn set_mtime_secs_ago(p: &Path, secs_ago: i64) {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64;
        let tv = libc::timeval {
            tv_sec: now - secs_ago,
            tv_usec: 0,
        };
        let times = [tv, tv];
        let c = std::ffi::CString::new(p.to_str().unwrap()).unwrap();
        unsafe {
            libc::utimes(c.as_ptr(), times.as_ptr());
        }
    }

    #[test]
    fn prune_removes_stale_fp_keeps_current_and_fresh() {
        let root = std::env::temp_dir().join(format!("sm-gc-test-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        let golden_dir = root.join(".kvmgolden");
        std::fs::create_dir_all(&golden_dir).unwrap();

        let cur = "aaaa11112222";
        let old = "bbbb33334444";

        // Warm dirs: current-fp (keep), stale-fp old (remove), stale-fp fresh (keep — grace).
        let warm_cur = root.join(format!("nginx__kvmwarm__512m1v__{cur}"));
        let warm_old = root.join(format!("nginx__kvmwarm__512m1v__{old}"));
        let warm_old_fresh = root.join(format!("alpine__kvmwarm__512m1v__{old}"));
        for d in [&warm_cur, &warm_old, &warm_old_fresh] {
            std::fs::create_dir_all(d).unwrap();
            std::fs::write(d.join("vm.snap"), b"x").unwrap();
        }
        // Goldens: current-fp (keep), stale-fp old (remove).
        let gold_cur = golden_dir.join(format!("512m1v__{cur}.snap"));
        let gold_old = golden_dir.join(format!("512m1v__{old}.snap"));
        std::fs::write(&gold_cur, b"x").unwrap();
        std::fs::write(&gold_old, b"x").unwrap();

        // Age everything past the grace window EXCEPT warm_old_fresh + its inode.
        for p in [&warm_cur, &warm_old, &gold_cur, &gold_old] {
            set_mtime_secs_ago(p, 120);
        }
        set_mtime_secs_ago(&warm_old_fresh, 1);

        prune_stale_warm_artifacts(&root, cur);

        assert!(warm_cur.is_dir(), "current-fp warm dir must be kept");
        assert!(
            !warm_old.exists(),
            "stale-fp warm dir (aged) must be pruned"
        );
        assert!(
            warm_old_fresh.is_dir(),
            "stale-fp warm dir within grace must be kept"
        );
        assert!(gold_cur.exists(), "current-fp golden must be kept");
        assert!(!gold_old.exists(), "stale-fp golden (aged) must be pruned");

        let _ = std::fs::remove_dir_all(&root);
    }
}

/// Stable per-cold-image-family key for a warm snapshot dir name: the cold
/// snapshot DIR's name (sanitized to path-safe chars), falling back to a hash of
/// the full path. Note `snapshot_path` for a cold KVM image loaded from a dir is
/// `<dir>/restore.snap`, so we key on the PARENT dir name (the family), not the
/// file name (which is the constant `restore.snap` for every cold image).
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn warm_family_key(image: &Image) -> String {
    let dir = image.snapshot_path.parent().unwrap_or(&image.snapshot_path);
    let raw = dir.file_name().and_then(|s| s.to_str()).unwrap_or("");
    let sanitized: String = raw
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
                c
            } else {
                '_'
            }
        })
        .collect();
    if sanitized.is_empty() || sanitized == "." || sanitized == ".." {
        // Hash the full path so distinct images never collide.
        use std::hash::{Hash, Hasher};
        let mut h = std::collections::hash_map::DefaultHasher::new();
        image.snapshot_path.hash(&mut h);
        format!("img-{:016x}", h.finish())
    } else {
        sanitized
    }
}

/// 12-hex-prefix fingerprint of the host's `supermachine-worker` binary, or
/// `"noworker"` if it can't be resolved/hashed. Folds the worker identity into
/// the warm-snapshot cache key so a rebuilt worker invalidates stale warms.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn warm_worker_fingerprint() -> String {
    std::env::var_os("SUPERMACHINE_WORKER_BIN")
        .map(PathBuf::from)
        .and_then(|p| current_worker_sha16(&p))
        .map(|sha| sha.chars().take(12).collect::<String>())
        .unwrap_or_else(|| "noworker".to_owned())
}

/// Rewrite a freshly-captured warm snapshot's `metadata.json` so it survives the
/// atomic dir rename and carries the fields the golden-base dedup matches on:
/// a dir-RELATIVE `kvm_snapshot` (`vm.snap`, resolved against the dir on load),
/// plus `image` (family) + `baked_by_version`. Preserves all other fields.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn finalize_warm_snapshot_metadata(
    partial: &Path,
    family: &str,
    mem: u32,
    worker_fp: &str,
) -> Result<(), Error> {
    let meta_path = partial.join("metadata.json");
    let text = std::fs::read_to_string(&meta_path).map_err(Error::Io)?;
    let mut meta: serde_json::Value =
        serde_json::from_str(&text).map_err(|e| Error::vm_msg(format!("warm metadata: {e}")))?;
    let obj = meta
        .as_object_mut()
        .ok_or_else(|| Error::vm_msg("warm metadata is not an object".to_owned()))?;
    obj.insert("kvm_snapshot".into(), serde_json::json!("vm.snap"));
    obj.insert("image".into(), serde_json::json!(family));
    obj.insert("memory_mib".into(), serde_json::json!(mem));
    obj.insert(
        "baked_by_version".into(),
        serde_json::json!(env!("CARGO_PKG_VERSION")),
    );
    obj.insert("warm_worker_fp".into(), serde_json::json!(worker_fp));
    obj.insert("source".into(), serde_json::json!("warm_snapshot_for_pool"));
    std::fs::write(
        &meta_path,
        serde_json::to_string_pretty(&meta)
            .map_err(|e| Error::vm_msg(format!("warm metadata serialize: {e}")))?,
    )
    .map_err(Error::Io)?;
    Ok(())
}

/// Cross-image memory dedup against a shared golden base (best-effort).
///
/// Maintains ONE golden full snapshot per `(mem, vcpus, worker_fp)` under
/// `<snapshots_dir>/.kvmgolden/`. The first warm of a given key promotes its own
/// full as the golden (hardlinked — instant, same inode); every warm (including
/// that first one) is then rewritten as a diff against the golden. On restore
/// the golden is `mmap`'d copy-on-write and SHARED across every VM on it, so N
/// concurrent VMs of different images cost ~golden-once + per-VM diff (~14% of a
/// full) instead of N full guests. Same-offset diffing captures ~86% sharing
/// (mostly the ~88% zero pages, which align across images); KSM mops up the
/// rest at runtime.
///
/// Any failure leaves the warm snapshot a full (still correct) snapshot.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn dedup_warm_against_golden(
    snapshots_dir: &Path,
    partial: &Path,
    mem: u32,
    vcpus: u32,
    worker_fp: &str,
    trace: bool,
) {
    if std::env::var("SUPERMACHINE_AUTO_DEDUP").as_deref() == Ok("0") {
        return;
    }
    let warm_snap = partial.join("vm.snap");
    let golden_dir = snapshots_dir.join(".kvmgolden");
    if std::fs::create_dir_all(&golden_dir).is_err() {
        return;
    }
    let golden = golden_dir.join(format!("{mem}m{vcpus}v__{worker_fp}.snap"));

    // Serialize golden creation across processes.
    let _lock = WarmDirLock::acquire(&golden_dir, &format!("{mem}m{vcpus}v__{worker_fp}"));

    if !golden.exists() {
        // Promote this full as the shared golden. Hardlink (instant, same
        // inode) so rewriting `warm_snap` below replaces only the warm dir's
        // entry — the golden keeps the original full bytes. Fall back to copy
        // across filesystems.
        if std::fs::hard_link(&warm_snap, &golden).is_err() {
            let tmp = golden.with_extension("snap.tmp");
            if std::fs::copy(&warm_snap, &tmp).is_err() || std::fs::rename(&tmp, &golden).is_err() {
                let _ = std::fs::remove_file(&tmp);
                return;
            }
        }
    }

    match crate::kvm::run::rewrite_full_as_diff(&warm_snap, &golden) {
        Ok(diff_size) => {
            // Record the base so future fleet hygiene knows the dependency.
            let meta_path = partial.join("metadata.json");
            if let Ok(text) = std::fs::read_to_string(&meta_path) {
                if let Ok(mut meta) = serde_json::from_str::<serde_json::Value>(&text) {
                    if let Some(obj) = meta.as_object_mut() {
                        obj.insert(
                            "kvm_snapshot_base".into(),
                            serde_json::json!(golden.to_string_lossy()),
                        );
                        if let Ok(s) = serde_json::to_string_pretty(&meta) {
                            let _ = std::fs::write(&meta_path, s);
                        }
                    }
                }
            }
            if trace {
                eprintln!(
                    "[warm-pool] deduped vs golden {}: diff {:.1} MiB",
                    golden.display(),
                    diff_size as f64 / 1048576.0
                );
            }
        }
        Err(e) => {
            if trace {
                eprintln!("[warm-pool] dedup skipped ({e}); warm snapshot kept full");
            }
        }
    }
}

/// Cross-process advisory lock for a warm/golden snapshot key. A sibling lock
/// file (`<dir>/.<name>.warmlock`) under `flock(LOCK_EX)`; released when the
/// guard drops (the OS also reclaims it on process death). Best-effort: if the
/// lock can't be taken we proceed unlocked (the atomic rename + exists-checks
/// keep correctness; we just risk a redundant warm).
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct WarmDirLock {
    _file: Option<std::fs::File>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl WarmDirLock {
    fn acquire(dir: &Path, name: &str) -> Self {
        use std::os::fd::AsRawFd;
        if std::fs::create_dir_all(dir).is_err() {
            return Self { _file: None };
        }
        let lock_path = dir.join(format!(".{name}.warmlock"));
        let Ok(file) = std::fs::OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .truncate(false)
            .open(&lock_path)
        else {
            return Self { _file: None };
        };
        // Blocking exclusive lock with a hard deadline so a wedged holder can't
        // gate forever.
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
        loop {
            let r = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
            if r == 0 {
                return Self { _file: Some(file) };
            }
            if std::time::Instant::now() > deadline {
                return Self { _file: None };
            }
            std::thread::sleep(std::time::Duration::from_millis(25));
        }
    }
}

/// Explicit, configured worker pool. Returned by
/// [`PoolBuilder::build`]. `Pool` is `Clone` (Arc-shared);
/// dropping every clone tears the pool down.
#[derive(Clone)]
pub struct Pool {
    /// macOS/HVF: the subprocess-worker pool.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    inner: Arc<HiddenPool>,
    /// Linux/KVM: shared pool state (warm image + idle pre-restored VMs).
    /// Exactly one of `kvm` / `sentry` is `Some`, chosen by the image backend.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    kvm: Option<Arc<KvmPoolInner>>,
    /// Linux no-virt: shared persistent exec-server pool (sentry backend).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    sentry: Option<Arc<SentryPool>>,
    /// Linux no-virt: external gVisor/runsc checkpoint-backed pool.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    runsc: Option<Arc<RunscPoolInner>>,
    /// Linux no-virt: native Rust kboxlike process-tree pool.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    kboxlike: Option<Arc<KboxlikePoolInner>>,
}

/// Linux/KVM pool state: the warm (snapshot-backed) source image plus a queue of
/// idle, pre-restored VMs kept topped up to `min` (so `acquire` is a ~µs pop
/// instead of a ~ms restore). A single background refiller tops the idle queue
/// back to `min` after each acquire / release.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct KvmPoolInner {
    image: Image,
    vm_config: VmConfig,
    min: usize,
    idle: Mutex<Vec<Vm>>,
    /// Hard max-concurrency admission: bounds total live VMs to `max` and blocks
    /// `acquire` at the cap instead of growing without bound (which would OOM the
    /// host). Carries the live count + `max` + `acquire_timeout`.
    admission: Admission,
    /// Warm-reuse switch (mirrors HVF's `restore_on_release`). When `false`,
    /// a released VM is returned ALIVE to `idle` and reused in place on the next
    /// acquire (guest stays parked, page cache hot) instead of being torn down
    /// and a fresh one restored — turning a ~ms restore + cold re-fault into a
    /// ~µs idle-pop. When `true` (default), each cycle tears down + restores
    /// fresh (clean isolation; the safe default for untrusted multi-tenant use).
    restore_on_release: bool,
    /// Ensures at most one background refiller runs at a time.
    refilling: std::sync::atomic::AtomicBool,
    /// Set by [`Pool::shutdown`]; once true, [`Self::take_or_restore`] refuses
    /// new acquires with [`Error::PoolExhausted`] instead of restoring more VMs.
    shut_down: std::sync::atomic::AtomicBool,
}

/// No-virt **sentry** pool state (Linux/x86_64). Most backends amortize one
/// persistent supervisor across the pool and hand out cheap [`PooledVm`] handles
/// pointing at that supervisor. Live-tree C4 snapshots are the exception: they
/// must start one restored supervisor per checkout so `max > 1` has the same
/// independent-restored-machine semantics as KVM/HVF.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
enum SentryBackend {
    /// Legacy/default: one persistent ExecServer whose accept loop forks a fresh
    /// sealed cell per exec connection. Pooled `Vm`s dial `exec_path`.
    ExecServer {
        server: crate::sentry::ExecServer,
        exec_path: PathBuf,
    },
    /// Warm-daemon pool: one persistent supervisor ([`crate::sentry::Pool`]) with a
    /// parked warm cell (warmed on the image's `cmd` to its SENTINEL checkpoint).
    /// `acquire` forks a detached daemon (`Pool::acquire_running`); a pooled `Vm`'s
    /// `exec` runs a CLIENT cell via `Pool::exec_capture` that reaches the daemon
    /// over the supervisor-global owned loopback. `Arc` so each acquired `Vm` carries
    /// a clone for its exec route without moving the `Pool` out of the `SentryPool`.
    WarmDaemon {
        pool: Arc<crate::sentry::Pool>,
        #[allow(dead_code)]
        daemon_argv: Vec<String>,
    },
    /// Live C4 snapshot restore: each acquire starts an independent supervisor,
    /// restores the captured process tree into it, and returns that restored root
    /// directly. This mirrors KVM/HVF's "one restored machine per checkout"
    /// behavior for `max > 1`; no process/fd/proctree state is shared between
    /// concurrent checkouts.
    LiveTreeC4 {
        image: Image,
        config: VmConfig,
        snapshot_dir: PathBuf,
    },
    /// Unified workload+exec backend (the faithful mac/linux model): ONE persistent
    /// supervisor backgrounds the image CMD (if any) and serves a STREAMING exec
    /// socket whose cells fork IN-PROCESS — so `exec` shares the workload's loopback
    /// + proctree. A pooled `Vm` dials `exec_path` with the normal `ExecBuilder`
    /// protocol (full streaming/stdin/separate-stderr/timeout/tty). `Arc` so each
    /// acquired `Vm` keeps the pool alive for its exec route.
    Unified {
        pool: Arc<crate::sentry::Pool>,
        exec_path: PathBuf,
        captures_live_state: bool,
    },
}

/// sentry analogue of the KVM warm pool — the supervisor is amortized across the
/// whole pool instead of cold-started by `Vm::start` on every acquire.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct SentryPool {
    /// The pool's backend (ExecServer per-exec-fresh-cell, or a warm-daemon
    /// persistent supervisor). Outlives every [`PooledVm`]; torn down on `Drop`
    /// (the build-pool path stops it before removing `build_root`).
    backend: SentryBackend,
    /// The warm rootfs dir the shared server serves from (`image.sentry.rootfs`).
    /// Pooled `Vm`s don't own snapshot machinery (`sentry_src_rootfs: None`), so
    /// the pool keeps the rootfs path here for [`PooledVm::snapshot`] to copy.
    rootfs: PathBuf,
    /// Optional temp dir we created for the socket (unlinked on pool Drop).
    own_dir: Option<PathBuf>,
    /// Private mutable rootfs scaffold for this sentry pool.
    ///
    /// Sentry serves a host directory directly. Unlike KVM/HVF, there is no block
    /// device or RAM overlay that naturally absorbs guest writes, so every pool
    /// gets a private clone of the image rootfs. For `restore_on_release=false`
    /// that private clone is reused dirty only for this pool's lifetime; the
    /// source image snapshot stays immutable. For build pools it is the layer
    /// under construction. [`rootfs`](Self::rootfs) points INTO this dir; Drop
    /// removes the whole tree after stopping the backend.
    build_root: Option<PathBuf>,
    /// Private clean copy of [`rootfs`](Self::rootfs), captured before the pool
    /// starts serving. Runtime pools with `restore_on_release=true` swap the
    /// served rootfs back to this baseline on every release; build pools disable
    /// this because their private rootfs is the layer-under-construction.
    baseline_rootfs: Option<PathBuf>,
    /// Owns the temp parent that contains [`baseline_rootfs`](Self::baseline_rootfs)
    /// so it can be removed on pool drop.
    baseline_root: Option<PathBuf>,
    /// Serializes rootfs reset with concurrent release paths. Sentry pools share
    /// one served rootfs, so reset must be a single pool-wide operation.
    restore_lock: Mutex<()>,
    /// Effective runtime restore policy for this sentry pool.
    restore_on_release: bool,
    /// Bounds concurrent checkouts to `max`, blocking `acquire` at the cap.
    admission: Admission,
    /// No-op pool-lifetime admission guard kept for struct layout churn
    /// containment; live sentry checkouts are charged per [`PooledVm`] instead
    /// so idle pools do not reserve their whole `max` concurrency footprint.
    _mem_admission: crate::memory_admission::AdmissionGuard,
    /// The cgroup memory limit one live sentry checkout charges.
    mem_mib: u64,
    /// The effective vCPU count exposed to this pool's guests.
    vcpus: u32,
    /// Set by [`Pool::shutdown`]; refuses further acquires once true.
    shut_down: std::sync::atomic::AtomicBool,
    /// True iff this pool backs a warm-daemon image (`parts.warm_daemon`). The
    /// warm state may include a live process tree, so pooled snapshots must use
    /// supervisor C4 capture in addition to copying the rootfs.
    warm_daemon: bool,
    /// Guards the idempotent shutdown cleanup path. JS users can call
    /// `pool.shutdown()` while the `Pool` object remains reachable until process
    /// exit, so cleanup cannot rely only on `Drop`.
    cleanup_done: std::sync::atomic::AtomicBool,
}

/// Remove the per-build private rootfs scaffold (if any) on pool Drop — RAII, so
/// it runs even on a panicking/aborted build. We `stop()` the shared exec server
/// FIRST (it joins the accept thread, kills the baked workload, and its
/// per-connection supervisors SIGKILL their straggler cells), so no exec is still
/// holding a cwd under `build_root` when we `remove_dir_all` it (else EBUSY /
/// leftover dirs would accumulate). `stop()` is idempotent — the `_server` field's
/// own `Drop` re-runs it harmlessly afterward.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for SentryPool {
    fn drop(&mut self) {
        self.shut_down
            .store(true, std::sync::atomic::Ordering::SeqCst);
        self.cleanup_now();
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl SentryPool {
    /// Build the persistent exec server from a sentry image + config, ready to
    /// back a pool. Mirrors `Vm::start_sentry`'s socket+sandbox setup, but the
    /// server is owned by the pool (shared) instead of by a single `Vm`.
    fn build(
        image: &Image,
        config: &VmConfig,
        max: usize,
        acquire_timeout: Option<Duration>,
        policy_restore_on_release: bool,
    ) -> Result<Arc<SentryPool>, Error> {
        let parts = image
            .sentry
            .as_ref()
            .ok_or_else(|| Error::vm_msg("SentryPool::build on a non-sentry image"))?;
        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        let mut own_dir = None;
        if !dir.is_dir() {
            std::fs::create_dir_all(&dir).map_err(Error::Io)?;
            own_dir = Some(dir.clone());
        }
        let exec_path = dir.join(format!(
            "supermachine-sentry-pool-{}-{}.sock",
            std::process::id(),
            unique_suffix(),
        ));
        // Runtime pools mirror `Vm::start_sentry`: a pooled `Vm`'s workload is
        // host-reachable via [`expose_tcp`] (host-proxy to a real
        // `127.0.0.1:guest_port` host socket), so the pool's shared server runs in
        // the host netns. Build pools are different: Dockerfile `RUN` steps do not
        // expose host-reachable listeners, and the default sentry netns gives them
        // the mediated egress path (`rehome_to_host_netns`) used for isolated
        // sandbox networking. Keeping build pools on that default avoids the
        // locked no-KVM sandbox split where runtime VMs could reach hosts that
        // shared-host-netns build steps could not.
        let mut sandbox = image
            .sentry_sandbox_with(config)
            .map_err(|e| Error::vm_msg(format!("sentry sandbox: {e}")))?;
        if !config.for_build {
            sandbox = sandbox.without_netns();
        }
        // The rootfs the pool serves AND `PooledVm::snapshot` copies. Always use a
        // PRIVATE clone: sentry path mutations operate on real host files, so
        // serving `parts.rootfs` directly would let `restoreOnRelease:false` dirty
        // the source image snapshot after pool shutdown. KVM/HVF dirty VMs never
        // mutate their source snapshot; this clone gives sentry the same lifetime
        // boundary. For build pools the private tree is also the layer under
        // construction.
        // A build pool must NOT auto-launch the base image's baked workload while
        // RUN steps execute — suppress it (KVM/HVF build pools likewise don't run
        // the base CMD; the builder drives every exec).
        let mut start_workload = parts.has_workload;
        if config.for_build {
            start_workload = false;
        }
        let private_kind = if config.for_build { "build" } else { "runtime" };
        let priv_dir = dir.join(format!(
            "supermachine-sentry-{private_kind}-{}-{}",
            std::process::id(),
            unique_suffix(),
        ));
        let private_rootfs = priv_dir.join("rootfs");
        copy_dir_recursive(&parts.rootfs, &private_rootfs).map_err(|e| {
            Error::vm_msg(format!("sentry {private_kind} private rootfs clone: {e}"))
        })?;
        // Place the base image's metadata.json as the SIBLING of the private
        // rootfs so `write_sentry_snapshot_dir` (which reads
        // `src_rootfs.parent()/metadata.json`) re-emits the run-config
        // (`image_env`/`cmd`/`sentry_has_workload`/…) — not a bare
        // `{"backend":"sentry"}` fallback.
        if let Some(base_meta) = parts.rootfs.parent().map(|p| p.join("metadata.json")) {
            if base_meta.is_file() {
                let _ = std::fs::copy(&base_meta, priv_dir.join("metadata.json"));
            }
        }
        crate::kboxlike::prepare_rootfs_network_config(&private_rootfs).map_err(|e| {
            Error::vm_msg(format!(
                "sentry {private_kind} private rootfs network config: {e}"
            ))
        })?;
        // Repoint the sandbox's confined ROOT to the private clone — identical
        // sandbox otherwise (egress/uid/mounts/limits unchanged), same
        // openat2(RESOLVE_IN_ROOT) confinement.
        sandbox = sandbox.with_root(private_rootfs.clone());
        let rootfs = private_rootfs;
        let build_root = Some(priv_dir);
        let restore_on_release = !config.for_build && policy_restore_on_release;
        let mut baseline_rootfs = None;
        let mut baseline_root = None;
        if restore_on_release {
            let base_dir = dir.join(format!(
                "supermachine-sentry-baseline-{}-{}",
                std::process::id(),
                unique_suffix(),
            ));
            let base_rootfs = base_dir.join("rootfs");
            copy_dir_recursive(&rootfs, &base_rootfs)
                .map_err(|e| Error::vm_msg(format!("sentry rootfs baseline clone: {e}")))?;
            baseline_rootfs = Some(base_rootfs);
            baseline_root = Some(base_dir);
        }
        // MEMORY admission is charged per checked-out PooledVm, not up front for
        // the pool's whole `max`. This mirrors KVM worker-lifetime accounting and
        // avoids a lazy sentry pool with default `max=64` reserving tens of GiB
        // while idle.
        let mem_mib = sentry_cell_mem_mib(image, config);
        let vcpus = config.vcpus.unwrap_or(image.vcpus).max(1);
        let mem_admission = crate::memory_admission::charge(0);
        let c4_snapshot_dir = (!config.for_build)
            .then(|| image.snapshot_path.parent())
            .flatten()
            .filter(|dir| dir.join("restore.snap").is_file() && dir.join("mem.blob").is_file())
            .map(Path::to_path_buf);
        let c4_entry_kind = if let Some(dir) = &c4_snapshot_dir {
            let mut f = std::fs::File::open(dir.join("restore.snap")).map_err(Error::Io)?;
            Some(
                crate::sentry::state_snap::StateSnapshot::read_from(&mut f)
                    .map_err(Error::Io)?
                    .entry_kind,
            )
        } else {
            None
        };
        // Unified workload+exec backend is the DEFAULT (mac/linux parity: one guest
        // runs the CMD + serves execs sharing its loopback + proctree). SENTRY_UNIFIED=0
        // selects the legacy per-exec ExecServer as an escape hatch during rollout.
        let unified = std::env::var("SENTRY_UNIFIED")
            .map(|v| v != "0")
            .unwrap_or(true);
        let c4_live_tree = matches!(
            c4_entry_kind,
            Some(crate::sentry::state_snap::SnapshotEntryKind::LiveTree)
        );
        let backend = if c4_live_tree {
            let snapshot_dir = c4_snapshot_dir
                .clone()
                .ok_or_else(|| Error::vm_msg("sentry live-tree C4 missing snapshot dir"))?;
            SentryBackend::LiveTreeC4 {
                image: image.clone(),
                config: config.clone(),
                snapshot_dir,
            }
        } else if unified && !parts.warm_daemon {
            // UNIFIED workload+exec backend: one persistent
            // supervisor serving a streaming exec socket whose cells fork in-process
            // (shared loopback + proctree). A pooled Vm dials `exec_path` with the full
            // ExecBuilder protocol. (Workload backgrounding + pid-file = the next step.)
            let unified_sock = dir.join(format!(
                "supermachine-sentry-unified-{}-{}.sock",
                std::process::id(),
                unique_suffix(),
            ));
            let pool = sandbox
                .pool_with_exec(unified_sock.clone())
                .map_err(|e| Error::vm_msg(format!("sentry unified pool: {e}")))?;
            // The supervisor binds the exec socket ASYNC (it forks + re-execs + runs
            // setup before binding), unlike ExecServer which binds synchronously. Wait
            // for the socket to appear so the first acquire/exec doesn't race a missing
            // path (ENOENT). Bounded; a never-appearing socket fails the build cleanly.
            let deadline = std::time::Instant::now() + Duration::from_secs(15);
            while !unified_sock.exists() {
                if std::time::Instant::now() >= deadline {
                    return Err(Error::vm_msg(
                        "sentry unified exec socket never appeared (supervisor bind failed)",
                    ));
                }
                std::thread::sleep(Duration::from_millis(5));
            }
            // Background the image's workload (mac/linux parity: the guest boots its
            // CMD). The bake renders `/.supermachine/run-workload` — a script that
            // exports the image Env, `cd`s into WORKDIR, then exec's Entrypoint+Cmd —
            // exactly what the ExecServer's start_workload runs. Background THAT (not
            // parts.cmd, which is empty for non-warm-daemon images and skips WORKDIR/Env)
            // as a detached instance IN this supervisor, so it shares the exec cells'
            // owned loopback + proctree: the checkpoint helper `warm`s to the SENTINEL,
            // `acquire_running` resumes past it into the script in its OWN ring slot
            // (execs borrow slot 0). Record the pid where the KVM/HVF init writes it. A
            // workload that exits immediately (a base image's `bash`) is fine — the
            // supervisor stays up serving execs, exactly like a booted VM.
            let mut captures_live_state = false;
            if start_workload && rootfs.join(".supermachine/run-workload").is_file() {
                let wrapped = vec![
                    "/.supermachine/sm-checkpoint".to_string(),
                    "/bin/sh".to_string(),
                    "/.supermachine/run-workload".to_string(),
                ];
                if pool.warm(wrapped.iter().cloned()).is_ok() {
                    if let Ok(wpid) = pool.acquire_running() {
                        captures_live_state = true;
                        let run_dir = rootfs.join("run");
                        let _ = std::fs::create_dir_all(&run_dir);
                        let _ = std::fs::write(
                            run_dir.join("supermachine-workload.pid"),
                            format!("{wpid}\n"),
                        );
                    }
                }
            }
            SentryBackend::Unified {
                pool: Arc::new(pool),
                exec_path: unified_sock,
                captures_live_state,
            }
        } else if parts.warm_daemon && !parts.cmd.is_empty() {
            // Warm-daemon: build a control-socket Pool over the (possibly
            // private-clone) rootfs, then park a warm cell at the daemon's SENTINEL
            // checkpoint. `Pool::warm` blocks until the workload checkpoints (syscall
            // 0x5359) — a warm-daemon image's workload MUST do so once listen-ready
            // (see `with_warm_daemon`). A workload that never checkpoints (or a cell
            // that dies first) no longer hangs the build: the supervisor's bounded
            // ready-wait (`wait_ready_deadline`, keyed on `exec_timeout_ms`) kills the
            // wedged cell and `warm` returns an Err surfaced here.
            {
                let pool = sandbox
                    .pool()
                    .map_err(|e| Error::vm_msg(format!("sentry warm-daemon pool: {e}")))?;
                if let Some(snapshot_dir) = &c4_snapshot_dir {
                    pool.warm_restore(snapshot_dir).map_err(|e| {
                        Error::vm_msg(format!(
                            "sentry warm_restore({}): {e}",
                            snapshot_dir.display()
                        ))
                    })?;
                } else {
                    pool.warm(parts.cmd.iter().cloned())
                        .map_err(|e| Error::vm_msg(format!("sentry warm({:?}): {e}", parts.cmd)))?;
                }
                SentryBackend::WarmDaemon {
                    pool: Arc::new(pool),
                    daemon_argv: parts.cmd.clone(),
                }
            }
        } else {
            let server = sandbox
                .serve_exec(&exec_path, start_workload)
                .map_err(|e| Error::vm_msg(format!("sentry serve_exec: {e}")))?;
            SentryBackend::ExecServer { server, exec_path }
        };
        Ok(Arc::new(SentryPool {
            backend,
            rootfs,
            own_dir,
            build_root,
            baseline_rootfs,
            baseline_root,
            restore_lock: Mutex::new(()),
            restore_on_release,
            admission: Admission::new(max, acquire_timeout),
            _mem_admission: mem_admission,
            mem_mib,
            vcpus,
            shut_down: std::sync::atomic::AtomicBool::new(false),
            warm_daemon: parts.warm_daemon,
            cleanup_done: std::sync::atomic::AtomicBool::new(false),
        }))
    }

    fn stop_backend(&self) {
        // Stop the active backend BEFORE remove_dir_all(build_root) (a cell may
        // hold a cwd under it). All stop paths are idempotent: the field's own
        // Drop re-runs teardown harmlessly.
        match &self.backend {
            SentryBackend::ExecServer { server, .. } => server.stop(),
            SentryBackend::WarmDaemon { pool, .. } => pool.stop(),
            SentryBackend::LiveTreeC4 { .. } => {}
            SentryBackend::Unified { pool, .. } => pool.stop(),
        }
    }

    fn cleanup_now(&self) {
        if self
            .cleanup_done
            .compare_exchange(
                false,
                true,
                std::sync::atomic::Ordering::SeqCst,
                std::sync::atomic::Ordering::SeqCst,
            )
            .is_err()
        {
            return;
        }
        self.stop_backend();
        if let Some(br) = &self.build_root {
            let _ = std::fs::remove_dir_all(br);
        }
        if let Some(br) = &self.baseline_root {
            let _ = std::fs::remove_dir_all(br);
        }
        if let Some(d) = &self.own_dir {
            let _ = std::fs::remove_dir_all(d);
        }
    }

    fn cleanup_if_shutdown_idle(&self) {
        if !self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
            return;
        }
        if self.admission.alive() != 0 {
            return;
        }
        self.cleanup_now();
    }

    fn restore_rootfs_from_baseline(&self) -> std::io::Result<()> {
        if !self.restore_on_release {
            return Ok(());
        }
        let Some(baseline) = &self.baseline_rootfs else {
            return Ok(());
        };
        let _guard = self.restore_lock.lock().unwrap_or_else(|e| e.into_inner());
        let parent = self.rootfs.parent().ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("sentry rootfs has no parent: {}", self.rootfs.display()),
            )
        })?;
        let suffix = format!("{}-{}", std::process::id(), unique_suffix());
        let staging = parent.join(format!(".supermachine-sentry-restore-{suffix}"));
        let _ = std::fs::remove_dir_all(&staging);
        copy_dir_recursive(baseline, &staging)?;
        replace_dir_contents_from_staging(&staging, &self.rootfs)
    }

    fn capture_live_state_into(&self, dest_dir: &Path) -> Result<(), Error> {
        match &self.backend {
            SentryBackend::WarmDaemon { pool, .. } => pool
                .snapshot(dest_dir)
                .map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}"))),
            SentryBackend::Unified {
                pool,
                captures_live_state: true,
                ..
            } => pool
                .snapshot(dest_dir)
                .map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}"))),
            SentryBackend::Unified {
                captures_live_state: false,
                ..
            } => Ok(()),
            SentryBackend::ExecServer { .. } | SentryBackend::LiveTreeC4 { .. } => Ok(()),
        }
    }

    /// Admission-gate (blocking up to `acquire_timeout`) then hand back a cheap
    /// `PooledVm` whose `Vm` execs against the shared server's socket. The slot
    /// is released in `PooledVm::drop`.
    fn acquire(self: &Arc<Self>) -> Result<PooledVm<'static>, Error> {
        use std::sync::atomic::Ordering::SeqCst;
        let deadline = self
            .admission
            .acquire_timeout
            .map(|d| std::time::Instant::now() + d);
        loop {
            if self.shut_down.load(SeqCst) {
                return Err(Error::vm_msg("sentry pool is shutting down"));
            }
            if self.admission.try_reserve() {
                break;
            }
            if !self.admission.wait_for_change(deadline)? {
                // Same error VARIANT as the KVM/HVF pool so callers can `matches!`
                // on `Error::PoolExhausted` regardless of backend (parity).
                return Err(Error::pool_exhausted(
                    "sentry pool acquire timed out (all max slots in use)",
                ));
            }
        }
        crate::memory_admission::await_pressure_relief();
        let sentry_mem_admission =
            crate::memory_admission::admit_with(|| sentry_admission_charge_mib(self.mem_mib, 1));
        // Per-backend: the ExecServer path dials the shared exec socket; the
        // WarmDaemon path forks a DETACHED daemon (acquire_running) and routes
        // `vm.exec` through the Pool over loopback (no exec socket).
        let mut exec_path = match &self.backend {
            SentryBackend::ExecServer { exec_path, .. } => exec_path.clone(),
            SentryBackend::Unified { exec_path, .. } => exec_path.clone(),
            SentryBackend::WarmDaemon { .. } => PathBuf::new(),
            SentryBackend::LiveTreeC4 { .. } => PathBuf::new(),
        };
        let (running_pid, warm_pool, running_pool, stop_running_pool_on_drop) = match &self.backend
        {
            SentryBackend::WarmDaemon { pool, .. } => {
                match pool.acquire_running() {
                    Ok(pid) => (
                        Some(pid),
                        Some(Arc::clone(pool)),
                        Some(Arc::clone(pool)),
                        false,
                    ),
                    Err(e) => {
                        // Release the just-claimed admission slot before erroring (the
                        // slot-leak guard) so a blocked acquirer isn't stranded.
                        self.admission.release();
                        return Err(Error::vm_msg(format!(
                            "sentry warm-daemon acquire_running: {e}"
                        )));
                    }
                }
            }
            SentryBackend::LiveTreeC4 {
                image,
                config,
                snapshot_dir,
            } => {
                let sandbox = match image.sentry_sandbox_with(config) {
                    Ok(s) => s.with_root(snapshot_dir.join("rootfs")).without_netns(),
                    Err(e) => {
                        self.admission.release();
                        return Err(e);
                    }
                };
                let dir = match &config.vsock_mux_dir {
                    Some(d) => d.clone(),
                    None => std::env::temp_dir(),
                };
                let live_exec_path = dir.join(format!(
                    "supermachine-sentry-live-c4-{}-{}.sock",
                    std::process::id(),
                    unique_suffix(),
                ));
                let pool = match sandbox.pool_with_exec(live_exec_path.clone()) {
                    Ok(pool) => Arc::new(pool),
                    Err(e) => {
                        self.admission.release();
                        return Err(Error::vm_msg(format!("sentry live-tree pool: {e}")));
                    }
                };
                let deadline = std::time::Instant::now() + Duration::from_secs(15);
                while !live_exec_path.exists() {
                    if std::time::Instant::now() >= deadline {
                        pool.stop();
                        self.admission.release();
                        return Err(Error::vm_msg(
                            "sentry live-tree exec socket never appeared (supervisor bind failed)",
                        ));
                    }
                    std::thread::sleep(Duration::from_millis(5));
                }
                if let Err(e) = pool.warm_restore(snapshot_dir) {
                    pool.stop();
                    self.admission.release();
                    return Err(Error::vm_msg(format!(
                        "sentry live-tree warm_restore({}): {e}",
                        snapshot_dir.display()
                    )));
                }
                let checkout = match pool.acquire_running() {
                    Ok(pid) => (Some(pid), None, Some(pool), true),
                    Err(e) => {
                        pool.stop();
                        self.admission.release();
                        return Err(Error::vm_msg(format!(
                            "sentry live-tree acquire_running: {e}"
                        )));
                    }
                };
                exec_path = live_exec_path;
                checkout
            }
            SentryBackend::ExecServer { .. } => (None, None, None, false),
            // Unified: exec dials the streaming socket (vsock_exec_path); no
            // per-acquire daemon fork and no warm-pool exec route.
            SentryBackend::Unified { .. } => (None, None, None, false),
        };
        // A pooled Vm: references the SHARED socket / warm Pool, owns no server,
        // never unlinks (skip_cleanup) — the SentryPool owns the lifecycle.
        let vm = Vm {
            running: None,
            sentry_exec: None,
            // Pooled sentry VMs share the served rootfs. Warm/live C4 checkouts
            // carry a Pool route for exec and snapshot state; ExecServer/Unified
            // use the shared socket path instead.
            sentry_src_rootfs: None,
            vsock_mux_path: exec_path.clone(),
            vsock_exec_path: exec_path,
            own_vsock_mux_dir: None,
            skip_cleanup: true,
            time_sync_stop: Arc::new(AtomicBool::new(false)),
            // The PooledVm carries the per-checkout memory reservation; the inner
            // Vm owns none separately.
            _sentry_mem_admission: None,
            // WarmDaemon: route `exec` through this Pool (Pool::exec_capture over
            // loopback). `None` for the ExecServer path (dials vsock_exec_path).
            sentry_warm_pool: warm_pool,
            sentry_unified_pool: None,
            runsc: None,
            kboxlike: None,
        };
        Ok(PooledVm {
            vm: Some(vm),
            pool: None,
            sentry_pool: Some(Arc::clone(self)),
            runsc_pool: None,
            kboxlike_pool: None,
            sentry_mem_admission: Some(sentry_mem_admission),
            // WarmDaemon: the detached daemon pid, released (SIGKILL+reap) in Drop.
            sentry_running_pid: running_pid,
            sentry_running_pool: running_pool,
            sentry_stop_running_pool_on_drop: stop_running_pool_on_drop,
            sentry_touched: AtomicBool::new(false),
            _image: std::marker::PhantomData,
        })
    }

    /// Cumulative exec metrics for the warm-daemon backend (counts, latency,
    /// seccomp/timeout/signal tallies), recorded library-side by the inner
    /// control-socket Pool's `record_exec`. `None` for the ExecServer backend,
    /// which routes execs over a shared socket with no per-Pool counter object.
    fn sentry_stats(&self) -> Option<crate::sentry::SentryPoolStats> {
        match &self.backend {
            SentryBackend::WarmDaemon { pool, .. } => Some(pool.stats()),
            SentryBackend::ExecServer { .. } => None,
            SentryBackend::LiveTreeC4 { .. } => None,
            SentryBackend::Unified { .. } => None,
        }
    }
}

/// Hard max-concurrency admission for the KVM pool. Bounds total live VMs
/// (idle + checked-out) to `max`: a fresh restore must `reserve_blocking` a slot,
/// blocking up to `acquire_timeout` when all `max` are in use until a peer
/// `release`s — rather than spawning unboundedly (which would exhaust host RAM).
/// Idle reuse does NOT reserve (the slot was claimed when that VM was created).
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct Admission {
    max: usize,
    alive: std::sync::atomic::AtomicUsize,
    lock: std::sync::Mutex<()>,
    available: std::sync::Condvar,
    acquire_timeout: Option<Duration>,
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Admission {
    fn new(max: usize, acquire_timeout: Option<Duration>) -> Self {
        Self {
            max: max.max(1),
            alive: std::sync::atomic::AtomicUsize::new(0),
            lock: std::sync::Mutex::new(()),
            available: std::sync::Condvar::new(),
            acquire_timeout,
        }
    }

    fn alive(&self) -> usize {
        self.alive.load(std::sync::atomic::Ordering::SeqCst)
    }

    /// Claim a slot if under `max`, non-blocking. Returns false at the cap. Used
    /// by the background refiller (which must never block).
    fn try_reserve(&self) -> bool {
        use std::sync::atomic::Ordering::SeqCst;
        loop {
            let cur = self.alive.load(SeqCst);
            if cur >= self.max {
                return false;
            }
            if self
                .alive
                .compare_exchange(cur, cur + 1, SeqCst, SeqCst)
                .is_ok()
            {
                return true;
            }
        }
    }

    /// Bounded wait for a slot-release OR a refiller idle-push, used by
    /// [`KvmPoolInner::take_or_restore`]'s acquire loop. Returns `Ok(true)` to
    /// retry the loop (re-check idle + try_reserve), or `Ok(false)` if the
    /// `acquire_timeout` deadline has passed. Each wait is capped at 100ms so a
    /// missed notify is a short stall, never a hang.
    ///
    /// CRITICAL: the acquire loop must re-check the IDLE QUEUE between waits,
    /// not just the slot count. A background refiller holds an admission slot
    /// while it restores, then pushes the VM to `idle` WITHOUT releasing the
    /// slot — so a caller that blocked purely on `alive < max` (the old
    /// `reserve_blocking`) would never observe that VM and would hang forever
    /// when `min == max` (the single slot is permanently the refiller's). This
    /// is why the wait lives here as a primitive and the idle re-check lives in
    /// `take_or_restore`.
    fn wait_for_change(&self, deadline: Option<std::time::Instant>) -> Result<bool, Error> {
        let g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
        let wait = match deadline {
            Some(dl) => {
                let now = std::time::Instant::now();
                if now >= dl {
                    return Ok(false);
                }
                (dl - now).min(Duration::from_millis(100))
            }
            None => Duration::from_millis(100),
        };
        let _ = self.available.wait_timeout(g, wait);
        Ok(true)
    }

    /// Free a slot + wake one waiter.
    fn release(&self) {
        self.alive.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
        self.available.notify_one();
    }

    /// Wake ALL acquirers blocked in [`wait_for_change`]. Called by a refiller
    /// after pushing a fresh VM to the idle queue — that push does not call
    /// [`release`] (the slot stays claimed by the now-idle VM), so blocked
    /// acquirers must be woken explicitly to re-check idle promptly.
    fn notify(&self) {
        self.available.notify_all();
    }
}

#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod admission_tests {
    use super::Admission;
    use std::sync::Arc;
    use std::time::{Duration, Instant};

    #[test]
    fn try_reserve_bounds_at_max() {
        let a = Admission::new(3, None);
        assert!(a.try_reserve());
        assert!(a.try_reserve());
        assert!(a.try_reserve());
        assert_eq!(a.alive(), 3);
        // At the cap: further non-blocking reservations fail.
        assert!(!a.try_reserve());
        assert_eq!(a.alive(), 3);
        // Releasing frees exactly one slot.
        a.release();
        assert_eq!(a.alive(), 2);
        assert!(a.try_reserve());
        assert_eq!(a.alive(), 3);
    }

    #[test]
    fn max_zero_clamps_to_one() {
        let a = Admission::new(0, None);
        assert_eq!(a.max, 1);
        assert!(a.try_reserve());
        assert!(!a.try_reserve());
    }

    #[test]
    fn wait_for_change_reports_deadline_passed() {
        // A past deadline → Ok(false) (the take_or_restore loop maps this to a
        // pool-saturated error) without blocking.
        let a = Admission::new(1, Some(Duration::from_millis(50)));
        assert!(a.try_reserve());
        let past = Instant::now() - Duration::from_secs(1);
        let start = Instant::now();
        assert_eq!(a.wait_for_change(Some(past)).unwrap(), false);
        assert!(
            start.elapsed() < Duration::from_millis(50),
            "a passed deadline must not block"
        );
        // No slot leaked by the wait.
        assert_eq!(a.alive(), 1);
    }

    #[test]
    fn wait_for_change_wakes_on_notify() {
        // A blocked acquirer is woken by notify() (the refiller-idle-push wake)
        // well before the 100ms cap, then re-loops to re-check idle.
        let a = Arc::new(Admission::new(1, Some(Duration::from_secs(5))));
        assert!(a.try_reserve());
        let peer = Arc::clone(&a);
        let waker = std::thread::spawn(move || {
            std::thread::sleep(Duration::from_millis(20));
            peer.notify();
        });
        let start = Instant::now();
        let deadline = Some(Instant::now() + Duration::from_secs(5));
        assert_eq!(a.wait_for_change(deadline).unwrap(), true);
        let elapsed = start.elapsed();
        waker.join().unwrap();
        assert!(
            elapsed < Duration::from_millis(100),
            "notify must wake the waiter promptly, waited {elapsed:?}"
        );
    }

    #[test]
    fn wait_for_change_caps_each_wait_without_notify() {
        // With no notify and a far deadline, a single wait returns within ~100ms
        // (the cap) so the acquire loop re-checks idle frequently — this is the
        // backstop that makes the min==max deadlock impossible even if a notify
        // is ever missed.
        let a = Admission::new(1, Some(Duration::from_secs(10)));
        assert!(a.try_reserve());
        let start = Instant::now();
        let deadline = Some(Instant::now() + Duration::from_secs(10));
        assert_eq!(a.wait_for_change(deadline).unwrap(), true);
        let elapsed = start.elapsed();
        assert!(
            elapsed < Duration::from_millis(250),
            "each wait must cap at ~100ms, waited {elapsed:?}"
        );
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KvmPoolInner {
    fn restore_one(&self) -> Result<Vm, Error> {
        Vm::start(&self.image, &self.vm_config)
    }

    /// Pop an idle pre-warmed VM (~µs) or restore a fresh one (~ms). A fresh
    /// restore reserves one of the `max` concurrency slots, BLOCKING (up to
    /// `acquire_timeout`) when the pool is saturated rather than over-spawning.
    /// Idle reuse takes no slot (it was reserved when that VM was created).
    fn take_or_restore(&self) -> Result<Vm, Error> {
        let deadline = self
            .admission
            .acquire_timeout
            .map(|d| std::time::Instant::now() + d);
        loop {
            if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
                return Err(Error::pool_exhausted("pool is shut down"));
            }
            // Re-check idle EVERY iteration. A background refiller holds an
            // admission slot while it restores, then pushes the VM here without
            // releasing the slot — so an acquirer that blocked purely on the
            // slot count would never see it and would hang forever when
            // min == max (the lone slot is the refiller's). Popping idle here
            // each loop is what breaks that deadlock.
            if let Some(vm) = self.idle.lock().unwrap_or_else(|e| e.into_inner()).pop() {
                return Ok(vm);
            }
            // No idle VM: claim a fresh slot if under `max` and restore.
            if self.admission.try_reserve() {
                return self.restore_one().inspect_err(|_| self.admission.release());
            }
            // At the cap with nothing idle: wait for a peer release OR a
            // refiller idle-push, then re-loop (which re-checks idle).
            if !self.admission.wait_for_change(deadline)? {
                return Err(Error::pool_exhausted(format!(
                    "pool saturated: all {} VM slots in use and none freed within \
                     acquire_timeout",
                    self.admission.max
                )));
            }
        }
    }

    /// Synchronously pre-warm the idle queue up to `min` (best-effort). Used at
    /// `build()` so eager pre-spawn (`min > 0`) is observable in `stats()` the
    /// instant `build()` returns, rather than racing a background thread.
    fn prewarm_to_min(&self) {
        loop {
            let idle_len = self.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
            // Stop at the idle target, or when another slot would exceed `max`.
            if idle_len >= self.min || !self.admission.try_reserve() {
                break;
            }
            match self.restore_one() {
                Ok(vm) => {
                    self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
                    // Wake any acquirer blocked at the cap so it re-checks idle.
                    self.admission.notify();
                }
                Err(_) => {
                    self.admission.release();
                    break;
                }
            }
        }
    }

    /// Spawn (at most one) background refiller to top the idle queue up to `min`.
    fn refill_async(self: &Arc<Self>) {
        use std::sync::atomic::Ordering::SeqCst;
        if self.min == 0 {
            return;
        }
        if self
            .refilling
            .compare_exchange(false, true, SeqCst, SeqCst)
            .is_err()
        {
            return; // a refiller is already running
        }
        let inner = Arc::clone(self);
        std::thread::spawn(move || {
            loop {
                let idle_len = inner.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
                // Stop at the idle target, or when claiming another slot would
                // exceed `max` (try_reserve returns false at the cap).
                if idle_len >= inner.min || !inner.admission.try_reserve() {
                    break;
                }
                match inner.restore_one() {
                    Ok(vm) => {
                        inner
                            .idle
                            .lock()
                            .unwrap_or_else(|e| e.into_inner())
                            .push(vm);
                        // Wake an acquirer blocked at the cap to re-check idle.
                        inner.admission.notify();
                    }
                    Err(_) => {
                        inner.admission.release();
                        break;
                    }
                }
            }
            inner.refilling.store(false, SeqCst);
        });
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KboxlikePoolInner {
    fn build(
        image: &Image,
        config: &VmConfig,
        min: usize,
        max: usize,
        acquire_timeout: Option<Duration>,
        restore_on_release: bool,
    ) -> Result<Arc<Self>, Error> {
        let mut parts = image
            .kboxlike
            .as_ref()
            .ok_or_else(|| Error::vm_msg("KboxlikePoolInner::build on a non-kboxlike image"))?
            .clone();
        let mut build_root = None;
        if config.for_build {
            let dir = match &config.vsock_mux_dir {
                Some(d) => d.clone(),
                None => std::env::temp_dir(),
            };
            std::fs::create_dir_all(&dir).map_err(Error::Io)?;
            let priv_dir = dir.join(format!(
                "supermachine-kboxlike-build-{}-{}",
                std::process::id(),
                unique_suffix()
            ));
            let private_rootfs = priv_dir.join("rootfs");
            copy_dir_recursive(&parts.rootfs, &private_rootfs)
                .map_err(|e| Error::vm_msg(format!("kboxlike build private rootfs clone: {e}")))?;
            if let Some(base_meta) = parts.rootfs.parent().map(|p| p.join("metadata.json")) {
                if base_meta.is_file() {
                    let _ = std::fs::copy(&base_meta, priv_dir.join("metadata.json"));
                }
            }
            crate::kboxlike::prepare_rootfs_network_config(&private_rootfs).map_err(|e| {
                Error::vm_msg(format!("kboxlike build private rootfs network config: {e}"))
            })?;
            parts.rootfs = private_rootfs;
            build_root = Some(priv_dir);
        }
        let max = max.max(1);
        let min = min.min(max);
        let inner = Arc::new(Self {
            parts,
            vm_config: config.clone(),
            build_root,
            min,
            admission: Admission::new(max, acquire_timeout),
            idle: Mutex::new(Vec::new()),
            restore_on_release,
            refilling: std::sync::atomic::AtomicBool::new(false),
            shut_down: std::sync::atomic::AtomicBool::new(false),
        });
        inner.prewarm_to_min();
        Ok(inner)
    }

    fn restore_one(&self) -> Result<Vm, Error> {
        if self.vm_config.for_build {
            return Vm::start_kboxlike_build_root(&self.parts, &self.vm_config);
        }
        Vm::start_kboxlike_parts(&self.parts, &self.vm_config)
    }

    fn take_or_restore(&self) -> Result<Vm, Error> {
        let deadline = self
            .admission
            .acquire_timeout
            .map(|d| std::time::Instant::now() + d);
        loop {
            if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
                return Err(Error::pool_exhausted("pool is shut down"));
            }
            if let Some(vm) = self.idle.lock().unwrap_or_else(|e| e.into_inner()).pop() {
                return Ok(vm);
            }
            if self.admission.try_reserve() {
                return self.restore_one().inspect_err(|_| self.admission.release());
            }
            if !self.admission.wait_for_change(deadline)? {
                return Err(Error::pool_exhausted(format!(
                    "kboxlike pool saturated: all {} VM slots in use and none freed within \
                     acquire_timeout",
                    self.admission.max
                )));
            }
        }
    }

    fn prewarm_to_min(&self) {
        loop {
            let idle_len = self.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
            if idle_len >= self.min || !self.admission.try_reserve() {
                break;
            }
            match self.restore_one() {
                Ok(vm) => {
                    self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
                    self.admission.notify();
                }
                Err(e) => {
                    eprintln!("supermachine(kboxlike): prewarm restore failed: {e}");
                    self.admission.release();
                    break;
                }
            }
        }
    }

    fn refill_async(self: &Arc<Self>) {
        use std::sync::atomic::Ordering::SeqCst;
        if self.min == 0 {
            return;
        }
        if self
            .refilling
            .compare_exchange(false, true, SeqCst, SeqCst)
            .is_err()
        {
            return;
        }
        let inner = Arc::clone(self);
        std::thread::spawn(move || {
            loop {
                let idle_len = inner.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
                if idle_len >= inner.min || !inner.admission.try_reserve() {
                    break;
                }
                match inner.restore_one() {
                    Ok(vm) => {
                        inner
                            .idle
                            .lock()
                            .unwrap_or_else(|e| e.into_inner())
                            .push(vm);
                        inner.admission.notify();
                    }
                    Err(e) => {
                        eprintln!("supermachine(kboxlike): refill restore failed: {e}");
                        inner.admission.release();
                        break;
                    }
                }
            }
            inner.refilling.store(false, SeqCst);
        });
    }

    fn release_vm(self: &Arc<Self>, vm: Vm) {
        if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
            drop(vm);
            self.admission.release();
            return;
        }

        let warm_slot = {
            let idle = self.idle.lock().unwrap_or_else(|e| e.into_inner());
            idle.len() < self.min
        };
        if !warm_slot {
            drop(vm);
            self.admission.release();
            return;
        }

        if !self.restore_on_release {
            self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
            self.admission.notify();
            return;
        }

        drop(vm);
        self.admission.release();
        self.refill_async();
    }
}

// Opaque Debug: the KVM `Pool` holds an `Image`, and `Image` (which derives
// Debug) caches an `Arc<Pool>` — a derived Debug would recurse infinitely if
// ever printed. Print opaquely instead.
impl std::fmt::Debug for Pool {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Pool").finish_non_exhaustive()
    }
}

/// Snapshot of pool state at a point in time. Cheap to fetch —
/// one mutex acquire — so it's safe to graph at modest rates.
#[derive(Debug, Clone, Copy)]
pub struct PoolStats {
    /// Total workers alive: idle + checked-out + in-flight spawn.
    pub alive: usize,
    /// Currently checked out (in use by a `PooledVm`).
    pub in_use: usize,
    /// Sitting in the idle queue waiting for next acquire.
    pub idle: usize,
    /// Acquire callers currently blocked waiting for a slot.
    pub waiting: usize,
    /// Configured maximum concurrency.
    pub max: usize,
    /// Configured baseline / always-warm count.
    pub min: usize,
    /// Cumulative dead-while-idle workers the watchdog has reaped. A
    /// steadily climbing value points at workers crashing post-restore.
    pub reaped: u64,
    /// High-water-mark resident footprint (phys_footprint, MiB) observed
    /// across this pool's workers, vs the configured `memory_mib` cap.
    /// `0` until the first measurement. Memory admission charges this
    /// (+25% margin) instead of the cap once measured, so the gap
    /// between this and `memory_mib` is density reclaimed for free.
    pub observed_footprint_mib: u64,
}

impl Pool {
    /// Acquire a fresh VM from this pool. Identical fast path to
    /// [`Image::acquire`] — on a hit, ~µs to pop the idle queue.
    /// On miss, follows the policy: auto-grow up to `max`, or
    /// block (with timeout) if at `max`.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn acquire(&self) -> Result<PooledVm<'_>, Error> {
        let _span = tracing::info_span!(
            "supermachine.pool.acquire",
            memory_mib = self.inner.spawn_cfg.memory_mib,
            vcpus = self.inner.spawn_cfg.vcpus,
        )
        .entered();
        let worker = self.inner.acquire()?;
        let vm = Vm {
            pool: None,
            vsock_mux_path: worker.vsock_mux_path.clone(),
            vsock_exec_path: worker.vsock_exec_path.clone(),
            own_vsock_mux_dir: None,
            skip_cleanup: true,
            // image_meta is filled in via Image's ensure_pool path;
            // when the pool is explicitly built we don't have
            // direct access to the source Image, so leave it None
            // and rely on `Vm::start`-derived snapshots if the
            // user wants `PooledVm::snapshot` to work. Most
            // pool-based callers don't need it.
            image_meta: Some(Arc::new(ImageMeta {
                memory_mib: self.inner.spawn_cfg.memory_mib,
                vcpus: self.inner.spawn_cfg.vcpus,
                layers: self.inner.spawn_cfg.layers.clone(),
                delta_squashfs: self.inner.spawn_cfg.delta_squashfs.clone(),
                baker_runtime_sha16: self.inner.spawn_cfg.baker_runtime_sha16.clone(),
                tsi_token: self.inner.spawn_cfg.tsi_token.clone(),
                egress_policy: self.inner.spawn_cfg.egress_policy.clone(),
                pre_exec_sync: self.inner.spawn_cfg.pre_exec_sync,
            })),
        };
        Ok(PooledVm {
            vm: Some(vm),
            worker: Some(worker),
            pool_arc: Arc::clone(&self.inner),
            _image: std::marker::PhantomData,
        })
    }

    /// Acquire a VM (Linux/KVM). Pops a pre-warmed idle VM (~µs) when one is
    /// ready, else restores a fresh one from the warm snapshot (~ms). Triggers a
    /// background refill back to `min`. Torn down + replaced when the
    /// [`PooledVm`] drops. The warming happened once at [`PoolBuilder::build`].
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn acquire(&self) -> Result<PooledVm<'_>, Error> {
        // No-virt sentry pool: admission-gate + cheap checkout against the
        // shared exec server (no idle queue / restore).
        if let Some(sp) = &self.sentry {
            return sp.acquire();
        }
        if let Some(rp) = &self.runsc {
            let vm = rp.acquire_vm()?;
            return Ok(PooledVm {
                vm: Some(vm),
                pool: None,
                sentry_pool: None,
                runsc_pool: Some(Arc::clone(rp)),
                kboxlike_pool: None,
                sentry_mem_admission: None,
                sentry_running_pid: None,
                sentry_running_pool: None,
                sentry_stop_running_pool_on_drop: false,
                sentry_touched: AtomicBool::new(false),
                _image: std::marker::PhantomData,
            });
        }
        if let Some(kp) = &self.kboxlike {
            let vm = kp.take_or_restore()?;
            return Ok(PooledVm {
                vm: Some(vm),
                pool: None,
                sentry_pool: None,
                runsc_pool: None,
                kboxlike_pool: Some(Arc::clone(kp)),
                sentry_mem_admission: None,
                sentry_running_pid: None,
                sentry_running_pool: None,
                sentry_stop_running_pool_on_drop: false,
                sentry_touched: AtomicBool::new(false),
                _image: std::marker::PhantomData,
            });
        }
        let kvm = self.kvm.as_ref().expect("Pool has no backend");
        let vm = kvm.take_or_restore()?;
        // No background refill on acquire: BOTH release modes now return the VM
        // ALIVE to idle (isolated = in-place reset, dirty = as-is), so the
        // checked-out VM IS its own replenishment. A refill here would restore
        // redundant fresh VMs during the checkout window and grow the pool past
        // its working set. The only teardown is the reset-failure fallback in
        // PooledVm::drop, which refills itself.
        Ok(PooledVm {
            vm: Some(vm),
            pool: Some(Arc::clone(kvm)),
            sentry_pool: None,
            runsc_pool: None,
            kboxlike_pool: None,
            sentry_mem_admission: None,
            sentry_running_pid: None,
            sentry_running_pool: None,
            sentry_stop_running_pool_on_drop: false,
            sentry_touched: AtomicBool::new(false),
            _image: std::marker::PhantomData,
        })
    }

    /// Snapshot of current pool state.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn stats(&self) -> PoolStats {
        let s = self.inner.state.lock().ok();
        let alive = s.as_ref().map(|s| s.alive).unwrap_or(0);
        let idle = s.as_ref().map(|s| s.idle.len()).unwrap_or(0);
        let waiting = s.as_ref().map(|s| s.waiting).unwrap_or(0);
        let reaped = s.as_ref().map(|s| s.reaped).unwrap_or(0);
        let observed_footprint_mib = self
            .inner
            .spawn_cfg
            .observed_footprint_mib
            .load(std::sync::atomic::Ordering::Relaxed);
        PoolStats {
            alive,
            in_use: alive.saturating_sub(idle),
            idle,
            waiting,
            max: self.inner.policy.max,
            min: self.inner.policy.min,
            reaped,
            observed_footprint_mib,
        }
    }

    /// Snapshot of current pool state (Linux/KVM): idle = pre-warmed VMs ready,
    /// alive = idle + checked-out, against the configured min/max.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn stats(&self) -> PoolStats {
        // Sentry pool: one always-warm shared server, no idle queue. `alive` =
        // concurrent checkouts, `max` = admission cap, `min`/`idle` = 0.
        if let Some(sp) = &self.sentry {
            let alive = sp.admission.alive();
            return PoolStats {
                alive,
                in_use: alive,
                idle: 0,
                waiting: 0,
                max: sp.admission.max,
                min: 0,
                reaped: 0,
                observed_footprint_mib: 0,
            };
        }
        if let Some(rp) = &self.runsc {
            let alive = rp.admission.alive();
            let idle = rp.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
            let in_use = rp.checked_out.load(std::sync::atomic::Ordering::SeqCst);
            return PoolStats {
                alive,
                in_use,
                idle,
                waiting: 0,
                max: rp.max,
                min: rp.min,
                reaped: 0,
                observed_footprint_mib: 0,
            };
        }
        if let Some(kp) = &self.kboxlike {
            let alive = kp.admission.alive();
            let idle = kp.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
            return PoolStats {
                alive,
                in_use: alive.saturating_sub(idle),
                idle,
                waiting: 0,
                max: kp.admission.max,
                min: kp.min,
                reaped: 0,
                observed_footprint_mib: 0,
            };
        }
        let kvm = self.kvm.as_ref().expect("Pool has no backend");
        let idle = kvm.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
        let alive = kvm.admission.alive();
        PoolStats {
            alive,
            in_use: alive.saturating_sub(idle),
            idle,
            waiting: 0,
            max: kvm.admission.max,
            min: kvm.min,
            reaped: 0,
            observed_footprint_mib: 0,
        }
    }

    /// Sentry-only: cumulative exec metrics for a **warm-daemon** pool — exec
    /// count, ok/nonzero/seccomp/signal/timeout/error tallies, and mean/max exec
    /// latency. Additive observability on top of the hypervisor-agnostic
    /// [`stats`](Self::stats) (which reports admission/liveness only). Returns
    /// `None` for non-sentry pools and for the sentry exec-server backend (no
    /// per-pool counter). The counters are cumulative over the pool's lifetime.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn sentry_stats(&self) -> Option<crate::sentry::SentryPoolStats> {
        self.sentry.as_ref().and_then(|sp| sp.sentry_stats())
    }

    /// Shut the pool down explicitly. Wakes any caller blocked on
    /// [`Self::acquire`] — they receive
    /// `Err(Error::vm_msg("pool is shutting down"))` instead of
    /// hanging — drains idle + dirty workers, and stops
    /// housekeeping threads.
    ///
    /// Idempotent. Pool is also shut down on Drop, so explicit
    /// `shutdown()` is for embedders who want a synchronous "the
    /// pool is gone now" guarantee (e.g. before re-creating a pool
    /// at a different size, or before a process-shutdown hook).
    pub fn shutdown(&self) {
        // macOS: tear down the worker pool. Linux/KVM no-reuse pool holds no
        // shared resources — per-acquire VMs stop when their PooledVm drops.
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        self.inner.shutdown_pool();
        // Linux/KVM: mark the pool dead so subsequent acquires error (instead of
        // restoring fresh VMs) + drain any pre-warmed idle VMs now.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        {
            if let Some(sp) = &self.sentry {
                // Sentry: refuse new acquires; the shared exec server is torn
                // down (socket unlinked + workload killed) immediately when no
                // checkout is active, or by the last `PooledVm` release when a
                // shutdown races an active checkout. This mirrors the API
                // contract on KVM/HVF: explicit shutdown frees host resources
                // without waiting for JS GC to drop the Pool object.
                sp.shut_down
                    .store(true, std::sync::atomic::Ordering::SeqCst);
                sp.admission.notify();
                sp.cleanup_if_shutdown_idle();
            } else if let Some(kvm) = &self.kvm {
                kvm.shut_down
                    .store(true, std::sync::atomic::Ordering::SeqCst);
                let drained: Vec<Vm> =
                    std::mem::take(&mut *kvm.idle.lock().unwrap_or_else(|e| e.into_inner()));
                for vm in drained {
                    kvm.admission.release();
                    drop(vm);
                }
            } else if let Some(runsc) = &self.runsc {
                runsc
                    .shut_down
                    .store(true, std::sync::atomic::Ordering::SeqCst);
                let drained: Vec<Vm> =
                    std::mem::take(&mut *runsc.idle.lock().unwrap_or_else(|e| e.into_inner()));
                for vm in drained {
                    drop(vm);
                    runsc.admission.release();
                }
                runsc.admission.notify();
            } else if let Some(kboxlike) = &self.kboxlike {
                kboxlike
                    .shut_down
                    .store(true, std::sync::atomic::Ordering::SeqCst);
                let drained: Vec<Vm> =
                    std::mem::take(&mut *kboxlike.idle.lock().unwrap_or_else(|e| e.into_inner()));
                for vm in drained {
                    drop(vm);
                    kboxlike.admission.release();
                }
                kboxlike.admission.notify();
            }
        }
    }
}

///
/// Every setter that affects the workload's behavior (env, cmd,
/// memory, guest_port) is part of the bake's input fingerprint:
/// changing it forces a re-bake and produces a different snapshot.
/// Use distinct `with_name` values if you want side-by-side
/// snapshots for the same image ref with different configs.
pub struct OciImageBuilder {
    image: String,
    name: Option<String>,
    pull_policy: PullPolicy,
    memory_mib: Option<u32>,
    vcpus: Option<u32>,
    guest_port: Option<u16>,
    cmd: Option<Vec<String>>,
    envs: Vec<(String, String)>,
    snapshots_dir: Option<PathBuf>,
    /// Optional post-bake warmup. After the bake completes, we
    /// `acquire` a VM from the freshly-baked image, run this
    /// closure, snapshot the post-warmup state, and replace
    /// `restore.snap` with the warm version. Subsequent acquires
    /// land at warm state — guest page cache for the workload
    /// already populated, so e.g. `rustc` cold-start drops from
    /// ~370 ms to ~50–100 ms.
    warmup: Option<Box<dyn FnOnce(&Vm) -> Result<(), Error> + Send>>,
    /// Stable tag for the warmup. Folded into the snapshot's
    /// cached fingerprint so changing the warmup invalidates the
    /// previous warm snapshot. If `None`, warmup is treated as
    /// "any change re-runs" (we use a hash-of-empty as the tag,
    /// which is stable across runs).
    warmup_tag: Option<String>,
    /// Bake for the warm-daemon pool backend (sentry only): forwarded into
    /// `SentryBakeConfig.warm_daemon` in `build_kvm`. No effect on KVM/HVF.
    warm_daemon: bool,
    /// Extra files to stage into the bake's delta layer at
    /// (host_path, guest_path). Useful for bundling per-snapshot
    /// auxiliary binaries (e.g. the snapshot-park kernel module
    /// at `/supermachine-smpark.ko`). Plumbed through to the
    /// bake step as `--extra-file <host>:<guest>` args.
    extra_files: Vec<(PathBuf, String)>,
    /// virtio-fs DAX mounts: (host_path, guest_tag, symlink_policy)
    /// (host_path, guest_tag, symlinks_policy, guest_path). Each is
    /// exposed as a virtio-fs device and auto-mounted by init-oci
    /// at `guest_path` before the workload starts. Plumbed through
    /// to the bake step as
    /// `--mount <host>:<tag>:<guest_path>[:<policy>]` args.
    mounts: Vec<(
        PathBuf,
        String,
        crate::vmm::resources::SymlinkPolicy,
        String,
    )>,
    /// Writable virtio-blk volumes: (host_path, guest_path, size_bytes).
    /// Each is plumbed through to the bake step as
    /// `--volume <host>:<guest>:<size_bytes>` args. Use for
    /// dependency caches (`node_modules`, `target/`) — block I/O
    /// bypasses the FUSE round-trip cost that makes `mounts` slow
    /// for thousands-of-small-files write workloads. Volume bytes
    /// persist across restores; only the mapping is snapshotted.
    volumes: Vec<(PathBuf, String, u64)>,
    /// Extra kernel cmdline tokens appended after supermachine's
    /// own defaults. Power-user / testing escape hatch — e.g.
    /// `["init=/nope"]` in a test to deliberately trigger an
    /// early-init kernel panic. Plumbed through to the bake step
    /// as `--cmdline-extra <token>` args.
    extra_kernel_args: Vec<String>,
    /// When `Some(true)`, the no-warmup `.build()` path waits for
    /// the workload's listener to come up (or the parked-PID-1
    /// fallback) before capturing — same as v0.4.22 behavior.
    /// When `None` or `Some(false)`, the default v0.4.23+ behavior
    /// applies: snapshot is captured at the pre-exec marker, BEFORE
    /// the workload runs. Each restore re-execs the workload fresh.
    ///
    /// Set to `true` for service-image bakes (nginx, redis, etc.)
    /// where you want the listener pre-bound at restore time but
    /// don't want the cost of routing through `with_warmup`. See
    /// [`OciImageBuilder::with_listener_required`].
    require_listener: Option<bool>,
    /// Target platform, in Docker `--platform` form. `None` →
    /// default `"linux/arm64"`. Setting `"linux/amd64"` makes the
    /// bake pull the amd64 variant of multi-arch images AND
    /// auto-mounts Apple's Rosetta runtime share, giving the
    /// guest end-to-end amd64-binary execution via the M1
    /// Rosetta-in-VM plumbing. See
    /// `docs/design/chrome-amd64-gap-2026-05-17.md`.
    pub(crate) platform: Option<String>,
}

impl OciImageBuilder {
    /// Start a new builder for `image_ref` (e.g. `"nginx:1.27-alpine"`,
    /// `"ghcr.io/owner/image@sha256:..."`).
    pub fn new(image_ref: impl Into<String>) -> Self {
        Self {
            image: image_ref.into(),
            name: None,
            pull_policy: PullPolicy::default(),
            memory_mib: None,
            vcpus: None,
            guest_port: None,
            cmd: None,
            envs: Vec::new(),
            snapshots_dir: None,
            warmup: None,
            warmup_tag: None,
            warm_daemon: false,
            extra_files: Vec::new(),
            mounts: Vec::new(),
            volumes: Vec::new(),
            extra_kernel_args: Vec::new(),
            require_listener: None,
            platform: None,
        }
    }

    /// Set the target platform for the image pull, in Docker
    /// `--platform` form. Two values are supported:
    ///
    ///   - `"linux/arm64"` (default) — native arm64 path, full perf.
    ///   - `"linux/amd64"` — pull the amd64 variant from a multi-
    ///     arch index; auto-mount Apple's Rosetta runtime share at
    ///     `/run/rosetta`; register binfmt_misc so amd64 ELFs run
    ///     via Rosetta translation. Requires Rosetta to be
    ///     installed on the host (`softwareupdate --install-rosetta`).
    ///
    /// ```no_run
    /// # use supermachine::Image;
    /// let img = Image::builder("mcr.microsoft.com/playwright:v1.59.1-jammy")
    ///     .with_platform("linux/amd64")
    ///     .with_memory_mib(4096)
    ///     .build()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_platform(mut self, platform: impl Into<String>) -> Self {
        self.platform = Some(platform.into());
        self
    }

    /// Stage `host_path` into the snapshot's delta layer at
    /// `guest_path`. The file appears at `guest_path` inside the
    /// guest's root filesystem after restore. Folded into the
    /// bake's content hash so changing the host file invalidates
    /// the cached snapshot.
    ///
    /// Used (e.g.) to ship `supermachine-smpark.ko` so init-oci
    /// can `finit_module` it on boot for multi-vCPU snapshot
    /// support.
    pub fn with_extra_file(
        mut self,
        host_path: impl Into<PathBuf>,
        guest_path: impl Into<String>,
    ) -> Self {
        self.extra_files.push((host_path.into(), guest_path.into()));
        self
    }

    /// Override where the image bytes come from. Default (when not
    /// called) treats the constructor's `image_ref` as a registry
    /// reference and pulls from Docker Hub or the registry encoded
    /// in the ref.
    ///
    /// Internally the bake driver also accepts `image_ref` directly
    /// in the prefixed forms `oci-archive:/path` and `oci-layout:/path`;
    /// this method is the structured convenience: the user keeps
    /// `image_ref` as a logical identifier (used to derive the
    /// snapshot dir name) and points at a local source separately.
    ///
    /// ```no_run
    /// # use supermachine::{Image, PullPolicy};
    /// let img = Image::builder("shopify-test-sm:latest")
    ///     .with_oci_archive("/tmp/shopify.tar")
    ///     .build()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_oci_archive(mut self, archive_path: impl Into<PathBuf>) -> Self {
        // Rewrite image ref to the prefix form; keep the user's
        // original ref as the auto-derived snapshot name if no
        // explicit `with_name` was called.
        let path: PathBuf = archive_path.into();
        let original =
            std::mem::replace(&mut self.image, format!("oci-archive:{}", path.display()));
        if self.name.is_none() {
            self.name = Some(crate::bake::snapshot_name_for_image(&original));
        }
        self
    }

    /// Like `with_oci_archive` but points at an OCI layout DIRECTORY
    /// (the un-tar'd form, with `index.json` + `oci-layout` +
    /// `blobs/sha256/...` at the top level).
    pub fn with_oci_layout(mut self, layout_dir: impl Into<PathBuf>) -> Self {
        let path: PathBuf = layout_dir.into();
        let original = std::mem::replace(&mut self.image, format!("oci-layout:{}", path.display()));
        if self.name.is_none() {
            self.name = Some(crate::bake::snapshot_name_for_image(&original));
        }
        self
    }

    /// Expose a host directory to the guest via virtio-fs (with DAX).
    /// The guest mounts it inside `init-oci` as
    /// `mount -t virtiofs <tag> <target>` — by convention init-oci
    /// mounts each declared tag at `/mnt/<tag>` and bind-mounts into
    /// the workload's filesystem if a per-image policy says so.
    ///
    /// Reads from the guest land in the host's page cache (DAX-mapped
    /// via `hv_vm_map`; validated by spike 22 to be zero-copy + shared
    /// across VMs that mount the same host path). The mount is added
    /// to the bake's input hash so changing `host_path` invalidates the
    /// cached snapshot.
    ///
    /// ```no_run
    /// # use supermachine::Image;
    /// let img = Image::builder("node:22-alpine")
    ///     .with_mount("/Users/me/myapp", "myapp", "/workspace")
    ///     .with_cmd(["node", "/workspace/index.js"])
    ///     .build()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_mount(
        mut self,
        host_path: impl Into<PathBuf>,
        guest_tag: impl Into<String>,
        guest_path: impl Into<String>,
    ) -> Self {
        self.mounts.push((
            host_path.into(),
            guest_tag.into(),
            crate::vmm::resources::SymlinkPolicy::default(),
            guest_path.into(),
        ));
        self
    }

    /// Like [`Self::with_mount`] but lets the caller pick a
    /// [`SymlinkPolicy`] other than the default `Opaque`.
    pub fn with_mount_symlinks(
        mut self,
        host_path: impl Into<PathBuf>,
        guest_tag: impl Into<String>,
        guest_path: impl Into<String>,
        symlinks: crate::vmm::resources::SymlinkPolicy,
    ) -> Self {
        self.mounts.push((
            host_path.into(),
            guest_tag.into(),
            symlinks,
            guest_path.into(),
        ));
        self
    }

    /// Attach a writable virtio-blk volume backing `host_path`
    /// (created sparse if missing, sized to `spec.size_bytes`) at
    /// `spec.guest_path` inside the guest. Use for dependency caches
    /// and other write-heavy paths where you'd otherwise pay the
    /// per-file FUSE round-trip cost of `with_mount`.
    ///
    /// # Durability contract (both backends)
    ///
    /// Volume writes become durable at **snapshot capture**: the bake
    /// (boot + warmup) writes the volume's canonical content, and every
    /// capture records the volume's at-capture bytes alongside the
    /// snapshot (macOS: a clonefile "pristine"; KVM: a materialized
    /// `volumes/<i>.img`) so restores always see content that matches
    /// the captured ext4-in-RAM state.
    ///
    /// Writes made **during a pool cycle are ephemeral by design**:
    /// each acquired VM works on an isolated copy-on-write view
    /// (macOS: per-worker temp clone; KVM: MAP_PRIVATE pages reset in
    /// O(1) on release), so the next acquire sees capture-time bytes
    /// again — never another cycle's half-written state. To persist a
    /// cycle's volume writes, snapshot the VM (`vm.snapshot(...)`)
    /// before releasing it.
    ///
    /// ```no_run
    /// # use supermachine::{Image, VolumeSpec};
    /// let img = Image::builder("node:22-alpine")
    ///     .with_volume(VolumeSpec::new("/var/cache/sm/node_modules.img",
    ///                                  "/work/node_modules"))
    ///     .build()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_volume(mut self, spec: crate::vmm::resources::VolumeSpec) -> Self {
        self.volumes.push((
            PathBuf::from(spec.host_path),
            spec.guest_path,
            spec.size_bytes,
        ));
        self
    }

    /// Append one extra token to the kernel cmdline. Tokens go
    /// after supermachine's own defaults (`earlycon`, `console`,
    /// `tsi_hijack`, etc.) but before the per-bake `tsi_token`.
    /// Power-user / testing escape hatch — e.g. pass
    /// `"init=/nope"` in a test to deliberately trigger an
    /// early-init kernel panic, or `"panic=1"` to make the
    /// kernel panic on any warning. Folded into the bake's input
    /// hash so changing this re-bakes.
    pub fn with_extra_kernel_arg(mut self, arg: impl Into<String>) -> Self {
        self.extra_kernel_args.push(arg.into());
        self
    }

    /// Override the number of vCPUs the snapshot is baked with.
    /// Default `1`. Multi-vCPU is opt-in: it lifts sustained
    /// HTTP-serving throughput (single-vCPU is the c=32+
    /// bottleneck) at the cost of slightly higher cold boot and
    /// some snapshot/restore caveats. See
    /// docs/design/concurrency-floor-2026-05-04.md.
    pub fn with_vcpus(mut self, vcpus: u32) -> Self {
        self.vcpus = Some(vcpus);
        self
    }

    /// Snapshot name. Default: derived from the image ref via
    /// `bake::snapshot_name_for_image`. Use this when you want
    /// `nginx:1.27-alpine` baked twice with different configs.
    pub fn with_name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Cache + registry policy. See [`PullPolicy`].
    pub fn with_pull_policy(mut self, policy: PullPolicy) -> Self {
        self.pull_policy = policy;
        self
    }

    /// Override the bake-time memory budget (MiB). The runtime
    /// memory is set on [`VmConfig`]; this is the size the
    /// snapshot is captured at.
    pub fn with_memory_mib(mut self, mib: u32) -> Self {
        self.memory_mib = Some(mib);
        self
    }

    /// Override the guest service port the bake waits for as the
    /// readiness signal. Default `80`.
    pub fn with_guest_port(mut self, port: u16) -> Self {
        self.guest_port = Some(port);
        self
    }

    /// Override the image's `CMD`. Pass an argv array, same shape
    /// as Docker's `--entrypoint` + arguments combined.
    ///
    /// ```no_run
    /// # use supermachine::Image;
    /// let img = Image::builder("python:3.12-alpine")
    ///     .with_cmd(["python", "-m", "http.server", "8080"])
    ///     .with_guest_port(8080)
    ///     .build()?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_cmd<I, S>(mut self, cmd: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.cmd = Some(cmd.into_iter().map(Into::into).collect());
        self
    }

    /// Add an environment variable for the workload. Repeatable.
    /// Mirrors `docker run -e KEY=VAL`.
    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.envs.push((key.into(), value.into()));
        self
    }

    /// Override the directory snapshots are stored in. Default
    /// is `~/.local/supermachine-snapshots`. Use this to keep
    /// per-project snapshot stores isolated from each other.
    pub fn with_snapshots_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.snapshots_dir = Some(dir.into());
        self
    }

    /// Run a warmup closure once after the bake, then re-snapshot
    /// the post-warmup state. Future restores from this image
    /// land at the warm state — guest page cache for the
    /// workload already populated, so e.g. compiling a small
    /// Rust program drops from ~370 ms to ~50–100 ms.
    ///
    /// Cached: if the warm snapshot already exists with a
    /// matching warmup tag (see [`Self::with_warmup_tag`]), the
    /// warmup is skipped on subsequent builds. Without an
    /// explicit tag, the warmup re-runs whenever the snapshot
    /// is invalidated by other inputs (image_ref, memory, etc.)
    /// — set a tag if you change the closure body and want the
    /// cache to invalidate.
    ///
    /// ```no_run
    /// # use std::time::Duration;
    /// # use supermachine::Image;
    /// let image = Image::ensure_baked("rust_warm", "rust:1-slim", |b| b
    ///     .with_memory_mib(2048)
    ///     .with_warmup(|vm| {
    ///         vm.write_file("/tmp/probe.rs", b"fn main(){}")?;
    ///         vm.exec_builder()
    ///             .argv(["sh", "-c", "rustc -O /tmp/probe.rs -o /tmp/probe && /tmp/probe"])
    ///             .timeout(Duration::from_secs(60))
    ///             .output()?;
    ///         Ok(())
    ///     })
    ///     .with_warmup_tag("v1")
    /// )?;
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_warmup<F>(mut self, warmup: F) -> Self
    where
        F: FnOnce(&Vm) -> Result<(), Error> + Send + 'static,
    {
        self.warmup = Some(Box::new(warmup));
        self
    }

    /// Stable tag for the warmup closure (see [`Self::with_warmup`]).
    /// Bump when you change the warmup body and want the previously
    /// cached warm snapshot invalidated.
    pub fn with_warmup_tag(mut self, tag: impl Into<String>) -> Self {
        self.warmup_tag = Some(tag.into());
        self
    }

    /// Bake this image for the warm-daemon pool backend (sentry only):
    /// `Image::builder(x).with_warm_daemon().build().pool().acquire()` warms a
    /// PERSISTENT sentry cell at the workload's SENTINEL checkpoint and forks a
    /// detached daemon per acquire; the acquired `Vm`'s `exec` runs client cells
    /// that reach the warm daemon over the supervisor-global owned loopback.
    ///
    /// The workload (the image's Cmd/Entrypoint) MUST checkpoint itself once it is
    /// ready to serve (`syscall(0x5359)`), exactly like a listening daemon — else
    /// `.pool().build()` blocks waiting for the checkpoint. Snapshotting a
    /// warm-daemon pool is refused (the warm state is a live cell, not the rootfs).
    pub fn with_warm_daemon(mut self) -> Self {
        self.warm_daemon = true;
        self
    }

    /// For the no-warmup `.build()` path, wait for the workload's
    /// in-guest listener to come up before capturing the snapshot
    /// (the v0.4.22 behavior). Without this, v0.4.23+ defaults to
    /// the `pre-exec` trigger which captures BEFORE the workload
    /// runs — fast bake, but each restore re-execs the workload
    /// fresh (workload's own startup time is paid per acquire).
    ///
    /// **When to call this:**
    ///
    /// - **You're baking a service image** (nginx, redis, postgres,
    ///   anything that binds a listener and stays up) AND you want
    ///   the listener pre-bound at restore-time so first acquire's
    ///   port-traffic works immediately, but you don't want to pay
    ///   the cost of routing through `with_warmup` (which adds a
    ///   warm-snapshot round-trip).
    ///
    /// **When NOT to call this:**
    ///
    /// - **You're using `vm.exec(...)`** for arbitrary commands and
    ///   don't care about the workload's listener. Default (pre-exec
    ///   trigger) is faster and gives you the same agent behavior.
    ///
    /// - **The workload doesn't bind a listener and doesn't exit
    ///   quickly** (e.g. a long-running daemon that never serves a
    ///   port). With `require_listener=true` you'd time out at
    ///   `--snapshot-after-ms` (~7 s default). The pre-exec trigger
    ///   handles this case in ~150 ms regardless.
    ///
    /// No effect when `with_warmup` is also set — the warmup path
    /// always uses listener-ready (or the warmup callback would run
    /// against a not-yet-ready guest).
    pub fn with_listener_required(mut self) -> Self {
        self.require_listener = Some(true);
        self
    }

    /// Run the bake (or reuse a cached snapshot per
    /// `with_pull_policy`) and return the resulting [`Image`].
    ///
    /// Dispatches by backend: KVM (Linux/x86_64) bakes through the
    /// self-contained KVM pipeline; HVF (macOS/aarch64) through the worker
    /// pipeline. The cache / pull-policy / version-skew handling is shared.
    pub fn build(self) -> Result<Image, Error> {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        {
            self.build_kvm()
        }
        #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
        {
            self.build_native()
        }
    }

    /// Linux/x86_64 bake. Routes through runsc by default for now, with explicit
    /// `SUPERMACHINE_BACKEND=kvm` / `sentry` overrides for backend bring-up.
    ///
    /// When backend selection resolves to the no-virt **runsc** or **sentry**
    /// backend, the builder's run-config
    /// (`with_cmd` / `with_env` / `with_mount` / `with_volume` /
    /// `with_extra_file` / `with_warmup`) IS applied — routed through
    /// [`Image::bake_sentry_with_config`] instead of the config-less
    /// `from_oci_to_dir` → `bake_sentry_from_ref` tail. (On the KVM backend
    /// only volumes are applied; the rest are still ignored with a warning.)
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn build_kvm(self) -> Result<Image, Error> {
        let snapshots_dir = self
            .snapshots_dir
            .clone()
            .unwrap_or_else(default_snapshots_dir);
        let derived_name = self
            .name
            .clone()
            .unwrap_or_else(|| crate::bake::snapshot_name_for_image(&self.image));
        let snap_dir = snapshots_dir.join(&derived_name);

        // Backend selection — same as `from_oci_to_dir`: Linux defaults to
        // runsc for now; explicit SUPERMACHINE_BACKEND can force kvm/sentry/
        // kboxlike.
        let backend = linux_default_backend();
        let use_runsc = backend == "runsc";
        let use_sentry = backend == "sentry";
        let use_kboxlike = backend == "kboxlike";
        if use_sentry || use_runsc || use_kboxlike {
            let backend_name = if use_runsc {
                "runsc"
            } else if use_kboxlike {
                "kboxlike"
            } else {
                "sentry"
            };
            let has_warmup = self.warmup.is_some();
            let warmup_tag = self.warmup_tag.as_deref().unwrap_or("default");
            let no_virt_snap_dir = if has_warmup {
                snapshots_dir.join(format!("{}__warm__{}", derived_name, warmup_tag))
            } else {
                snap_dir.clone()
            };
            let requested_mounts: Vec<crate::vmm::resources::MountSpec> = self
                .mounts
                .iter()
                .map(|(host, tag, policy, guest_path)| {
                    crate::vmm::resources::MountSpec::new(
                        host.to_string_lossy().to_string(),
                        tag.clone(),
                        guest_path.clone(),
                    )
                    .with_symlinks(policy.clone())
                })
                .collect();
            let cfg = SentryBakeConfig {
                memory_mib: self.memory_mib,
                vcpus: self.vcpus,
                cmd: self.cmd,
                env: self.envs,
                working_dir: None,
                user: None,
                extra_files: self.extra_files,
                mounts: self.mounts,
                volumes: self.volumes,
                warmup: self.warmup,
                // Footgun fix: the sentry branch previously DROPPED warmup_tag (the
                // struct had no field) and had no warm-daemon flag, so both were lost
                // before the bake ran. Thread them through now.
                warmup_tag: self.warmup_tag,
                warm_daemon: self.warm_daemon,
            };
            let no_virt_bake_key = sentry_bake_cache_key(&self.image, &cfg)?;
            // Cache fast-path for PullPolicy::Missing: reuse a loadable no-virt
            // snapshot only when the builder inputs match. Artifacts are named by
            // `with_name`, so without this key a changed extra file/env/cmd could
            // silently serve an old rootfs under the same name.
            if self.pull_policy == PullPolicy::Missing {
                if let Ok(img) = Image::from_snapshot(&no_virt_snap_dir) {
                    let backend_matches = if use_runsc {
                        img.runsc.is_some()
                    } else if use_kboxlike {
                        img.kboxlike.is_some()
                    } else {
                        img.sentry.is_some()
                    };
                    if backend_matches {
                        let meta_matches = std::fs::read(no_virt_snap_dir.join("metadata.json"))
                            .ok()
                            .and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
                            .map(|m| {
                                let key_matches = m
                                    .get(if use_runsc {
                                        "runsc_bake_key"
                                    } else if use_kboxlike {
                                        "kboxlike_bake_key"
                                    } else {
                                        "sentry_bake_key"
                                    })
                                    .or_else(|| m.get("sentry_bake_key"))
                                    .and_then(|v| v.as_str())
                                    == Some(no_virt_bake_key.as_str());
                                let memory_matches = match cfg.memory_mib {
                                    Some(expected) => {
                                        m.get("memory_mib").and_then(|v| v.as_u64())
                                            == Some(expected as u64)
                                    }
                                    None => true,
                                };
                                let vcpus_matches = match cfg.vcpus {
                                    Some(expected) => {
                                        m.get("vcpus").and_then(|v| v.as_u64())
                                            == Some(expected as u64)
                                    }
                                    None => true,
                                };
                                key_matches && memory_matches && vcpus_matches
                            })
                            == Some(true);
                        if !meta_matches {
                            invalidate_stale_snapshot_tree(&no_virt_snap_dir);
                        } else if has_warmup && img.mounts != requested_mounts {
                            eprintln!(
                                "supermachine: {backend_name} warm snapshot at {} was baked with mounts \
                                 {:?} but this build requests {:?}. Auto-rebaking with the current mounts.",
                                no_virt_snap_dir.display(),
                                img.mounts,
                                requested_mounts,
                            );
                            invalidate_stale_snapshot_tree(&no_virt_snap_dir);
                        } else {
                            return Ok(img);
                        }
                    }
                }
            }
            if use_runsc {
                return Image::bake_runsc_with_config(&self.image, &no_virt_snap_dir, cfg);
            }
            if use_kboxlike {
                return Image::bake_kboxlike_with_config(&self.image, &no_virt_snap_dir, cfg);
            }
            return Image::bake_sentry_with_config(&self.image, &no_virt_snap_dir, cfg);
        }

        // KVM backend: build through the in-process KVM runtime. The old native
        // worker pipeline is macOS/HVF-only on Linux, so routing KVM warmup
        // through it wedges waiting for a worker control socket.
        self.build_kvm_in_process(snapshots_dir, derived_name, snap_dir)
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn build_kvm_in_process(
        self,
        snapshots_dir: PathBuf,
        derived_name: String,
        snap_dir: PathBuf,
    ) -> Result<Image, Error> {
        if self.platform.as_deref().is_some_and(|p| p != "linux/amd64") {
            return Err(Error::bake_msg(format!(
                "KVM backend on linux/x86_64 only supports platform=linux/amd64, got {}",
                self.platform.as_deref().unwrap_or_default()
            )));
        }

        let has_warmup = self.warmup.is_some();
        let tag = self.warmup_tag.as_deref().unwrap_or("default");
        let output_dir = if has_warmup {
            snapshots_dir.join(format!("{}__warm__{}", derived_name, tag))
        } else {
            snap_dir.clone()
        };
        let requested_vcpus = self.vcpus.unwrap_or(1);
        let effective_vcpus = kvm_effective_vcpus(requested_vcpus, "Image.build");
        let key_config = SentryBakeConfig {
            memory_mib: self.memory_mib,
            vcpus: Some(effective_vcpus),
            cmd: self.cmd.clone(),
            env: self.envs.clone(),
            working_dir: None,
            user: None,
            extra_files: self.extra_files.clone(),
            mounts: self.mounts.clone(),
            volumes: self.volumes.clone(),
            warmup: if has_warmup {
                Some(Box::new(|_| Ok(())))
            } else {
                None
            },
            warm_daemon: self.warm_daemon,
            warmup_tag: self.warmup_tag.clone(),
        };
        let kvm_bake_key = sentry_bake_cache_key(&self.image, &key_config)?;

        match self.pull_policy {
            PullPolicy::Never => {
                let image = Image::from_snapshot(&output_dir)?;
                if kvm_bake_key_matches(&output_dir, &kvm_bake_key) {
                    return Ok(image);
                }
                return Err(Error::cache_invalid(format!(
                    "cached KVM snapshot at {} was baked with different builder inputs \
                     (PullPolicy::Never won't auto-rebake)",
                    output_dir.display()
                )));
            }
            PullPolicy::Missing => {
                if let Ok(image) = Image::from_snapshot(&output_dir) {
                    if kvm_bake_key_matches(&output_dir, &kvm_bake_key) {
                        return Ok(image);
                    }
                    invalidate_stale_snapshot_tree(&snap_dir);
                    if output_dir != snap_dir {
                        let _ = std::fs::remove_dir_all(&output_dir);
                    }
                }
            }
            PullPolicy::Always => {
                invalidate_stale_snapshot_tree(&snap_dir);
                if output_dir != snap_dir {
                    let _ = std::fs::remove_dir_all(&output_dir);
                }
            }
        }

        let cold_dir = snap_dir.clone();
        std::fs::create_dir_all(&cold_dir).map_err(Error::Io)?;
        let (kernel, initrd) = Image::extract_bundled_kvm_boot_assets(&cold_dir)?;
        let pull_work = cold_dir.join("oci-pull");
        let (layers, image_workload, oci_config) =
            crate::bake::pull_oci_layers(&self.image, "amd64", &pull_work)
                .map_err(Error::bake_msg)?;
        let (workload, env_pairs, effective_workdir, effective_user) = effective_builder_workload(
            image_workload,
            oci_config.as_ref(),
            self.cmd.as_ref(),
            &self.envs,
        );

        let rootfs = cold_dir.join("rootfs.squashfs");
        let stage = cold_dir.join("rootfs-stage");
        let _ = std::fs::remove_dir_all(&stage);
        crate::bake::build_sentry_rootfs(&layers, &stage, workload.as_deref())
            .map_err(Error::bake_msg)?;
        for (host, guest) in &self.extra_files {
            let rel = guest.trim_start_matches('/');
            let target = confined_rootfs_join(&stage, rel).ok_or_else(|| {
                Error::bake_msg(format!(
                    "extra-file guest path escapes the rootfs: {guest:?}"
                ))
            })?;
            if let Some(parent) = target.parent() {
                std::fs::create_dir_all(parent).map_err(Error::Io)?;
            }
            std::fs::copy(host, &target).map_err(|e| {
                Error::bake_msg(format!(
                    "stage extra-file {} -> {}: {e}",
                    host.display(),
                    target.display()
                ))
            })?;
        }
        crate::bake::write_kvm_rootfs_squashfs(&stage, &rootfs).map_err(Error::bake_msg)?;
        let _ = std::fs::remove_dir_all(&stage);
        let _ = std::fs::remove_dir_all(&pull_work);

        let memory_mib = self.memory_mib.unwrap_or(512);
        let vcpus = effective_vcpus;
        let phys = std::fs::metadata(&rootfs).map(|m| m.len()).unwrap_or(0)
            + std::fs::metadata(&kernel).map(|m| m.len()).unwrap_or(0)
            + std::fs::metadata(&initrd).map(|m| m.len()).unwrap_or(0);
        let mounts = builder_mount_specs(&self.mounts);
        write_kvm_cold_builder_metadata(
            &cold_dir,
            &self.image,
            &kvm_bake_key,
            &kernel,
            &initrd,
            &rootfs,
            memory_mib,
            vcpus,
            phys,
            &env_pairs,
            self.cmd.as_ref(),
            oci_config.as_ref(),
            effective_workdir.as_deref(),
            effective_user.as_deref(),
            self.warm_daemon,
            self.warmup_tag.as_deref(),
            &mounts,
            &self.volumes,
        )?;
        let cold_image = Image::from_snapshot(&cold_dir)?;

        let Some(warmup) = self.warmup else {
            return Ok(cold_image);
        };

        let vm = Vm::start(&cold_image, &VmConfig::new())?;
        let result = warmup(&vm);
        if let Err(e) = result {
            let _ = vm.stop();
            return Err(e);
        }
        let warm_image = vm.snapshot_live(&output_dir);
        let _ = vm.stop();
        let warm_image = warm_image?;
        persist_kvm_builder_run_config(
            &output_dir,
            &self.image,
            &kvm_bake_key,
            &env_pairs,
            self.cmd.as_ref(),
            oci_config.as_ref(),
            effective_workdir.as_deref(),
            effective_user.as_deref(),
            self.warm_daemon,
            self.warmup_tag.as_deref(),
            &mounts,
            &self.volumes,
        )?;
        // Re-load so the returned Image sees the post-snapshot metadata update.
        drop(warm_image);
        Image::from_snapshot(&output_dir)
    }

    /// Native hypervisor bake (macOS/HVF or Linux/KVM worker pipeline).
    fn build_native(self) -> Result<Image, Error> {
        let snapshots_dir = self.snapshots_dir.unwrap_or_else(default_snapshots_dir);
        let derived_name = self
            .name
            .clone()
            .unwrap_or_else(|| crate::bake::snapshot_name_for_image(&self.image));
        let snap_dir = snapshots_dir.join(&derived_name);

        // Cache fast-path: same as Image::from_oci_to_dir, but on
        // the builder we have to assume the cache might be stale
        // for a different config under the same name. We trust the
        // bake pipeline's input-hash check (`native_bake_key`) to
        // re-bake when the inputs changed; on cache hit it's a
        // no-op and we just load the existing snapshot.
        let cache_loadable = Image::from_snapshot(&snap_dir).is_ok();
        // Version-skew check: if the cached snapshot was baked under
        // a different supermachine binary, the init/kernel/agent
        // shipped IN the snapshot are stale and likely missing
        // recent fixes. Treat as cache-miss and silently invalidate
        // — the user sees a fresh re-bake, not a runtime warning
        // they have to act on. (For PullPolicy::Never, we keep the
        // hard error so the operator notices.)
        if cache_loadable {
            if let Some(baked) = snap_dir_baked_under_other_version(&snap_dir) {
                let current = env!("CARGO_PKG_VERSION");
                eprintln!(
                    "supermachine: snapshot at {} was baked under v{baked}; \
                     current binaries are v{current}. Auto-rebaking \
                     (this is a one-time cost on the next build after upgrade).",
                    snap_dir.display()
                );
                invalidate_stale_snapshot_tree(&snap_dir);
            }
        }
        // Re-check after potential invalidation — the dir may have
        // gone away.
        let cache_loadable = cache_loadable && snap_dir.is_dir();
        match self.pull_policy {
            PullPolicy::Never => {
                if cache_loadable {
                    return Image::from_snapshot(&snap_dir);
                }
                let restore_snap = snap_dir.join("restore.snap");
                if restore_snap.is_file() {
                    return Err(Error::cache_invalid(format!(
                        "snapshot present at {} but not loadable on this binary; \
                         rebake required (PullPolicy::Never won't auto-rebake)",
                        snap_dir.display()
                    )));
                }
                return Err(Error::cache_miss(format!(
                    "no cached snapshot for {} at {} (PullPolicy::Never)",
                    self.image,
                    snap_dir.display()
                )));
            }
            // Missing+invalid OR Always: fall through to bake. The
            // bake pipeline will short-circuit on input-hash match
            // even on Always policy.
            _ => {}
        }

        // Encode env / cmd into the form `bake::run_push` accepts.
        let mut extra_args: Vec<String> = Vec::new();
        for (k, v) in &self.envs {
            extra_args.push("--env".to_owned());
            extra_args.push(format!("{k}={v}"));
        }
        for (host, guest) in &self.extra_files {
            extra_args.push("--extra-file".to_owned());
            extra_args.push(format!("{}:{}", host.display(), guest));
        }
        // Effective mounts = user mounts + (Rosetta auto-mount when amd64).
        // For `linux/amd64` we silently add two mounts that wire up
        // Apple's Rosetta translation pipeline end-to-end:
        //
        //   1. Tag `"rosetta"` → the read-only Rosetta runtime share at
        //      `/Library/Apple/usr/libexec/oah/RosettaLinux` on the host
        //      (the `rosetta` interpreter + `rosettad` daemon). init-oci
        //      mounts this at `/run/rosetta` and registers binfmt_misc
        //      for the amd64 ELF magic.
        //
        //   2. Tag `"rosettad-cache"` → a writable host-side cache dir at
        //      `~/.cache/supermachine/rosetta-aot/`. init-oci mounts this
        //      at `/var/cache/rosettad` and starts the rosettad daemon
        //      against it. `rosettad` writes `.aotcache` files there
        //      (content-addressable by the amd64 binary's build-id), so
        //      repeated runs of the same binary skip retranslation —
        //      across VM restarts, across snapshot bakes, across
        //      separate images that happen to share binaries (the
        //      build-id keying makes cross-image sharing safe).
        //
        // If the host doesn't have Rosetta installed, error out *here*
        // with an actionable hint rather than letting the bake fail
        // downstream with a confusing "mount: no such directory".
        use crate::vmm::resources::SymlinkPolicy;
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        let effective_mounts: Vec<(PathBuf, String, SymlinkPolicy, String)> = {
            let mut effective_mounts = self.mounts.clone();
            let platform_amd64 = self.platform.as_deref() == Some("linux/amd64");
            if platform_amd64
                && !effective_mounts
                    .iter()
                    .any(|(_, tag, _, _)| tag == "rosetta")
            {
                const ROSETTA_HOST: &str = "/Library/Apple/usr/libexec/oah/RosettaLinux";
                let rosetta_path = PathBuf::from(ROSETTA_HOST);
                if !rosetta_path.join("rosetta").is_file()
                    || !rosetta_path.join("rosettad").is_file()
                {
                    return Err(Error::bake_msg(format!(
                        "platform=linux/amd64 requires Apple Rosetta runtime at \
                         {ROSETTA_HOST}; install with `softwareupdate --install-rosetta`"
                    )));
                }
                effective_mounts.push((
                    rosetta_path,
                    "rosetta".to_owned(),
                    SymlinkPolicy::Opaque,
                    "/run/rosetta".to_owned(),
                ));
            }
            // Auto-mount the persistent AOT cache for amd64. Skipped when the
            // user explicitly provided a `"rosettad-cache"` tag (escape
            // hatch for tests / sandboxed environments) or when HOME isn't
            // resolvable.
            if platform_amd64
                && !effective_mounts
                    .iter()
                    .any(|(_, tag, _, _)| tag == "rosettad-cache")
            {
                if let Some(home) = std::env::var_os("HOME") {
                    let cache_dir = PathBuf::from(home)
                        .join(".cache")
                        .join("supermachine")
                        .join("rosetta-aot");
                    if let Err(e) = std::fs::create_dir_all(&cache_dir) {
                        eprintln!(
                            "supermachine: could not create Rosetta AOT cache dir \
                             {}: {} — amd64 binaries will still run via JIT but \
                             translations won't persist across VM restarts",
                            cache_dir.display(),
                            e
                        );
                    } else {
                        effective_mounts.push((
                            cache_dir,
                            "rosettad-cache".to_owned(),
                            SymlinkPolicy::Opaque,
                            "/var/cache/rosettad".to_owned(),
                        ));
                    }
                }
            }
            effective_mounts
        };
        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
        let effective_mounts: Vec<(PathBuf, String, SymlinkPolicy, String)> = self.mounts.clone();
        for (host, tag, symlinks, guest_path) in &effective_mounts {
            extra_args.push("--mount".to_owned());
            // Encoding: `HOST:TAG:GUEST_PATH` (Opaque default policy)
            // or `HOST:TAG:GUEST_PATH:POLICY` when the user picked
            // a non-default policy. Parallel to `--volume HOST:GUEST:
            // SIZE_BYTES`. Splits left-to-right on `:`.
            let s = match symlinks {
                SymlinkPolicy::Opaque => {
                    format!("{}:{}:{}", host.display(), tag, guest_path)
                }
                SymlinkPolicy::Deny => {
                    format!("{}:{}:{}:deny", host.display(), tag, guest_path)
                }
                SymlinkPolicy::Follow => {
                    format!("{}:{}:{}:follow", host.display(), tag, guest_path)
                }
            };
            extra_args.push(s);
        }
        for (host, guest, size_bytes) in &self.volumes {
            extra_args.push("--volume".to_owned());
            // Encoding: HOST:GUEST:SIZE_BYTES. The bake-time worker
            // parses this and creates the sparse backing file +
            // attaches it as a virtio-blk to the guest.
            extra_args.push(format!("{}:{}:{}", host.display(), guest, size_bytes));
        }
        for token in &self.extra_kernel_args {
            extra_args.push("--cmdline-extra".to_owned());
            extra_args.push(token.clone());
        }
        // Signal to the bake pipeline whether the caller asked for
        // listener-required semantics. When set, the bake's
        // `--snapshot-after-ms` fallback stays at the conservative
        // 7s (room for postgres/JVM-class slow binds). When unset,
        // it drops to 200 ms — there's no listener to wait for in
        // the pre-exec / cmd-is-`sleep` / warmup-no-listener cases,
        // and the 7s wait was wasted on every bake of those shapes.
        if self.require_listener.unwrap_or(false) {
            extra_args.push("--supermachine-listener-required".to_owned());
        }
        let cmd_override = match &self.cmd {
            Some(argv) => Some(
                serde_json::to_string(argv)
                    .map_err(|e| Error::bake_msg(format!("encode cmd: {e}")))?,
            ),
            None => None,
        };

        let root = repo_root_for_bake()?;
        let request = crate::bake::BakeRequest {
            image: self.image.clone(),
            name: self.name.clone(),
            runtime: "supermachine".to_owned(),
            guest_port: self.guest_port.unwrap_or(80),
            memory_mib: self.memory_mib.unwrap_or(256),
            vcpus: self.vcpus.unwrap_or(1),
            pull_policy: self.pull_policy.as_bake_str().to_owned(),
            snapshots_dir: snapshots_dir.clone(),
            cmd_override,
            extra_args,
            platform: self.platform.clone().unwrap_or_else(default_guest_platform),
        };
        // No warmup → ALWAYS-PIPELINED-SKIP-WARM path.
        //
        // We route plain `.build()` through the pipelined-bake
        // driver with `skip_warm_snapshot=true` AND
        // `keep_alive=true`. The driver:
        //   1. Boots the worker, signals BAKE_READY.
        //   2. Issues SNAPSHOT_ASYNC for the user's snapshot —
        //      capture is fast (~5 ms), the disk write runs in a
        //      background thread on the worker.
        //   3. Skips the warm SNAPSHOT entirely (no second capture).
        //   4. Returns the live worker (BakedWorker) to us before
        //      the bg save necessarily completes. The first
        //      `Pool::acquire()` claims this worker as a pre-warm
        //      idle entry — saves ~50 ms spawn + ~5 ms restore
        //      versus a cold-from-disk worker.
        //
        // Synchronization: `save_compact_to_file` writes to
        // `<path>.partial` and atomic-renames to `<path>`, so file
        // existence ↔ save complete. `Pool::spawn_one` polls for
        // `snapshot_path.is_file()` before invoking the worker
        // (which would otherwise fail-fast on `--restore-from`
        // pointing at a missing file). The first acquire doesn't
        // need the file at all — it uses the warm worker's
        // in-memory state.
        //
        // Returned `Image` uses `from_snapshot_pending` because
        // metadata.json IS on disk by the time we reach here, but
        // restore.snap may still be in flight. The non-pending
        // `Image::from_snapshot` would reject that; the pending
        // variant is identical except for the file-existence check.
        if self.warmup.is_none() {
            let trace = crate::trace::enabled("bake");
            let bake_t0 = std::time::Instant::now();
            // Empty warm_dir path: skip_warm_snapshot=true
            // suppresses both the warm SNAPSHOT round-trip AND
            // the warm metadata write, so warm_dir is unused.
            // Keep a sibling sentinel for diagnostic clarity in
            // case bake-trace logs reference it.
            // Default trigger: pre-exec (fast, ~150 ms bake on
            // slow-listener images, ~17× speedup on workloads that
            // would otherwise hit the 7-second wall-clock fallback).
            // Caller can opt out via `.with_listener_required()` to
            // get the v0.4.22 listener-ready capture instead — slower
            // bake, but guaranteed listener-up at restore time.
            #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
            let use_pre_exec = false;
            #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
            let use_pre_exec = !self.require_listener.unwrap_or(false);
            let pipelined = crate::bake::PipelinedWarmup {
                warm_dir: snapshots_dir.join(format!("{}__warm__unused", derived_name)),
                warm_tag: "unused".to_owned(),
                keep_alive: cfg!(all(target_os = "macos", target_arch = "aarch64")),
                skip_warm_snapshot: true,
                use_pre_exec_trigger: use_pre_exec,
                callback: Box::new(|_ctx| Ok(())),
            };
            match crate::bake::run_push_pipelined(&request, bake_t0, &root, pipelined) {
                Ok(warm_handoff) => {
                    if trace {
                        eprintln!(
                            "[bake-trace] always-pipelined (skip-warm) total: {:?} (bg save \
                             may still be in flight)",
                            bake_t0.elapsed()
                        );
                    }
                    let img = Image::from_snapshot_pending(&snap_dir)?;
                    if let Some(bw) = warm_handoff {
                        *img.warm_baked_worker.inner.lock().unwrap() = Some(bw);
                    }
                    return Ok(img);
                }
                Err(msg) => {
                    return Err(map_bake_error(&request.image, msg));
                }
            }
        }
        let warmup = self.warmup.unwrap();
        // Warmup path: derive a sibling directory keyed by the
        // warmup tag. Same name → cache hit (no warmup re-run);
        // different name → fresh warm bake.
        let tag = self.warmup_tag.as_deref().unwrap_or("default");
        let warm_dir = snapshots_dir.join(format!("{}__warm__{}", derived_name, tag));
        if let Ok(image) = Image::from_snapshot(&warm_dir) {
            // The warm cache hit is keyed only by name + warmup tag —
            // unlike the base path it never compared bake inputs. If
            // THIS request's mount config differs from what the cached
            // warm snapshot recorded, reusing it would restore a VM
            // wired to the OLD host dirs (stale FDT device set + stale
            // paths; the missing-dir case is already a from_snapshot
            // error above). Treat as a cache miss: invalidate and fall
            // through to a fresh warm bake against the current mounts.
            let requested_mounts = crate::bake::mounts_meta_from_extra_args(&request.extra_args);
            let recorded_mounts = std::fs::read_to_string(warm_dir.join("metadata.json"))
                .ok()
                .and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
                .and_then(|m| m.get("mounts").cloned())
                .and_then(|v| v.as_array().cloned())
                .unwrap_or_default();
            if recorded_mounts != requested_mounts {
                eprintln!(
                    "supermachine: warm snapshot at {} was baked with mounts {} but \
                     this build requests {}. Auto-rebaking with the current mounts.",
                    warm_dir.display(),
                    serde_json::Value::Array(recorded_mounts),
                    serde_json::Value::Array(requested_mounts),
                );
                invalidate_stale_snapshot_tree(&warm_dir);
            } else if let Some(baked) = snap_dir_baked_under_other_version(&warm_dir) {
                let current = env!("CARGO_PKG_VERSION");
                eprintln!(
                    "supermachine: warm snapshot at {} was baked under v{baked}; \
                     current binaries are v{current}. Auto-rebaking.",
                    warm_dir.display()
                );
                invalidate_stale_snapshot_tree(&warm_dir);
                // Also invalidate the base snapshot — re-running the
                // bake pipeline below needs a fresh base it can warm
                // up from. The base may be the same version as warm
                // (paired bake) or different (rare).
                invalidate_stale_snapshot_tree(&snap_dir);
            } else {
                return Ok(image);
            }
        }

        // Pipelined bake. The bake worker boots, signals
        // BAKE_READY, captures the base async (background save
        // overlapping with warmup), runs the warmup closure
        // against the still-live guest, captures warm sync, then
        // QUITs. Cuts ~900 ms vs the sequential path on
        // rust:1-slim because the base save and warmup overlap,
        // and we save one boot+restore round-trip.
        let trace = crate::trace::enabled("bake");
        let bake_t0 = std::time::Instant::now();

        // The user's warmup is `FnOnce(&Vm) -> Result<(), Error>`.
        // Inside the pipelined-bake driver we have only the
        // worker's vsock paths, so we synthesize a minimal `Vm`
        // around them and hand that to the user. The synthetic
        // Vm has `pool: None` and `image_meta: None` — neither
        // matters for warmup workloads (which call `vm.exec`,
        // `vm.write_file`, `vm.read_file`, etc., all of which
        // talk over vsock-exec).
        //
        // We capture any error in a Mutex so the bake driver can
        // surface it. The Box<FnOnce> escape-hatch dodges the
        // type system limitation that we can't move a non-Send
        // FnOnce through a closure without a wrapper.
        let warmup_err: std::sync::Arc<std::sync::Mutex<Option<Error>>> =
            std::sync::Arc::new(std::sync::Mutex::new(None));
        let warmup_err_inner = warmup_err.clone();
        let warmup_t0_capture: std::sync::Arc<std::sync::Mutex<Option<std::time::Instant>>> =
            std::sync::Arc::new(std::sync::Mutex::new(None));
        let warmup_t0_inner = warmup_t0_capture.clone();
        let pipelined = crate::bake::PipelinedWarmup {
            warm_dir: warm_dir.clone(),
            warm_tag: tag.to_owned(),
            // Opt in to warm-handoff: bake driver returns the live
            // worker; first Pool::acquire() claims it. Saves the
            // ~50 ms spawn + ~5 ms restore on the first cycle for
            // every with_warmup user. Falls back transparently if
            // the worker is dropped without anyone claiming it.
            keep_alive: cfg!(all(target_os = "macos", target_arch = "aarch64")),
            // Take the warm SNAPSHOT — this is the with-warmup path,
            // user expects a separate warm artifact at warm_dir.
            skip_warm_snapshot: false,
            // Use pre-exec as the BASE trigger for warmup bakes too
            // (0.7.0): init-oci's "workload-pre-exec" marker fires
            // BAKE_READY at the exact moment the guest is ready to
            // be probed by the warmup callback — no timer, no
            // listener-wait, no 7s/200ms fallback. The previous
            // contract said "warmup needs the workload running" but
            // the vsock_exec channel is live from boot independent
            // of the workload's state, so user-supplied vm.exec
            // calls inside the warmup work the same against an
            // init-oci-in-nanosleep guest as against a
            // post-workload-fork one. Caller can opt out via
            // `.with_listener_required()` if they really do depend
            // on the workload binding a port before warmup runs.
            use_pre_exec_trigger: {
                #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
                {
                    false
                }
                #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
                {
                    !self.require_listener.unwrap_or(false)
                }
            },
            callback: Box::new(move |ctx| {
                if let Ok(mut g) = warmup_t0_inner.lock() {
                    *g = Some(std::time::Instant::now());
                }
                #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
                {
                    let synth_vm = Vm {
                        pool: None,
                        vsock_mux_path: ctx.vsock_mux_path.clone(),
                        vsock_exec_path: ctx.vsock_exec_path.clone(),
                        own_vsock_mux_dir: None,
                        skip_cleanup: true,
                        image_meta: None,
                    };
                    let result = warmup(&synth_vm);
                    // Drop the synth Vm without running cleanup
                    // (skip_cleanup=true already; explicit drop for
                    // clarity).
                    drop(synth_vm);
                    match result {
                        Ok(()) => Ok(()),
                        Err(e) => {
                            let msg = e.to_string();
                            if let Ok(mut g) = warmup_err_inner.lock() {
                                *g = Some(e);
                            }
                            Err(msg)
                        }
                    }
                }
                #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
                {
                    let synth_vm = Vm {
                        running: None,
                        sentry_exec: None,
                        sentry_src_rootfs: None,
                        vsock_mux_path: ctx.vsock_mux_path.clone(),
                        vsock_exec_path: ctx.vsock_exec_path.clone(),
                        own_vsock_mux_dir: None,
                        skip_cleanup: true,
                        time_sync_stop: Arc::new(AtomicBool::new(false)),
                        _sentry_mem_admission: None,
                        sentry_warm_pool: None,
                        sentry_unified_pool: None,
                        runsc: None,
                        kboxlike: None,
                    };
                    let result = warmup(&synth_vm);
                    drop(synth_vm);
                    match result {
                        Ok(()) => Ok(()),
                        Err(e) => {
                            let msg = e.to_string();
                            if let Ok(mut g) = warmup_err_inner.lock() {
                                *g = Some(e);
                            }
                            Err(msg)
                        }
                    }
                }
                #[cfg(not(any(
                    all(target_os = "macos", target_arch = "aarch64"),
                    all(target_os = "linux", target_arch = "x86_64")
                )))]
                {
                    let _ = (&ctx, &warmup, &warmup_err_inner);
                    Ok(())
                }
            }),
        };

        match crate::bake::run_push_pipelined(&request, bake_t0, &root, pipelined) {
            Ok(warm_handoff) => {
                if trace {
                    eprintln!("[bake-trace] pipelined bake total: {:?}", bake_t0.elapsed());
                    if let Some(t0) = warmup_t0_capture.lock().ok().and_then(|g| *g) {
                        eprintln!("[bake-trace] (warmup ran at +{:?})", t0 - bake_t0);
                    }
                }
                let img = Image::from_snapshot(&warm_dir)?;
                // Stash the bake-time warm worker so the first
                // Pool::acquire() can claim it instead of spawning
                // fresh + restoring from disk. See
                // `Image::warm_baked_worker` for the full lifecycle
                // contract (claim-or-drop, race semantics, fall-
                // through behavior on worker death).
                if let Some(bw) = warm_handoff {
                    *img.warm_baked_worker.inner.lock().unwrap() = Some(bw);
                }
                Ok(img)
            }
            Err(msg) => {
                // If the warmup callback was the failure source,
                // bubble the typed Error back instead of the
                // stringified bake message.
                if let Some(e) = warmup_err.lock().ok().and_then(|mut g| g.take()) {
                    return Err(e);
                }
                Err(map_bake_error(&request.image, msg))
            }
        }
    }
}

fn default_guest_platform() -> String {
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    {
        "linux/amd64".to_owned()
    }
    #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
    {
        "linux/arm64".to_owned()
    }
}

/// Returns `Some(baked_version)` if the snapshot was baked under a
/// version different from `CARGO_PKG_VERSION`, or `None` if it's
/// current (or version can't be determined).
///
/// Used by:
///  - [`Image::from_snapshot`] (HARD error on load — a version-skewed
///    restore can yield a silently broken VM, e.g. no egress).
///  - [`OciImageBuilder::build`] (treats version-skewed snapshots
///    as cache-miss → silently re-bakes).
fn snapshot_baked_under_other_version(meta: &serde_json::Value) -> Option<String> {
    let current = env!("CARGO_PKG_VERSION");
    let baked = meta
        .get("baked_by_version")
        .and_then(|v| v.as_str())
        .map(|s| s.to_owned())
        .or_else(|| {
            meta.get("kernel")
                .and_then(|v| v.as_str())
                .and_then(parse_version_segment)
        })?;
    if baked == current {
        None
    } else {
        Some(baked)
    }
}

/// Read `<snap_dir>/metadata.json` and return `Some(baked_version)`
/// if the snapshot was baked under a non-current version. Returns
/// `None` on parse errors / missing fields / version match — every
/// "can't tell" case is treated as up-to-date (we'd rather miss a
/// re-bake than churn through an unnecessary one).
fn snap_dir_baked_under_other_version(snap_dir: &Path) -> Option<String> {
    let meta_text = std::fs::read_to_string(snap_dir.join("metadata.json")).ok()?;
    let meta: serde_json::Value = serde_json::from_str(&meta_text).ok()?;
    snapshot_baked_under_other_version(&meta)
}

fn snap_dir_backend(snap_dir: &Path) -> Option<String> {
    let meta_text = std::fs::read_to_string(snap_dir.join("metadata.json")).ok()?;
    let meta: serde_json::Value = serde_json::from_str(&meta_text).ok()?;
    meta.get("backend")
        .and_then(|v| v.as_str())
        .map(|s| s.to_owned())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn linux_default_backend() -> &'static str {
    match std::env::var("SUPERMACHINE_BACKEND").as_deref() {
        Ok("kvm") => "kvm",
        Ok("sentry") => "sentry",
        Ok("kboxlike") => "kboxlike",
        Ok("runsc") | Ok("auto") | Err(_) => "runsc",
        Ok(other) => {
            eprintln!(
                "supermachine: warning: unsupported SUPERMACHINE_BACKEND={other:?}; \
                 defaulting to runsc"
            );
            "runsc"
        }
    }
}

/// Delete a snapshot directory + all its `__warm__<tag>` siblings.
/// Used to invalidate version-skewed caches before re-bake, so the
/// next build produces a clean snapshot tree under the same name.
///
/// Best-effort: any path that fails to remove (e.g. user lacks
/// permission, file in use) is logged via stderr and skipped. The
/// caller's re-bake will surface a clearer error if the leftover
/// blocks it.
fn invalidate_stale_snapshot_tree(snap_dir: &Path) {
    // Base dir (`<name>/`).
    if let Err(e) = std::fs::remove_dir_all(snap_dir) {
        if e.kind() != std::io::ErrorKind::NotFound {
            eprintln!(
                "supermachine: warning: failed to remove stale snapshot {}: {e}",
                snap_dir.display()
            );
        }
    }
    // Warm siblings (`<name>__warm__<tag>/`). Read the parent dir
    // and match by prefix so we catch every tag the user has ever
    // used, not just the well-known ones.
    let Some(parent) = snap_dir.parent() else {
        return;
    };
    let Some(base_name) = snap_dir.file_name().and_then(|s| s.to_str()) else {
        return;
    };
    let prefix = format!("{base_name}__warm__");
    let Ok(read) = std::fs::read_dir(parent) else {
        return;
    };
    for entry in read.flatten() {
        let path = entry.path();
        if path
            .file_name()
            .and_then(|s| s.to_str())
            .is_some_and(|n| n.starts_with(&prefix))
        {
            if let Err(e) = std::fs::remove_dir_all(&path) {
                if e.kind() != std::io::ErrorKind::NotFound {
                    eprintln!(
                        "supermachine: warning: failed to remove stale warm snapshot {}: {e}",
                        path.display()
                    );
                }
            }
        }
    }
}

/// Extract the `v0.4.27` slug from a path like
/// `/…/supermachine/v0.4.27/kernel` → `Some("0.4.27")`. Returns `None`
/// for paths that don't follow this layout (custom kernel paths,
/// SUPERMACHINE_KERNEL_PATH overrides, etc.) — in that case we can't
/// reason about version skew so we don't warn.
fn parse_version_segment(kernel_path: &str) -> Option<String> {
    let p = Path::new(kernel_path);
    for comp in p.components() {
        let Some(s) = comp.as_os_str().to_str() else {
            continue;
        };
        let Some(rest) = s.strip_prefix('v') else {
            continue;
        };
        if rest.matches('.').count() < 2 {
            continue;
        }
        if !rest
            .split('.')
            .all(|seg| seg.chars().all(|c| c.is_ascii_digit()))
        {
            continue;
        }
        return Some(rest.to_owned());
    }
    None
}

/// Parse the wire / metadata representation of a [`SymlinkPolicy`]
/// (`"deny" | "opaque" | "follow"`). Returns `None` for unrecognized
/// inputs so callers can fall back to the default.
fn parse_symlink_policy(s: &str) -> Option<crate::vmm::resources::SymlinkPolicy> {
    use crate::vmm::resources::SymlinkPolicy;
    match s {
        "deny" => Some(SymlinkPolicy::Deny),
        "opaque" => Some(SymlinkPolicy::Opaque),
        "follow" => Some(SymlinkPolicy::Follow),
        _ => None,
    }
}

/// Sweep `cache_dir` (typically `~/.cache/supermachine/rosetta-aot`)
/// for AOT cache orphans before worker spawn. See the call site in
/// `build_pool_arc` for the rationale.
///
/// Evicts two classes of file:
///   - `<x>.aotcache` with `st_size == 0`: migration cleanup for
///     caches built before the FUSE-layer atomic-publish protocol
///     existed. These would trigger rosetta's segment_count
///     assertion or SIGBUS-on-mmap-past-EOF on the next load.
///   - `.<x>.aotcache.partial`: crash-recovery cleanup for the new
///     publish protocol. A `.partial` exists on disk iff a prior
///     VM was SIGKILLed mid-write; it's safe to unlink because by
///     construction the partial belongs to no currently-running
///     writer (the live writer holds an O_CREAT|O_EXCL fd and
///     would lose it on VM teardown).
///
/// Best-effort: silently no-op if the dir doesn't exist or any
/// individual stat / unlink fails (e.g. a file vanishes between
/// stat and unlink — fine, it's gone). Returns the count of
/// evicted entries for tests / observability.
fn scrub_aotcache_orphans(cache_dir: &Path) -> usize {
    use std::os::unix::ffi::OsStrExt;
    let entries = match std::fs::read_dir(cache_dir) {
        Ok(it) => it,
        Err(_) => return 0,
    };
    let mut evicted_zero = 0usize;
    let mut evicted_partial = 0usize;
    for ent in entries.flatten() {
        let name = ent.file_name();
        let bytes = name.as_bytes();
        // Class 1: `<x>.aotcache` of size 0 (pre-fix orphan)
        let is_aotcache_final = bytes.ends_with(b".aotcache") && !bytes.starts_with(b".");
        // Class 2: `.<x>.aotcache.partial` (post-fix crash recovery)
        let is_aotcache_partial = bytes.starts_with(b".") && bytes.ends_with(b".aotcache.partial");
        if !is_aotcache_final && !is_aotcache_partial {
            continue;
        }
        let path = ent.path();
        let Ok(md) = std::fs::metadata(&path) else {
            continue;
        };
        if !md.is_file() {
            continue;
        }
        if is_aotcache_partial {
            // Unconditionally unlink partials — by construction they
            // belong to no currently-running writer.
            if std::fs::remove_file(&path).is_ok() {
                evicted_partial += 1;
            }
        } else if md.len() == 0 {
            // Final-name aotcache with zero size — migration cleanup.
            if std::fs::remove_file(&path).is_ok() {
                evicted_zero += 1;
            }
        }
    }
    let evicted = evicted_zero + evicted_partial;
    if evicted > 0 {
        eprintln!(
            "supermachine: scrubbed {} AOT cache orphan(s) from {} \
             ({} zero-byte aotcache, {} mid-flight partial) \u{2014} \
             would have trapped rosetta with the segment_count \
             assertion or SIGBUS on mmap past EOF",
            evicted,
            cache_dir.display(),
            evicted_zero,
            evicted_partial,
        );
    }
    evicted
}

/// Default snapshots directory: `~/.local/supermachine-snapshots`,
/// matching the CLI's default. Customizable via
/// [`Image::from_oci_to_dir`] or `$SUPERMACHINE_SNAPSHOTS`.
fn default_snapshots_dir() -> PathBuf {
    if let Some(d) = std::env::var_os("SUPERMACHINE_SNAPSHOTS") {
        return PathBuf::from(d);
    }
    let home = std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    home.join(".local/supermachine-snapshots")
}

/// `bake::run_push` wants a "repo root" so it can locate the
/// supermachine-worker binary, the kernel image, the entitlements
/// plist, etc. The CLI walks up from its own exe to find it. From
/// a library context the same auto-discovery applies (an embedder
/// running their app from the dev tree finds the workspace; a
/// release-tarball install finds `<prefix>/share/supermachine`).
fn repo_root_for_bake() -> Result<PathBuf, Error> {
    if let Some(root) = std::env::var_os("SUPERMACHINE_ROOT") {
        return Ok(PathBuf::from(root));
    }
    let exe = std::env::current_exe().map_err(|e| Error::bake_msg(format!("current_exe: {e}")))?;
    for ancestor in exe.ancestors() {
        if ancestor.join("tools/supermachine-push").is_file() {
            return Ok(ancestor.to_path_buf());
        }
        if ancestor.join("share/supermachine/kernel").is_file() {
            return Ok(ancestor.to_path_buf());
        }
    }
    std::env::current_dir().map_err(|e| Error::bake_msg(format!("current_dir: {e}")))
}

/// Map a `bake::run_push` error string into the right
/// [`Error`] variant. The bake pipeline returns flat strings, so
/// we pattern-match keywords.
fn map_bake_error(image: &str, msg: String) -> Error {
    // Kernel-panic sentinel emitted by bake.rs when its serial-log
    // watcher detects a panic banner. Format:
    //   `KERNEL_PANIC|<first_line>|<stack_line_1>\x1F<stack_line_2>...`
    // We turn that into a typed `Error::KernelPanic` so callers can
    // pattern-match (vs. groveling through the bake message string).
    if let Some(rest) = msg.strip_prefix("KERNEL_PANIC|") {
        let mut parts = rest.splitn(2, '|');
        let first_line = parts.next().unwrap_or("").to_owned();
        let stack: Vec<String> = parts
            .next()
            .map(|s| s.split('\x1F').map(ToOwned::to_owned).collect())
            .unwrap_or_default();
        return Error::kernel_panic(first_line, stack);
    }
    let lc = msg.to_ascii_lowercase();

    // Registry HTTP-status-based classification. The bake produces
    // strings like "registry manifest request failed for X with HTTP 404"
    // or "registry token request failed with HTTP 401" — match those
    // shapes.
    let has_status = |code: u16| -> bool {
        let needle = format!("http {code}");
        lc.contains(&needle)
    };
    if has_status(404)
        || lc.contains("manifest unknown")
        || lc.contains("name unknown")
        || lc.contains("not found")
            && (lc.contains("registry") || lc.contains("manifest") || lc.contains("image"))
    {
        return Error::image_not_found(image, msg);
    }
    if has_status(401)
        || has_status(403)
        || lc.contains("unauthorized")
        || lc.contains("forbidden")
        || lc.contains("auth challenge")
    {
        return Error::registry_auth(image, msg);
    }
    if lc.contains("could not resolve")
        || lc.contains("dns")
        || lc.contains("connection refused")
        || lc.contains("connection reset")
        || lc.contains("network is unreachable")
        || lc.contains("ssl_connect")
        || lc.contains("tls handshake")
        || lc.contains("curl: (6)")  // Couldn't resolve host
        || lc.contains("curl: (7)")  // Failed to connect
        || lc.contains("curl: (28)") // Operation timeout
        || lc.contains("curl: (35)") // SSL connect error
        || lc.contains("curl: (56)")
    // Recv failure
    {
        return Error::registry_unreachable(msg);
    }

    // Generic registry/manifest/auth strings that didn't match a
    // specific HTTP status fall through to the catch-all Network
    // variant — same as before.
    if lc.contains("registry") || lc.contains("manifest") || lc.contains("docker pull") {
        Error::network_msg(msg)
    } else {
        // Snapshot timeouts, listener-readiness failures, and
        // anything else that wasn't a network-layer issue.
        Error::bake_msg(msg)
    }
}

/// Configuration for [`Vm::start`]. Built via the chainable
/// `VmConfig::with_*` methods or constructed directly:
///
/// ```
/// use supermachine::VmConfig;
/// let cfg = VmConfig::new()
///     .with_memory_mib(512)
///     .with_vcpus(2);
/// # let _ = cfg;
/// ```
#[derive(Debug, Clone, Default)]
pub struct VmConfig {
    /// Override the image's baked memory. `None` = use Image's value.
    memory_mib: Option<u32>,
    /// Override the image's baked vCPUs. `None` = use Image's value.
    vcpus: Option<u32>,
    assets: Option<AssetPaths>,
    vsock_mux_dir: Option<PathBuf>,
    restore_timeout: Option<Duration>,
    /// Data volumes to attach + mount in the guest (KVM). Each is a host-backed
    /// virtio-blk (vdb, vdc, …) mounted at its `guest_path`. Honored on the
    /// cold-boot path; data persists in the host backing files.
    volumes: Vec<crate::vmm::resources::VolumeSpec>,
    /// virtio-fs mounts (KVM): each exposes a host directory to the guest over
    /// FUSE-over-virtio, mounted at its `guest_path`. Cold-boot only for now
    /// (no DAX window yet — see docs/design/kvm-virtiofs-dax-2026-06-07.md).
    virtiofs: Vec<crate::vmm::resources::MountSpec>,
    /// Attach a virtio-balloon device (KVM cold boot). When set, the guest's
    /// balloon driver binds and the host can reclaim guest memory via
    /// [`Vm::request_balloon_inflate`]. Cold-boot only ([`Image::start`]); the
    /// warm-pool `acquire` path ignores it (balloon isn't snapshotted).
    enable_balloon: bool,
    /// Sentry-only cap on the guest cell tree's total processes+threads
    /// (cgroup-v2 `pids.max`, fork-bomb protection). `None` = the sentry default
    /// (`SENTRY_DEFAULT_PIDS_MAX`). Has no effect on the KVM/HVF backends.
    sentry_pids_max: Option<u64>,
    /// Sentry-only: drop the guest cell to this `(uid, gid)`, OVERRIDING the image's
    /// baked `User`. Set by the orchestration to a UNIQUE uid per tenant — the
    /// cross-tenant isolation barrier: distinct uids make the kernel deny every
    /// cross-tenant process_vm / ptrace / signal / same-uid file access. `None` =
    /// fall back to the image's baked User (then the sentry's own uid). No effect on
    /// the KVM/HVF backends (hardware-isolated).
    sentry_uid: Option<(u32, u32)>,
    /// Sentry-only opt-out from the default memory + CPU + pids caps. `true` =
    /// the guest cell tree runs UNCAPPED (only the syscall wall + page-table
    /// isolation apply). Off by default — the sentry caps every guest from the
    /// image's `memory_mib`/`vcpus` so a single tenant can't exhaust the host.
    sentry_uncapped: bool,
    /// Sentry-only: this pool backs a Dockerfile BUILD stage (the executor's
    /// build-pool chains set it). `true` makes [`SentryPool::build`] (a) serve the
    /// stage's RUN steps from a PER-BUILD private reflink clone of the warm rootfs
    /// — so each build/stage gets an isolated, throwaway filesystem (per-build
    /// isolation + failed-RUN rollback + no cross-stage collision) discarded on
    /// pool Drop — and (b) NOT auto-launch the base image's baked workload (a build
    /// base must not run its `CMD` while `RUN` steps execute). Default `false`; the
    /// KVM/HVF backends ignore it (they already isolate per-layer via a tmpfs
    /// overlay-upper).
    for_build: bool,
    /// Sentry-only bounded-execution watchdog applied to every `exec` cell tree.
    /// `None` = the generous production default (`SENTRY_DEFAULT_EXEC_TIMEOUT`,
    /// 10 min) is applied in `Image::sentry_sandbox_with`, so a wedged or
    /// fork-and-signal-trapping `RUN` can never hang the production caller forever
    /// while a legitimate long install/build still completes. `Some(d)` (non-zero)
    /// sets the ceiling to `d`; `Some(Duration::ZERO)` opts OUT (unlimited —
    /// trusted tooling only). No effect on the KVM/HVF backends.
    sentry_exec_timeout: Option<Duration>,
}

/// Default sentry `pids.max` when neither [`VmConfig::with_sentry_pids_max`] nor
/// an opt-out is set — a generous ceiling that still stops a runaway fork bomb.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SENTRY_DEFAULT_PIDS_MAX: u64 = 4096;
/// CPU-quota accounting period for the sentry's derived `cpu.max` (microseconds).
/// The quota is `vcpus * SENTRY_CPU_PERIOD_US`, i.e. `vcpus` cores' worth of CPU.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SENTRY_CPU_PERIOD_US: u64 = 100_000;
/// Default sentry per-`exec` watchdog when [`VmConfig::with_sentry_exec_timeout`]
/// is unset — a GENEROUS 10-minute ceiling. Production-bounded out of the box: a
/// forked workload that traps signals (or the deferred C1 reentrancy hang) becomes
/// a bounded, reported [`crate::sentry::SentryError::Timeout`] (exit 137) instead
/// of hanging `Vm::start` / the `SentryPool` forever, while a long but legitimate
/// install/build `RUN` still finishes. Opt out (unlimited) with
/// `VmConfig::with_sentry_exec_timeout(Duration::ZERO)`.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SENTRY_DEFAULT_EXEC_TIMEOUT: Duration = Duration::from_secs(600);

impl VmConfig {
    /// Use the image's baked defaults for memory + vCPUs;
    /// auto-discover assets; vsock-mux socket in `$TMPDIR`;
    /// 10 s restore timeout.
    pub fn new() -> Self {
        Self::default()
    }

    /// Override the image's baked memory.
    pub fn with_memory_mib(mut self, mib: u32) -> Self {
        self.memory_mib = Some(mib);
        self
    }

    /// Override the image's baked vCPU count.
    pub fn with_vcpus(mut self, vcpus: u32) -> Self {
        self.vcpus = Some(vcpus);
        self
    }

    /// Attach a virtio-balloon device so the host can reclaim guest memory via
    /// [`Vm::request_balloon_inflate`] (KVM cold boot — [`Image::start`]). The
    /// guest needs `CONFIG_VIRTIO_BALLOON` (the bundled supermachine kernel has
    /// it). Off by default; the warm-pool `acquire` path ignores it (balloon is
    /// a cold-boot reclaim lever, not snapshotted — matching HVF's restore default).
    pub fn with_balloon(mut self, enable: bool) -> Self {
        self.enable_balloon = enable;
        self
    }

    /// Attach a data volume (KVM): a host-backed virtio-blk mounted in the guest
    /// at `spec.guest_path`. The backing file is created sparse + formatted ext4
    /// on first use and reused (so data persists across runs); pass the same
    /// `host_path` to keep state. Multiple volumes become vdb, vdc, … Honored on
    /// the cold-boot start path (snapshots/pools don't carry volume mounts yet).
    ///
    /// ```no_run
    /// # use supermachine::{Image, VmConfig};
    /// # use supermachine::vmm::resources::VolumeSpec;
    /// let image = Image::from_oci("postgres:16-alpine")?;
    /// let cfg = VmConfig::new()
    ///     .with_volume(VolumeSpec::new("/srv/pgdata.img", "/var/lib/postgresql/data"));
    /// let vm = image.start(&cfg)?;
    /// # let _ = vm; Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_volume(mut self, spec: crate::vmm::resources::VolumeSpec) -> Self {
        self.volumes.push(spec);
        self
    }

    /// Expose a host directory to the guest over virtio-fs (KVM), mounted at
    /// `spec.guest_path`. Served by an in-process FUSE backend with a DAX window
    /// (the guest init mounts `-o dax` for zero-copy reads). Mounts survive a
    /// snapshot: the device + FUSE backend tables + DAX slot table are captured
    /// (SMSNAP05) and re-attached on restore (lazy fd reopen + eager DAX rebind).
    ///
    /// ```no_run
    /// # use supermachine::{Image, VmConfig};
    /// # use supermachine::vmm::resources::MountSpec;
    /// let image = Image::from_oci("alpine")?;
    /// let cfg = VmConfig::new()
    ///     .with_virtiofs(MountSpec::new("/srv/assets", "assets", "/mnt/assets"));
    /// let vm = image.start(&cfg)?;
    /// # let _ = vm; Ok::<(), supermachine::Error>(())
    /// ```
    pub fn with_virtiofs(mut self, spec: crate::vmm::resources::MountSpec) -> Self {
        self.virtiofs.push(spec);
        self
    }

    /// Cap the sentry guest cell tree's total processes+threads (cgroup-v2
    /// `pids.max`) — fork-bomb protection. Sentry backend only. Default is
    /// `SENTRY_DEFAULT_PIDS_MAX`; the KVM/HVF backends ignore this.
    pub fn with_sentry_pids_max(mut self, n: u64) -> Self {
        self.sentry_pids_max = Some(n);
        self
    }

    /// Drop the sentry guest cell to `(uid, gid)`, overriding the image's baked
    /// `User`. The MULTI-TENANT isolation knob: assign a UNIQUE uid per tenant and
    /// the kernel denies every cross-tenant process_vm / ptrace / signal / same-uid
    /// file access — one tenant's cell cannot reach another's. Sentry backend only
    /// (KVM/HVF are hardware-isolated and ignore this). Requires the sentry to run
    /// privileged enough to drop to the uid (it does the confined fs as its own uid;
    /// the guest loses no capability).
    pub fn with_sentry_uid(mut self, uid: u32, gid: u32) -> Self {
        self.sentry_uid = Some((uid, gid));
        self
    }

    /// Run the sentry guest cell tree UNCAPPED (no cgroup-v2 memory/CPU/pids
    /// caps). Sentry backend only; the syscall wall + page-table isolation still
    /// apply. Off by default — leave it off in production so a single tenant
    /// can't exhaust the host. Use it only for trusted single-tenant tooling that
    /// legitimately needs the whole box.
    pub fn with_sentry_uncapped(mut self, uncapped: bool) -> Self {
        self.sentry_uncapped = uncapped;
        self
    }

    /// Bound how long any single sentry `exec` cell tree may run before the
    /// supervisor SIGKILLs it and reports `SentryError::Timeout` (exit `137`).
    /// Sentry backend only; the KVM/HVF backends ignore it.
    ///
    /// The production release-gate knob: left UNSET, the sentry path applies a
    /// generous default ceiling (`SENTRY_DEFAULT_EXEC_TIMEOUT`, 10 min) in
    /// `Image::sentry_sandbox_with`, so `Vm::start` (sentry path) and a
    /// `SentryPool` are bounded out of the box — a wedged or fork-and-signal-
    /// trapping `RUN` can't hang the caller forever — while a long but legitimate
    /// install/build still completes. Pass a tighter `dur` for snappier SLAs, or
    /// `Duration::ZERO` to run UNLIMITED (trusted single-tenant tooling).
    pub fn with_sentry_exec_timeout(mut self, dur: Duration) -> Self {
        self.sentry_exec_timeout = Some(dur);
        self
    }

    /// Mark this config as backing a Dockerfile BUILD stage. No-KVM rootfs
    /// backends serve the stage from a per-build private clone of the warm rootfs
    /// (per-build/per-stage filesystem isolation + failed-RUN rollback) and
    /// suppress the base image's baked-workload auto-start. Set by the builder's
    /// build-pool chains via [`PoolBuilder::for_build`]; KVM/HVF ignore it. Off
    /// by default.
    pub fn with_for_build(mut self, on: bool) -> Self {
        self.for_build = on;
        self
    }

    /// Override asset auto-discovery. Useful for `.app` bundles
    /// that ship the kernel + init shim under
    /// `Contents/Resources/`.
    pub fn with_assets(mut self, assets: AssetPaths) -> Self {
        self.assets = Some(assets);
        self
    }

    /// Where to put the host-side vsock-mux unix socket. Default
    /// is `$TMPDIR`. Use this if you need the socket inside an
    /// app-private dir for sandboxing reasons.
    pub fn with_vsock_mux_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.vsock_mux_dir = Some(dir.into());
        self
    }

    /// How long to wait for the snapshot to restore. Default 10 s.
    pub fn with_restore_timeout(mut self, timeout: Duration) -> Self {
        self.restore_timeout = Some(timeout);
        self
    }
}

/// A running microVM. Holds an internal worker process and the
/// host-side vsock-mux unix socket through which you talk to the
/// guest.
///
/// Drop the value to stop the VM, or call [`Vm::stop`] for an
/// explicit shutdown report.
///
/// The in-process `Vm` runtime needs a hypervisor backend; gated to
/// macOS/HVF until the KVM backend lands (broaden the gate then).
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub struct Vm {
    pool: Option<WarmPool>,
    vsock_mux_path: PathBuf,
    /// `<vsock_mux>-exec.sock` for the in-guest exec agent. Only
    /// useful once the agent crate ships in the initramfs (see
    /// `docs/design/exec-2026-05-03.md`); until then dialing this
    /// path will fail with "no listener" because the agent isn't
    /// running guest-side. The unix socket itself is created
    /// unconditionally so `Vm::exec` can wire to it once the agent
    /// lands.
    vsock_exec_path: PathBuf,
    /// Best-effort cleanup of the temp socket dir we created.
    own_vsock_mux_dir: Option<PathBuf>,
    /// `true` for [`PooledVm`]'s wrapped Vm — Drop must NOT
    /// shut down the pool or unlink sockets, since those are
    /// owned by [`Image`]'s [`HiddenPool`] and reused across
    /// [`Image::acquire`] calls.
    skip_cleanup: bool,
    /// Source image metadata for [`Vm::snapshot`] — needed to
    /// emit a `metadata.json` describing the new snapshot's
    /// layers / memory / vCPUs (the snapshot file alone isn't
    /// loadable as an Image without these).  `None` for VMs
    /// that didn't come from an Image (currently impossible via
    /// the public API; reserved for future use cases).
    image_meta: Option<Arc<ImageMeta>>,
}

/// Linux/KVM backend `Vm`: holds the [`RunningVm`](crate::kvm::run::RunningVm)
/// control handle (vCPU threads + on-demand snapshot/stop) plus the shared
/// vsock socket paths. The agnostic exec/file/forward methods operate against
/// the same fields as the macOS `Vm`.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub struct Vm {
    running: Option<crate::kvm::run::RunningVm>,
    /// No-virt sentry backend: when the image was baked for the sentry (no KVM),
    /// `running` is `None` and this holds the exec server that speaks the agent
    /// wire protocol on `vsock_exec_path`. Dropped on stop/Drop (unlinks + kills
    /// the workload). See [`crate::sentry::ExecServer`].
    sentry_exec: Option<crate::sentry::ExecServer>,
    /// Sentry backend: the live rootfs dir this Vm's cells run against (==
    /// `image.sentry.rootfs`). `Some` only for a cold-start sentry `Vm` from
    /// [`Image::start`]; `Vm::snapshot` copies it to capture the warm filesystem.
    /// `None` for KVM and for pooled sentry VMs (which can't snapshot the rootfs).
    sentry_src_rootfs: Option<PathBuf>,
    vsock_mux_path: PathBuf,
    vsock_exec_path: PathBuf,
    own_vsock_mux_dir: Option<PathBuf>,
    skip_cleanup: bool,
    /// Stops the background clock-resync thread (spawned in [`Vm::start`]) when
    /// the VM is dropped. Without it that thread keeps retrying for its full
    /// 30 s deadline against a dead VM — leaking a thread per VM across pool
    /// churn. Drop sets it; the thread checks it each iteration and exits.
    time_sync_stop: Arc<AtomicBool>,
    /// No-virt sentry one-shot: MEMORY admission reservation for this `Vm`'s
    /// single cell's worst-case footprint (cgroup `memory.max` + overhead),
    /// charged in [`Vm::start_sentry`] and freed when the `Vm` drops. `None` for
    /// the KVM backend (the in-process VM thread isn't memory-admitted here) and
    /// for pooled sentry VMs (the [`SentryPool`] holds the pool-wide reservation).
    /// Underscore-prefixed: held purely for its `Drop`.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    _sentry_mem_admission: Option<crate::memory_admission::AdmissionGuard>,
    /// No-virt sentry WARM-DAEMON pool: the shared `Pool` this pooled `Vm`'s `exec`
    /// routes client cells through ([`crate::sentry::Pool::exec_capture`] over the
    /// supervisor-global owned loopback). `Some` only for a warm-daemon pooled `Vm`;
    /// `None` for the ExecServer pooled path, cold sentry `Vm`s, and KVM (they dial
    /// `vsock_exec_path`).
    sentry_warm_pool: Option<Arc<crate::sentry::Pool>>,
    /// No-virt sentry cold unified supervisor: held only to keep the persistent
    /// workload+exec supervisor alive while this cold `Vm` exists. Exec still dials
    /// `vsock_exec_path` for the streaming agent protocol; do not route it through
    /// `sentry_warm_pool`, which is the warm-daemon request/response special case.
    sentry_unified_pool: Option<Arc<crate::sentry::Pool>>,
    /// External no-KVM runsc backend runtime. `running` is `None` for this path
    /// too, so code that wants the sentry supervisor must check this first.
    runsc: Option<RunscRuntime>,
    /// Native kbox-inspired no-KVM backend runtime. This currently owns the
    /// captured/restored host process tree; exec/file/network parity is wired in
    /// later and must fail explicitly until then.
    kboxlike: Option<KboxlikeRuntime>,
}

/// Transient stub for platforms with NO hypervisor backend (neither macOS/HVF
/// nor Linux/KVM): the `Vm` TYPE is still named in portable signatures
/// (`PooledVm::vm`, `OciImageBuilder::warmup: FnOnce(&Vm)`,
/// `Image::start -> Result<Vm>`), so this uninhabitable stub lets them resolve.
#[cfg(not(any(
    all(target_os = "macos", target_arch = "aarch64"),
    all(target_os = "linux", target_arch = "x86_64")
)))]
pub struct Vm {
    _never: std::convert::Infallible,
}

/// Subset of [`Image`] fields snapshotted into [`Vm`] so
/// [`Vm::snapshot`] can write a self-contained `metadata.json`
/// next to the captured snapshot file.
#[derive(Clone, Debug)]
pub(crate) struct ImageMeta {
    pub memory_mib: u32,
    pub vcpus: u32,
    pub layers: Vec<PathBuf>,
    pub delta_squashfs: Option<PathBuf>,
    pub baker_runtime_sha16: Option<String>,
    pub tsi_token: Option<String>,
    pub egress_policy: Option<String>,
    pub pre_exec_sync: bool,
}

#[cfg(any(
    all(target_os = "macos", target_arch = "aarch64"),
    all(target_os = "linux", target_arch = "x86_64")
))]
impl Vm {
    /// Start a microVM from `image` with the supplied configuration.
    ///
    /// What this does, in order:
    ///
    /// 1. Resolves the kernel path. Preferences (first hit wins):
    ///    `image`'s bundled kernel (if the snapshot dir shipped one),
    ///    `config.assets.kernel` (if set explicitly), then
    ///    [`AssetPaths::discover`]. Fails with [`Error::Assets`] if
    ///    none is found.
    /// 2. Creates a unique unix socket path for vsock-mux under
    ///    the configured directory.
    /// 3. Spawns an in-process VM thread that restores from
    ///    `image.snapshot_path()`. (The library runs the VM
    ///    in-process via [`crate::internal::vmm::pool::WarmPool`].
    ///    The standalone `supermachine-worker` binary is only used
    ///    by the router daemon for SCM_RIGHTS process isolation.)
    /// 4. Waits up to [`VmConfig::with_restore_timeout`] for the
    ///    restore to complete.
    /// 5. Returns the [`Vm`] handle. The vsock-mux socket is
    ///    available immediately at [`Vm::vsock_path`].
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn start(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
        // Vm::start runs the VM thread in this process, so this
        // process itself calls hv_vm_create. Without the HVF
        // entitlement that fails with HV_DENIED (Hv(-85377017)),
        // which is cryptic. Surface a clear error up front instead.
        // Image::acquire callers don't pay this cost — the worker
        // subprocess handles HVF for them.
        #[cfg(target_os = "macos")]
        if let Err(msg) = crate::codesign::check_self_has_hvf_entitlement() {
            return Err(Error::vm_msg(msg));
        }
        let assets = match &config.assets {
            Some(a) => a.clone(),
            None => AssetPaths::discover(),
        };
        // Kernel preference: bundled (snapshot dir) > config.assets >
        // AssetPaths::discover. A bundled kernel makes the snapshot
        // self-contained so a `.app` ships everything it needs.
        let kernel: PathBuf = if let Some(k) = image.bundled_kernel.as_ref() {
            k.clone()
        } else if let Some(k) = assets.kernel.as_ref() {
            k.clone()
        } else {
            return Err(Error::assets_msg(
                "no kernel found: snapshot dir has no bundled kernel and AssetPaths::discover() came up empty; set VmConfig::with_assets() or $SUPERMACHINE_ASSETS_DIR".to_owned(),
            ));
        };
        let kernel = kernel.as_path();

        // Per-VM unix socket path under the chosen dir.
        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        let mut own_dir = None;
        if !dir.is_dir() {
            std::fs::create_dir_all(&dir).map_err(Error::Io)?;
            own_dir = Some(dir.clone());
        }
        let vsock_mux_path = dir.join(format!(
            "supermachine-vm-{}-{}.sock",
            std::process::id(),
            unique_suffix(),
        ));
        // `<vsock_mux>-exec` is the convention that worker.rs and
        // the design doc agree on. Same parent dir so unlinking the
        // mux on shutdown sweeps it too.
        let vsock_exec_path = {
            let mut p = vsock_mux_path.clone();
            let mut name = p.file_name().unwrap().to_owned();
            name.push("-exec");
            p.set_file_name(name);
            p
        };

        // Build VmResources for snapshot restore. Memory + vCPUs
        // come from the image's bake metadata unless the caller
        // explicitly overrode them.
        let memory_mib = config.memory_mib.unwrap_or(image.memory_mib);
        let vcpus = config.vcpus.unwrap_or(image.vcpus);
        let mut resources = VmResources::new()
            .with_kernel_path(kernel.to_string_lossy().to_string())
            .with_memory_mib(memory_mib as usize)
            .with_vcpus(vcpus)
            .with_cow_restore(true)
            .with_restore(image.snapshot_path.to_string_lossy().to_string())
            .with_vsock_mux(vsock_mux_path.to_string_lossy().to_string())
            .with_vsock_exec(vsock_exec_path.to_string_lossy().to_string());

        // Attach the OCI image's virtio-blk layers in bake order.
        // The guest's overlayfs union is built bottom-up over these.
        for layer in &image.layers {
            resources = resources.with_block_device(layer.to_string_lossy().to_string());
        }
        if let Some(delta) = &image.delta_squashfs {
            resources = resources.with_block_device(delta.to_string_lossy().to_string());
        }

        // Pool of size 1 — single worker, single VM.
        let options = RunOptions::default();
        let pool = WarmPool::start(resources, options).map_err(Error::from)?;

        // Restore from the snapshot. WarmPool's restore_timeout
        // dispatches the RESTORE command to the pre-spawned worker
        // and blocks until the guest is up.
        let timeout = config
            .restore_timeout
            .unwrap_or_else(|| Duration::from_secs(10));
        let _ = pool
            .restore_timeout(image.snapshot_path.to_string_lossy().to_string(), timeout)
            .map_err(Error::from)?;

        Ok(Vm {
            pool: Some(pool),
            vsock_mux_path,
            vsock_exec_path,
            own_vsock_mux_dir: own_dir,
            skip_cleanup: false,
            image_meta: Some(Arc::new(ImageMeta {
                memory_mib,
                vcpus,
                layers: image.layers.clone(),
                delta_squashfs: image.delta_squashfs.clone(),
                baker_runtime_sha16: image.baker_runtime_sha16.clone(),
                tsi_token: image.tsi_token.clone(),
                egress_policy: image.egress_policy.clone(),
                pre_exec_sync: image.pre_exec_sync,
            })),
        })
    }

    /// Linux/KVM `Vm::start` — not yet wired to the bake/Image rootfs pipeline.
    ///
    /// The KVM runtime exists and is validated end-to-end (boot → exec over
    /// vsock → snapshot) via [`crate::kvm::run::LinuxVm`] /
    /// [`RunningVm`](crate::kvm::run::RunningVm) and the `kvm_boot` example. The
    /// remaining work is the Linux bake path that produces a LinuxVm-runnable
    /// rootfs (a single merged squashfs + the agent init cpio) and an `Image`
    /// that carries them — see the KVM product-wiring plan (Step 2/4). Until
    /// then this returns a clear error rather than silently doing nothing.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn start(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
        use crate::kvm::run::{LinuxVm, LinuxVmConfig};
        // No-virt sentry backend: a `"backend":"sentry"` image carries an extracted
        // rootfs (no kernel/disk/snapshot) and runs under process isolation. The
        // bake path selects sentry when KVM is unavailable; dispatch to it here so
        // the exec/file surface is identical to the KVM/HVF backends.
        if image.sentry.is_some() {
            return Vm::start_sentry(image, config);
        }
        if image.runsc.is_some() {
            return Vm::start_runsc(image, config);
        }
        if image.kboxlike.is_some() {
            return Vm::start_kboxlike(image, config);
        }
        let parts = image.kvm.as_ref().ok_or_else(|| {
            Error::vm_msg(
                "Image has no KVM artifacts: metadata.json needs \"backend\":\"kvm\" \
                 with kvm_kernel + kvm_initrd (produced by the KVM bake path)."
                    .to_owned(),
            )
        })?;
        // Restore from a snapshot if the image carries one (CoW mmap, ~ms);
        // otherwise cold-boot from kernel + agent initramfs (+ optional disk).
        let vm = if let Some(snap) = &parts.snapshot {
            if !config.volumes.is_empty() {
                eprintln!(
                    "supermachine(KVM): {} volume(s) requested via VmConfig but this image \
                     restores from a snapshot — the snapshot's OWN recorded volume attachments \
                     are re-attached (SMSNAP04), so the config volumes are ignored. To attach \
                     different volumes, cold-boot the image (e.g. bake_kvm_auto) with them.",
                    config.volumes.len()
                );
            }
            if !config.virtiofs.is_empty() {
                eprintln!(
                    "supermachine(KVM): {} virtio-fs mount(s) requested via VmConfig but this image \
                     restores from a snapshot — the snapshot's OWN recorded virtio-fs mounts are \
                     re-attached (SMSNAP05, DAX slots eagerly re-bound), so the config mounts are \
                     ignored. To attach different mounts, cold-boot the image with them.",
                    config.virtiofs.len()
                );
            }
            LinuxVm::restore_from_file(snap)
                .map_err(|e| Error::vm_msg(format!("restore snapshot {}: {e}", snap.display())))?
        } else {
            let kernel_path = parts
                .kernel
                .as_ref()
                .expect("kvm cold-boot kernel (validated in from_snapshot)");
            let initrd_path = parts
                .initrd
                .as_ref()
                .expect("kvm cold-boot initrd (validated in from_snapshot)");
            let kernel = std::fs::read(kernel_path).map_err(|e| {
                Error::vm_msg(format!("read kernel {}: {e}", kernel_path.display()))
            })?;
            let initrd = std::fs::read(initrd_path).map_err(|e| {
                Error::vm_msg(format!("read initrd {}: {e}", initrd_path.display()))
            })?;
            let disk_str: Option<String> = parts
                .disk
                .as_ref()
                .map(|p| p.to_string_lossy().into_owned());
            let disk_size = parts
                .disk
                .as_ref()
                .and_then(|p| std::fs::metadata(p).ok())
                .map(|m| m.len())
                .unwrap_or(0);
            let mem_mib = config.memory_mib.unwrap_or(image.memory_mib).max(1);
            let num_cpus = kvm_effective_vcpus(config.vcpus.unwrap_or(image.vcpus), "Vm::start")
                .clamp(1, 255) as u8;
            // The agent initramfs is PID 1 (exec over AF_VSOCK 1028); a rootfs
            // disk, if present, is attached at /dev/vda for the guest to mount.
            //
            // `tsi_hijack` turns on transparent egress: the AF_TSI-patched guest
            // kernel routes the workload's AF_INET/AF_INET6 connect()/sendto()
            // over vsock to the host vsock muxer, which opens the real host
            // socket (subject to egress_policy). No virtio-net, no guest NIC —
            // the same transparent-networking model as the HVF backend. Requires
            // the TSI-enabled guest kernel (CONFIG_TSI=y + af-tsi patch series).
            // `supermachine.host_time=<epoch_secs>` lets the generated init set
            // the guest wall clock from the host: a KVM guest has no RTC and
            // boots at ~1999, which makes TLS cert validation fail ("certificate
            // not trusted") and breaks apk/pip/https in workloads + builder RUNs.
            let host_time = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_secs())
                .unwrap_or(0);
            let cmdline = format!(
                "console=ttyS0 panic=-1 reboot=t tsi_hijack supermachine.host_time={host_time}"
            );
            // Mint a per-VM vsock TSI control-channel auth token. `LinuxVm::new`
            // appends `supermachine.tsi_token=<hex>` (the guest's af_tsi driver
            // stamps it on every control DGRAM) and arms the muxer to reject any
            // control packet without it — so an in-guest workload can't forge TSI
            // egress ops to bypass egress policy. Best-effort: if the CSPRNG read
            // fails we boot without enforcement rather than failing the VM.
            let tsi_token = crate::cli::TsiToken::generate()
                .map(|t| t.bytes)
                .map_err(|e| {
                    eprintln!("supermachine(KVM): tsi token generation failed ({e}); booting without vsock control-channel auth");
                })
                .ok();
            // Data volumes: ensure each host backing file exists + is ext4, then
            // attach as vdb/vdc/… mounted at guest_path (the init reads the
            // sm.volume cmdline tokens run.rs appends). Runtime volumes
            // (VmConfig::with_volume) take precedence; baked-in volumes from the
            // image (OciImageBuilder::with_volume, persisted in metadata) are
            // appended for any guest_path the runtime didn't already cover.
            let mut vol_specs: Vec<crate::vmm::resources::VolumeSpec> = config.volumes.clone();
            for (host_file, guest_path, size_bytes, _pristine) in &image.volumes {
                if !vol_specs.iter().any(|v| v.guest_path == *guest_path) {
                    vol_specs.push(
                        crate::vmm::resources::VolumeSpec::new(
                            host_file.to_string_lossy().to_string(),
                            guest_path.clone(),
                        )
                        .with_size_bytes(*size_bytes),
                    );
                }
            }
            let volumes = prepare_kvm_volumes(&vol_specs)?;
            // virtio-fs mounts: expose each host dir to the guest over
            // FUSE-over-virtio (init reads the sm.virtiofs cmdline tokens).
            let mut mount_specs: Vec<crate::vmm::resources::MountSpec> = config.virtiofs.clone();
            for m in &image.mounts {
                if !mount_specs.iter().any(|existing| {
                    existing.guest_tag == m.guest_tag || existing.guest_path == m.guest_path
                }) {
                    mount_specs.push(m.clone());
                }
            }
            let virtiofs: Vec<crate::kvm::run::VirtioFsAttach> = mount_specs
                .iter()
                .map(|m| crate::kvm::run::VirtioFsAttach {
                    host_path: m.host_path.clone(),
                    tag: m.guest_tag.clone(),
                    mount: m.guest_path.clone(),
                })
                .collect();
            let lcfg = LinuxVmConfig {
                mem_size: (mem_mib as usize) * 1024 * 1024,
                num_cpus,
                kernel: &kernel,
                initrd: Some(&initrd),
                disk_path: disk_str.as_deref(),
                disk_size,
                cmdline: &cmdline,
                enable_vsock: true,
                volumes: &volumes,
                virtiofs: &virtiofs,
                tsi_token,
                // Balloon is a cold-boot-only memory-reclaim lever (not
                // snapshotted). Honored here on the cold-boot path; the warm-pool
                // restore path (above) never reaches this branch.
                enable_balloon: config.enable_balloon,
            };
            LinuxVm::new(&lcfg).map_err(|e| Error::vm_msg(format!("LinuxVm::new: {e}")))?
        };
        // Host-side unix socket bridged to the guest exec agent (AF_VSOCK 1028);
        // crate::exec dials this for exec/write_file/read_file/workload_signal.
        let exec_path = vm
            .start_exec_bridge(1028)
            .map_err(|e| Error::vm_msg(format!("start_exec_bridge: {e}")))?;
        // Workload TSI listener frontend (host→guest ingress for `expose_tcp` /
        // `connect`). Egress (guest→host `connect`) needs no frontend — the
        // shared vsock muxer terminates the guest's TSI control packets off the
        // TX queue and opens real host sockets (subject to egress_policy). If
        // binding the mux socket fails, fall back to an unwired path so exec
        // still works and only ingress is unavailable.
        let mux_path = vm
            .start_tsi_mux()
            .unwrap_or_else(|_| exec_path.with_extension("mux-unwired"));
        // Launch the vCPUs on background threads — the guest is now live and the
        // agent comes up to serve exec. The handle is held for stop/Drop.
        let running = vm.start_running();
        // Re-sync the guest clock to host time once the agent is up. The cold-boot
        // init already sets it from the cmdline, but a RESTORE inherits the
        // snapshot's (possibly stale) captured clock — so refresh it in the
        // background (non-blocking) to keep TLS/cert validation correct. Retries
        // until the agent answers or a deadline.
        let sync_path = exec_path.clone();
        let time_sync_stop = Arc::new(AtomicBool::new(false));
        let stop_c = time_sync_stop.clone();
        std::thread::spawn(move || {
            let deadline = std::time::Instant::now() + Duration::from_secs(30);
            loop {
                // Exit promptly once the VM is dropped, instead of retrying
                // against a dead VM for the whole deadline (per-VM thread leak).
                if stop_c.load(Ordering::SeqCst) {
                    break;
                }
                if sync_time_via_agent(&sync_path) {
                    break;
                }
                if std::time::Instant::now() >= deadline {
                    break;
                }
                std::thread::sleep(Duration::from_millis(200));
            }
        });
        Ok(Vm {
            running: Some(running),
            sentry_exec: None,
            sentry_src_rootfs: None,
            vsock_mux_path: mux_path,
            vsock_exec_path: exec_path,
            own_vsock_mux_dir: None,
            skip_cleanup: false,
            time_sync_stop,
            // KVM: the in-process VM thread is not memory-admitted on this path.
            _sentry_mem_admission: None,
            sentry_warm_pool: None,
            sentry_unified_pool: None,
            runsc: None,
            kboxlike: None,
        })
    }

    /// Start the no-virt **sentry** backend for a `"backend":"sentry"` image: run
    /// a process-isolation sandbox over the image's extracted rootfs and expose an
    /// [`ExecServer`](crate::sentry::ExecServer) speaking the SAME framed exec
    /// protocol the in-guest agent speaks. The returned [`Vm`] has no
    /// `RunningVm`; its `exec`/`write_file`/`read_file`/`spawn` go straight to the
    /// server socket, so the product surface is unchanged. The image's baked CMD
    /// (if any) is launched in the background, mirroring a VM booting its CMD.
    ///
    /// Used by [`Vm::start`] when [`kvm_usable`](Image::kvm_usable) is false /
    /// the image was sentry-baked. `expose_tcp` works (host-side proxy to the
    /// guest's host-netns listener) and `Vm::snapshot` captures the warm rootfs.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn start_sentry(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
        let parts = image
            .sentry
            .as_ref()
            .expect("start_sentry called on a non-sentry image");
        // Exec socket under the configured vsock dir (or a temp dir), named like
        // the KVM exec socket so tooling that scans the dir behaves the same.
        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        let mut own_dir = None;
        if !dir.is_dir() {
            std::fs::create_dir_all(&dir).map_err(Error::Io)?;
            own_dir = Some(dir.clone());
        }
        let exec_path = dir.join(format!(
            "supermachine-sentry-{}-{}.sock",
            std::process::id(),
            unique_suffix(),
        ));
        // Build the sandbox from the image (egress policy etc. from metadata),
        // then serve the exec protocol; launch the baked workload in the
        // background like a VM boots its CMD.
        //
        // SHARED host netns (`without_netns`): a `Vm`'s workload is meant to be
        // host-reachable — [`expose_tcp`]/[`expose_tls`] host-proxy to the guest's
        // listener on `127.0.0.1:guest_port`, which must be a real host socket.
        // The handle API (`Image::sentry_sandbox`) keeps the isolation-safe netns
        // default; this single-tenant exposed `Vm` is the documented opt-out.
        let sandbox = image
            .sentry_sandbox_with(config)
            .map_err(|e| Error::vm_msg(format!("sentry sandbox: {e}")))?
            .without_netns();
        // MEMORY admission: reserve this one cell's worst-case footprint (its
        // cgroup `memory.max` + overhead) against the process-wide budget before
        // serving — the one-shot analogue of `SentryPool::build`'s pool-wide
        // charge and of the KVM worker's per-VM charge. The guard rides in the
        // returned `Vm` and frees on Drop. Pressure backstop first, like
        // `spawn_one` / the pool path.
        crate::memory_admission::await_pressure_relief();
        let mem_mib = sentry_cell_mem_mib(image, config);
        let mem_admission =
            crate::memory_admission::admit_with(|| sentry_admission_charge_mib(mem_mib, 1));
        let (sentry_exec, sentry_unified_pool) = if !parts.warm_daemon {
            let pool = sandbox
                .pool_with_exec(exec_path.clone())
                .map_err(|e| Error::vm_msg(format!("sentry unified pool: {e}")))?;
            let deadline = std::time::Instant::now() + Duration::from_secs(15);
            while !exec_path.exists() {
                if std::time::Instant::now() >= deadline {
                    return Err(Error::vm_msg(
                        "sentry unified exec socket never appeared (supervisor bind failed)",
                    ));
                }
                std::thread::sleep(Duration::from_millis(5));
            }
            if parts.has_workload && parts.rootfs.join(".supermachine/run-workload").is_file() {
                let wrapped = vec![
                    "/.supermachine/sm-checkpoint".to_string(),
                    "/bin/sh".to_string(),
                    "/.supermachine/run-workload".to_string(),
                ];
                if pool.warm(wrapped.iter().cloned()).is_ok() {
                    if let Ok(wpid) = pool.acquire_running() {
                        let run_dir = parts.rootfs.join("run");
                        let _ = std::fs::create_dir_all(&run_dir);
                        let _ = std::fs::write(
                            run_dir.join("supermachine-workload.pid"),
                            format!("{wpid}\n"),
                        );
                    }
                }
            }
            (None, Some(Arc::new(pool)))
        } else {
            let server = sandbox
                .serve_exec(&exec_path, parts.has_workload)
                .map_err(|e| Error::vm_msg(format!("sentry serve_exec: {e}")))?;
            (Some(server), None)
        };
        Ok(Vm {
            running: None,
            sentry_exec,
            // The cells run against the image's rootfs; `Vm::snapshot` copies it
            // to capture the warm filesystem state.
            sentry_src_rootfs: Some(parts.rootfs.clone()),
            // The sentry has no TSI mux; point the mux path at the exec socket so
            // `vsock_path()` returns something valid and cleanup unlinks one file.
            vsock_mux_path: exec_path.clone(),
            vsock_exec_path: exec_path,
            own_vsock_mux_dir: own_dir,
            skip_cleanup: false,
            time_sync_stop: Arc::new(AtomicBool::new(false)),
            _sentry_mem_admission: Some(mem_admission),
            sentry_warm_pool: None,
            sentry_unified_pool,
            runsc: None,
            kboxlike: None,
        })
    }

    /// Start the experimental external no-KVM **runsc** backend. This first
    /// product slice is a cold detached runsc container plus `runsc exec` for
    /// `ExecBuilder::output`; checkpoint/restore-backed pools are layered on top
    /// of this lifecycle.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn start_runsc(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
        let parts = image
            .runsc
            .as_ref()
            .expect("start_runsc called on a non-runsc image");
        let runsc_bin = std::env::var_os("SUPERMACHINE_RUNSC_BIN")
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from("runsc"));
        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        std::fs::create_dir_all(&dir).map_err(Error::Io)?;
        let runtime_dir = dir.join(format!(
            "supermachine-runsc-{}-{}",
            std::process::id(),
            unique_suffix()
        ));
        let bundle_dir = runtime_dir.join("bundle");
        let private_rootfs = bundle_dir.join("rootfs");
        let runsc_root = runtime_dir.join("runsc-root");
        std::fs::create_dir_all(&runsc_root).map_err(Error::Io)?;
        let network = runsc_network_for_config(config).to_owned();
        let build_netns = if network == crate::exec::RUNSC_NETWORK_SANDBOX {
            Some(RunscBuildNetwork::create()?)
        } else {
            None
        };
        let netns_path = build_netns.as_ref().map(|net| net.path());
        prepare_runsc_bundle(
            &runsc_bin,
            parts,
            &bundle_dir,
            &private_rootfs,
            &network,
            netns_path.as_deref(),
        )?;
        let container_id = format!("sm-runsc-{}-{}", std::process::id(), unique_suffix());
        let pid_file = runtime_dir.join("container.pid");
        if let Some(checkpoint) = &parts.checkpoint {
            if let Err(e) = runsc_restore_detached(
                &runsc_bin,
                &runsc_root,
                &network,
                &bundle_dir,
                checkpoint,
                &pid_file,
                &container_id,
            ) {
                let _ = std::fs::remove_dir_all(&runtime_dir);
                return Err(e);
            }
        } else {
            let mut run = Command::new(&runsc_bin);
            crate::exec::add_runsc_global_flags_with_network(&mut run, &runsc_root, &network);
            let run_out = run
                .arg("run")
                .arg("-detach")
                .arg("-bundle")
                .arg(&bundle_dir)
                .arg("-pid-file")
                .arg(&pid_file)
                .arg(&container_id)
                .stdin(Stdio::null())
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .status()
                .map_err(|e| Error::vm_msg(format!("runsc run {container_id}: {e}")))?;
            if !run_out.success() {
                let _ = std::fs::remove_dir_all(&runtime_dir);
                return Err(Error::vm_msg(format!(
                    "runsc run failed with status {:?}",
                    run_out.code()
                )));
            }
        }

        Ok(Vm {
            running: None,
            sentry_exec: None,
            sentry_src_rootfs: None,
            vsock_mux_path: runtime_dir.join("runsc-unwired.sock"),
            vsock_exec_path: runtime_dir.join("runsc-exec-unwired.sock"),
            own_vsock_mux_dir: None,
            skip_cleanup: false,
            time_sync_stop: Arc::new(AtomicBool::new(false)),
            _sentry_mem_admission: None,
            sentry_warm_pool: None,
            sentry_unified_pool: None,
            runsc: Some(RunscRuntime {
                runsc_bin,
                runsc_root,
                network,
                build_netns,
                container_id,
                bundle_dir,
                rootfs: private_rootfs,
                parts: parts.clone(),
                file_agent_exec: false,
            }),
            kboxlike: None,
        })
    }

    /// Start the native kbox-inspired no-KVM backend. This first product slice
    /// launches the baked workload under the extracted rootfs, captures the live
    /// tree at a stopped boundary, restores it into replacement host processes,
    /// then owns those restored processes as the VM lifetime.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn start_kboxlike(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
        let parts = image
            .kboxlike
            .as_ref()
            .expect("start_kboxlike called on a non-kboxlike image");
        Self::start_kboxlike_parts(parts, config)
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_mounts_for_start(
        parts: &KboxlikeImageParts,
        config: &VmConfig,
    ) -> Result<Vec<(PathBuf, String)>, Error> {
        let mut mounts = Vec::new();
        let mut covered_guest_paths = std::collections::BTreeSet::new();

        // Runtime config wins, matching the KVM cold-boot path: callers can
        // override a baked mapping by reusing the same guest path.
        for m in &config.virtiofs {
            covered_guest_paths.insert(m.guest_path.clone());
            mounts.push((PathBuf::from(&m.host_path), m.guest_path.clone()));
        }
        for m in &parts.mounts {
            if covered_guest_paths.insert(m.guest_path.clone()) {
                mounts.push((PathBuf::from(&m.host_path), m.guest_path.clone()));
            }
        }

        // No-virt volumes are directory-backed bind mounts. Append them after
        // regular mounts so nested volumes overlay parent workspace mounts.
        for v in &config.volumes {
            let host_dir = PathBuf::from(&v.host_path);
            std::fs::create_dir_all(&host_dir).map_err(Error::Io)?;
            covered_guest_paths.insert(v.guest_path.clone());
            mounts.push((host_dir, v.guest_path.clone()));
        }
        for (host_dir, guest_path, _size, _pristine) in &parts.volumes {
            if !covered_guest_paths.insert(guest_path.clone()) {
                continue;
            }
            std::fs::create_dir_all(host_dir).map_err(Error::Io)?;
            mounts.push((host_dir.clone(), guest_path.clone()));
        }

        Ok(mounts)
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn start_kboxlike_parts(parts: &KboxlikeImageParts, config: &VmConfig) -> Result<Vm, Error> {
        if parts.warm_daemon {
            return Err(Error::vm_msg(
                "kboxlike warm-daemon images are not implemented yet".to_owned(),
            ));
        }

        let workload = parts.rootfs.join(".supermachine/run-workload");
        let argv = if parts.has_workload && workload.is_file() {
            vec![
                "/bin/sh".to_owned(),
                "/.supermachine/run-workload".to_owned(),
            ]
        } else if !parts.cmd.is_empty() {
            parts.cmd.clone()
        } else {
            return Err(Error::vm_msg(
                "kboxlike image has no staged workload or recorded cmd".to_owned(),
            ));
        };
        let model_exe = kboxlike_host_exe_path(parts, argv.first().map(String::as_str));

        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        std::fs::create_dir_all(&dir).map_err(Error::Io)?;
        let runtime_dir = dir.join(format!(
            "supermachine-kboxlike-{}-{}",
            std::process::id(),
            unique_suffix()
        ));
        let shadow_root = runtime_dir.join("shadow");
        std::fs::create_dir_all(&shadow_root).map_err(Error::Io)?;

        let settle_ms = std::env::var("SUPERMACHINE_KBOXLIKE_CAPTURE_SETTLE_MS")
            .ok()
            .and_then(|raw| raw.parse::<u64>().ok())
            .unwrap_or(750);
        let min_tracees = std::env::var("SUPERMACHINE_KBOXLIKE_MIN_TRACEES")
            .ok()
            .and_then(|raw| raw.parse::<usize>().ok())
            .unwrap_or(1);
        let max_stops = std::env::var("SUPERMACHINE_KBOXLIKE_MAX_STOPS")
            .ok()
            .and_then(|raw| raw.parse::<usize>().ok())
            .unwrap_or(200000);
        let runtime_mounts = Self::kboxlike_mounts_for_start(parts, config)?;

        let captured =
            crate::kboxlike::capture_live_ptrace_rootfs_exec_snapshot_after_sigstop_request(
                &crate::kboxlike::KboxlikeRootfsExecConfig {
                    rootfs: parts.rootfs.clone(),
                    argv,
                    env: parts.image_env.clone(),
                    cwd: "/".to_owned(),
                    user: parts.user,
                    mounts: runtime_mounts.clone(),
                },
                min_tracees,
                max_stops,
                std::time::Duration::from_millis(settle_ms),
            )
            .map_err(|e| {
                let _ = std::fs::remove_dir_all(&runtime_dir);
                Error::vm_msg(format!("kboxlike capture workload: {e:?}"))
            })?;

        let mut source_root_host_pid = Some(captured.root_host_pid);
        let mut replacement_factory =
            if let Some(path) = std::env::var_os("SUPERMACHINE_KBOXLIKE_REPLACEMENT_PATH") {
                crate::kboxlike::LinuxStoppedTraceeReplacementFactory::with_static_replacement_path(
                    PathBuf::from(path),
                )
            } else if Path::new("/bin/busybox").is_file() {
                crate::kboxlike::LinuxStoppedTraceeReplacementFactory::with_static_replacement_path(
                    "/bin/busybox",
                )
            } else {
                crate::kboxlike::LinuxStoppedTraceeReplacementFactory::default()
            };
        let mut resumer = crate::kboxlike::PtraceDetachRestoredTraceeResumer;
        let trace_label =
            std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_VM_START").map(|_| "KBOXLIVE_VM_START");
        let restore = crate::kboxlike::restore_runtime_snapshot_with_replacements(
            &captured.snapshot,
            &model_exe,
            crate::kboxlike::KboxlikeRuntimeRestoreOptions {
                shadow_root: shadow_root.clone(),
                trace_label,
                ..Default::default()
            },
            &mut replacement_factory,
            &mut resumer,
        )
        .map_err(|e| {
            if let Some(pid) = source_root_host_pid.take() {
                crate::kboxlike::kill_and_reap_live_process_group(pid);
            }
            replacement_factory.kill_created_tracees();
            let _ = std::fs::remove_dir_all(&runtime_dir);
            Error::vm_msg(format!("kboxlike restore workload: {e:?}"))
        })?;

        if let Some(pid) = source_root_host_pid.take() {
            crate::kboxlike::kill_and_reap_live_process_group(pid);
        }
        let restored_host_pids = restore
            .restore
            .replacement_host_pids
            .values()
            .copied()
            .collect::<std::collections::BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();

        Ok(Vm {
            running: None,
            sentry_exec: None,
            sentry_src_rootfs: None,
            vsock_mux_path: runtime_dir.join("kboxlike-unwired.sock"),
            vsock_exec_path: runtime_dir.join("kboxlike-exec-unwired.sock"),
            own_vsock_mux_dir: None,
            skip_cleanup: false,
            time_sync_stop: Arc::new(AtomicBool::new(false)),
            _sentry_mem_admission: None,
            sentry_warm_pool: None,
            sentry_unified_pool: None,
            runsc: None,
            kboxlike: Some(KboxlikeRuntime {
                runtime_dir,
                source_root_host_pid,
                restored_host_pids,
                rootfs: parts.rootfs.clone(),
                parts: parts.clone(),
                mounts: runtime_mounts,
            }),
        })
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn start_kboxlike_build_root(
        parts: &KboxlikeImageParts,
        config: &VmConfig,
    ) -> Result<Vm, Error> {
        let dir = match &config.vsock_mux_dir {
            Some(d) => d.clone(),
            None => std::env::temp_dir(),
        };
        std::fs::create_dir_all(&dir).map_err(Error::Io)?;
        let runtime_dir = dir.join(format!(
            "supermachine-kboxlike-build-vm-{}-{}",
            std::process::id(),
            unique_suffix()
        ));
        std::fs::create_dir_all(&runtime_dir).map_err(Error::Io)?;
        let runtime_mounts = Self::kboxlike_mounts_for_start(parts, config)?;

        Ok(Vm {
            running: None,
            sentry_exec: None,
            sentry_src_rootfs: None,
            vsock_mux_path: runtime_dir.join("kboxlike-build-unwired.sock"),
            vsock_exec_path: runtime_dir.join("kboxlike-build-exec-unwired.sock"),
            own_vsock_mux_dir: None,
            skip_cleanup: false,
            time_sync_stop: Arc::new(AtomicBool::new(false)),
            _sentry_mem_admission: None,
            sentry_warm_pool: None,
            sentry_unified_pool: None,
            runsc: None,
            kboxlike: Some(KboxlikeRuntime {
                runtime_dir,
                source_root_host_pid: None,
                restored_host_pids: Vec::new(),
                rootfs: parts.rootfs.clone(),
                parts: parts.clone(),
                mounts: runtime_mounts,
            }),
        })
    }

    /// Path to the host-side unix socket that proxies bytes to /
    /// from the first TSI listener inside the guest. Connect to it
    /// with [`UnixStream::connect`] (or via [`Vm::connect`]).
    pub fn vsock_path(&self) -> &Path {
        &self.vsock_mux_path
    }

    /// Path to the host-side unix socket that bridges to the
    /// in-guest exec agent (native AF_VSOCK on the guest side).
    /// Reachable once the agent lands in the initramfs and is
    /// running guest-side; until then dialing it returns an
    /// immediate EOF.
    pub fn exec_path(&self) -> &Path {
        &self.vsock_exec_path
    }

    /// Opaque token for language bindings that need to reconstruct an
    /// [`crate::exec::ExecBuilder`] later. For socket-backed backends this is
    /// just the exec socket path. For runsc it encodes the host-control-plane
    /// target needed by `runsc exec`.
    #[doc(hidden)]
    pub fn exec_binding_token(&self) -> String {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            let target = crate::exec::RunscExecTarget {
                runsc_bin: runsc.runsc_bin.clone(),
                runsc_root: runsc.runsc_root.clone(),
                network: Some(runsc.network.clone()),
                container_id: runsc.container_id.clone(),
                image_env: runsc.parts.image_env.clone(),
                agent_path: Some(runsc.parts.agent_path.clone()),
                file_agent_rootfs: runsc.file_agent_exec.then(|| runsc.rootfs.clone()),
            };
            if let Ok(bytes) = serde_json::to_vec(&target) {
                return format!(
                    "{}{}",
                    crate::exec::RUNSC_EXEC_TOKEN_PREFIX,
                    b64_encode(&bytes)
                );
            }
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            let target = crate::exec::KboxlikeExecTarget {
                rootfs: kboxlike.rootfs.clone(),
                image_env: kboxlike.parts.image_env.clone(),
                user: kboxlike.parts.user,
                mounts: kboxlike.mounts.clone(),
            };
            if let Ok(bytes) = serde_json::to_vec(&target) {
                return format!(
                    "{}{}",
                    crate::exec::KBOXLIKE_EXEC_TOKEN_PREFIX,
                    b64_encode(&bytes)
                );
            }
        }
        self.vsock_exec_path.to_string_lossy().to_string()
    }

    /// Spawn a process inside the running guest. Equivalent to
    /// `docker exec`. Returns an [`crate::exec::ExecChild`] handle
    /// you can read stdout/stderr from, write stdin to, and
    /// `wait()` for an exit status.
    ///
    /// ```no_run
    /// # use std::io::Read;
    /// # use supermachine::{Image, Vm, VmConfig};
    /// let image = Image::from_snapshot("path/to/snapshot")?;
    /// let vm = Vm::start(&image, &VmConfig::new())?;
    /// let mut child = vm.exec(["sh", "-c", "echo hi"])?;
    /// let mut buf = String::new();
    /// child.stdout().unwrap().read_to_string(&mut buf)?;
    /// assert_eq!(buf, "hi\n");
    /// child.wait()?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn exec<I, S>(&self, argv: I) -> std::io::Result<crate::exec::ExecChild>
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let argv: Vec<String> = argv.into_iter().map(|s| s.into()).collect();
        let _span = tracing::info_span!(
            "supermachine.exec",
            argv0 = argv.first().map(|s| s.as_str()).unwrap_or(""),
            argc = argv.len(),
        )
        .entered();
        self.exec_builder().argv(argv).spawn()
    }

    /// Configurable exec — TTY, env vars, cwd, initial winsize,
    /// timeout, and the [`crate::exec::ExecBuilder::output`]
    /// convenience that drains stdio + collects exit status into
    /// one [`crate::exec::ExecOutcome`].
    pub fn exec_builder(&self) -> crate::exec::ExecBuilder {
        let b = crate::exec::ExecBuilder::new(self.vsock_exec_path.clone());
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return b.with_runsc_exec(crate::exec::RunscExecTarget {
                runsc_bin: runsc.runsc_bin.clone(),
                runsc_root: runsc.runsc_root.clone(),
                network: Some(runsc.network.clone()),
                container_id: runsc.container_id.clone(),
                image_env: runsc.parts.image_env.clone(),
                agent_path: Some(runsc.parts.agent_path.clone()),
                file_agent_rootfs: runsc.file_agent_exec.then(|| runsc.rootfs.clone()),
            });
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return b.with_kboxlike_exec(crate::exec::KboxlikeExecTarget {
                rootfs: kboxlike.rootfs.clone(),
                image_env: kboxlike.parts.image_env.clone(),
                user: kboxlike.parts.user,
                mounts: kboxlike.mounts.clone(),
            });
        }
        // WarmDaemon pooled Vm: route `.output()` through the warm Pool
        // (Pool::exec_capture over the owned loopback) — a warm-daemon pooled Vm has
        // no exec socket. `Vm::exec` (streaming spawn) stays socket-only; the
        // request/response daemon-client model uses `.output()`.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(pool) = &self.sentry_warm_pool {
            return b.with_warm_pool(Arc::clone(pool));
        }
        b
    }

    /// Write `bytes` to `path` inside the guest, atomically.
    /// Native vsock RPC — no exec, no shell. Roughly ~100 µs per
    /// call regardless of file size (up to the 12 MiB raw limit
    /// imposed by the agent's frame cap).
    ///
    /// The guest agent stages to a sibling tmp file then renames
    /// for atomicity, so partial writes don't leave a half-baked
    /// file at `path`.
    ///
    /// ```no_run
    /// # use supermachine::{Image, VmConfig};
    /// let image = Image::from_snapshot("path/to/snapshot")?;
    /// let vm = image.start(&VmConfig::new())?;
    /// vm.write_file("/tmp/main.rs", b"fn main() { println!(\"hi\"); }")?;
    /// let out = vm.exec_builder()
    ///     .argv(["rustc", "/tmp/main.rs", "-o", "/tmp/main"])
    ///     .output()?;
    /// assert!(out.success());
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn write_file(&self, path: &str, bytes: &[u8]) -> std::io::Result<()> {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return runsc_write_file(runsc, path, bytes);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return kboxlike_write_file(kboxlike, path, bytes);
        }
        let body = serde_json::json!({
            "action": "write_file",
            "path": path,
            "data_b64": b64_encode(bytes),
        });
        crate::exec::send_control(&self.vsock_exec_path, &body)
    }

    /// Read `path` from inside the guest. Default cap is 32 MiB
    /// (raised in 0.5.0 from the prior 4 MiB to accommodate
    /// Playwright trace.zip artifacts and similar mid-sized blobs).
    /// For larger files use [`Vm::read_file_with_max_bytes`] with
    /// an explicit cap, or stream via [`Vm::exec`] (`cat`).
    pub fn read_file(&self, path: &str) -> std::io::Result<Vec<u8>> {
        self.read_file_with_max_bytes(path, 32 * 1024 * 1024)
    }

    /// Like [`Vm::read_file`] but with an explicit byte cap. Returns
    /// an error if the file exceeds `max_bytes` so the caller never
    /// gets a partial result silently. Cap is enforced inside the
    /// guest before any bytes leave it.
    pub fn read_file_with_max_bytes(&self, path: &str, max_bytes: u64) -> std::io::Result<Vec<u8>> {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return runsc_read_file(runsc, path, max_bytes);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return kboxlike_read_file(kboxlike, path, max_bytes);
        }
        let body = serde_json::json!({
            "action": "read_file",
            "path": path,
            "max_bytes": max_bytes,
        });
        // Generous read timeout for large reads — file IO inside
        // the VM is fast, but we want to tolerate cold-cache cases.
        let ack = crate::exec::send_control_with_ack(
            &self.vsock_exec_path,
            &body,
            Some(std::time::Duration::from_secs(30)),
        )?;
        let data_b64 = ack
            .get("data_b64")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "read_file: agent ack missing data_b64",
                )
            })?;
        b64_decode(data_b64).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
    }

    /// Send a Unix signal to the guest's main workload process.
    /// Use this for `docker stop`-style graceful shutdown:
    ///
    /// ```no_run
    /// # use supermachine::{Image, Vm, VmConfig};
    /// # let image = Image::from_snapshot("path")?;
    /// # let vm = Vm::start(&image, &VmConfig::new())?;
    /// vm.workload_signal(libc::SIGTERM)?;
    /// // ...wait for the workload to clean up...
    /// vm.stop()?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// Implementation: dials the in-guest exec agent on a fresh
    /// connection with a CONTROL frame; the agent reads
    /// `/run/supermachine-workload.pid` (written by init-oci's
    /// PID-1 supervisor) and `kill(pid, signum)` it. Returns
    /// `Err(NotFound)` if the workload hasn't been spawned yet
    /// (only happens during the bake-time window).
    pub fn workload_signal(&self, signum: i32) -> std::io::Result<()> {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return runsc_workload_signal(runsc, signum);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return kboxlike_workload_signal(kboxlike, signum);
        }
        let body = serde_json::json!({
            "action": "signal",
            "signum": signum,
        });
        crate::exec::send_control(&self.vsock_exec_path, &body)
    }

    /// Connect to the guest's first TSI listener. The returned
    /// `UnixStream` is byte-equivalent to a `TcpStream` to the
    /// guest's `:80` (or whatever port it bound).
    ///
    /// For HTTP, just write a request and read the response:
    /// supermachine's vsock-mux is a transparent proxy.
    ///
    /// On the sentry backend the persistent supervisor provides the equivalent
    /// first-listener route through its owned LoopNet.
    pub fn connect(&self) -> std::io::Result<UnixStream> {
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return runsc_connect_port(runsc, 0);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return kboxlike_connect_port(kboxlike, 0);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if self.running.is_none() {
            return connect_port_sentry(&self.vsock_exec_path, 0);
        }
        UnixStream::connect(&self.vsock_mux_path)
    }

    /// Connect to the guest workload's listener on `port`, returning a
    /// [`UnixStream`] that is byte-equivalent to a `TcpStream` to the
    /// guest's `:port` — write a request and read the response.
    ///
    /// This is [`connect`](Vm::connect) with an explicit destination
    /// port. On the KVM/HVF backend it dials the vsock-mux and prefixes
    /// the connection with the `SMUX-PORT-V1` routing header (the same
    /// header [`expose_tcp`](Vm::expose_tcp) writes), so the mux routes
    /// to the matching guest TSI listener rather than the first one;
    /// `port == 0` opts out of the header and keeps the legacy
    /// first-listener behavior.
    ///
    /// On the sentry backend this dials the persistent supervisor and asks it to
    /// connect to the workload's supervisor-owned LoopNet listener. `port == 0`
    /// preserves the legacy first-listener behavior.
    pub fn connect_port(&self, port: u16) -> std::io::Result<UnixStream> {
        // A sentry-backed `Vm` has no `RunningVm` handle. Its persistent
        // supervisor owns the LoopNet listener and supplies the bridge.
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return runsc_connect_port(runsc, port);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return kboxlike_connect_port(kboxlike, port);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if self.running.is_none() {
            return connect_port_sentry(&self.vsock_exec_path, port);
        }
        let unix = UnixStream::connect(&self.vsock_mux_path)?;
        if port != 0 {
            // 16-byte magic + 4-byte BE port — same routing header as
            // `splice_tcp_to_unix`. Keep in sync with
            // `vmm::vsock_mux::SMUX_PORT_MAGIC`.
            let mut hdr = [0u8; 20];
            hdr[..16].copy_from_slice(b"SMUX-PORT-V1\0\0\0\0");
            hdr[16..].copy_from_slice(&(port as u32).to_be_bytes());
            (&unix).write_all(&hdr)?;
        }
        Ok(unix)
    }

    /// Reclaim guest memory via virtio-balloon: ask the guest to release `pages`
    /// 4 KiB pages, which the device then `madvise(MADV_FREE)`s on the host RAM
    /// map (the kernel can drop them under pressure). Requires the VM to have been
    /// started with [`VmConfig::with_balloon(true)`](VmConfig::with_balloon) on a
    /// cold boot; returns `false` if no balloon device is attached. Asynchronous —
    /// the guest's balloon driver frees pages in the background.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn request_balloon_inflate(&self, pages: u32) -> bool {
        self.running
            .as_ref()
            .map(|r| r.request_balloon_inflate(pages))
            .unwrap_or(false)
    }

    /// Bind a TCP listener on `127.0.0.1:host_port` that forwards
    /// each accepted connection to the guest's TSI listener (the
    /// same destination as [`Vm::connect`]). Returns a
    /// [`TcpForwarder`] that owns the accept-loop thread; drop it
    /// (or call [`TcpForwarder::stop`]) to stop accepting new
    /// connections. In-flight connections continue until they close
    /// naturally.
    ///
    /// `host_port = 0` lets the OS pick a free port; read the actual
    /// address back via [`TcpForwarder::local_addr`].
    ///
    /// `guest_port` pins the host-port → guest-port mapping. The
    /// host writes a small per-connection routing header (`SMUX-PORT
    /// -V1\0\0\0\0` + u32 BE port) before piping bytes, and the
    /// vsock-mux peeks (`MSG_PEEK`) that header and routes to the
    /// matching TSI listener instead of falling back to "first
    /// AF_INET listener." Pass `guest_port = 0` to opt out of the
    /// header and keep the legacy first-listener behavior — useful
    /// when the guest only binds one port and you want a router-
    /// style any-port forward.
    ///
    /// Use this when you want the embedded VM to look like a normal
    /// localhost service (e.g. `http://127.0.0.1:9090/`) rather than
    /// having every caller go through `vm.connect()`.
    ///
    /// ```no_run
    /// # use supermachine::{Image, Vm, VmConfig};
    /// let image = Image::from_snapshot("path/to/snapshot")?;
    /// let vm = Vm::start(&image, &VmConfig::new())?;
    /// let fwd = vm.expose_tcp(9090, 80)?;
    /// println!("nginx is on {}", fwd.local_addr());
    /// // ... do work ...
    /// drop(fwd); // stop forwarding
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    pub fn expose_tcp(&self, host_port: u16, guest_port: u16) -> std::io::Result<TcpForwarder> {
        // Sentry has no vsock/TSI mux. Its persistent supervisor owns the
        // in-process LoopNet, so ingress must be handed back to that supervisor.
        // A sentry-backed `Vm` has no `RunningVm` (`running.is_none()`).
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(runsc) = &self.runsc {
            return self.expose_tcp_runsc(host_port, guest_port, runsc);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if let Some(kboxlike) = &self.kboxlike {
            return self.expose_tcp_kboxlike(host_port, guest_port, kboxlike);
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        if self.running.is_none() {
            return self.expose_tcp_sentry(host_port, guest_port);
        }
        let listener = TcpListener::bind(("127.0.0.1", host_port))?;
        let bound = listener.local_addr()?;
        // Short accept timeout so the stop flag is responsive.
        listener.set_nonblocking(false)?;
        let stop = Arc::new(AtomicBool::new(false));
        let stop_thread = stop.clone();
        let vsock_path = self.vsock_mux_path.clone();
        let handle = std::thread::Builder::new()
            .name(format!("supermachine-tcp-{host_port}"))
            .spawn(move || {
                accept_loop(listener, vsock_path, stop_thread, guest_port);
            })
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
        // Best-effort: poke the listener to unblock its accept on
        // shutdown. We rely on `stop` flag + a self-connect during
        // drop. See TcpForwarder::drop.
        Ok(TcpForwarder {
            stop,
            handle: Some(handle),
            bound,
        })
    }

    /// Sentry backend variant of [`expose_tcp`](Vm::expose_tcp): a host-side TCP
    /// proxy into the persistent supervisor's owned LoopNet.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn expose_tcp_sentry(&self, host_port: u16, guest_port: u16) -> std::io::Result<TcpForwarder> {
        let listener = TcpListener::bind(("127.0.0.1", host_port))?;
        let bound = listener.local_addr()?;
        listener.set_nonblocking(false)?;
        let stop = Arc::new(AtomicBool::new(false));
        let stop_thread = stop.clone();
        let exec_path = self.vsock_exec_path.clone();
        let handle = std::thread::Builder::new()
            .name(format!("supermachine-tcp-{host_port}"))
            .spawn(move || sentry_accept_loop(listener, exec_path, guest_port, stop_thread))
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
        Ok(TcpForwarder {
            stop,
            handle: Some(handle),
            bound,
        })
    }

    /// Runsc backend variant of [`expose_tcp`](Vm::expose_tcp): a host-side TCP
    /// proxy that creates one `runsc exec` + `supermachine-agent --tcp-stdio`
    /// bridge per accepted connection.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn expose_tcp_runsc(
        &self,
        host_port: u16,
        guest_port: u16,
        runsc: &RunscRuntime,
    ) -> std::io::Result<TcpForwarder> {
        let listener = TcpListener::bind(("127.0.0.1", host_port))?;
        let bound = listener.local_addr()?;
        listener.set_nonblocking(false)?;
        let stop = Arc::new(AtomicBool::new(false));
        let stop_thread = stop.clone();
        let target = runsc_port_forward_target(runsc);
        let handle = std::thread::Builder::new()
            .name(format!("supermachine-tcp-{host_port}"))
            .spawn(move || runsc_accept_loop(listener, target, guest_port, stop_thread))
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
        Ok(TcpForwarder {
            stop,
            handle: Some(handle),
            bound,
        })
    }

    /// Kboxlike backend variant of [`expose_tcp`](Vm::expose_tcp): while the
    /// backend still restores into the host network namespace, ingress is a
    /// host-side TCP proxy into the restored workload's loopback listener.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn expose_tcp_kboxlike(
        &self,
        host_port: u16,
        guest_port: u16,
        kboxlike: &KboxlikeRuntime,
    ) -> std::io::Result<TcpForwarder> {
        let listener = TcpListener::bind(("127.0.0.1", host_port))?;
        let bound = listener.local_addr()?;
        listener.set_nonblocking(false)?;
        let stop = Arc::new(AtomicBool::new(false));
        let stop_thread = stop.clone();
        let target = kboxlike_port_forward_target(kboxlike);
        let handle = std::thread::Builder::new()
            .name(format!("supermachine-tcp-{host_port}"))
            .spawn(move || kboxlike_accept_loop(listener, target, guest_port, stop_thread))
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
        Ok(TcpForwarder {
            stop,
            handle: Some(handle),
            bound,
        })
    }

    /// (Linux/KVM) Start a host-side TLS terminator for this VM: accept HTTPS on
    /// `cfg.listen_addr`, terminate with rustls, and bridge decrypted plaintext
    /// to the guest's TSI listener — the guest serves plain HTTP, no TLS code in
    /// the guest. This is the in-process counterpart of the macOS/HVF worker's
    /// `--tls-*` flags (feature parity). Fire-and-forget: the acceptor runs for
    /// the VM's lifetime. Requires vsock enabled.
    ///
    /// On the sentry backend (`running.is_none()`) there is no vsock/TSI mux. The
    /// persistent supervisor owns the workload LoopNet, so the same rustls
    /// terminator bridges decrypted plaintext through the supervisor's bridge to
    /// `127.0.0.1:guest_port`. The guest TCP port is taken from `cfg.vm_port`,
    /// which must be `Some(p)` with `p` a non-zero value that fits a `u16` (the
    /// sentry has no "first TSI listener" fallback to route a 0 against, mirroring
    /// [`expose_tcp`](Vm::expose_tcp)'s `guest_port` rule).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn expose_tls(
        &self,
        cfg: crate::vmm::tls::TlsConfig,
    ) -> std::io::Result<std::net::SocketAddr> {
        // Sentry backend: no RunningVm handle, no vsock mux. Terminate TLS and
        // splice plaintext through the persistent supervisor's LoopNet bridge.
        if self.runsc.is_some() {
            let runsc = self.runsc.as_ref().ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::NotConnected, "runsc VM is not running")
            })?;
            let guest_port = match cfg.vm_port {
                Some(p) if p != 0 && p <= u16::MAX as u32 => p as u16,
                _ => {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "expose_tls on the runsc backend requires cfg.vm_port = Some(non-zero u16 guest port)",
                    ))
                }
            };
            let target = runsc_port_forward_target(runsc);
            return crate::vmm::tls::start_to_unix_stream(
                cfg,
                format!("runsc bridge guest 127.0.0.1:{guest_port}"),
                move || runsc_connect_port_target(&target, guest_port),
            )
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()));
        }
        if let Some(kboxlike) = &self.kboxlike {
            let guest_port = match cfg.vm_port {
                Some(p) if p != 0 && p <= u16::MAX as u32 => p as u16,
                _ => {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        "expose_tls on the kboxlike backend requires cfg.vm_port = Some(non-zero u16 guest port)",
                    ))
                }
            };
            let target = kboxlike_port_forward_target(kboxlike);
            return crate::vmm::tls::start_to_unix_stream(
                cfg,
                format!("kboxlike bridge guest 127.0.0.1:{guest_port}"),
                move || kboxlike_connect_port_target(&target, guest_port),
            )
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()));
        }
        if self.running.is_none() {
            return self.expose_tls_sentry(cfg);
        }
        let running = self.running.as_ref().ok_or_else(|| {
            std::io::Error::new(std::io::ErrorKind::NotConnected, "VM is not running")
        })?;
        running
            .expose_tls(cfg)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
    }

    /// Sentry backend variant of [`expose_tls`](Vm::expose_tls): terminate TLS on
    /// `cfg.listen_addr` with the backend-agnostic rustls terminator
    /// ([`crate::vmm::tls::start_to_addr`]) and bridge decrypted plaintext to the
    /// guest workload's LoopNet listener through the persistent supervisor bridge.
    /// The guest TCP port comes from `cfg.vm_port`, which must be `Some(p)` with
    /// `p != 0` and `p <= u16::MAX` (no first-listener fallback to route a
    /// 0/oversized vm_port against — same contract as [`expose_tcp`](Vm::expose_tcp)'s
    /// `guest_port`).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn expose_tls_sentry(
        &self,
        cfg: crate::vmm::tls::TlsConfig,
    ) -> std::io::Result<std::net::SocketAddr> {
        let guest_port = match cfg.vm_port {
            Some(p) if p != 0 && p <= u16::MAX as u32 => p as u16,
            _ => {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    "expose_tls on the sentry backend requires cfg.vm_port = Some(non-zero u16 guest port)",
                ))
            }
        };
        let exec_path = self.vsock_exec_path.clone();
        crate::vmm::tls::start_to_unix_stream(
            cfg,
            format!("sentry bridge guest 127.0.0.1:{guest_port}"),
            move || connect_port_sentry(&exec_path, guest_port),
        )
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
    }

    /// Stop the VM. Equivalent to dropping it, but returns errors
    /// rather than swallowing them.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn stop(mut self) -> Result<(), Error> {
        if let Some(pool) = self.pool.take() {
            let _ = pool.shutdown().map_err(Error::from)?;
        }
        self.cleanup_socket();
        Ok(())
    }

    /// Stop the VM (Linux/KVM): force-exit + join the vCPU threads via the
    /// [`RunningVm`](crate::kvm::run::RunningVm) handle, then unlink the sockets.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn stop(mut self) -> Result<(), Error> {
        if let Some(running) = self.running.take() {
            let _ = running.stop();
        }
        // Sentry backend: stop the exec server (kills the workload + unlinks).
        if let Some(srv) = self.sentry_exec.take() {
            srv.stop();
        }
        if let Some(pool) = self.sentry_unified_pool.take() {
            pool.stop();
        }
        if let Some(mut runtime) = self.kboxlike.take() {
            runtime.cleanup();
        }
        self.cleanup_socket();
        Ok(())
    }

    /// In-place reset this VM to its snapshot baseline (isolated warm-reuse) —
    /// see [`RunningVm::reset_to_snapshot`](crate::kvm::run::RunningVm::reset_to_snapshot).
    /// The pool calls this on release for the `restore_on_release=true`
    /// (isolated, default) path: it returns the guest byte-identical to the
    /// snapshot point — clean per-cycle isolation — without the full teardown +
    /// fresh-restore cost, since the KVM VM/vCPUs/threads/RAM all persist.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub(crate) fn reset_to_snapshot(&self) -> Result<(), Error> {
        self.running
            .as_ref()
            .ok_or_else(|| Error::vm_msg("reset_to_snapshot: VM has no running handle".to_owned()))?
            .reset_to_snapshot()
            .map_err(|e| Error::vm_msg(format!("reset_to_snapshot: {e}")))
    }

    /// Capture a snapshot of the running VM into `dest_dir`. The
    /// dir gets `restore.snap` (the captured VM state) and a
    /// `metadata.json` describing the layers/memory/vCPUs from
    /// the source [`Image`] — together they form a fresh
    /// snapshot loadable via [`Image::from_snapshot`].
    ///
    /// This is the **"rustc-warm snapshot" pattern** — boot a VM
    /// from a base image (e.g. `rust:1-slim`), populate
    /// expensive in-VM state (run `cargo build` to fill
    /// `target/` with cached deps), capture, then re-use the new
    /// snapshot via `Image::from_snapshot(...).acquire()` for
    /// fast subsequent iterations.
    ///
    /// ```no_run
    /// # use supermachine::{Image, VmConfig};
    /// let base = Image::from_snapshot("path/to/rust-slim")?;
    /// let vm = base.start(&VmConfig::new())?;
    /// // Pre-warm: populate target/ with cached deps.
    /// vm.exec_builder()
    ///     .argv(["sh", "-c", "cd /src && cargo build --release"])
    ///     .output()?;
    /// // Capture; vm is consumed (and stopped).
    /// let warm = vm.snapshot("/tmp/rust-warm")?;
    /// // Now `warm.acquire()` gets you a VM with target/
    /// // already populated — every subsequent compile re-uses
    /// // the cached deps.
    /// # Ok::<(), supermachine::Error>(())
    /// ```
    ///
    /// **Only works on a Vm produced by [`Image::start`]**.
    /// Pooled VMs (from [`Image::acquire`]) live in worker
    /// subprocesses and don't have host-side access to the
    /// snapshot machinery; snapshot them by re-starting the
    /// source image and snapshotting that.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    pub fn snapshot(mut self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
        let dest_dir = dest_dir.into();
        let meta = self.image_meta.clone().ok_or_else(|| {
            Error::vm_msg(
                "Vm::snapshot requires an in-process Vm (use image.start, not image.acquire)"
                    .to_owned(),
            )
        })?;
        let pool = self.pool.as_ref().ok_or_else(|| {
            Error::vm_msg("Vm::snapshot: no pool to drive the capture".to_owned())
        })?;
        std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
        let snap_path = dest_dir.join("restore.snap");
        // Flush guest dirty pages to host-persistent backing files
        // (virtio-blk volumes, writeback-cached virtio-fs) before
        // the capture — see `guest_sync_via_agent`.
        let _ = guest_sync_via_agent(&self.vsock_exec_path);
        // Trigger the capture via the pool RPC. Generous
        // timeout — capture is fast (~10s of ms) but disk
        // saving for big VMs can take a moment.
        let _result = pool
            .snapshot_timeout(
                snap_path.to_string_lossy().to_string(),
                Duration::from_secs(60),
            )
            .map_err(|e| Error::Vm {
                msg: format!("snapshot capture failed: {e:?}"),
                source: None,
            })?;
        // Write a metadata.json that mirrors what the bake step
        // emits, so Image::from_snapshot can load this dir.
        let metadata = serde_json::json!({
            "memory_mib": meta.memory_mib,
            "vcpus": meta.vcpus,
            "layers": meta
                .layers
                .iter()
                .map(|p| p.to_string_lossy().to_string())
                .collect::<Vec<_>>(),
            "delta_squashfs": meta
                .delta_squashfs
                .as_ref()
                .map(|p| p.to_string_lossy().to_string()),
            "snapshot_base": snap_path.to_string_lossy().to_string(),
            "baked_at": chrono_rfc3339_now(),
            "source": "Vm::snapshot",
        });
        std::fs::write(
            dest_dir.join("metadata.json"),
            serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
        )
        .map_err(Error::Io)?;
        // Cleanly shut down our worker — the snapshot is on disk.
        if let Some(pool) = self.pool.take() {
            let _ = pool.shutdown();
        }
        self.cleanup_socket();
        // Suppress the Drop, we already cleaned up.
        self.skip_cleanup = true;
        // Load the freshly-written dir as a new Image.
        Image::from_snapshot(&dest_dir)
    }

    /// Capture a snapshot (Linux/KVM) and return it as a loadable, snapshot-
    /// backed [`Image`] (`"backend":"kvm"`, `kvm_snapshot` → the saved file).
    /// Quiesces all vCPUs at a clean boundary, captures the full `VmSnapshot`,
    /// and writes `vm.snap` + `metadata.json` under `dest_dir`. Restoring the
    /// returned image via [`Vm::start`] is a CoW mmap (~ms) — this is what backs
    /// the warm pool and lets callers checkpoint a live guest.
    ///
    /// **Sentry backend:** there is no guest RAM/CPU to checkpoint; the warm
    /// state is the filesystem. This captures the live rootfs (e.g. a populated
    /// `node_modules`/`target` after a warming `exec`) into a new sentry image
    /// dir + re-emits `metadata.json`, so `Image::from_snapshot(dest).start()`
    /// boots a sandbox that already has that filesystem state. Capture at a
    /// quiescent point (after your warming exec has returned). Only works on a
    /// cold-start `Vm` from [`Image::start`] (pooled VMs return an error).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn snapshot(mut self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
        let dest_dir = dest_dir.into();
        if let Some(runsc) = self.runsc.as_ref() {
            let image = write_runsc_snapshot_dir(runsc, &dest_dir, "Vm::snapshot", false)?;
            self.cleanup_socket();
            self.skip_cleanup = true;
            return Ok(image);
        }
        if self.kboxlike.is_some() {
            return Err(Error::vm_msg(
                "Vm::snapshot is not implemented for the kboxlike backend yet".to_owned(),
            ));
        }
        // Sentry backend (no `RunningVm`): capture the warm rootfs.
        if self.running.is_none() {
            return self.snapshot_sentry(dest_dir);
        }
        let running = self.running.take().ok_or_else(|| {
            Error::vm_msg("Vm::snapshot: this VM is not running (already stopped?)".to_owned())
        })?;
        std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
        // Flush guest dirty pages to host-persistent backing files
        // (virtio-blk volumes, writeback-cached virtio-fs) before
        // the capture — see `guest_sync_via_agent`.
        let _ = guest_sync_via_agent(&self.vsock_exec_path);
        // Quiesce all vCPUs at a clean boundary + capture full VM state.
        let snap = running
            .snapshot(Some(&dest_dir.join("volumes")))
            .map_err(|e| Error::vm_msg(format!("capture snapshot: {e}")))?;
        let snap_path = dest_dir.join("vm.snap");
        snap.save(&snap_path)
            .map_err(|e| Error::vm_msg(format!("write snapshot {}: {e}", snap_path.display())))?;
        // metadata.json a `backend":"kvm"` restore-image points at the snapshot.
        let metadata = serde_json::json!({
            "backend": "kvm",
            "kvm_snapshot": snap_path.to_string_lossy(),
            "memory_mib": (snap.mem_size() >> 20) as u32,
            "vcpus": snap.num_cpus() as u32,
            "baked_at": chrono_rfc3339_now(),
            "source": "Vm::snapshot",
        });
        std::fs::write(
            dest_dir.join("metadata.json"),
            serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
        )
        .map_err(Error::Io)?;
        // The running VM was consumed by the capture; just unlink our sockets
        // and suppress the Drop's redundant cleanup.
        self.cleanup_socket();
        self.skip_cleanup = true;
        Image::from_snapshot(&dest_dir)
    }

    /// Sentry backend snapshot: copy the live rootfs (warm filesystem state) into
    /// a new sentry image dir + re-emit `metadata.json` (repointing
    /// `sentry_rootfs`, restamping `baked_at`/`source`, recomputing physical
    /// bytes, and preserving `sentry_has_workload`/`image_env`/`cmd`/… from the
    /// source). The result loads via [`Image::from_snapshot`].
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn snapshot_sentry(mut self, dest_dir: PathBuf) -> Result<Image, Error> {
        let src_rootfs = self.sentry_src_rootfs.clone().ok_or_else(|| {
            Error::vm_msg(
                "Vm::snapshot on the sentry backend requires an in-process Vm from \
                 Image::start (pooled VMs share a server and can't snapshot the rootfs)"
                    .to_owned(),
            )
        })?;
        // Copy the warm rootfs + re-emit metadata (shared with the pooled path).
        let image = write_sentry_snapshot_dir_inner(
            &src_rootfs,
            &dest_dir,
            "Vm::snapshot",
            false,
            None,
            None,
        )?;
        // Stop our exec server (kills the workload + unlinks), suppress Drop.
        if let Some(srv) = self.sentry_exec.take() {
            srv.stop();
        }
        self.cleanup_socket();
        self.skip_cleanup = true;
        Ok(image)
    }

    /// LIVE snapshot (Linux/KVM): capture a loadable [`Image`] WITHOUT stopping
    /// the guest — the VM keeps running and can be snapshotted again. Backs the
    /// builder's one-VM-per-layer snapshotting. `&self` (non-consuming).
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn snapshot_live(&self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
        let dest_dir = dest_dir.into();
        let running = self
            .running
            .as_ref()
            .ok_or_else(|| Error::vm_msg("Vm::snapshot_live: VM not running".to_owned()))?;
        std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
        // Flush guest dirty pages to host-persistent backing files
        // (virtio-blk volumes — incl. the builder's volume-backed
        // cache mounts) before the capture — see
        // `guest_sync_via_agent`.
        let _ = guest_sync_via_agent(&self.vsock_exec_path);
        let snap = running
            .snapshot_live(Some(&dest_dir.join("volumes")))
            .map_err(|e| Error::vm_msg(format!("capture live snapshot: {e}")))?;
        // `restore.snap` + a dir-RELATIVE `kvm_snapshot` is the builder's snapshot
        // -dir convention (is_snapshot_dir / clone_snapshot_dir): the relative
        // path resolves against the dir, so a cloned layer dir stays loadable.
        let snap_path = dest_dir.join("restore.snap");
        snap.save(&snap_path)
            .map_err(|e| Error::vm_msg(format!("write snapshot {}: {e}", snap_path.display())))?;
        write_kvm_snapshot_metadata(
            &dest_dir,
            "restore.snap",
            None,
            snap.mem_size(),
            snap.num_cpus(),
        )?;
        Image::from_snapshot(&dest_dir)
    }

    /// LIVE differential snapshot (Linux/KVM): like [`Vm::snapshot_live`] but
    /// stores only the guest-RAM pages changed vs `base_snap` (a full snapshot's
    /// `restore.snap`). Non-consuming — the VM keeps running. The builder chains
    /// these per layer off one VM.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn snapshot_diff_live(
        &self,
        dest_dir: impl Into<PathBuf>,
        base_snap: &Path,
    ) -> Result<Image, Error> {
        let dest_dir = dest_dir.into();
        let running = self
            .running
            .as_ref()
            .ok_or_else(|| Error::vm_msg("Vm::snapshot_diff_live: VM not running".to_owned()))?;
        std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
        // Same flush-before-capture as `Vm::snapshot_live`.
        let _ = guest_sync_via_agent(&self.vsock_exec_path);
        let snap = running
            .snapshot_live(Some(&dest_dir.join("volumes")))
            .map_err(|e| Error::vm_msg(format!("capture live snapshot: {e}")))?;
        let snap_path = dest_dir.join("restore.snap");
        snap.save_diff(&snap_path, base_snap).map_err(|e| {
            Error::vm_msg(format!("write diff snapshot {}: {e}", snap_path.display()))
        })?;
        write_kvm_snapshot_metadata(
            &dest_dir,
            "restore.snap",
            Some(base_snap),
            snap.mem_size(),
            snap.num_cpus(),
        )?;
        Image::from_snapshot(&dest_dir)
    }

    /// Differential snapshot (Linux/KVM): capture the running guest but store
    /// only the guest-RAM pages that changed vs `base` (another KVM-backed
    /// [`Image`], e.g. the previous layer) — the full vCPU/device state is always
    /// stored. Restoring the returned image transparently mmaps the base RAM
    /// copy-on-write and overlays the delta, so a chain of these (e.g. per-layer
    /// builder snapshots) each costs only its changed pages on disk.
    ///
    /// The base's snapshot file must remain in place (the diff references it by
    /// path). `base` must be a full (non-diff) KVM snapshot of the same memory
    /// size; pass an [`Image`] produced by [`Vm::snapshot`] / `bake_kvm_*`.
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    pub fn snapshot_diff(
        mut self,
        dest_dir: impl Into<PathBuf>,
        base: &Image,
    ) -> Result<Image, Error> {
        let dest_dir = dest_dir.into();
        let base_snap = base
            .kvm
            .as_ref()
            .and_then(|k| k.snapshot.clone())
            .ok_or_else(|| {
                Error::vm_msg(
                    "Vm::snapshot_diff: base Image has no KVM snapshot to diff against".to_owned(),
                )
            })?;
        let running = self.running.take().ok_or_else(|| {
            Error::vm_msg("Vm::snapshot_diff: this VM is not running (already stopped?)".to_owned())
        })?;
        std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
        // Same flush-before-capture as `Vm::snapshot_live`.
        let _ = guest_sync_via_agent(&self.vsock_exec_path);
        let snap = running
            .snapshot(Some(&dest_dir.join("volumes")))
            .map_err(|e| Error::vm_msg(format!("capture snapshot: {e}")))?;
        let snap_path = dest_dir.join("vm.snap");
        snap.save_diff(&snap_path, &base_snap).map_err(|e| {
            Error::vm_msg(format!("write diff snapshot {}: {e}", snap_path.display()))
        })?;
        let metadata = serde_json::json!({
            "backend": "kvm",
            "kvm_snapshot": snap_path.to_string_lossy(),
            "kvm_snapshot_base": base_snap.to_string_lossy(),
            "memory_mib": (snap.mem_size() >> 20) as u32,
            "vcpus": snap.num_cpus() as u32,
            "baked_at": chrono_rfc3339_now(),
            "source": "Vm::snapshot_diff",
        });
        std::fs::write(
            dest_dir.join("metadata.json"),
            serde_json::to_string_pretty(&metadata)
                .map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
        )
        .map_err(Error::Io)?;
        self.cleanup_socket();
        self.skip_cleanup = true;
        Image::from_snapshot(&dest_dir)
    }

    fn cleanup_socket(&self) {
        let _ = std::fs::remove_file(&self.vsock_mux_path);
        let _ = std::fs::remove_file(&self.vsock_exec_path);
        if let Some(dir) = &self.own_vsock_mux_dir {
            // Only unlink the dir if it's still empty (best-effort).
            let _ = std::fs::remove_dir(dir);
        }
    }
}

// ---------- minimal base64 (RFC 4648, mirror of agent's) ----------
//
// Inlined to keep deps minimal. Round-trip-tested against the
// agent's implementation; identical alphabet + padding rules.

const B64_ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/// Public so the napi binding can encode bytes for the agent's
/// `write_file` action JSON without re-implementing this. The
/// shape is internal — embedders outside this workspace should
/// stick to [`crate::Vm::write_file`] / [`crate::Vm::read_file`]
/// which call into this for you.
pub fn b64_encode(bytes: &[u8]) -> String {
    let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);
    let mut i = 0;
    while i + 3 <= bytes.len() {
        let b0 = bytes[i] as u32;
        let b1 = bytes[i + 1] as u32;
        let b2 = bytes[i + 2] as u32;
        let n = (b0 << 16) | (b1 << 8) | b2;
        out.push(B64_ALPHA[((n >> 18) & 0x3f) as usize] as char);
        out.push(B64_ALPHA[((n >> 12) & 0x3f) as usize] as char);
        out.push(B64_ALPHA[((n >> 6) & 0x3f) as usize] as char);
        out.push(B64_ALPHA[(n & 0x3f) as usize] as char);
        i += 3;
    }
    let rem = bytes.len() - i;
    if rem == 1 {
        let b0 = bytes[i] as u32;
        let n = b0 << 16;
        out.push(B64_ALPHA[((n >> 18) & 0x3f) as usize] as char);
        out.push(B64_ALPHA[((n >> 12) & 0x3f) as usize] as char);
        out.push('=');
        out.push('=');
    } else if rem == 2 {
        let b0 = bytes[i] as u32;
        let b1 = bytes[i + 1] as u32;
        let n = (b0 << 16) | (b1 << 8);
        out.push(B64_ALPHA[((n >> 18) & 0x3f) as usize] as char);
        out.push(B64_ALPHA[((n >> 12) & 0x3f) as usize] as char);
        out.push(B64_ALPHA[((n >> 6) & 0x3f) as usize] as char);
        out.push('=');
    }
    out
}

/// Public counterpart to [`b64_encode`]. Same shape, same caveat.
pub fn b64_decode(s: &str) -> Result<Vec<u8>, String> {
    let mut tbl = [255u8; 256];
    for (i, &b) in B64_ALPHA.iter().enumerate() {
        tbl[b as usize] = i as u8;
    }
    let bytes: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
    if bytes.len() % 4 != 0 {
        return Err(format!(
            "base64 length {} is not a multiple of 4",
            bytes.len()
        ));
    }
    let nchunks = bytes.len() / 4;
    let mut out = Vec::with_capacity(nchunks * 3);
    for (ci, chunk) in bytes.chunks_exact(4).enumerate() {
        let v: [u8; 4] = chunk.try_into().unwrap();
        // `=` padding is only valid as the 1 or 2 TRAILING characters of
        // the FINAL chunk. Reject everything else — leading/embedded `=`
        // ("=AAA"), a padded non-final chunk ("AA==AAAA"), or 3-4 pads
        // ("A===", "===="). The previous code counted `=` anywhere and
        // emitted a spurious byte instead of erroring, so a hostile/buggy
        // in-guest agent could make read_file silently return wrong bytes.
        let pad = v.iter().filter(|&&b| b == b'=').count();
        if pad > 0 {
            let last_chunk = ci == nchunks - 1;
            let trailing_only = match pad {
                1 => v[3] == b'=',
                2 => v[2] == b'=' && v[3] == b'=',
                _ => false,
            };
            if !last_chunk || !trailing_only {
                return Err(format!("base64 misplaced or excess padding in chunk {ci}"));
            }
        }
        let mut acc: u32 = 0;
        for &b in &v {
            let d = if b == b'=' {
                0
            } else {
                let d = tbl[b as usize];
                if d == 255 {
                    return Err(format!("invalid base64 character {:#x}", b));
                }
                d
            };
            acc = (acc << 6) | (d as u32);
        }
        out.push(((acc >> 16) & 0xff) as u8);
        if pad < 2 {
            out.push(((acc >> 8) & 0xff) as u8);
        }
        if pad < 1 {
            out.push((acc & 0xff) as u8);
        }
    }
    Ok(out)
}

#[cfg(any(
    all(target_os = "macos", target_arch = "aarch64"),
    all(target_os = "linux", target_arch = "x86_64")
))]
impl Drop for Vm {
    fn drop(&mut self) {
        // PooledVm sets skip_cleanup so its inner Vm doesn't
        // shut down the shared pool or unlink sockets the
        // HiddenPool keeps alive.
        if self.skip_cleanup {
            return;
        }
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        if let Some(pool) = self.pool.take() {
            let _ = pool.shutdown();
        }
        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        {
            // Stop the background clock-resync thread so it doesn't keep
            // retrying (and leaking) for 30 s against a VM that's going away.
            self.time_sync_stop.store(true, Ordering::SeqCst);
            if let Some(running) = self.running.take() {
                let _ = running.stop();
            }
            // Sentry backend: stop the exec server (kills the workload + unlinks
            // its socket) BEFORE cleanup_socket removes the path.
            if let Some(srv) = self.sentry_exec.take() {
                srv.stop();
            }
            if let Some(pool) = self.sentry_unified_pool.take() {
                pool.stop();
            }
            if let Some(mut runtime) = self.kboxlike.take() {
                runtime.cleanup();
            }
        }
        self.cleanup_socket();
    }
}

/// Owns the accept-loop thread for a [`Vm::expose_tcp`] forwarder.
///
/// Drop this to stop accepting new connections. In-flight
/// connections continue until they close naturally — they're owned
/// by their own splice threads, not by the forwarder.
pub struct TcpForwarder {
    stop: Arc<AtomicBool>,
    handle: Option<JoinHandle<()>>,
    bound: SocketAddr,
}

impl TcpForwarder {
    /// The address the forwarder is listening on. Useful when you
    /// asked for `host_port = 0` and want to know the OS-assigned
    /// port.
    pub fn local_addr(&self) -> SocketAddr {
        self.bound
    }

    /// Stop accepting new connections. Equivalent to dropping the
    /// forwarder, but returns when the accept thread has actually
    /// exited.
    pub fn stop(mut self) {
        self.shutdown();
    }

    fn shutdown(&mut self) {
        self.stop.store(true, Ordering::SeqCst);
        // Self-connect to unblock the listener's accept(). We don't
        // care about the result — the connection just exists to wake
        // the loop, which then sees `stop` set and exits.
        let _ = TcpStream::connect_timeout(&self.bound, Duration::from_millis(200));
        if let Some(h) = self.handle.take() {
            let _ = h.join();
        }
    }
}

impl Drop for TcpForwarder {
    fn drop(&mut self) {
        self.shutdown();
    }
}

/// Accept loop for `Vm::expose_tcp`. Spawns a per-connection splice
/// thread for each accepted TCP stream; the splice threads live
/// independently of the forwarder so in-flight requests survive
/// `TcpForwarder::drop`.
fn accept_loop(listener: TcpListener, vsock_path: PathBuf, stop: Arc<AtomicBool>, guest_port: u16) {
    for incoming in listener.incoming() {
        if stop.load(Ordering::SeqCst) {
            break;
        }
        let tcp = match incoming {
            Ok(s) => s,
            Err(_) => continue,
        };
        // Linux: request/response traffic stalls ~40 ms per round-trip
        // without NODELAY (Nagle + delayed-ACK interaction): measured
        // 778 req/s with a flat 41 ms latency through this forwarder;
        // with NODELAY, 5.8k req/s at 5.6 ms (wrk -t4 -c32, nginx 1
        // vCPU). macOS is the OPPOSITE: its delayed-ACK behavior never
        // produces the stall, and Nagle usefully coalesces the
        // vsock-chunked response writes — NODELAY there measured a
        // consistent ~20% THROUGHPUT LOSS (23.5–25k → 18.4–19.9k
        // req/s, same harness, back-to-back A/B). Hence the cfg gate;
        // a write-coalescing splice would let both platforms run
        // NODELAY (future work).
        #[cfg(target_os = "linux")]
        let _ = tcp.set_nodelay(true);
        let vsock = vsock_path.clone();
        std::thread::Builder::new()
            .name("supermachine-tcp-conn".into())
            .spawn(move || {
                if let Err(e) = splice_tcp_to_unix(tcp, &vsock, guest_port) {
                    // Log to stderr — this is best-effort; the
                    // embedder's preferred logging is out of scope.
                    eprintln!("supermachine: tcp forward: {e}");
                }
            })
            .ok();
    }
}

/// Bridge a single TCP connection to the vsock-mux unix socket.
/// Two threads per connection: one shovels TCP→Unix, the other
/// Unix→TCP. Either side closing tears the bridge down.
///
/// If `guest_port != 0`, writes a 20-byte routing header
/// (`SMUX-PORT-V1\0\0\0\0` + u32 BE port) at the front of the unix
/// connection so the worker's vsock-mux accept handler routes this
/// stream to the matching guest TSI listener. With `guest_port == 0`
/// (or for any unix client that doesn't write the header), the mux
/// falls back to "first AF_INET TSI listener" — the legacy behavior.
fn splice_tcp_to_unix(tcp: TcpStream, vsock_path: &Path, guest_port: u16) -> std::io::Result<()> {
    let mut unix = UnixStream::connect(vsock_path)?;
    if guest_port != 0 {
        // 16-byte magic + 4-byte BE port. Keep this constant in sync
        // with `vmm::vsock_mux::SMUX_PORT_MAGIC`.
        let mut hdr = [0u8; 20];
        hdr[..16].copy_from_slice(b"SMUX-PORT-V1\0\0\0\0");
        hdr[16..].copy_from_slice(&(guest_port as u32).to_be_bytes());
        unix.write_all(&hdr)?;
    }
    // try_clone so each direction owns its own handle.
    let tcp_w = tcp.try_clone()?;
    let unix_w = unix.try_clone()?;
    let t1 = std::thread::Builder::new()
        .name("supermachine-tcp-c2g".into())
        .spawn(move || {
            let _ = pump(tcp, unix_w);
        })?;
    let t2 = std::thread::Builder::new()
        .name("supermachine-tcp-g2c".into())
        .spawn(move || {
            let _ = pump(unix, tcp_w);
        })?;
    let _ = t1.join();
    let _ = t2.join();
    Ok(())
}

/// Accept loop for the sentry backend's [`Vm::expose_tcp`]. Each connection is
/// handed to the persistent supervisor, which connects it to owned LoopNet.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_accept_loop(
    listener: TcpListener,
    exec_path: PathBuf,
    guest_port: u16,
    stop: Arc<AtomicBool>,
) {
    for incoming in listener.incoming() {
        if stop.load(Ordering::SeqCst) {
            break;
        }
        let tcp = match incoming {
            Ok(s) => s,
            Err(_) => continue,
        };
        // Same NODELAY rationale as `accept_loop` (Linux-only; the sentry backend
        // is Linux/x86_64-only anyway).
        let _ = tcp.set_nodelay(true);
        let exec_path = exec_path.clone();
        std::thread::Builder::new()
            .name("supermachine-tcp-conn".into())
            .spawn(move || {
                if let Err(e) = splice_tcp_to_sentry(tcp, &exec_path, guest_port) {
                    eprintln!("supermachine: sentry tcp forward: {e}");
                }
            })
            .ok();
    }
}

/// Accept loop for the runsc backend's [`Vm::expose_tcp`]. Each inbound host
/// TCP connection gets its own agent stdio bridge into the sandbox.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_accept_loop(
    listener: TcpListener,
    target: RunscPortForwardTarget,
    guest_port: u16,
    stop: Arc<AtomicBool>,
) {
    for incoming in listener.incoming() {
        if stop.load(Ordering::SeqCst) {
            break;
        }
        let tcp = match incoming {
            Ok(s) => s,
            Err(_) => continue,
        };
        let _ = tcp.set_nodelay(true);
        let target = target.clone();
        std::thread::Builder::new()
            .name("supermachine-tcp-conn".into())
            .spawn(move || {
                if let Err(e) = splice_tcp_to_runsc(tcp, &target, guest_port) {
                    eprintln!("supermachine: runsc tcp forward: {e}");
                }
            })
            .ok();
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_accept_loop(
    listener: TcpListener,
    target: KboxlikePortForwardTarget,
    guest_port: u16,
    stop: Arc<AtomicBool>,
) {
    for incoming in listener.incoming() {
        if stop.load(Ordering::SeqCst) {
            break;
        }
        let tcp = match incoming {
            Ok(s) => s,
            Err(_) => continue,
        };
        let _ = tcp.set_nodelay(true);
        let target = target.clone();
        std::thread::Builder::new()
            .name("supermachine-tcp-conn".into())
            .spawn(move || {
                if let Err(e) = splice_tcp_to_kboxlike(tcp, &target, guest_port) {
                    eprintln!("supermachine: kboxlike tcp forward: {e}");
                }
            })
            .ok();
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn splice_tcp_to_runsc(
    client: TcpStream,
    target: &RunscPortForwardTarget,
    guest_port: u16,
) -> std::io::Result<()> {
    let upstream = runsc_connect_port_target(target, guest_port)?;
    let client_w = client.try_clone()?;
    let upstream_w = upstream.try_clone()?;
    let t1 = std::thread::Builder::new()
        .name("supermachine-tcp-c2g".into())
        .spawn(move || {
            let _ = pump(client, upstream_w);
        })?;
    let t2 = std::thread::Builder::new()
        .name("supermachine-tcp-g2c".into())
        .spawn(move || {
            let _ = pump(upstream, client_w);
        })?;
    let _ = t1.join();
    let _ = t2.join();
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn splice_tcp_to_kboxlike(
    client: TcpStream,
    target: &KboxlikePortForwardTarget,
    guest_port: u16,
) -> std::io::Result<()> {
    let upstream = kboxlike_connect_tcp_target(target, guest_port)?;
    let client_w = client.try_clone()?;
    let upstream_w = upstream.try_clone()?;
    let t1 = std::thread::Builder::new()
        .name("supermachine-tcp-c2g".into())
        .spawn(move || {
            let _ = pump(client, upstream_w);
        })?;
    let t2 = std::thread::Builder::new()
        .name("supermachine-tcp-g2c".into())
        .spawn(move || {
            let _ = pump(upstream, client_w);
        })?;
    let _ = t1.join();
    let _ = t2.join();
    Ok(())
}

/// Bridge one inbound TCP connection to a raw supervisor bridge stream.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn splice_tcp_to_sentry(
    client: TcpStream,
    exec_path: &Path,
    guest_port: u16,
) -> std::io::Result<()> {
    let upstream = connect_port_sentry(exec_path, guest_port)?;
    let client_w = client.try_clone()?;
    let upstream_w = upstream.try_clone()?;
    let t1 = std::thread::Builder::new()
        .name("supermachine-tcp-c2g".into())
        .spawn(move || {
            let _ = pump(client, upstream_w);
        })?;
    let t2 = std::thread::Builder::new()
        .name("supermachine-tcp-g2c".into())
        .spawn(move || {
            let _ = pump(upstream, client_w);
        })?;
    let _ = t1.join();
    let _ = t2.join();
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_port_forward_target(runtime: &RunscRuntime) -> RunscPortForwardTarget {
    RunscPortForwardTarget {
        runsc_bin: runtime.runsc_bin.clone(),
        runsc_root: runtime.runsc_root.clone(),
        network: runtime.network.clone(),
        container_id: runtime.container_id.clone(),
        agent_path: runtime.parts.agent_path.clone(),
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_port_forward_target(runtime: &KboxlikeRuntime) -> KboxlikePortForwardTarget {
    KboxlikePortForwardTarget {
        restored_host_pids: runtime.restored_host_pids.clone(),
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_connect_port(runtime: &RunscRuntime, guest_port: u16) -> std::io::Result<UnixStream> {
    let port = if guest_port == 0 {
        runsc_first_listening_port(runtime)?
    } else {
        guest_port
    };
    let target = runsc_port_forward_target(runtime);
    runsc_connect_port_target(&target, port)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_port(
    runtime: &KboxlikeRuntime,
    guest_port: u16,
) -> std::io::Result<UnixStream> {
    let target = kboxlike_port_forward_target(runtime);
    kboxlike_connect_port_target(&target, guest_port)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_connect_port_target(
    target: &RunscPortForwardTarget,
    guest_port: u16,
) -> std::io::Result<UnixStream> {
    if guest_port == 0 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "runsc bridge requires a non-zero guest port",
        ));
    }
    let (client, bridge) = UnixStream::pair()?;
    let mut cmd = Command::new(&target.runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, &target.network);
    cmd.arg("exec")
        .arg(&target.container_id)
        .arg(&target.agent_path)
        .arg("--tcp-stdio")
        .arg("127.0.0.1")
        .arg(guest_port.to_string())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let mut child = cmd.spawn().map_err(|e| {
        std::io::Error::new(
            e.kind(),
            format!(
                "runsc exec {} {} --tcp-stdio 127.0.0.1 {}: {e}",
                target.container_id, target.agent_path, guest_port
            ),
        )
    })?;
    let child_stdin = child.stdin.take().ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::BrokenPipe,
            "runsc tcp bridge stdin was not available",
        )
    })?;
    let child_stdout = child.stdout.take().ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::BrokenPipe,
            "runsc tcp bridge stdout was not available",
        )
    })?;

    let startup_deadline = Instant::now() + Duration::from_millis(200);
    while Instant::now() < startup_deadline {
        if let Some(status) = child.try_wait()? {
            let out = child.wait_with_output()?;
            let detail = String::from_utf8_lossy(&out.stderr).trim().to_owned();
            let msg = if detail.is_empty() {
                format!("runsc tcp bridge to guest port {guest_port} exited with status {status}")
            } else {
                format!(
                    "runsc tcp bridge to guest port {guest_port} exited with status {status}: {detail}"
                )
            };
            return Err(std::io::Error::new(
                std::io::ErrorKind::ConnectionRefused,
                msg,
            ));
        }
        std::thread::sleep(Duration::from_millis(10));
    }

    let bridge_to_child = bridge.try_clone()?;
    let child_to_bridge = bridge.try_clone()?;
    let to_child = std::thread::Builder::new()
        .name("supermachine-runsc-tcp-c2g".into())
        .spawn(move || {
            let _ = pump(bridge_to_child, child_stdin);
        })?;
    let from_child = std::thread::Builder::new()
        .name("supermachine-runsc-tcp-g2c".into())
        .spawn(move || {
            let _ = pump(child_stdout, child_to_bridge);
        })?;
    std::thread::Builder::new()
        .name("supermachine-runsc-tcp-bridge".into())
        .spawn(move || {
            let _ = to_child.join();
            let _ = from_child.join();
            match child.wait_with_output() {
                Ok(out) if !out.status.success() => {
                    let detail = String::from_utf8_lossy(&out.stderr).trim().to_owned();
                    if detail.is_empty() {
                        eprintln!(
                            "supermachine: runsc tcp bridge exited with status {}",
                            out.status
                        );
                    } else {
                        eprintln!(
                            "supermachine: runsc tcp bridge exited with status {}: {}",
                            out.status, detail
                        );
                    }
                }
                Ok(_) => {}
                Err(e) => eprintln!("supermachine: runsc tcp bridge wait failed: {e}"),
            }
            drop(bridge);
        })?;
    Ok(client)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_port_target(
    target: &KboxlikePortForwardTarget,
    guest_port: u16,
) -> std::io::Result<UnixStream> {
    let upstream = kboxlike_connect_tcp_target(target, guest_port)?;
    let (client, bridge) = UnixStream::pair()?;
    let bridge_to_upstream = bridge.try_clone()?;
    let upstream_to_bridge = upstream.try_clone()?;
    let to_upstream = std::thread::Builder::new()
        .name("supermachine-kboxlike-tcp-c2g".into())
        .spawn(move || {
            let _ = pump(bridge_to_upstream, upstream);
        })?;
    let from_upstream = std::thread::Builder::new()
        .name("supermachine-kboxlike-tcp-g2c".into())
        .spawn(move || {
            let _ = pump(upstream_to_bridge, bridge);
        })?;
    std::thread::Builder::new()
        .name("supermachine-kboxlike-tcp-bridge".into())
        .spawn(move || {
            let _ = to_upstream.join();
            let _ = from_upstream.join();
        })?;
    Ok(client)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_tcp_target(
    target: &KboxlikePortForwardTarget,
    guest_port: u16,
) -> std::io::Result<TcpStream> {
    let port = if guest_port == 0 {
        kboxlike_first_listening_port(target)?
    } else {
        guest_port
    };
    kboxlike_connect_loopback(port)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_loopback(port: u16) -> std::io::Result<TcpStream> {
    let timeout = Duration::from_secs(5);
    let v4 = SocketAddr::from(([127, 0, 0, 1], port));
    match TcpStream::connect_timeout(&v4, timeout) {
        Ok(stream) => {
            let _ = stream.set_nodelay(true);
            Ok(stream)
        }
        Err(v4_err) => {
            let v6 = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], port));
            match TcpStream::connect_timeout(&v6, timeout) {
                Ok(stream) => {
                    let _ = stream.set_nodelay(true);
                    Ok(stream)
                }
                Err(v6_err) => Err(std::io::Error::new(
                    v6_err.kind(),
                    format!(
                        "kboxlike bridge: connect to loopback port {port} failed: IPv4={v4_err}; IPv6={v6_err}"
                    ),
                )),
            }
        }
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_first_listening_port(runtime: &RunscRuntime) -> std::io::Result<u16> {
    let out = runsc_exec_shell(
        runtime,
        "cat /proc/net/tcp /proc/net/tcp6 2>/dev/null || true",
        None,
    )?;
    if !out.status.success() {
        return Err(runsc_io_error("runsc list listening ports", out));
    }
    parse_first_listening_port(&out.stdout).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::ConnectionRefused,
            "runsc bridge: no listening TCP port found in the guest",
        )
    })
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_first_listening_port(target: &KboxlikePortForwardTarget) -> std::io::Result<u16> {
    let socket_inodes = kboxlike_owned_socket_inodes(&target.restored_host_pids);
    if socket_inodes.is_empty() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::ConnectionRefused,
            "kboxlike bridge: no socket fds owned by the restored workload",
        ));
    }
    for pid in &target.restored_host_pids {
        let mut proc_net_tcp = Vec::new();
        for name in ["tcp", "tcp6"] {
            let path = format!("/proc/{pid}/net/{name}");
            if let Ok(bytes) = std::fs::read(path) {
                proc_net_tcp.extend_from_slice(&bytes);
                proc_net_tcp.push(b'\n');
            }
        }
        if let Some(port) = parse_first_owned_listening_port(&proc_net_tcp, &socket_inodes) {
            return Ok(port);
        }
    }
    Err(std::io::Error::new(
        std::io::ErrorKind::ConnectionRefused,
        "kboxlike bridge: no listening TCP port owned by the restored workload",
    ))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_owned_socket_inodes(pids: &[i32]) -> std::collections::BTreeSet<u64> {
    let mut inodes = std::collections::BTreeSet::new();
    for pid in pids {
        let fd_dir = format!("/proc/{pid}/fd");
        let Ok(entries) = std::fs::read_dir(fd_dir) else {
            continue;
        };
        for entry in entries.flatten() {
            let Ok(target) = std::fs::read_link(entry.path()) else {
                continue;
            };
            let Some(text) = target.to_str() else {
                continue;
            };
            let Some(inner) = text
                .strip_prefix("socket:[")
                .and_then(|rest| rest.strip_suffix(']'))
            else {
                continue;
            };
            if let Ok(inode) = inner.parse::<u64>() {
                inodes.insert(inode);
            }
        }
    }
    inodes
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_first_owned_listening_port(
    proc_net_tcp: &[u8],
    socket_inodes: &std::collections::BTreeSet<u64>,
) -> Option<u16> {
    let text = String::from_utf8_lossy(proc_net_tcp);
    for line in text.lines() {
        let mut fields = line.split_whitespace();
        let _slot = fields.next()?;
        let local = fields.next()?;
        let _remote = fields.next()?;
        let state = fields.next()?;
        if state != "0A" {
            continue;
        }
        for _ in 0..5 {
            fields.next()?;
        }
        let inode = fields.next()?.parse::<u64>().ok()?;
        if !socket_inodes.contains(&inode) {
            continue;
        }
        let Some((_addr, port_hex)) = local.rsplit_once(':') else {
            continue;
        };
        let Ok(port) = u16::from_str_radix(port_hex, 16) else {
            continue;
        };
        if port != 0 {
            return Some(port);
        }
    }
    None
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_first_listening_port(proc_net_tcp: &[u8]) -> Option<u16> {
    let text = String::from_utf8_lossy(proc_net_tcp);
    for line in text.lines() {
        let mut fields = line.split_whitespace();
        let _slot = fields.next()?;
        let local = fields.next()?;
        let _remote = fields.next()?;
        let state = fields.next()?;
        if state != "0A" {
            continue;
        }
        let Some((_addr, port_hex)) = local.rsplit_once(':') else {
            continue;
        };
        let Ok(port) = u16::from_str_radix(port_hex, 16) else {
            continue;
        };
        if port != 0 {
            return Some(port);
        }
    }
    None
}

/// Dial the persistent sentry supervisor's raw LoopNet bridge. The request starts
/// as one exec-wire frame; after the one-byte ack the same UnixStream carries raw
/// workload bytes in both directions.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn connect_port_sentry(exec_path: &Path, guest_port: u16) -> std::io::Result<UnixStream> {
    let mut stream = UnixStream::connect(exec_path)?;
    let mut frame = [0u8; 7];
    frame[0] = 0xfd; // EW_BRIDGE
    frame[1..5].copy_from_slice(&2u32.to_be_bytes());
    frame[5..7].copy_from_slice(&guest_port.to_be_bytes());
    stream.write_all(&frame)?;
    let mut ack = [0u8; 1];
    stream.read_exact(&mut ack)?;
    if ack[0] != 1 {
        let dst = if guest_port == 0 {
            "first listener".to_string()
        } else {
            format!("guest port {guest_port}")
        };
        return Err(std::io::Error::new(
            std::io::ErrorKind::ConnectionRefused,
            format!("sentry bridge: no listener on {dst}"),
        ));
    }
    Ok(stream)
}

/// Generic byte pump from `r` → `w` until EOF or error. We use
/// `Read + Write` trait objects via concrete types so this works
/// for both TcpStream and UnixStream. Half-close on EOF: the writer
/// gets shutdown so the peer of `w` sees the FIN.
fn pump<R, W>(mut r: R, mut w: W) -> std::io::Result<()>
where
    R: Read,
    W: Write + Shutdownable,
{
    let mut buf = [0u8; 16 * 1024];
    loop {
        let n = match r.read(&mut buf) {
            Ok(0) => break,
            Ok(n) => n,
            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        };
        w.write_all(&buf[..n])?
    }
    let _ = w.shutdown_write();
    Ok(())
}

/// Trait letting `pump` call `shutdown(Write)` on either a
/// `TcpStream` or a `UnixStream` without dynamic dispatch.
trait Shutdownable {
    fn shutdown_write(&mut self) -> std::io::Result<()>;
}

impl Shutdownable for TcpStream {
    fn shutdown_write(&mut self) -> std::io::Result<()> {
        TcpStream::shutdown(self, std::net::Shutdown::Write)
    }
}

impl Shutdownable for UnixStream {
    fn shutdown_write(&mut self) -> std::io::Result<()> {
        UnixStream::shutdown(self, std::net::Shutdown::Write)
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Shutdownable for ChildStdin {
    fn shutdown_write(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

fn unique_suffix() -> u64 {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos() as u64)
        .unwrap_or(0);
    nanos.wrapping_add(COUNTER.fetch_add(1, Ordering::Relaxed))
}

/// Tiny RFC 3339 timestamp formatter — used by [`Vm::snapshot`]'s
/// metadata. Avoids pulling in `chrono` for a single timestamp.
/// Recursive on-disk size of a directory tree (regular files only; best-effort,
/// errors skipped). Used for the sentry image's `snapshot_physical_bytes` so
/// `supermachine images` can show a SIZE without a backend-specific probe.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn dir_size_bytes(dir: &Path) -> u64 {
    let mut total = 0u64;
    let mut stack = vec![dir.to_path_buf()];
    while let Some(d) = stack.pop() {
        let Ok(rd) = std::fs::read_dir(&d) else {
            continue;
        };
        for entry in rd.flatten() {
            let Ok(ft) = entry.file_type() else { continue };
            if ft.is_dir() {
                stack.push(entry.path());
            } else if ft.is_file() {
                if let Ok(m) = entry.metadata() {
                    total += m.len();
                }
            }
        }
    }
    total
}

/// Parse an OCI `User` string into a numeric `(uid, gid)` for the sentry's
/// uid-drop. Accepts only the numeric forms — `"1000"` (gid := uid) and
/// `"1000:1000"` / `"1000:1000:1000"` (a trailing supplementary-group field, if
/// any, is ignored). Returns `None` for a username (no passwd lookup in the
/// sentry), an empty string, or `root`/`0` (a no-op drop the supervisor skips).
/// Whitespace is trimmed; a non-numeric component yields `None` (best-effort:
/// the guest just runs at the sentry's own uid).
fn parse_numeric_user(user: &str) -> Option<(u32, u32)> {
    let user = user.trim();
    if user.is_empty() || user == "root" {
        return None;
    }
    let mut parts = user.split(':');
    let uid: u32 = parts.next()?.trim().parse().ok()?;
    let gid: u32 = match parts.next() {
        Some(g) => g.trim().parse().ok()?,
        None => uid,
    };
    if uid == 0 {
        return None;
    }
    Some((uid, gid))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_bin_path() -> PathBuf {
    std::env::var_os("SUPERMACHINE_RUNSC_BIN")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("runsc"))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_warmup_checkpoint(
    image: &Image,
    dest: &Path,
    warmup: Box<dyn FnOnce(&Vm) -> Result<(), Error> + Send>,
) -> Result<(), Error> {
    let parts = image
        .runsc
        .as_ref()
        .ok_or_else(|| Error::vm_msg("runsc warmup image missing runsc metadata"))?;
    let runsc_bin = runsc_bin_path();
    let runtime_dir = std::env::temp_dir().join(format!(
        "supermachine-runsc-warmup-{}-{}",
        std::process::id(),
        unique_suffix()
    ));
    let bundle_dir = runtime_dir.join("bundle");
    let runsc_root = runtime_dir.join("runsc-root");
    let pid_file = runtime_dir.join("container.pid");
    let checkpoint_dir = dest.join("runsc-checkpoint");
    let container_id = format!("sm-runsc-warmup-{}-{}", std::process::id(), unique_suffix());
    std::fs::create_dir_all(&runsc_root).map_err(Error::Io)?;
    let cleanup_dir = |err: Error, runtime_dir: &Path| -> Error {
        let _ = std::fs::remove_dir_all(runtime_dir);
        err
    };
    let network = crate::exec::RUNSC_NETWORK_SANDBOX.to_owned();
    let build_netns = RunscBuildNetwork::create().map_err(|e| cleanup_dir(e, &runtime_dir))?;
    let netns_path = build_netns.path();
    write_runsc_bundle_config(
        &runsc_bin,
        parts,
        &bundle_dir,
        &parts.rootfs,
        &network,
        Some(&netns_path),
    )
    .map_err(|e| cleanup_dir(e, &runtime_dir))?;
    runsc_run_detached(
        &runsc_bin,
        &runsc_root,
        &network,
        &bundle_dir,
        &pid_file,
        &container_id,
    )
    .map_err(|e| cleanup_dir(e, &runtime_dir))?;

    let vm = Vm {
        running: None,
        sentry_exec: None,
        sentry_src_rootfs: None,
        vsock_mux_path: runtime_dir.join("runsc-unwired.sock"),
        vsock_exec_path: runtime_dir.join("runsc-exec-unwired.sock"),
        own_vsock_mux_dir: None,
        skip_cleanup: false,
        time_sync_stop: Arc::new(AtomicBool::new(false)),
        _sentry_mem_admission: None,
        sentry_warm_pool: None,
        sentry_unified_pool: None,
        runsc: Some(RunscRuntime {
            runsc_bin: runsc_bin.clone(),
            runsc_root: runsc_root.clone(),
            network: network.clone(),
            build_netns: Some(build_netns),
            container_id: container_id.clone(),
            bundle_dir: bundle_dir.clone(),
            rootfs: parts.rootfs.clone(),
            parts: parts.clone(),
            file_agent_exec: true,
        }),
        kboxlike: None,
    };

    let warmup_res = warmup(&vm);
    if warmup_res.is_ok() {
        let _ = std::fs::remove_dir_all(&checkpoint_dir);
        std::fs::create_dir_all(&checkpoint_dir).map_err(Error::Io)?;
        runsc_checkpoint(
            &runsc_bin,
            &runsc_root,
            &network,
            &checkpoint_dir,
            &container_id,
        )?;
    }
    drop(vm);
    warmup_res?;

    let meta_path = dest.join("metadata.json");
    let meta_text = std::fs::read_to_string(&meta_path)
        .map_err(|e| Error::bake_msg(format!("read {}: {e}", meta_path.display())))?;
    let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
        .map_err(|e| Error::bake_msg(format!("parse {}: {e}", meta_path.display())))?;
    if let Some(obj) = meta.as_object_mut() {
        obj.insert(
            "runsc_checkpoint".to_owned(),
            serde_json::json!(checkpoint_dir.to_string_lossy()),
        );
    }
    let meta_out = serde_json::to_string_pretty(&meta)
        .map_err(|e| Error::bake_msg(format!("runsc metadata serialize: {e}")))?;
    std::fs::write(&meta_path, meta_out).map_err(Error::Io)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_runsc_bundle(
    runsc_bin: &Path,
    parts: &RunscImageParts,
    bundle_dir: &Path,
    rootfs: &Path,
    network: &str,
    netns_path: Option<&Path>,
) -> Result<(), Error> {
    std::fs::create_dir_all(bundle_dir).map_err(Error::Io)?;
    copy_dir_recursive(&parts.rootfs, rootfs)
        .map_err(|e| Error::vm_msg(format!("runsc rootfs clone: {e}")))?;
    write_runsc_bundle_config(runsc_bin, parts, bundle_dir, rootfs, network, netns_path)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_runsc_restore_rootfs(
    base_rootfs: &Path,
    runtime_dir: &Path,
    rootfs: &Path,
) -> Result<(), Error> {
    let upper = runtime_dir.join("overlay-upper");
    let work = runtime_dir.join("overlay-work");
    std::fs::create_dir_all(rootfs).map_err(Error::Io)?;
    std::fs::create_dir_all(&upper).map_err(Error::Io)?;
    std::fs::create_dir_all(&work).map_err(Error::Io)?;
    let opts = format!(
        "lowerdir={},upperdir={},workdir={}",
        base_rootfs.display(),
        upper.display(),
        work.display()
    );
    match Command::new("mount")
        .args(["-t", "overlay", "overlay", "-o", &opts])
        .arg(rootfs)
        .status()
    {
        Ok(status) if status.success() => Ok(()),
        Ok(status) => {
            eprintln!(
                "supermachine(runsc): overlay rootfs mount failed with status {:?}; \
                 falling back to full rootfs copy",
                status.code()
            );
            let _ = std::fs::remove_dir_all(rootfs);
            let _ = std::fs::remove_dir_all(&upper);
            let _ = std::fs::remove_dir_all(&work);
            copy_dir_recursive(base_rootfs, rootfs)
                .map_err(|e| Error::vm_msg(format!("runsc restore rootfs clone: {e}")))
        }
        Err(e) => {
            eprintln!(
                "supermachine(runsc): overlay rootfs mount failed ({e}); \
                 falling back to full rootfs copy"
            );
            let _ = std::fs::remove_dir_all(rootfs);
            let _ = std::fs::remove_dir_all(&upper);
            let _ = std::fs::remove_dir_all(&work);
            copy_dir_recursive(base_rootfs, rootfs)
                .map_err(|e| Error::vm_msg(format!("runsc restore rootfs clone: {e}")))
        }
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_runsc_bundle_config(
    runsc_bin: &Path,
    parts: &RunscImageParts,
    bundle_dir: &Path,
    rootfs: &Path,
    network: &str,
    netns_path: Option<&Path>,
) -> Result<(), Error> {
    std::fs::create_dir_all(bundle_dir).map_err(Error::Io)?;
    ensure_runsc_mountpoints(parts, rootfs)?;
    ensure_runsc_resolver(rootfs, network)?;
    ensure_runsc_file_agent(rootfs)?;
    let spec = Command::new(runsc_bin)
        .arg("spec")
        .current_dir(bundle_dir)
        .output()
        .map_err(|e| Error::vm_msg(format!("runsc spec: {e}")))?;
    if !spec.status.success() {
        return Err(Error::vm_msg(format!(
            "runsc spec failed with status {:?}: {}",
            spec.status.code(),
            String::from_utf8_lossy(&spec.stderr)
        )));
    }
    let config_path = bundle_dir.join("config.json");
    let config_text = std::fs::read_to_string(&config_path)
        .map_err(|e| Error::vm_msg(format!("read runsc config.json: {e}")))?;
    let mut spec_json: serde_json::Value = serde_json::from_str(&config_text)
        .map_err(|e| Error::vm_msg(format!("parse runsc config.json: {e}")))?;
    patch_runsc_spec(&mut spec_json, parts, rootfs, network, netns_path)?;
    let spec_out = serde_json::to_string_pretty(&spec_json)
        .map_err(|e| Error::vm_msg(format!("serialize runsc config.json: {e}")))?;
    std::fs::write(&config_path, spec_out)
        .map_err(|e| Error::vm_msg(format!("write runsc config.json: {e}")))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_runsc_file_agent(rootfs: &Path) -> Result<(), Error> {
    let dir = rootfs.join(".supermachine");
    std::fs::create_dir_all(dir.join("runsc-exec/requests")).map_err(|e| {
        Error::vm_msg(format!(
            "create runsc file-agent dir {}: {e}",
            dir.display()
        ))
    })?;
    let script_path = dir.join("runsc-file-agent.sh");
    let script = r#"#!/bin/sh
set +e
DIR=/.supermachine/runsc-exec
mkdir -p "$DIR/requests" /run
if [ -f /.supermachine/run-workload ]; then
  /bin/sh /.supermachine/run-workload &
  echo $! >/run/supermachine-workload.pid
fi
while :; do
  for req in "$DIR"/requests/*; do
    [ -d "$req" ] || continue
    [ -f "$req/ready" ] || continue
    [ ! -f "$req/done" ] || continue
    /bin/sh "$req/script" >"$req/stdout" 2>"$req/stderr"
    rc=$?
    echo "$rc" >"$req/status"
    echo 1 >"$req/done"
  done
  sleep 0.05
done
"#;
    std::fs::write(&script_path, script).map_err(|e| {
        Error::vm_msg(format!(
            "write runsc file-agent {}: {e}",
            script_path.display()
        ))
    })?;
    let mut perms = std::fs::metadata(&script_path)
        .map_err(Error::Io)?
        .permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&script_path, perms).map_err(Error::Io)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_runsc_mountpoints(parts: &RunscImageParts, rootfs: &Path) -> Result<(), Error> {
    for mount in &parts.mounts {
        let rel = mount.guest_path.trim_start_matches('/');
        let target = confined_rootfs_join(rootfs, rel).ok_or_else(|| {
            Error::vm_msg(format!(
                "runsc mount guest path escapes the rootfs: {:?}",
                mount.guest_path
            ))
        })?;
        std::fs::create_dir_all(&target).map_err(|e| {
            Error::vm_msg(format!("create runsc mountpoint {}: {e}", target.display()))
        })?;
    }
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_runsc_resolver(rootfs: &Path, network: &str) -> Result<(), Error> {
    if network != crate::exec::RUNSC_NETWORK_SANDBOX {
        return Ok(());
    }
    let Some(text) = host_resolv_conf_for_runsc() else {
        return Ok(());
    };
    let target = confined_rootfs_join(rootfs, "etc/resolv.conf")
        .ok_or_else(|| Error::vm_msg("runsc resolver path escaped rootfs".to_owned()))?;
    if let Some(parent) = target.parent() {
        std::fs::create_dir_all(parent).map_err(Error::Io)?;
    }
    std::fs::write(&target, text)
        .map_err(|e| Error::vm_msg(format!("write runsc resolver {}: {e}", target.display())))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn host_resolv_conf_for_runsc() -> Option<String> {
    for path in ["/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"] {
        let Ok(text) = std::fs::read_to_string(path) else {
            continue;
        };
        if let Some(text) = sanitize_runsc_resolver(&text) {
            return Some(text);
        }
    }
    None
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sanitize_runsc_resolver(text: &str) -> Option<String> {
    let nameserver = text.lines().find_map(|line| {
        let mut fields = line.split_whitespace();
        if !matches!(fields.next(), Some("nameserver")) {
            return None;
        }
        let addr = fields.next()?;
        if addr.starts_with("127.") || addr == "::1" || addr.eq_ignore_ascii_case("localhost") {
            return None;
        }
        if addr.contains(':') {
            return None;
        }
        Some(addr.to_owned())
    })?;

    Some(format!(
        "# generated by supermachine for runsc build networking\nnameserver {nameserver}\noptions single-request no-aaaa\n"
    ))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn copy_runsc_bundle_config(
    base_bundle_dir: &Path,
    bundle_dir: &Path,
    parts: &RunscImageParts,
    rootfs: &Path,
    network: &str,
    netns_path: Option<&Path>,
) -> Result<(), Error> {
    std::fs::create_dir_all(bundle_dir).map_err(Error::Io)?;
    let config_path = base_bundle_dir.join("config.json");
    let config_text = std::fs::read_to_string(&config_path)
        .map_err(|e| Error::vm_msg(format!("read base runsc config.json: {e}")))?;
    let mut spec_json: serde_json::Value = serde_json::from_str(&config_text)
        .map_err(|e| Error::vm_msg(format!("parse base runsc config.json: {e}")))?;
    ensure_runsc_mountpoints(parts, rootfs)?;
    ensure_runsc_resolver(rootfs, network)?;
    patch_runsc_spec(&mut spec_json, parts, rootfs, network, netns_path)?;
    let spec_out = serde_json::to_string_pretty(&spec_json)
        .map_err(|e| Error::vm_msg(format!("serialize restore runsc config.json: {e}")))?;
    std::fs::write(bundle_dir.join("config.json"), spec_out)
        .map_err(|e| Error::vm_msg(format!("write restore runsc config.json: {e}")))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_run_detached(
    runsc_bin: &Path,
    runsc_root: &Path,
    network: &str,
    bundle_dir: &Path,
    pid_file: &Path,
    container_id: &str,
) -> Result<(), Error> {
    let mut run = Command::new(runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(&mut run, runsc_root, network);
    let status = run
        .arg("run")
        .arg("-detach")
        .arg("-bundle")
        .arg(bundle_dir)
        .arg("-pid-file")
        .arg(pid_file)
        .arg(container_id)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map_err(|e| Error::vm_msg(format!("runsc run {container_id}: {e}")))?;
    if status.success() {
        Ok(())
    } else {
        Err(Error::vm_msg(format!(
            "runsc run failed with status {:?}",
            status.code()
        )))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_checkpoint(
    runsc_bin: &Path,
    runsc_root: &Path,
    network: &str,
    checkpoint_dir: &Path,
    container_id: &str,
) -> Result<(), Error> {
    runsc_checkpoint_with_options(
        runsc_bin,
        runsc_root,
        network,
        checkpoint_dir,
        container_id,
        false,
    )
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_checkpoint_with_options(
    runsc_bin: &Path,
    runsc_root: &Path,
    network: &str,
    checkpoint_dir: &Path,
    container_id: &str,
    leave_running: bool,
) -> Result<(), Error> {
    let mut cmd = Command::new(runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(&mut cmd, runsc_root, network);
    cmd.arg("checkpoint").arg("-image-path").arg(checkpoint_dir);
    if leave_running {
        cmd.arg("-leave-running");
    }
    let out = cmd
        .arg(container_id)
        .output()
        .map_err(|e| Error::vm_msg(format!("runsc checkpoint {container_id}: {e}")))?;
    if out.status.success() {
        Ok(())
    } else {
        Err(Error::vm_msg(format!(
            "runsc checkpoint{} failed with status {:?}: {}",
            if leave_running { " -leave-running" } else { "" },
            out.status.code(),
            String::from_utf8_lossy(&out.stderr)
        )))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_restore_detached(
    runsc_bin: &Path,
    runsc_root: &Path,
    network: &str,
    bundle_dir: &Path,
    checkpoint_dir: &Path,
    pid_file: &Path,
    container_id: &str,
) -> Result<(), Error> {
    let mut cmd = Command::new(runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(&mut cmd, runsc_root, network);
    let status = cmd
        .arg("restore")
        .arg("-detach")
        .arg("-bundle")
        .arg(bundle_dir)
        .arg("-image-path")
        .arg(checkpoint_dir)
        .arg("-pid-file")
        .arg(pid_file)
        .arg(container_id)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map_err(|e| Error::vm_msg(format!("runsc restore {container_id}: {e}")))?;
    if status.success() {
        Ok(())
    } else {
        Err(Error::vm_msg(format!(
            "runsc restore failed with status {:?}",
            status.code()
        )))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_delete_force(
    runsc_bin: &Path,
    runsc_root: &Path,
    network: &str,
    container_id: &str,
) -> Result<(), Error> {
    let mut kill = Command::new(runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(&mut kill, runsc_root, network);
    let _ = kill.arg("kill").arg(container_id).arg("KILL").status();

    let mut delete = Command::new(runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(&mut delete, runsc_root, network);
    let status = delete
        .arg("delete")
        .arg("-force")
        .arg(container_id)
        .status()
        .map_err(|e| Error::vm_msg(format!("runsc delete {container_id}: {e}")))?;
    if status.success() {
        Ok(())
    } else {
        Err(Error::vm_msg(format!(
            "runsc delete failed with status {:?}",
            status.code()
        )))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_exec_shell(
    runtime: &RunscRuntime,
    script: &str,
    stdin_bytes: Option<&[u8]>,
) -> std::io::Result<std::process::Output> {
    let mut cmd = Command::new(&runtime.runsc_bin);
    crate::exec::add_runsc_global_flags_with_network(
        &mut cmd,
        &runtime.runsc_root,
        &runtime.network,
    );
    cmd.arg("exec")
        .arg(&runtime.container_id)
        .arg("/bin/sh")
        .arg("-c")
        .arg(script)
        .stdin(if stdin_bytes.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let mut child = cmd.spawn().map_err(|e| {
        std::io::Error::new(
            e.kind(),
            format!("runsc exec {} for control RPC: {e}", runtime.container_id),
        )
    })?;
    if let Some(bytes) = stdin_bytes {
        let mut stdin = child.stdin.take().ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::BrokenPipe,
                "runsc exec stdin was not available",
            )
        })?;
        stdin.write_all(bytes)?;
    }
    child.wait_with_output()
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_io_error(action: &str, out: std::process::Output) -> std::io::Error {
    let detail = String::from_utf8_lossy(&out.stderr).trim().to_owned();
    std::io::Error::new(
        std::io::ErrorKind::Other,
        if detail.is_empty() {
            format!("{action} failed with status {:?}", out.status.code())
        } else {
            format!(
                "{action} failed with status {:?}: {detail}",
                out.status.code()
            )
        },
    )
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_write_file(runtime: &RunscRuntime, path: &str, bytes: &[u8]) -> std::io::Result<()> {
    let q = sh_quote_runsc(path);
    let script = format!(
        r#"set -e
p={q}
d=$(dirname -- "$p")
b=$(basename -- "$p")
tmp="$d/.${{b}}.supermachine.$$"
rm -f "$tmp"
cat > "$tmp"
chmod 0644 "$tmp" 2>/dev/null || true
mv -f "$tmp" "$p"
"#
    );
    let out = runsc_exec_shell(runtime, &script, Some(bytes))?;
    if out.status.success() {
        Ok(())
    } else {
        Err(runsc_io_error("runsc write_file", out))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_read_file(runtime: &RunscRuntime, path: &str, max_bytes: u64) -> std::io::Result<Vec<u8>> {
    let cap = max_bytes.saturating_add(1);
    let q = sh_quote_runsc(path);
    let script = format!(
        r#"set -e
p={q}
if [ ! -f "$p" ]; then
  echo "read_file: not a regular file: $p" >&2
  exit 1
fi
size=$(wc -c < "$p" | tr -d ' ')
if [ "$size" -gt "{max_bytes}" ]; then
  echo "read_file: file exceeds max_bytes ($size > {max_bytes})" >&2
  exit 1
fi
head -c {cap} -- "$p"
"#
    );
    let out = runsc_exec_shell(runtime, &script, None)?;
    if !out.status.success() {
        return Err(runsc_io_error("runsc read_file", out));
    }
    if out.stdout.len() as u64 > max_bytes {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!(
                "read_file: file exceeds max_bytes ({} > {max_bytes})",
                out.stdout.len()
            ),
        ));
    }
    Ok(out.stdout)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_rootfs_join_io(rootfs: &Path, guest_path: &str) -> std::io::Result<PathBuf> {
    let rel = guest_path.strip_prefix('/').unwrap_or(guest_path);
    let p = Path::new(rel);
    if rel.is_empty()
        || p.components().any(|component| {
            matches!(
                component,
                std::path::Component::ParentDir | std::path::Component::RootDir
            )
        })
    {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("guest path escapes rootfs: {guest_path}"),
        ));
    }
    Ok(rootfs.join(p))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_write_file(runtime: &KboxlikeRuntime, path: &str, bytes: &[u8]) -> std::io::Result<()> {
    let target = kboxlike_rootfs_join_io(&runtime.rootfs, path)?;
    let parent = target.parent().ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("write_file path has no parent: {path}"),
        )
    })?;
    std::fs::create_dir_all(parent)?;
    let name = target
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("write");
    let tmp = parent.join(format!(".supermachine-write-{}-{name}", std::process::id()));
    std::fs::write(&tmp, bytes)?;
    std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o644))?;
    std::fs::rename(&tmp, target)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_read_file(
    runtime: &KboxlikeRuntime,
    path: &str,
    max_bytes: u64,
) -> std::io::Result<Vec<u8>> {
    let target = kboxlike_rootfs_join_io(&runtime.rootfs, path)?;
    let meta = std::fs::metadata(&target)?;
    if !meta.is_file() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("read_file: not a regular file: {path}"),
        ));
    }
    if meta.len() > max_bytes {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!(
                "read_file: file exceeds max_bytes ({} > {max_bytes})",
                meta.len()
            ),
        ));
    }
    std::fs::read(target)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_workload_signal(runtime: &RunscRuntime, signum: i32) -> std::io::Result<()> {
    let script = format!(
        r#"set -e
pid=$(cat /run/supermachine-workload.pid)
kill -s {signum} "$pid"
"#
    );
    let out = runsc_exec_shell(runtime, &script, None)?;
    if out.status.success() {
        Ok(())
    } else {
        Err(runsc_io_error("runsc workload_signal", out))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_workload_signal(runtime: &KboxlikeRuntime, signum: i32) -> std::io::Result<()> {
    let Some(pid) = runtime.restored_host_pids.first().copied() else {
        return Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "kboxlike workload has no restored host pid",
        ));
    };
    let ret = unsafe { libc::kill(pid, signum) };
    if ret == 0 {
        Ok(())
    } else {
        Err(std::io::Error::last_os_error())
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sh_quote_runsc(s: &str) -> String {
    let mut out = String::from("'");
    for ch in s.chars() {
        if ch == '\'' {
            out.push_str("'\\''");
        } else {
            out.push(ch);
        }
    }
    out.push('\'');
    out
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn patch_runsc_spec(
    spec: &mut serde_json::Value,
    parts: &RunscImageParts,
    rootfs: &Path,
    network: &str,
    netns_path: Option<&Path>,
) -> Result<(), Error> {
    let root = spec
        .get_mut("root")
        .and_then(|v| v.as_object_mut())
        .ok_or_else(|| Error::vm_msg("runsc config.json missing root object".to_owned()))?;
    root.insert(
        "path".to_owned(),
        serde_json::json!(rootfs.to_string_lossy().to_string()),
    );
    root.insert("readonly".to_owned(), serde_json::json!(false));

    let process = spec
        .get_mut("process")
        .and_then(|v| v.as_object_mut())
        .ok_or_else(|| Error::vm_msg("runsc config.json missing process object".to_owned()))?;
    let mut env: Vec<String> = parts
        .image_env
        .iter()
        .map(|(k, v)| format!("{k}={v}"))
        .collect();
    if !parts.image_env.iter().any(|(k, _)| k == "PATH") {
        env.push("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_owned());
    }
    process.insert("env".to_owned(), serde_json::json!(env));
    process.insert("cwd".to_owned(), serde_json::json!("/"));
    process.insert(
        "args".to_owned(),
        serde_json::json!(["/bin/sh", "/.supermachine/runsc-file-agent.sh"]),
    );
    if let Some((uid, gid)) = parts.user {
        process.insert(
            "user".to_owned(),
            serde_json::json!({
                "uid": uid,
                "gid": gid,
            }),
        );
    }
    let caps = serde_json::json!([
        "CAP_AUDIT_WRITE",
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FOWNER",
        "CAP_KILL",
        "CAP_NET_BIND_SERVICE",
        "CAP_SETGID",
        "CAP_SETUID"
    ]);
    process.insert(
        "capabilities".to_owned(),
        serde_json::json!({
            "bounding": caps,
            "effective": caps,
            "inheritable": caps,
            "permitted": caps,
        }),
    );
    process.insert("terminal".to_owned(), serde_json::json!(false));

    if network == crate::exec::RUNSC_NETWORK_SANDBOX {
        patch_runsc_network_namespace(spec, netns_path)?;
    }

    if !parts.mounts.is_empty() {
        let mounts = spec
            .get_mut("mounts")
            .and_then(|v| v.as_array_mut())
            .ok_or_else(|| Error::vm_msg("runsc config.json missing mounts array".to_owned()))?;
        for mount in &parts.mounts {
            // runsc has no virtio-fs symlink-policy hook. The backend-level
            // parity here is the host-dir-at-guest-path mapping; policy-specific
            // enforcement needs a later runsc file-access adapter.
            let opts = if mount.read_only {
                vec!["rbind", "ro"]
            } else {
                vec!["rbind", "rw"]
            };
            mounts.push(serde_json::json!({
                "destination": mount.guest_path,
                "type": "bind",
                "source": mount.host_path,
                "options": opts,
            }));
        }
    }
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn patch_runsc_network_namespace(
    spec: &mut serde_json::Value,
    netns_path: Option<&Path>,
) -> Result<(), Error> {
    let Some(namespaces) = spec
        .get_mut("linux")
        .and_then(|v| v.get_mut("namespaces"))
        .and_then(|v| v.as_array_mut())
    else {
        return Ok(());
    };
    let Some(netns_path) = netns_path else {
        return Ok(());
    };
    for ns in namespaces {
        if ns
            .get("type")
            .and_then(|v| v.as_str())
            .map(|ty| ty == "network")
            .unwrap_or(false)
        {
            ns["path"] = serde_json::json!(netns_path.to_string_lossy().to_string());
            return Ok(());
        }
    }
    Ok(())
}

/// Recursively copy `src` → `dst`, preserving directories (+ perms), symlinks
/// (not followed), regular files (+ perms), and **hardlinks** (same `(dev,ino)`
/// → a single `hard_link`, so busybox's ~hundreds of applet links don't bloat
/// the copy). Regular files try a reflink (`clonefile_one`) first and fall back
/// to a byte copy. Non-regular/non-symlink entries (sockets, fifos, devices) are
/// skipped — a sentry rootfs doesn't ship them. Backs the sentry `Vm::snapshot`.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
    use std::collections::HashMap;
    use std::ffi::CString;
    use std::os::unix::ffi::OsStrExt;
    use std::os::unix::fs::MetadataExt;
    fn set_owner(path: &Path, uid: u32, gid: u32, follow: bool) -> std::io::Result<()> {
        let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
            std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL")
        })?;
        let r = unsafe {
            if follow {
                libc::chown(c.as_ptr(), uid as libc::uid_t, gid as libc::gid_t)
            } else {
                libc::lchown(c.as_ptr(), uid as libc::uid_t, gid as libc::gid_t)
            }
        };
        if r == 0 {
            Ok(())
        } else {
            Err(std::io::Error::last_os_error())
        }
    }
    std::fs::create_dir_all(dst)?;
    let root_md = std::fs::symlink_metadata(src)?;
    let _ = std::fs::set_permissions(dst, root_md.permissions());
    set_owner(dst, root_md.uid(), root_md.gid(), true)?;
    let mut hardlinks: HashMap<(u64, u64), PathBuf> = HashMap::new();
    let mut stack = vec![(src.to_path_buf(), dst.to_path_buf())];
    while let Some((s, d)) = stack.pop() {
        for entry in std::fs::read_dir(&s)? {
            let entry = entry?;
            let sp = entry.path();
            let dp = d.join(entry.file_name());
            let md = std::fs::symlink_metadata(&sp)?;
            let ft = md.file_type();
            if ft.is_dir() {
                std::fs::create_dir_all(&dp)?;
                let _ = std::fs::set_permissions(&dp, md.permissions());
                set_owner(&dp, md.uid(), md.gid(), true)?;
                stack.push((sp, dp));
            } else if ft.is_symlink() {
                let target = std::fs::read_link(&sp)?;
                let _ = std::fs::remove_file(&dp);
                std::os::unix::fs::symlink(&target, &dp)?;
                set_owner(&dp, md.uid(), md.gid(), false)?;
            } else if ft.is_file() {
                // Preserve hardlinks: re-link to the first copy of this inode.
                if md.nlink() > 1 {
                    let key = (md.dev(), md.ino());
                    if let Some(first) = hardlinks.get(&key) {
                        let _ = std::fs::remove_file(&dp);
                        std::fs::hard_link(first, &dp)?;
                        set_owner(&dp, md.uid(), md.gid(), true)?;
                        continue;
                    }
                    hardlinks.insert(key, dp.clone());
                }
                if crate::bake::clonefile_one(&sp, &dp).is_err() {
                    let _ = std::fs::remove_file(&dp);
                    std::fs::copy(&sp, &dp)?;
                }
                set_owner(&dp, md.uid(), md.gid(), true)?;
            }
            // else: socket/fifo/device — skip (not present in a sentry rootfs).
        }
    }
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn commit_staged_snapshot_dir(staging: &Path, dest_dir: &Path) -> Result<(), Error> {
    use std::os::unix::ffi::OsStrExt;

    let staging_c = std::ffi::CString::new(staging.as_os_str().as_bytes())
        .map_err(|_| Error::vm_msg("snapshot: staging path has NUL".to_owned()))?;
    let dest_c = std::ffi::CString::new(dest_dir.as_os_str().as_bytes())
        .map_err(|_| Error::vm_msg("snapshot: dest path has NUL".to_owned()))?;
    const RENAME_EXCHANGE: libc::c_uint = 1 << 1;
    let exch = unsafe {
        libc::renameat2(
            libc::AT_FDCWD,
            staging_c.as_ptr(),
            libc::AT_FDCWD,
            dest_c.as_ptr(),
            RENAME_EXCHANGE,
        )
    };
    if exch == 0 {
        let _ = std::fs::remove_dir_all(staging);
        return Ok(());
    }

    let err = std::io::Error::last_os_error();
    if err.raw_os_error() == Some(libc::ENOENT) || !dest_dir.exists() {
        std::fs::rename(staging, dest_dir).map_err(|e| {
            Error::vm_msg(format!(
                "snapshot: atomic commit rename {} -> {}: {e}",
                staging.display(),
                dest_dir.display()
            ))
        })?;
        Ok(())
    } else {
        Err(Error::vm_msg(format!(
            "snapshot: gap-free directory exchange {} <-> {} failed: {err}",
            staging.display(),
            dest_dir.display()
        )))
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_mounts_metadata(mounts: &[crate::vmm::resources::MountSpec]) -> Vec<serde_json::Value> {
    mounts
        .iter()
        .map(|m| {
            let mut obj = serde_json::Map::new();
            obj.insert("host_path".to_owned(), serde_json::json!(&m.host_path));
            obj.insert("guest_tag".to_owned(), serde_json::json!(&m.guest_tag));
            obj.insert("guest_path".to_owned(), serde_json::json!(&m.guest_path));
            if m.read_only {
                obj.insert("read_only".to_owned(), serde_json::json!(true));
            }
            match m.symlinks {
                crate::vmm::resources::SymlinkPolicy::Opaque => {}
                crate::vmm::resources::SymlinkPolicy::Deny => {
                    obj.insert("symlinks".to_owned(), serde_json::json!("deny"));
                }
                crate::vmm::resources::SymlinkPolicy::Follow => {
                    obj.insert("symlinks".to_owned(), serde_json::json!("follow"));
                }
            }
            serde_json::Value::Object(obj)
        })
        .collect()
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_runsc_snapshot_dir(
    runtime: &RunscRuntime,
    dest_dir: &Path,
    source: &str,
    leave_running: bool,
) -> Result<Image, Error> {
    if dest_dir == runtime.rootfs || dest_dir.starts_with(&runtime.rootfs) {
        return Err(Error::vm_msg(
            "snapshot: dest_dir must be outside the source rootfs".to_owned(),
        ));
    }
    let parent = dest_dir
        .parent()
        .ok_or_else(|| Error::vm_msg("snapshot: dest_dir must have a parent dir".to_owned()))?;
    std::fs::create_dir_all(parent).map_err(Error::Io)?;
    let staging = parent.join(format!(
        ".{}.tmp-{}-{}",
        dest_dir
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| "snap".to_owned()),
        std::process::id(),
        unique_suffix(),
    ));
    let _ = std::fs::remove_dir_all(&staging);
    let cleanup = |e: Error, staging: &Path| -> Error {
        let _ = std::fs::remove_dir_all(staging);
        e
    };

    let staging_rootfs = staging.join("rootfs");
    let staging_checkpoint = staging.join("runsc-checkpoint");
    let checkpointed = runtime.network != crate::exec::RUNSC_NETWORK_SANDBOX;
    if checkpointed {
        std::fs::create_dir_all(&staging_checkpoint)
            .map_err(|e| cleanup(Error::Io(e), &staging))?;
        runsc_checkpoint_with_options(
            &runtime.runsc_bin,
            &runtime.runsc_root,
            &runtime.network,
            &staging_checkpoint,
            &runtime.container_id,
            leave_running,
        )
        .map_err(|e| cleanup(e, &staging))?;
    }
    copy_dir_recursive(&runtime.rootfs, &staging_rootfs).map_err(|e| {
        cleanup(
            Error::vm_msg(format!("snapshot: copy rootfs: {e}")),
            &staging,
        )
    })?;

    let mut env_obj = serde_json::Map::new();
    for (k, v) in &runtime.parts.image_env {
        env_obj.insert(k.clone(), serde_json::json!(v));
    }
    let user = runtime.parts.user.map(|(uid, gid)| format!("{uid}:{gid}"));
    let final_rootfs = dest_dir.join("rootfs");
    let final_checkpoint = dest_dir.join("runsc-checkpoint");
    let mut metadata = serde_json::json!({
        "backend": "runsc",
        "runsc_rootfs": final_rootfs.to_string_lossy(),
        "runsc_has_workload": runtime.parts.has_workload,
        "runsc_agent_path": &runtime.parts.agent_path,
        "runsc_agent_unix_env": &runtime.parts.agent_unix_env,
        "memory_mib": runtime.parts.memory_mib,
        "vcpus": runtime.parts.vcpus,
        "image_env": serde_json::Value::Object(env_obj),
        "cmd": &runtime.parts.cmd,
        "user": user,
        "warm_daemon": runtime.parts.warm_daemon,
        "mounts": runsc_mounts_metadata(&runtime.parts.mounts),
        "baked_by_version": env!("CARGO_PKG_VERSION"),
        "baked_at": chrono_rfc3339_now(),
        "source": source,
        "snapshot_physical_bytes": dir_size_bytes(&staging_rootfs) + if checkpointed { dir_size_bytes(&staging_checkpoint) } else { 0 },
    });
    if checkpointed {
        metadata["runsc_checkpoint"] = serde_json::json!(final_checkpoint.to_string_lossy());
    }
    let meta_text = serde_json::to_string_pretty(&metadata)
        .map_err(|e| cleanup(Error::vm_msg(format!("metadata serialize: {e}")), &staging))?;
    std::fs::write(staging.join("metadata.json"), meta_text)
        .map_err(|e| cleanup(Error::Io(e), &staging))?;
    std::fs::write(staging.join("restore.snap"), b"runsc-checkpoint\n")
        .map_err(|e| cleanup(Error::Io(e), &staging))?;

    commit_staged_snapshot_dir(&staging, dest_dir).map_err(|e| cleanup(e, &staging))?;
    Image::from_snapshot(dest_dir)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_kboxlike_snapshot_dir(
    runtime: &KboxlikeRuntime,
    dest_dir: &Path,
    source: &str,
) -> Result<Image, Error> {
    if dest_dir == runtime.rootfs || dest_dir.starts_with(&runtime.rootfs) {
        return Err(Error::vm_msg(
            "snapshot: dest_dir must be outside the source rootfs".to_owned(),
        ));
    }
    let parent = dest_dir
        .parent()
        .ok_or_else(|| Error::vm_msg("snapshot: dest_dir must have a parent dir".to_owned()))?;
    std::fs::create_dir_all(parent).map_err(Error::Io)?;
    let staging = parent.join(format!(
        ".{}.tmp-{}-{}",
        dest_dir
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| "snap".to_owned()),
        std::process::id(),
        unique_suffix(),
    ));
    let _ = std::fs::remove_dir_all(&staging);
    let cleanup = |e: Error, staging: &Path| -> Error {
        let _ = std::fs::remove_dir_all(staging);
        e
    };

    let staging_rootfs = staging.join("rootfs");
    std::fs::create_dir_all(&staging).map_err(|e| cleanup(Error::Io(e), &staging))?;
    copy_dir_recursive(&runtime.rootfs, &staging_rootfs).map_err(|e| {
        cleanup(
            Error::vm_msg(format!("snapshot: copy rootfs: {e}")),
            &staging,
        )
    })?;

    let final_rootfs = dest_dir.join("rootfs");
    let mut meta = runtime
        .rootfs
        .parent()
        .map(|p| p.join("metadata.json"))
        .and_then(|p| std::fs::read(p).ok())
        .and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
        .unwrap_or_else(|| serde_json::json!({ "backend": "kboxlike" }));
    if let Some(obj) = meta.as_object_mut() {
        obj.insert("backend".to_owned(), serde_json::json!("kboxlike"));
        obj.insert(
            "kboxlike_rootfs".to_owned(),
            serde_json::json!(final_rootfs.to_string_lossy()),
        );
        obj.insert(
            "kboxlike_has_workload".to_owned(),
            serde_json::json!(runtime.parts.has_workload),
        );
        obj.insert(
            "memory_mib".to_owned(),
            serde_json::json!(runtime.parts.memory_mib),
        );
        obj.insert("vcpus".to_owned(), serde_json::json!(runtime.parts.vcpus));
        obj.insert(
            "baked_by_version".to_owned(),
            serde_json::json!(env!("CARGO_PKG_VERSION")),
        );
        obj.insert(
            "baked_at".to_owned(),
            serde_json::json!(chrono_rfc3339_now()),
        );
        obj.insert("source".to_owned(), serde_json::json!(source));
        obj.insert(
            "snapshot_physical_bytes".to_owned(),
            serde_json::json!(dir_size_bytes(&staging_rootfs)),
        );
    }
    let meta_text = serde_json::to_string_pretty(&meta)
        .map_err(|e| cleanup(Error::vm_msg(format!("metadata serialize: {e}")), &staging))?;
    std::fs::write(staging.join("metadata.json"), meta_text)
        .map_err(|e| cleanup(Error::Io(e), &staging))?;
    std::fs::write(staging.join("restore.snap"), b"kboxlike-rootfs\n")
        .map_err(|e| cleanup(Error::Io(e), &staging))?;

    commit_staged_snapshot_dir(&staging, dest_dir).map_err(|e| cleanup(e, &staging))?;
    Image::from_snapshot(dest_dir)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn remove_dir_contents(dir: &Path) -> std::io::Result<()> {
    std::fs::create_dir_all(dir)?;
    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let md = std::fs::symlink_metadata(&path)?;
        if md.file_type().is_dir() {
            std::fs::remove_dir_all(&path)?;
        } else {
            std::fs::remove_file(&path)?;
        }
    }
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn replace_dir_contents_from_staging(staging: &Path, dst: &Path) -> std::io::Result<()> {
    std::fs::create_dir_all(dst)?;
    remove_dir_contents(dst)?;
    for entry in std::fs::read_dir(staging)? {
        let entry = entry?;
        std::fs::rename(entry.path(), dst.join(entry.file_name()))?;
    }
    std::fs::remove_dir_all(staging)?;
    Ok(())
}

/// Capture a sentry warm rootfs into a self-contained snapshot image dir:
/// recursively copy `src_rootfs` → `dest_dir/rootfs` and re-emit a
/// `"backend":"sentry"` `metadata.json` (repointing `sentry_rootfs` to the copy,
/// restamping `baked_at`/`source`/`baked_by_version`, recomputing
/// `snapshot_physical_bytes`, and preserving
/// `sentry_has_workload`/`image_env`/`cmd`/… from the SOURCE's metadata, which is
/// the sibling of `src_rootfs`). Returns the loaded [`Image`].
///
/// Non-consuming + side-effect-free w.r.t. the caller's `Vm`/pool: it neither
/// stops the exec server nor unlinks sockets, so both the consuming cold-path
/// [`Vm::snapshot`] and the non-consuming pooled [`PooledVm::snapshot`] can share
/// it. The source rootfs is left intact (the live VM / pool keeps running).
///
/// The sentry writes straight to the host rootfs dir (no guest-kernel page
/// cache), so no fsync is needed; this is a point-in-time copy — call it after
/// your warming exec has returned.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_sentry_snapshot_dir(
    src_rootfs: &Path,
    dest_dir: &Path,
    source: &str,
) -> Result<Image, Error> {
    write_sentry_snapshot_dir_inner(src_rootfs, dest_dir, source, false, None, None)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_sentry_snapshot_dir_inner(
    src_rootfs: &Path,
    dest_dir: &Path,
    source: &str,
    allow_warm_daemon: bool,
    memory_mib: Option<u32>,
    vcpus: Option<u32>,
) -> Result<Image, Error> {
    if dest_dir == src_rootfs || dest_dir.starts_with(src_rootfs) {
        return Err(Error::vm_msg(
            "snapshot: dest_dir must be outside the source rootfs".to_owned(),
        ));
    }
    // Defense-in-depth gate (metadata-driven; covers the cold Vm::start ->
    // Vm::snapshot path that bypasses the typed SentryPool.warm_daemon check): a
    // warm-daemon image's warm state is a LIVE parked cell, not the rootfs dir, so a
    // cold rootfs copy would silently emit an un-warm image. Refuse UP FRONT (before
    // copying the whole rootfs). The bare {"backend":"sentry"} fallback has no
    // warm_daemon key, so cold/derived images stay snapshottable.
    if !allow_warm_daemon
        && src_rootfs
            .parent()
            .map(|p| p.join("metadata.json"))
            .and_then(|p| std::fs::read(p).ok())
            .and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
            .and_then(|m| m.get("warm_daemon").and_then(|v| v.as_bool()))
            == Some(true)
    {
        return Err(Error::vm_msg(
            "snapshot: source image is a warm-daemon image; cold rootfs capture would \
             drop the live warm cell (unsupported)"
                .to_owned(),
        ));
    }
    // ATOMIC COMMIT (concurrent-build rootfs race, task_96ba9a6d): a sentry
    // layer-cache dir is read LOCK-FREE by the builder's longest-prefix scan
    // (`builder::executor::build_stage` step 2: `is_snapshot_dir` +
    // `Image::from_snapshot`), while a concurrent producer holds only the
    // per-key `CacheLock`. If we wrote `rootfs/` then `metadata.json` IN PLACE,
    // a reader could observe the torn middle — a half-copied `rootfs/` (or stale
    // metadata) that `is_snapshot_dir` accepts → the verify-exec then fails with
    // code 127 (missing /bin/sh) / code 1 (cannot open root). Instead, build the
    // WHOLE image (rootfs + metadata) in a uniquely-named sibling staging dir,
    // then `rename(2)` it onto `dest_dir` in one step. POSIX directory rename on
    // the same filesystem is atomic, so a concurrent reader sees `dest_dir` as
    // either entirely absent/old (→ a safe cache MISS that rebuilds) or fully
    // complete — never a partial tree. Per-build isolation is unchanged: the
    // per-build PRIVATE clone (`SentryPool::build`) and this capture are
    // independent.
    let parent = dest_dir
        .parent()
        .ok_or_else(|| Error::vm_msg("snapshot: dest_dir must have a parent dir".to_owned()))?;
    std::fs::create_dir_all(parent).map_err(Error::Io)?;
    let staging = parent.join(format!(
        ".{}.tmp-{}-{}",
        dest_dir
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| "snap".to_owned()),
        std::process::id(),
        unique_suffix(),
    ));
    // A failed earlier attempt may have left a same-named staging dir behind.
    let _ = std::fs::remove_dir_all(&staging);
    // Helper so every fallible step below tears the staging dir down (never leak
    // a partial sibling next to the cache dir).
    let cleanup = |e: Error, staging: &Path| -> Error {
        let _ = std::fs::remove_dir_all(staging);
        e
    };
    let staging_rootfs = staging.join("rootfs");
    std::fs::create_dir_all(&staging).map_err(|e| cleanup(Error::Io(e), &staging))?;
    copy_dir_recursive(src_rootfs, &staging_rootfs).map_err(|e| {
        cleanup(
            Error::vm_msg(format!("snapshot: copy rootfs: {e}")),
            &staging,
        )
    })?;
    // `sentry_rootfs` records the FINAL location (`dest_dir/rootfs`), not the
    // staging path — after the rename below that is where the bytes live, and
    // `Image::from_snapshot` resolves it post-commit.
    let final_rootfs = dest_dir.join("rootfs");
    // Re-emit metadata from the SOURCE (sibling of the source rootfs),
    // repointing the rootfs and restamping. Falls back to a minimal doc if the
    // source metadata is missing/unreadable.
    let mut meta = src_rootfs
        .parent()
        .map(|p| p.join("metadata.json"))
        .and_then(|p| std::fs::read(p).ok())
        .and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
        .unwrap_or_else(|| serde_json::json!({ "backend": "sentry" }));
    if let Some(obj) = meta.as_object_mut() {
        obj.insert("backend".to_owned(), serde_json::json!("sentry"));
        obj.insert(
            "sentry_rootfs".to_owned(),
            serde_json::json!(final_rootfs.to_string_lossy()),
        );
        let inherited_memory_mib = obj
            .get("memory_mib")
            .and_then(|v| v.as_u64())
            .map(|v| v as u32);
        let effective_memory_mib = memory_mib.or(inherited_memory_mib).unwrap_or(512);
        let inherited_vcpus = obj.get("vcpus").and_then(|v| v.as_u64()).map(|v| v as u32);
        let effective_vcpus = vcpus.or(inherited_vcpus).unwrap_or(1).max(1);
        obj.insert(
            "memory_mib".to_owned(),
            serde_json::json!(effective_memory_mib),
        );
        obj.insert("vcpus".to_owned(), serde_json::json!(effective_vcpus));
        // Stamp the CURRENT binary version — the rootfs we just copied was
        // materialized by these binaries, so the snapshot must declare them.
        // `Image::from_snapshot`'s backend-agnostic version gate
        // (`snapshot_baked_under_other_version`) HARD-refuses a sentry dir
        // restored under a different version, mirroring the KVM gate; without
        // this stamp the gate stays permanently permissive for sentry captures
        // (and a stale version carried over from the source would mislabel a
        // freshly-baked snapshot).
        obj.insert(
            "baked_by_version".to_owned(),
            serde_json::json!(env!("CARGO_PKG_VERSION")),
        );
        obj.insert(
            "baked_at".to_owned(),
            serde_json::json!(chrono_rfc3339_now()),
        );
        obj.insert("source".to_owned(), serde_json::json!(source));
        obj.insert(
            "snapshot_physical_bytes".to_owned(),
            serde_json::json!(dir_size_bytes(&staging_rootfs)),
        );
    }
    let meta_text = serde_json::to_string_pretty(&meta)
        .map_err(|e| cleanup(Error::vm_msg(format!("metadata serialize: {e}")), &staging))?;
    std::fs::write(staging.join("metadata.json"), meta_text)
        .map_err(|e| cleanup(Error::Io(e), &staging))?;
    // Write a uniform `restore.snap` so the on-disk layout matches the KVM/HVF
    // snapshot dirs (metadata.json + restore.snap + filesystem artifact) and
    // `snapshot_path()` resolves to a REAL file on every backend. A sentry
    // exec-server snapshot's durable state IS the filesystem (the rootfs copied
    // above) — captured at a quiescent point there is no live process/memory to
    // checkpoint — so the state container honestly records the host CPU-feature
    // baseline with empty memory/proctree/netstack/fd/cwd sections. The sentry
    // restore path serves from `sentry_rootfs`; this file makes the snapshot
    // format self-consistent and version-stamped (read_from HARD-refuses a skew).
    {
        use crate::sentry::state_snap::{
            CpuFeatures, FdTableSnapshot, MemImage, ProcTreeSnapshot, SnapshotEntryKind,
            StateSnapshot, CURRENT_VERSION,
        };
        let state = StateSnapshot {
            version: CURRENT_VERSION,
            cpu_feature_baseline: CpuFeatures::current(),
            entry_kind: SnapshotEntryKind::WarmZygote,
            mem: MemImage::default(),
            proctree: ProcTreeSnapshot::default(),
            netstack: crate::sentry::netstack::LoopSnapshot::default(),
            fdtable: FdTableSnapshot::default(),
            cwds: Vec::new(),
            cmdlines: Vec::new(),
            exes: Vec::new(),
        };
        let mut snap_buf = Vec::new();
        state.write_to(&mut snap_buf).map_err(|e| {
            cleanup(
                Error::vm_msg(format!("snapshot: state container serialize: {e}")),
                &staging,
            )
        })?;
        std::fs::write(staging.join("restore.snap"), &snap_buf)
            .map_err(|e| cleanup(Error::Io(e), &staging))?;
    }
    // Commit. `rename(2)` requires the target to be absent or an EMPTY dir, so
    // clear any prior (stale/partial) `dest_dir` first. The producer holds the
    // per-key `CacheLock` across this call, so no peer is mid-rename on the same
    // key; a lock-free READER landing in the tiny remove→rename gap simply sees
    // `dest_dir` absent (a safe cache miss → rebuild), never a torn tree.
    // GAP-FREE publish. A `remove_dir_all(dest)` + `rename(staging, dest)` leaves a
    // window where `dest` is absent/half-removed — and `dest` is read LOCK-FREE by the
    // builder's longest-prefix scan AND cloned (as a resume base) by concurrent stages,
    // so a reader landing in that window sees a torn/missing tree (the
    // sentry_concurrent_layer_cache_no_torn_tree flake: "metadata.json EOF", "cannot
    // read ld.so", "No such file or directory"). Instead, atomically EXCHANGE our
    // complete staging tree with `dest` via renameat2(RENAME_EXCHANGE): `dest` is at
    // every instant a COMPLETE tree (old or new), never absent. Then drop the
    // swapped-out old tree (now at `staging`). If `dest` doesn't exist yet, EXCHANGE
    // fails with ENOENT and a plain `rename` is already atomic (create-in-place).
    {
        use std::os::unix::ffi::OsStrExt;
        let staging_c = std::ffi::CString::new(staging.as_os_str().as_bytes()).map_err(|_| {
            cleanup(
                Error::vm_msg("snapshot: staging path has NUL".to_owned()),
                &staging,
            )
        })?;
        let dest_c = std::ffi::CString::new(dest_dir.as_os_str().as_bytes()).map_err(|_| {
            cleanup(
                Error::vm_msg("snapshot: dest path has NUL".to_owned()),
                &staging,
            )
        })?;
        const RENAME_EXCHANGE: libc::c_uint = 1 << 1; // (RENAME_NOREPLACE=1, EXCHANGE=2)
        let exch = unsafe {
            libc::renameat2(
                libc::AT_FDCWD,
                staging_c.as_ptr(),
                libc::AT_FDCWD,
                dest_c.as_ptr(),
                RENAME_EXCHANGE,
            )
        };
        if exch == 0 {
            // dest now holds the new tree; staging holds the prior one — drop it.
            let _ = std::fs::remove_dir_all(&staging);
        } else {
            let err = std::io::Error::last_os_error();
            // ENOENT: dest didn't exist → a plain rename is an atomic create. If the
            // destination exists and RENAME_EXCHANGE is unsupported, there is no
            // lock-free-reader-safe fallback for replacing a non-empty directory; do
            // not reintroduce the remove→rename gap that this path exists to avoid.
            if err.raw_os_error() == Some(libc::ENOENT) || !dest_dir.exists() {
                std::fs::rename(&staging, dest_dir).map_err(|e| {
                    cleanup(
                        Error::vm_msg(format!(
                            "snapshot: atomic commit rename {} -> {}: {e}",
                            staging.display(),
                            dest_dir.display()
                        )),
                        &staging,
                    )
                })?;
            } else {
                return Err(cleanup(
                    Error::vm_msg(format!(
                        "snapshot: gap-free directory exchange {} <-> {} failed: {err}",
                        staging.display(),
                        dest_dir.display()
                    )),
                    &staging,
                ));
            }
        }
    }
    Image::from_snapshot(dest_dir)
}

fn chrono_rfc3339_now() -> String {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);
    // Days since 1970-01-01 / seconds within day.
    let days = secs.div_euclid(86_400);
    let sod = secs.rem_euclid(86_400);
    let hh = sod / 3600;
    let mm = (sod % 3600) / 60;
    let ss = sod % 60;
    // Civil-from-days (Howard Hinnant's algorithm).
    let z = days + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}Z")
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn set_executable(path: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    let mut perms = std::fs::metadata(path)?.permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(path, perms)
}

#[cfg(test)]
mod version_skew_tests {
    //! `Image::from_snapshot` must HARD-refuse snapshots baked under a
    //! different binary version — a field report had a 0.7.74 layer
    //! snapshot restore under 0.7.80 binaries into a VM with zero egress,
    //! with only a warning printed. Cache-or-bake callers treat this
    //! error as a miss and rebake; direct callers get an actionable error
    //! instead of a silently broken VM.
    use super::Image;
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    use super::{Error, SentryBakeConfig, VmConfig};
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    use std::io::{Read as _, Write as _};

    fn snap_dir(meta: serde_json::Value) -> std::path::PathBuf {
        // Unique per call even across parallel tests — SystemTime nanos
        // can collide between threads.
        static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
        let dir = std::env::temp_dir().join(format!(
            "sm-verskew-{}-{}",
            std::process::id(),
            SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
        ));
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(
            dir.join("metadata.json"),
            serde_json::to_string_pretty(&meta).unwrap(),
        )
        .unwrap();
        dir
    }

    #[test]
    fn from_snapshot_refuses_version_skewed_snapshot() {
        // backend=kvm sidesteps the restore.snap existence check, so the
        // version gate is what fires (the gate itself is backend-agnostic).
        let dir = snap_dir(serde_json::json!({
            "backend": "kvm",
            "kvm_snapshot": "vm.snap",
            "memory_mib": 256,
            "vcpus": 1,
            "baked_by_version": "0.0.1",
        }));
        let err = Image::from_snapshot(&dir).expect_err("must refuse version skew");
        let msg = format!("{err:?}");
        assert!(
            msg.contains("0.0.1") && msg.contains("Re-bake"),
            "error must name the baked version and tell the user to rebake: {msg}"
        );
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn from_snapshot_refuses_version_skewed_sentry_snapshot() {
        // The no-virt sentry backend also stamps `baked_by_version` at bake and
        // (like kvm) skips the restore.snap existence check, so the backend-agnostic
        // version gate must fire for a sentry dir too — a skewed sentry snapshot can
        // carry an incompatible pinned init/agent. Errors at the gate BEFORE any
        // sentry_rootfs handling, so no real rootfs is needed here. Explicit
        // coverage guards against a future sentry-branch bypass of the gate.
        let dir = snap_dir(serde_json::json!({
            "backend": "sentry",
            "sentry_rootfs": "rootfs",
            "memory_mib": 256,
            "baked_by_version": "0.0.1",
        }));
        let err = Image::from_snapshot(&dir).expect_err("must refuse sentry version skew");
        let msg = format!("{err:?}");
        assert!(
            msg.contains("0.0.1") && msg.contains("Re-bake"),
            "sentry skew error must name the baked version and tell the user to rebake: {msg}"
        );
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn from_snapshot_allows_current_version_and_indeterminate() {
        for meta in [
            serde_json::json!({
                "backend": "kvm",
                "kvm_snapshot": "vm.snap",
                "baked_by_version": env!("CARGO_PKG_VERSION"),
            }),
            // No version info at all (bundles, custom kernels): permissive.
            serde_json::json!({
                "backend": "kvm",
                "kvm_snapshot": "vm.snap",
            }),
        ] {
            let dir = snap_dir(meta);
            match Image::from_snapshot(&dir) {
                Ok(_) => {}
                Err(e) => {
                    let msg = format!("{e:?}");
                    assert!(
                        !msg.contains("Re-bake"),
                        "version gate must not fire here: {msg}"
                    );
                }
            }
            let _ = std::fs::remove_dir_all(&dir);
        }
    }

    #[test]
    fn from_snapshot_loads_runsc_rootfs_metadata() {
        let dir = snap_dir(serde_json::json!({
            "backend": "runsc",
            "runsc_rootfs": "rootfs",
            "runsc_has_workload": true,
            "memory_mib": 384,
            "vcpus": 2,
            "image_env": {"PATH": "/usr/bin"},
            "cmd": ["/bin/sh", "-lc", "echo hi"],
            "runsc_agent_path": "/.supermachine/supermachine-agent",
            "runsc_agent_unix_env": "SUPERMACHINE_AGENT_UNIX_SOCKET",
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));
        std::fs::create_dir_all(dir.join("rootfs")).unwrap();
        let image = Image::from_snapshot(&dir).expect("runsc snapshot loads");
        assert!(image.is_runsc());
        assert!(!image.is_sentry());
        assert_eq!(image.memory_mib, 384);
        assert_eq!(image.vcpus, 2);
        let parts = image.runsc.as_ref().expect("runsc parts");
        assert!(parts.has_workload);
        assert_eq!(parts.cmd, ["/bin/sh", "-lc", "echo hi"]);
        assert_eq!(parts.agent_path, "/.supermachine/supermachine-agent");
        assert_eq!(parts.agent_unix_env, "SUPERMACHINE_AGENT_UNIX_SOCKET");
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn from_snapshot_loads_kboxlike_rootfs_metadata() {
        let volume_host = std::env::temp_dir().join(format!(
            "sm-kboxlike-metadata-volume-{}",
            std::process::id()
        ));
        let dir = snap_dir(serde_json::json!({
            "backend": "kboxlike",
            "kboxlike_rootfs": "rootfs",
            "kboxlike_has_workload": true,
            "memory_mib": 512,
            "vcpus": 3,
            "user": "1000:1001",
            "image_env": {"PATH": "/usr/local/bin"},
            "cmd": ["/bin/sh", "-lc", "echo kboxlike"],
            "volumes": [{"host_file": volume_host.to_string_lossy(), "guest_path": "/var/lib/data", "size_bytes": 12345}],
            "warm_daemon": false,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));
        std::fs::create_dir_all(dir.join("rootfs")).unwrap();
        let image = Image::from_snapshot(&dir).expect("kboxlike snapshot loads");
        assert!(image.is_kboxlike());
        assert!(!image.is_runsc());
        assert!(!image.is_sentry());
        assert_eq!(image.memory_mib, 512);
        assert_eq!(image.vcpus, 3);
        let parts = image.kboxlike.as_ref().expect("kboxlike parts");
        assert!(parts.has_workload);
        assert_eq!(parts.user, Some((1000, 1001)));
        assert_eq!(
            parts.image_env,
            [("PATH".to_string(), "/usr/local/bin".to_string())]
        );
        assert_eq!(parts.cmd, ["/bin/sh", "-lc", "echo kboxlike"]);
        assert_eq!(parts.volumes.len(), 1);
        assert_eq!(parts.volumes[0].0, volume_host);
        assert_eq!(parts.volumes[0].1, "/var/lib/data");
        assert_eq!(parts.volumes[0].2, 12345);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    #[ignore = "requires Linux root/chroot permissions and live ptrace restore"]
    fn kboxlike_vm_start_restores_host_root_sleep_smoke() {
        let workload_uid_path = format!(
            "/tmp/sm-kboxlike-workload-user-{}-{}",
            std::process::id(),
            1
        );
        let host_mount = std::env::temp_dir().join(format!(
            "sm-kboxlike-host-mount-{}-{}",
            std::process::id(),
            1
        ));
        std::fs::create_dir_all(&host_mount).unwrap();
        std::fs::write(host_mount.join("input.txt"), b"kboxlike-mounted\n").unwrap();
        let guest_mount = format!("/tmp/sm-kboxlike-guest-mount-{}-{}", std::process::id(), 1);
        let dir = snap_dir(serde_json::json!({
            "backend": "kboxlike",
            "kboxlike_rootfs": "/",
            "kboxlike_has_workload": false,
            "memory_mib": 64,
            "vcpus": 1,
            "user": "65534:65534",
            "image_env": {"PATH": "/usr/bin:/bin"},
            "cmd": ["/bin/sh", "-lc", format!("id -u > {workload_uid_path}; id -g >> {workload_uid_path}; sleep 30")],
            "mounts": [{"host_path": host_mount.to_string_lossy(), "guest_tag": "mnt", "guest_path": guest_mount}],
            "warm_daemon": false,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));
        let image = Image::from_snapshot(&dir).expect("kboxlike host-root snapshot loads");
        let vm = image
            .start(&super::VmConfig::new())
            .expect("kboxlike vm starts");
        let mut workload_user = Vec::new();
        for _ in 0..20 {
            if let Ok(bytes) = vm.read_file(&workload_uid_path) {
                workload_user = bytes;
                if String::from_utf8_lossy(&workload_user).contains("65534\n65534") {
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(100));
        }
        assert_eq!(
            String::from_utf8_lossy(&workload_user),
            "65534\n65534\n",
            "kboxlike workload did not run as metadata user"
        );
        let path = format!("/tmp/sm-kboxlike-file-{}-{}", std::process::id(), 1);
        vm.write_file(&path, b"kboxlike-file\n")
            .expect("kboxlike write_file");
        assert_eq!(
            vm.read_file(&path).expect("kboxlike read_file"),
            b"kboxlike-file\n"
        );
        let staged = format!("/tmp/sm-kboxlike-stage-{}-{}", std::process::id(), 1);
        let out = vm
            .exec_builder()
            .argv([
                "/bin/sh",
                "-lc",
                "id -u; id -g; pwd; cat \"$SM_FILE\"; cat \"$SM_STAGE\"; cat \"$SM_MOUNT/input.txt\"",
            ])
            .cwd("/tmp")
            .env("SM_FILE", &path)
            .env("SM_STAGE", &staged)
            .env("SM_MOUNT", &guest_mount)
            .stage_file(&staged, b"staged\n")
            .output()
            .expect("kboxlike exec output");
        assert!(out.success(), "kboxlike exec failed: {out:?}");
        assert_eq!(
            out.stdout,
            b"65534\n65534\n/tmp\nkboxlike-file\nstaged\nkboxlike-mounted\n"
        );
        let mut child = vm
            .exec_builder()
            .argv([
                "/bin/sh",
                "-lc",
                "cat; printf 'kboxlike-stream-err\\n' >&2; exit 7",
            ])
            .spawn()
            .expect("kboxlike streaming exec spawn");
        let mut stdin = child.stdin().expect("kboxlike streaming stdin");
        stdin
            .write_all(b"kboxlike-stream-out\n")
            .expect("kboxlike streaming stdin write");
        stdin.close().expect("kboxlike streaming stdin close");
        let mut stream_stdout = String::new();
        child
            .stdout()
            .expect("kboxlike streaming stdout")
            .read_to_string(&mut stream_stdout)
            .expect("kboxlike streaming stdout read");
        let mut stream_stderr = String::new();
        child
            .stderr()
            .expect("kboxlike streaming stderr")
            .read_to_string(&mut stream_stderr)
            .expect("kboxlike streaming stderr read");
        let status = child.wait().expect("kboxlike streaming wait");
        assert_eq!(status.code(), Some(7));
        assert_eq!(stream_stdout, "kboxlike-stream-out\n");
        assert_eq!(stream_stderr, "kboxlike-stream-err\n");
        vm.workload_signal(libc::SIGTERM)
            .expect("kboxlike workload_signal");
        vm.stop().expect("kboxlike vm stops");

        let pool = image
            .pool()
            .min(1)
            .max(1)
            .restore_on_release(false)
            .build()
            .expect("kboxlike pool builds");
        let pooled_path = format!("/tmp/sm-kboxlike-pooled-{}-{}", std::process::id(), 1);
        {
            let pooled = pool.acquire().expect("kboxlike pool acquire");
            let stats = pool.stats();
            assert_eq!(stats.alive, 1);
            assert_eq!(stats.in_use, 1);
            assert_eq!(stats.max, 1);
            let out = pooled
                .exec_builder()
                .argv(["/bin/sh", "-lc", "id -u; id -g; echo pooled > \"$P\""])
                .env("P", &pooled_path)
                .output()
                .expect("kboxlike pooled exec output");
            assert!(out.success(), "kboxlike pooled exec failed: {out:?}");
            assert_eq!(out.stdout, b"65534\n65534\n");
        }
        let stats = pool.stats();
        assert_eq!(stats.alive, 1);
        assert_eq!(stats.idle, 1);
        assert_eq!(
            std::fs::read_to_string(&pooled_path).unwrap_or_default(),
            "pooled\n"
        );
        pool.shutdown();

        let _ = std::fs::remove_file(path);
        let _ = std::fs::remove_file(staged);
        let _ = std::fs::remove_file(pooled_path);
        let _ = std::fs::remove_dir_all(host_mount);
        let _ = std::fs::remove_dir_all(guest_mount);
        let _ = std::fs::remove_file(workload_uid_path);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    #[ignore = "requires Linux root/chroot permissions and live ptrace restore"]
    fn kboxlike_vm_start_honors_runtime_virtiofs_mount() {
        let host_mount = std::env::temp_dir().join(format!(
            "sm-kboxlike-runtime-mount-{}-{}",
            std::process::id(),
            1
        ));
        std::fs::create_dir_all(&host_mount).unwrap();
        std::fs::write(host_mount.join("input.txt"), b"runtime-mounted\n").unwrap();
        let guest_mount = format!(
            "/tmp/sm-kboxlike-runtime-guest-mount-{}-{}",
            std::process::id(),
            1
        );
        let dir = snap_dir(serde_json::json!({
            "backend": "kboxlike",
            "kboxlike_rootfs": "/",
            "kboxlike_has_workload": false,
            "memory_mib": 64,
            "vcpus": 1,
            "image_env": {"PATH": "/usr/bin:/bin"},
            "cmd": ["/bin/sh", "-lc", "sleep 30"],
            "warm_daemon": false,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));
        let image = Image::from_snapshot(&dir).expect("kboxlike runtime-mount snapshot loads");
        let cfg = VmConfig::new().with_virtiofs(crate::vmm::resources::MountSpec::new(
            host_mount.to_string_lossy().to_string(),
            "runtime",
            guest_mount.clone(),
        ));
        let vm = image
            .start(&cfg)
            .expect("kboxlike vm starts with runtime mount");
        let out = vm
            .exec_builder()
            .argv(["/bin/sh", "-lc", "cat \"$SM_MOUNT/input.txt\""])
            .env("SM_MOUNT", &guest_mount)
            .timeout(std::time::Duration::from_secs(10))
            .output()
            .expect("kboxlike exec reads runtime mount");
        assert!(out.success(), "kboxlike runtime mount exec failed: {out:?}");
        assert_eq!(out.stdout, b"runtime-mounted\n");
        vm.stop().expect("kboxlike runtime-mount vm stops");

        let _ = std::fs::remove_dir_all(host_mount);
        let _ = std::fs::remove_dir_all(guest_mount);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    #[ignore = "requires Linux root/chroot permissions and live ptrace restore"]
    fn kboxlike_vm_start_honors_runtime_volume_mount() {
        let host_volume = std::env::temp_dir().join(format!(
            "sm-kboxlike-runtime-volume-{}-{}",
            std::process::id(),
            1
        ));
        let guest_volume = format!(
            "/tmp/sm-kboxlike-runtime-guest-volume-{}-{}",
            std::process::id(),
            1
        );
        let dir = snap_dir(serde_json::json!({
            "backend": "kboxlike",
            "kboxlike_rootfs": "/",
            "kboxlike_has_workload": false,
            "memory_mib": 64,
            "vcpus": 1,
            "image_env": {"PATH": "/usr/bin:/bin"},
            "cmd": ["/bin/sh", "-lc", "sleep 30"],
            "warm_daemon": false,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));
        let image = Image::from_snapshot(&dir).expect("kboxlike runtime-volume snapshot loads");
        let cfg = VmConfig::new().with_volume(crate::vmm::resources::VolumeSpec::new(
            host_volume.to_string_lossy().to_string(),
            guest_volume.clone(),
        ));
        let vm = image
            .start(&cfg)
            .expect("kboxlike vm starts with runtime volume");
        let out = vm
            .exec_builder()
            .argv([
                "/bin/sh",
                "-lc",
                "echo runtime-volume > \"$SM_VOLUME/marker\" && cat \"$SM_VOLUME/marker\"",
            ])
            .env("SM_VOLUME", &guest_volume)
            .timeout(std::time::Duration::from_secs(10))
            .output()
            .expect("kboxlike exec writes runtime volume");
        assert!(
            out.success(),
            "kboxlike runtime volume exec failed: {out:?}"
        );
        assert_eq!(out.stdout, b"runtime-volume\n");
        assert_eq!(
            std::fs::read_to_string(host_volume.join("marker")).unwrap_or_default(),
            "runtime-volume\n"
        );
        vm.stop().expect("kboxlike runtime-volume vm stops");

        let _ = std::fs::remove_dir_all(host_volume);
        let _ = std::fs::remove_dir_all(guest_volume);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    #[ignore = "requires Linux root/chroot permissions, OCI pull, and live ptrace restore"]
    fn kboxlike_bake_warmup_mount_exec_smoke() {
        let host_mount = std::env::temp_dir().join(format!(
            "sm-kboxlike-bake-host-mount-{}-{}",
            std::process::id(),
            1
        ));
        std::fs::create_dir_all(&host_mount).unwrap();
        std::fs::write(host_mount.join("input.txt"), b"mounted-from-host\n").unwrap();
        let dir = std::env::temp_dir().join(format!(
            "sm-kboxlike-bake-snap-{}-{}",
            std::process::id(),
            1
        ));
        let _ = std::fs::remove_dir_all(&dir);

        let mut cfg = SentryBakeConfig {
            memory_mib: Some(128),
            vcpus: Some(1),
            cmd: Some(vec![
                "/bin/sh".to_owned(),
                "-lc".to_owned(),
                "sleep 30".to_owned(),
            ]),
            warmup_tag: Some("kboxlike-bake-warmup-mount-smoke-v1".to_owned()),
            ..Default::default()
        };
        cfg.mounts.push((
            host_mount.clone(),
            "workspace".to_owned(),
            crate::vmm::resources::SymlinkPolicy::Opaque,
            "/workspace".to_owned(),
        ));
        cfg.warmup = Some(Box::new(|vm| {
            let out = vm
                .exec_builder()
                .argv([
                    "/bin/sh",
                    "-lc",
                    "cat /workspace/input.txt > /warmup-mounted.txt && echo warmed > /warmup-created.txt",
                ])
                .timeout(std::time::Duration::from_secs(30))
                .output()?;
            if !out.success() {
                return Err(Error::vm_msg(format!("warmup failed: {out:?}")));
            }
            Ok(())
        }));

        let image = Image::bake_kboxlike_with_config("alpine:3.20", &dir, cfg)
            .expect("kboxlike bake with warmup and mount");
        assert!(image.is_kboxlike());
        let vm = image
            .start(&VmConfig::new())
            .expect("kboxlike baked vm starts");
        let out = vm
            .exec_builder()
            .argv([
                "/bin/sh",
                "-lc",
                "cat /warmup-mounted.txt; cat /warmup-created.txt; cat /workspace/input.txt",
            ])
            .timeout(std::time::Duration::from_secs(30))
            .output()
            .expect("kboxlike baked exec");
        assert!(out.success(), "kboxlike baked exec failed: {out:?}");
        assert_eq!(
            out.stdout,
            b"mounted-from-host\nwarmed\nmounted-from-host\n"
        );
        vm.stop().expect("kboxlike baked vm stops");
        let _ = std::fs::remove_dir_all(host_mount);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    #[ignore = "requires Linux root/chroot permissions, live ptrace restore, and python3"]
    fn kboxlike_vm_start_exposes_restored_tcp_listener_smoke() {
        let python = if std::path::Path::new("/usr/bin/python3").is_file() {
            "/usr/bin/python3"
        } else {
            "/bin/python3"
        };
        assert!(
            std::path::Path::new(python).is_file(),
            "python3 binary not found at /usr/bin/python3 or /bin/python3"
        );
        let port = kboxlike_free_host_port();
        let server_code = format!(
            r#"import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", {port}))
s.listen(16)
body = b"kboxlike-http\n"
resp = b"HTTP/1.0 200 OK\r\nContent-Length: " + str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body
while True:
    conn, _addr = s.accept()
    try:
        conn.recv(4096)
        conn.sendall(resp)
    finally:
        conn.close()
"#
        );
        let dir = snap_dir(serde_json::json!({
            "backend": "kboxlike",
            "kboxlike_rootfs": "/",
            "kboxlike_has_workload": false,
            "memory_mib": 128,
            "vcpus": 1,
            "image_env": {"PATH": "/usr/bin:/bin"},
            "cmd": [
                python,
                "-u",
                "-c",
                server_code
            ],
            "warm_daemon": false,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));
        let cert = dir.join("cert.pem");
        let key = dir.join("key.pem");
        let openssl_ok = std::process::Command::new("openssl")
            .args([
                "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1",
            ])
            .arg("-keyout")
            .arg(&key)
            .arg("-out")
            .arg(&cert)
            .args(["-subj", "/CN=localhost"])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map(|s| s.success())
            .unwrap_or(false);
        let curl_ok = std::process::Command::new("curl")
            .arg("--version")
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map(|s| s.success())
            .unwrap_or(false);

        let result = (|| {
            let image = Image::from_snapshot(&dir).expect("kboxlike HTTP snapshot loads");
            let vm = image
                .start(&super::VmConfig::new())
                .expect("kboxlike HTTP vm starts");

            let direct_addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
            let direct = kboxlike_http_get_tcp_until(direct_addr, "/payload.txt")
                .expect("direct TCP reaches restored HTTP listener");
            eprintln!("KBOXLIVE_VM_START_HTTP_DIRECT ok=true");
            assert!(
                direct.contains("kboxlike-http"),
                "unexpected direct TCP response: {direct:?}"
            );

            let explicit = kboxlike_http_get_unix_until(|| vm.connect_port(port), "/payload.txt")
                .expect("connect_port reaches restored HTTP listener");
            eprintln!("KBOXLIVE_VM_START_HTTP_CONNECT_PORT ok=true");
            assert!(
                explicit.contains("kboxlike-http"),
                "unexpected connect_port response: {explicit:?}"
            );

            let first = kboxlike_http_get_unix_until(|| vm.connect(), "/payload.txt")
                .expect("connect reaches first restored HTTP listener");
            eprintln!("KBOXLIVE_VM_START_HTTP_CONNECT ok=true");
            assert!(
                first.contains("kboxlike-http"),
                "unexpected connect response: {first:?}"
            );

            let fwd = vm.expose_tcp(0, port).expect("kboxlike expose_tcp starts");
            let exposed = kboxlike_http_get_tcp_until(fwd.local_addr(), "/payload.txt")
                .expect("expose_tcp reaches restored HTTP listener");
            eprintln!("KBOXLIVE_VM_START_HTTP_EXPOSE_TCP ok=true");
            assert!(
                exposed.contains("kboxlike-http"),
                "unexpected expose_tcp response: {exposed:?}"
            );
            drop(fwd);

            if openssl_ok && curl_ok {
                let addr = vm
                    .expose_tls(crate::vmm::tls::TlsConfig {
                        listen_addr: "127.0.0.1:0".into(),
                        vm_port: Some(port as u32),
                        cert_path: cert.to_string_lossy().into_owned(),
                        key_path: key.to_string_lossy().into_owned(),
                    })
                    .expect("kboxlike expose_tls starts");
                let mut tls_body = String::new();
                for _ in 0..20 {
                    tls_body = std::process::Command::new("curl")
                        .args(["-sk", "--max-time", "8"])
                        .arg(format!("https://{addr}/payload.txt"))
                        .output()
                        .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
                        .unwrap_or_default();
                    if tls_body.contains("kboxlike-http") {
                        break;
                    }
                    std::thread::sleep(std::time::Duration::from_millis(300));
                }
                eprintln!("KBOXLIVE_VM_START_HTTP_EXPOSE_TLS ok=true");
                assert!(
                    tls_body.contains("kboxlike-http"),
                    "unexpected expose_tls response: {tls_body:?}"
                );
            } else {
                eprintln!(
                    "KBOXLIVE_VM_START_HTTP_EXPOSE_TLS skipped openssl_ok={openssl_ok} curl_ok={curl_ok}"
                );
            }

            vm.stop().expect("kboxlike HTTP vm stops");
        })();

        let _ = std::fs::remove_dir_all(&dir);
        result
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    #[test]
    #[ignore = "requires Linux root/chroot permissions, live ptrace restore, and Chromium"]
    fn kboxlike_vm_start_restores_host_root_chromium_devtools_smoke() {
        let chrome = std::env::var("SUPERMACHINE_KBOXLIVE_CHROME").unwrap_or_else(|_| {
            "/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome".to_owned()
        });
        assert!(
            std::path::Path::new(&chrome).is_file(),
            "Chromium binary not found at {chrome}; set SUPERMACHINE_KBOXLIVE_CHROME"
        );
        let profile_dir = std::env::temp_dir().join(format!(
            "kboxlike-vm-chromium-profile-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&profile_dir).unwrap();
        let user_data_dir = format!("--user-data-dir={}", profile_dir.display());
        let image_env = std::env::vars()
            .map(|(key, value)| (key, serde_json::json!(value)))
            .collect::<serde_json::Map<_, _>>();
        let dir = snap_dir(serde_json::json!({
            "backend": "kboxlike",
            "kboxlike_rootfs": "/",
            "kboxlike_has_workload": false,
            "memory_mib": 512,
            "vcpus": 2,
            "image_env": image_env,
            "cmd": [
                chrome,
                "--headless=new",
                "--disable-gpu",
                "--disable-dev-shm-usage",
                "--no-sandbox",
                "--remote-debugging-port=0",
                user_data_dir,
                "about:blank"
            ],
            "warm_daemon": false,
            "baked_by_version": env!("CARGO_PKG_VERSION"),
        }));

        let result = (|| {
            let image = Image::from_snapshot(&dir).expect("kboxlike Chromium snapshot loads");
            let vm = image
                .start(&super::VmConfig::new())
                .expect("kboxlike Chromium vm starts");
            let port = kboxlike_read_devtools_active_port_until(
                &profile_dir,
                std::time::Duration::from_secs(3),
            )
            .expect("restored Chromium DevTools active port");
            let status =
                kboxlike_probe_devtools_version_until(port, std::time::Duration::from_secs(3))
                    .expect("restored Chromium DevTools /json/version response");
            eprintln!("KBOXLIVE_VM_START_CHROMIUM_DEVTOOLS port={port} ok=true {status}");
            vm.stop().expect("kboxlike Chromium vm stops");
        })();

        let _ = std::fs::remove_dir_all(&profile_dir);
        let _ = std::fs::remove_dir_all(&dir);
        result
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_free_host_port() -> u16 {
        std::net::TcpListener::bind(("127.0.0.1", 0))
            .unwrap()
            .local_addr()
            .unwrap()
            .port()
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_http_get_unix_until<F>(mut connect: F, path: &str) -> std::io::Result<String>
    where
        F: FnMut() -> std::io::Result<std::os::unix::net::UnixStream>,
    {
        use std::io::Write as _;

        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
        loop {
            match connect() {
                Ok(mut stream) => {
                    stream.set_read_timeout(Some(std::time::Duration::from_secs(2)))?;
                    stream.write_all(
                        format!("GET {path} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").as_bytes(),
                    )?;
                    let _ = stream.shutdown(std::net::Shutdown::Write);
                    let response = kboxlike_read_http_response_chunks(&mut stream)?;
                    if response.contains("200 OK") && response.contains("kboxlike-http") {
                        return Ok(response);
                    }
                    if std::time::Instant::now() >= deadline {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            format!("HTTP response was not 200: {response:?}"),
                        ));
                    }
                }
                Err(err) if std::time::Instant::now() >= deadline => return Err(err),
                Err(_) => {}
            }
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_http_get_tcp_until(
        addr: std::net::SocketAddr,
        path: &str,
    ) -> std::io::Result<String> {
        use std::io::Write as _;

        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
        loop {
            match std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(200))
            {
                Ok(mut stream) => {
                    stream.set_read_timeout(Some(std::time::Duration::from_secs(2)))?;
                    stream.write_all(
                        format!("GET {path} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").as_bytes(),
                    )?;
                    let _ = stream.shutdown(std::net::Shutdown::Write);
                    let response = kboxlike_read_http_response_chunks(&mut stream)?;
                    if response.contains("200 OK") && response.contains("kboxlike-http") {
                        return Ok(response);
                    }
                    if std::time::Instant::now() >= deadline {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            format!("HTTP response was not 200: {response:?}"),
                        ));
                    }
                }
                Err(err) if std::time::Instant::now() >= deadline => return Err(err),
                Err(_) => {}
            }
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_read_http_response_chunks<R: std::io::Read>(
        reader: &mut R,
    ) -> std::io::Result<String> {
        let mut response = Vec::new();
        let mut chunk = [0u8; 1024];
        loop {
            match reader.read(&mut chunk) {
                Ok(0) => break,
                Ok(n) => {
                    response.extend_from_slice(&chunk[..n]);
                    if let Ok(text) = std::str::from_utf8(&response) {
                        if text.contains("200 OK") && text.contains("kboxlike-http") {
                            return Ok(text.to_owned());
                        }
                    }
                }
                Err(err)
                    if matches!(
                        err.kind(),
                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
                    ) =>
                {
                    break;
                }
                Err(err) => return Err(err),
            }
        }
        Ok(String::from_utf8_lossy(&response).into_owned())
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_read_devtools_active_port_until(
        profile_dir: &std::path::Path,
        timeout: std::time::Duration,
    ) -> std::io::Result<u16> {
        let path = profile_dir.join("DevToolsActivePort");
        let deadline = std::time::Instant::now() + timeout;
        loop {
            match std::fs::read_to_string(&path) {
                Ok(text) => {
                    if let Some(first) = text.lines().next() {
                        if let Ok(port) = first.trim().parse::<u16>() {
                            return Ok(port);
                        }
                    }
                }
                Err(err)
                    if err.kind() != std::io::ErrorKind::NotFound
                        && std::time::Instant::now() >= deadline =>
                {
                    return Err(err);
                }
                Err(_) => {}
            }
            if std::time::Instant::now() >= deadline {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::TimedOut,
                    format!("{} did not appear", path.display()),
                ));
            }
            std::thread::sleep(std::time::Duration::from_millis(25));
        }
    }

    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    fn kboxlike_probe_devtools_version_until(
        port: u16,
        timeout: std::time::Duration,
    ) -> std::io::Result<String> {
        use std::io::{Read as _, Write as _};

        let deadline = std::time::Instant::now() + timeout;
        let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
        loop {
            match std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(200))
            {
                Ok(mut stream) => {
                    stream.set_read_timeout(Some(std::time::Duration::from_millis(500)))?;
                    stream.write_all(b"GET /json/version HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")?;
                    let mut response = Vec::new();
                    let mut chunk = [0u8; 1024];
                    loop {
                        match stream.read(&mut chunk) {
                            Ok(0) => break,
                            Ok(n) => {
                                response.extend_from_slice(&chunk[..n]);
                                if let Ok(text) = std::str::from_utf8(&response) {
                                    if let Some(status) = text.lines().next() {
                                        if status.contains("200") {
                                            return Ok(status.to_owned());
                                        }
                                    }
                                }
                            }
                            Err(err)
                                if matches!(
                                    err.kind(),
                                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
                                ) =>
                            {
                                break;
                            }
                            Err(err) => return Err(err),
                        }
                    }
                    if let Ok(text) = std::str::from_utf8(&response) {
                        if let Some(status) = text.lines().next() {
                            if status.contains("200") {
                                return Ok(status.to_owned());
                            }
                        }
                    }
                }
                Err(err) if std::time::Instant::now() >= deadline => return Err(err),
                Err(_) => {}
            }
            if std::time::Instant::now() >= deadline {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::TimedOut,
                    format!("DevTools on 127.0.0.1:{port} did not answer"),
                ));
            }
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
    }
}

#[cfg(test)]
mod oci_image_builder_mount_tests {
    //! Coverage for the `OciImageBuilder` mount setters: every
    //! mount carries a `guest_path` (required field), policy is
    //! orthogonal, and multiple mounts compose in insertion order.
    use super::OciImageBuilder;
    use crate::vmm::resources::SymlinkPolicy;
    use std::path::PathBuf;

    #[test]
    fn with_mount_stores_all_four_fields() {
        let b = OciImageBuilder::new("alpine").with_mount("/host/x", "tag", "/workspace");
        assert_eq!(b.mounts.len(), 1);
        let (h, t, p, gp) = &b.mounts[0];
        assert_eq!(h, &PathBuf::from("/host/x"));
        assert_eq!(t, "tag");
        assert_eq!(p, &SymlinkPolicy::Opaque);
        assert_eq!(gp, "/workspace");
    }

    #[test]
    fn with_mount_symlinks_combines_policy_and_guest_path() {
        let b = OciImageBuilder::new("alpine").with_mount_symlinks(
            "/host/x",
            "ws",
            "/workspace",
            SymlinkPolicy::Follow,
        );
        let (h, t, p, gp) = &b.mounts[0];
        assert_eq!(h, &PathBuf::from("/host/x"));
        assert_eq!(t, "ws");
        assert_eq!(p, &SymlinkPolicy::Follow);
        assert_eq!(gp, "/workspace");
    }

    #[test]
    fn with_mount_symlinks_deny_policy_round_trips() {
        let b = OciImageBuilder::new("alpine").with_mount_symlinks(
            "/h",
            "t",
            "/g",
            SymlinkPolicy::Deny,
        );
        assert_eq!(b.mounts[0].2, SymlinkPolicy::Deny);
    }

    #[test]
    fn multiple_mounts_compose_independently() {
        // The auth-flow shape: one virtio-fs bind for source, one
        // for a sibling cache share, one with a non-default policy.
        let b = OciImageBuilder::new("alpine")
            .with_mount("/host/src", "workspace", "/workspace")
            .with_mount("/host/cache", "ro-cache", "/cache")
            .with_mount_symlinks("/host/strict", "strict", "/strict", SymlinkPolicy::Deny);
        assert_eq!(b.mounts.len(), 3);
        assert_eq!(b.mounts[0].3, "/workspace");
        assert_eq!(b.mounts[1].3, "/cache");
        assert_eq!(b.mounts[2].3, "/strict");
        assert_eq!(b.mounts[2].2, SymlinkPolicy::Deny);
    }

    #[test]
    fn mounts_preserve_insertion_order() {
        // Order matters for init-oci auto-mount sequencing: when
        // two binds nest (e.g. /a then /a/b), the parent must come
        // first or the child's mkdir + mount sequence misses.
        let b = OciImageBuilder::new("alpine")
            .with_mount("/h1", "first", "/a/b")
            .with_mount("/h2", "second", "/a")
            .with_mount("/h3", "third", "/a/c");
        let tags: Vec<&str> = b.mounts.iter().map(|m| m.1.as_str()).collect();
        assert_eq!(tags, vec!["first", "second", "third"]);
    }
}

#[cfg(test)]
mod map_bake_error_tests {
    use super::{map_bake_error, Error};

    #[test]
    fn decodes_kernel_panic_sentinel() {
        let msg = "KERNEL_PANIC|Kernel panic - not syncing: oops|frame_a\x1Fframe_b\x1Fframe_c"
            .to_string();
        let err = map_bake_error("nginx:alpine", msg);
        match err {
            Error::KernelPanic { first_line, stack } => {
                assert!(first_line.contains("Kernel panic"));
                assert_eq!(stack, vec!["frame_a", "frame_b", "frame_c"]);
            }
            other => panic!("expected KernelPanic, got {other:?}"),
        }
    }

    #[test]
    fn kernel_panic_with_empty_stack() {
        let msg = "KERNEL_PANIC|Internal error: Oops|".to_owned();
        match map_bake_error("img", msg) {
            Error::KernelPanic { first_line, stack } => {
                assert_eq!(first_line, "Internal error: Oops");
                // splitn(2) with empty-string second half yields one empty
                // stack frame; consumer sees a single empty line which is
                // fine — `Display` skips printing meaningfully.
                assert!(stack.len() <= 1);
            }
            other => panic!("expected KernelPanic, got {other:?}"),
        }
    }

    #[test]
    fn non_panic_messages_unchanged() {
        let err = map_bake_error(
            "img",
            "supermachine snapshot timeout; see bake.log".to_owned(),
        );
        match err {
            Error::Bake { msg, .. } => assert!(msg.contains("timeout")),
            other => panic!("expected Bake, got {other:?}"),
        }
    }
}

#[cfg(test)]
mod base64_tests {
    //! The `wire` base64 codec carries read_file payloads as
    //! agent-controlled `data_b64`. Decoding must round-trip what we
    //! encode and REJECT malformed input rather than emit spurious bytes.
    use super::{b64_decode, b64_encode};
    use proptest::prelude::*;

    #[test]
    fn known_vectors_round_trip() {
        // RFC 4648 §10 test vectors.
        let cases: &[(&[u8], &str)] = &[
            (b"", ""),
            (b"f", "Zg=="),
            (b"fo", "Zm8="),
            (b"foo", "Zm9v"),
            (b"foob", "Zm9vYg=="),
            (b"fooba", "Zm9vYmE="),
            (b"foobar", "Zm9vYmFy"),
        ];
        for (raw, enc) in cases {
            assert_eq!(b64_encode(raw), *enc, "encode {raw:?}");
            assert_eq!(b64_decode(enc).unwrap(), *raw, "decode {enc:?}");
        }
    }

    #[test]
    fn decode_rejects_malformed_padding() {
        // The bug this fix closes: misplaced / excess padding must error,
        // not decode to spurious bytes.
        for bad in ["====", "A===", "=AAA", "AA==AAAA", "Zm==Zm9v", "=Zm9"] {
            assert!(
                b64_decode(bad).is_err(),
                "{bad:?} must be rejected as malformed padding"
            );
        }
    }

    #[test]
    fn decode_rejects_bad_length_and_chars() {
        assert!(b64_decode("ABC").is_err(), "length not a multiple of 4");
        assert!(b64_decode("Zm9").is_err());
        assert!(b64_decode("Zm$v").is_err(), "invalid character");
        assert!(b64_decode("Zm9*").is_err());
    }

    #[test]
    fn decode_tolerates_whitespace() {
        // The codec strips ASCII whitespace before decoding (wrapped
        // base64 from agents / PEM-style line breaks).
        assert_eq!(b64_decode("Zm9v YmFy").unwrap(), b"foobar");
        assert_eq!(b64_decode("Zm9v\nYmFy\n").unwrap(), b"foobar");
    }

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(1024))]

        /// decode(encode(x)) == x for ALL byte strings, and the encoding
        /// is always a clean multiple of 4 with no interior padding.
        #[test]
        fn round_trips_for_all_bytes(data in proptest::collection::vec(any::<u8>(), 0..512)) {
            let enc = b64_encode(&data);
            prop_assert_eq!(enc.len() % 4, 0);
            prop_assert_eq!(b64_decode(&enc).unwrap(), data);
        }
    }
}
