//! Sandbox backends + a PTY-backed sandbox the broker drives. //! //! The broker (owner's client) spawns a sandbox shell inside a PTY. Output bytes //! are pumped out of a reader thread onto an mpsc channel; the broker encrypts //! them with the room key and relays them to the clergy as `sbx pty_data` frames. //! Input frames (`sbx pty_input`) are written back into the PTY. The server only //! ever sees ciphertext — identical trust model to chat/file transfer. use anyhow::{Context, Result}; use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize}; use std::io::{Read, Write}; use std::process::{Command, Stdio}; use std::sync::mpsc; /// Helper that ensures the Docker daemon is running (ships in hh/scripts/). const ENSURE_DOCKER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-docker.sh"); /// Detect-first Multipass installer (ships in hh/scripts/). const ENSURE_MULTIPASS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-multipass.sh"); /// Detect-first Podman installer (apt; rootless preflight). Ships in hh/scripts/. const ENSURE_PODMAN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-podman.sh"); /// Detect-first VirtualBox installer (ships in hh/scripts/). const ENSURE_VBOX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/ensure-vbox.sh"); /// Baseline dev-toolchain installer run inside Docker sandboxes (hh/scripts/). const SBX_BOOTSTRAP: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/sandbox-bootstrap.sh"); /// Editable package schema the bootstrap installs — vim+curl always added. const SBX_TOOLS_JSON: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/sandbox-tools.json"); /// Creates a fresh, cloud-init-provisioned Ubuntu VirtualBox VM (hh/scripts/). const VBOX_NEW: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/vbox-new.sh"); /// Browse + locally-build VMs from the curated library (hh/scripts/). const VBOX_LIBRARY_SH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/vbox-library.sh"); /// The library manifest: pointers (URLs/pages) to VMs, never the images. The /// loader cross-references `list_vms` to mark which are already built locally. const VBOX_LIBRARY_JSON: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/vbox-library.json"); /// Is the `docker` binary installed? (`docker --version` succeeds.) This is a /// weaker check than `docker_daemon_up`: the CLI can be present while the daemon /// is down. We need both before a Docker sandbox can launch. pub fn docker_installed() -> bool { Command::new("docker") .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Is the Docker daemon accepting connections? (`docker info` succeeds.) pub fn docker_daemon_up() -> bool { Command::new("docker") .arg("info") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Is the `podman` binary installed? (`podman --version` succeeds.) Podman is /// daemonless, so unlike Docker there is no separate daemon-up check: if the CLI /// is present a rootless container can launch immediately (no daemon, no sudo). pub fn podman_installed() -> bool { Command::new("podman") .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Is this a Docker Desktop (Linux) install? Its engine runs in a per-user VM /// started by the *user* unit `docker-desktop.service` — there's no root /// `docker.service`, so starting the daemon needs **no sudo**. Detect it so the /// launch path doesn't pop a (useless, and on this box failing) sudo prompt. pub fn docker_desktop() -> bool { Command::new("systemctl") .args(["--user", "cat", "docker-desktop.service"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Can we sudo *without* a password prompt right now? (`sudo -n true` succeeds /// when credentials are cached via a prior `sudo -v`, or NOPASSWD is configured.) /// The launch paths that need root check this first: a raw-mode TUI can't host /// sudo's interactive tty prompt, so we must never let sudo block on one. pub fn sudo_ready() -> bool { Command::new("sudo") .args(["-n", "true"]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Run an `ensure-*.sh` installer `--yes` with the given extra args, optionally /// feeding a sudo password to the script's `sudo -S` via stdin. Shared by every /// backend installer (docker/podman/multipass/vbox) so sudo capture is uniform. /// /// Sudo ladder (the scripts honour all three): /// * password present ⇒ `--stdin-pass` ⇒ the script uses `sudo -S -p ''`, /// reading the secret from stdin — never the controlling tty (a raw-mode TUI /// would corrupt a tty prompt). The first sudo caches the credential. /// * no password ⇒ stdin closed; the script's `--yes` selects `sudo -n`, which /// fails fast (clear error) if creds aren't cached rather than hanging on a /// tty prompt the TUI can't host. /// /// Secret handling: the password (if any) only ever travels parent→child stdin; /// the local buffer is wiped immediately after. It is NEVER echoed, logged, or /// surfaced — sudo never prints the password, so the captured stderr (used for /// error messages) can't contain it. fn run_ensure(script: &str, extra: &[&str], password: Option) -> Result<(bool, String)> { let mut cmd = Command::new("bash"); cmd.arg(script).arg("--yes"); for a in extra { cmd.arg(a); } if password.is_some() { cmd.arg("--stdin-pass").stdin(Stdio::piped()); } else { cmd.stdin(Stdio::null()); } cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd .spawn() .with_context(|| format!("running {script}"))?; if let Some(mut pw) = password { if let Some(mut stdin) = child.stdin.take() { // Feed the password to the first `sudo -S`; it caches the credential // so the rest of the plan authenticates without re-reading stdin. let _ = writeln!(stdin, "{pw}"); // stdin drops here → EOF } // Best-effort wipe of our copy of the secret. unsafe { for b in pw.as_bytes_mut() { *b = 0; } } pw.clear(); } let out = child .wait_with_output() .with_context(|| format!("{script}"))?; Ok((out.status.success(), String::from_utf8_lossy(&out.stderr).into_owned())) } /// Start the Docker daemon via `ensure-docker.sh --yes`, waiting until it's /// ready. With `password`, escalation goes through `sudo -S` (read from stdin); /// without it the script uses `sudo -n` and fails fast if creds aren't cached. fn start_docker_daemon(password: Option) -> Result<()> { let (ok, err) = run_ensure(ENSURE_DOCKER, &[], password)?; if !ok { let last = err .lines() .last() .unwrap_or("could not start the docker daemon"); anyhow::bail!("{last}"); } Ok(()) } /// Install Docker via `ensure-docker.sh --install --yes` (Docker's official, /// GPG-verified repo), then leave the daemon started. Consent is the caller's /// job (they passed `install`); the script is idempotent if Docker is present. /// `password` feeds `sudo -S` as above. pub fn ensure_docker_install(password: Option) -> Result<()> { let (ok, err) = run_ensure(ENSURE_DOCKER, &["--install"], password)?; if !ok { let last = err.lines().last().unwrap_or("could not install Docker"); anyhow::bail!("{last}"); } Ok(()) } /// Install Podman via `ensure-podman.sh --yes` (apt + a rootless subuid/subgid /// preflight). Consent is the caller's job (they passed `install`); the script is /// idempotent if Podman is already present. Daemonless — nothing to start after. /// `password` feeds the script's `sudo -S` (apt needs root); without it the /// script's `sudo -n` fails fast rather than hanging. Returns the last error line. pub fn ensure_podman_install(password: Option) -> Result<()> { let (ok, err) = run_ensure(ENSURE_PODMAN, &[], password)?; if !ok { let last = err.lines().last().unwrap_or("could not install Podman"); anyhow::bail!("{last}"); } Ok(()) } /// Is Multipass installed? (`multipass version` succeeds.) pub fn multipass_installed() -> bool { Command::new("multipass") .arg("version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Install Multipass via `ensure-multipass.sh --yes`. On Linux this is a snap /// install (the only supported channel). Consent is the caller's job; the /// script is idempotent if Multipass is already present. Returns the script's /// last error line on failure (e.g. snapd missing, or needs sudo). pub fn ensure_multipass_install(password: Option) -> Result<()> { let (ok, err) = run_ensure(ENSURE_MULTIPASS, &[], password)?; if !ok { let last = err.lines().last().unwrap_or("could not install Multipass"); anyhow::bail!("{last}"); } Ok(()) } // ---- VirtualBox (local GUI VMs) --------------------------------------------- // VirtualBox is integrated as a *local* facility rather than a shared-PTY // backend: a room shares a VM by handing out its appliance, and each member // boots it in the real VirtualBox GUI on their own machine. None of this relays // over the room — only the (separately `/send`-ed) image does — so the // zero-knowledge model is untouched. A Windows guest has no sshd/guestcontrol // shell to drive, so the GUI launch is the honest fit, not a faked PTY. /// Is VirtualBox installed? (`VBoxManage --version` succeeds.) pub fn vbox_installed() -> bool { Command::new("VBoxManage") .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// VirtualBox version string (e.g. `7.1.2r164945`), or None if not installed. pub fn vbox_version() -> Option { let out = Command::new("VBoxManage").arg("--version").output().ok()?; let v = String::from_utf8_lossy(&out.stdout).trim().to_string(); (out.status.success() && !v.is_empty()).then_some(v) } /// Install VirtualBox via `ensure-vbox.sh --yes`. Consent is the caller's job /// (they passed `--install`); detection is the script's (idempotent if present). /// `password` feeds the script's `sudo -S` (apt needs root); without it the /// script's `sudo -n` fails fast rather than hanging on a tty prompt the TUI /// can't host. Returns the script's last error line on failure. pub fn ensure_vbox_install(password: Option) -> Result<()> { let (ok, err) = run_ensure(ENSURE_VBOX, &[], password)?; if !ok { let last = err.lines().last().unwrap_or("could not install VirtualBox"); anyhow::bail!("{last}"); } Ok(()) } /// Names of registered VirtualBox VMs (`VBoxManage list vms`). Each line is /// `"name" {uuid}`; we return the unquoted names. pub fn list_vms() -> Result> { let out = Command::new("VBoxManage") .args(["list", "vms"]) .output() .context("VBoxManage list vms (is VirtualBox installed?)")?; Ok(String::from_utf8_lossy(&out.stdout) .lines() .filter_map(|l| { let start = l.find('"')? + 1; let end = l[start..].find('"')? + start; Some(l[start..end].to_string()) }) .filter(|s| !s.is_empty()) .collect()) } /// Is a VM with this name registered locally? (i.e. present in `list_vms`). /// Used to tell a "host" (already has the appliance imported) from someone who /// still needs it pulled in before `gui_launch` can find it. pub fn vm_registered(name: &str) -> bool { list_vms() .map(|vms| vms.iter().any(|v| v == name)) .unwrap_or(false) } /// Import a VirtualBox appliance (`.ova`/`.ovf`) so its VM registers locally and /// becomes launchable via `gui_launch`. This is how a non-host "pulls" a shared /// VM onto their own machine after the appliance arrives over the encrypted /// channel. Blocking — run off the UI thread. Returns the imported VM's name. pub fn import_appliance(ova: &std::path::Path) -> Result { let before = list_vms().unwrap_or_default(); let out = Command::new("VBoxManage") .arg("import") .arg(ova) .output() .context("VBoxManage import (is VirtualBox installed?)")?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); anyhow::bail!( "VBoxManage import failed: {}", err.lines().last().unwrap_or("").trim() ); } // The imported name is whatever's newly registered; fall back to the file's // stem if the diff is ambiguous (e.g. a re-import of an existing name). let after = list_vms().unwrap_or_default(); let added = after.into_iter().find(|v| !before.contains(v)); Ok(added.unwrap_or_else(|| { ova.file_stem() .and_then(|s| s.to_str()) .unwrap_or("imported-vm") .to_string() })) } /// Is a VM currently running? (`VBoxManage list runningvms`) pub fn vm_running(name: &str) -> bool { Command::new("VBoxManage") .args(["list", "runningvms"]) .output() .map(|o| { let needle = format!("\"{name}\""); String::from_utf8_lossy(&o.stdout) .lines() .any(|l| l.contains(&needle)) }) .unwrap_or(false) } /// Launch a registered VM's GUI locally (`VBoxManage startvm --type gui`). /// The window opens on the caller's own desktop — this is the "share a VM, run /// it locally" path; nothing about the display is relayed to the room. pub fn gui_launch(name: &str) -> Result { if vm_running(name) { return Ok(format!("{name} is already running")); } let out = Command::new("VBoxManage") .args(["startvm", name, "--type", "gui"]) .output() .context("VBoxManage startvm (is VirtualBox installed?)")?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); anyhow::bail!( "startvm failed: {}", err.lines().last().unwrap_or("").trim() ); } Ok(format!("launched {name} (GUI)")) } /// Create + boot a *fresh* Ubuntu VirtualBox VM pre-provisioned with the dev /// toolchain, by driving `scripts/vbox-new.sh` (downloads a cloud image, builds /// a cloud-init NoCloud seed installing scripts/sandbox-tools.json, then boots /// the GUI). Unlike `/sbx launch vbox `, which opens an *existing* image, /// this builds one from scratch — the only path that doesn't need a pre-made VM. /// /// Blocking and slow on first run (~600MB image download). Runs the script /// non-interactively (`--yes`); returns its final status line(s) — including the /// generated login password, which is shown exactly once. pub fn vbox_new(name: &str, user: &str) -> Result { let out = Command::new("bash") .arg(VBOX_NEW) .args(["--yes", "--name", name, "--user", user]) .output() .context("running vbox-new.sh (is bash available?)")?; let stderr = String::from_utf8_lossy(&out.stderr); if !out.status.success() { anyhow::bail!( "vbox-new failed: {}", stderr.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("").trim() ); } // The script narrates progress on stderr. Surface the final success line and, // if one was generated, the one-time login password. let last = stderr .lines() .rev() .find(|l| l.trim_start().starts_with('✓')) .unwrap_or("VM created") .trim(); let pw = stderr.lines().find(|l| l.contains("login password")); Ok(match pw { Some(p) => format!("{last} · {}", p.trim()), None => last.to_string(), }) } /// Names of VMs that are currently running (`VBoxManage list runningvms`), so the /// `/sbx vms` listing can flag which of the registered VMs are live. pub fn running_vms() -> Vec { Command::new("VBoxManage") .args(["list", "runningvms"]) .output() .map(|o| { String::from_utf8_lossy(&o.stdout) .lines() .filter_map(|l| { let start = l.find('"')? + 1; let end = l[start..].find('"')? + start; Some(l[start..end].to_string()) }) .filter(|s| !s.is_empty()) .collect() }) .unwrap_or_default() } // ---- VM library (curated pointers; built locally on demand) ----------------- // // The repo ships scripts/vbox-library.json — POINTERS only (download URLs/pages), // never the multi-GB images. Choosing a VM builds it on the caller's own machine // via scripts/vbox-library.sh. Here we parse the manifest for listing/info; the // heavy build is shelled out to the script (which already handles download + // VBoxManage create/import + boot, with rollback). /// One curated VM as declared in the manifest. Only the fields we surface in the /// TUI are deserialized; the rest of the JSON (specs, firmware, etc.) is consumed /// by the build script. #[derive(Clone, Debug, serde::Deserialize)] pub struct LibraryVm { pub id: String, pub name: String, pub os: String, pub size: String, #[serde(default)] pub page: String, #[serde(default)] pub notes: String, /// Filled in by `vbox_library()` (not in the JSON): is a VM of this name /// already registered in VirtualBox on this machine? #[serde(skip)] pub installed: bool, } #[derive(serde::Deserialize)] struct LibraryManifest { vms: Vec, } /// Parse the VM library manifest, marking each entry `installed` if a VM of that /// display name is already registered locally (`list_vms`). The catalog is the /// same for everyone; `installed` is per-machine, so this works identically for a /// host or any room member — VirtualBox VMs are always local to the caller. pub fn vbox_library() -> Result> { let text = std::fs::read_to_string(VBOX_LIBRARY_JSON) .with_context(|| format!("reading VM library manifest ({VBOX_LIBRARY_JSON})"))?; let manifest: LibraryManifest = serde_json::from_str(&text).context("parsing vbox-library.json")?; let have = list_vms().unwrap_or_default(); Ok(manifest .vms .into_iter() .map(|mut v| { v.installed = have.iter().any(|n| n == &v.name); v }) .collect()) } /// Look up a single library entry by its id (with `installed` resolved). pub fn library_vm(id: &str) -> Option { vbox_library().ok()?.into_iter().find(|v| v.id == id) } /// Build a library VM locally by driving scripts/vbox-library.sh `--install`. /// Blocking and potentially slow (large download); run off the UI thread. No /// sudo is needed — VBoxManage create/import run as the user. `iso` supplies a /// locally-downloaded installer for entries with no auto-download (e.g. Windows). /// Returns the script's final status line(s). pub fn vbox_library_install(id: &str, iso: Option) -> Result { let mut cmd = Command::new("bash"); cmd.arg(VBOX_LIBRARY_SH).args(["--install", id, "--yes"]); if let Some(path) = iso.as_deref() { cmd.args(["--iso", path]); } let out = cmd .output() .context("running vbox-library.sh (is bash available?)")?; let stderr = String::from_utf8_lossy(&out.stderr); if !out.status.success() { // The script narrates on stderr and ends with a ✖ reason / pointer. let reason = stderr .lines() .rev() .find(|l| !l.trim().is_empty()) .unwrap_or("build failed") .trim(); anyhow::bail!("{reason}"); } // Surface the final ✓/ⓘ status line. let last = stderr .lines() .rev() .find(|l| { let t = l.trim_start(); t.starts_with('✓') || t.starts_with('ⓘ') }) .unwrap_or("done") .trim(); Ok(last.to_string()) } /// A running hypervisor that holds the CPU's VT-x/VMX root mode. While one is /// live, VirtualBox can't boot a *hardware* VM (it aborts with /// `VERR_VMX_IN_VMX_ROOT_MODE`). We only mark holders we know how to stop /// cleanly (`stoppable`); an unrecognised qemu/kvm process is reported but never /// killed — the caller aborts and lets the user deal with it. #[derive(Clone, Debug)] pub struct VtxHolder { /// Human label, e.g. "Docker Desktop" or "multipass VM 'molt-mania-vm'". pub label: String, /// "docker" | "multipass" | "unknown" — selects the stop strategy. pub kind: String, /// Instance name to `multipass stop`; empty for non-multipass holders. pub instance: String, /// Whether we know a clean, reversible way to stop it (restartable after). pub stoppable: bool, } /// Is a KVM kernel module loaded? (Cheap early-out before scanning processes.) fn kvm_loaded() -> bool { std::fs::read_to_string("/proc/modules") .map(|s| { s.lines() .any(|l| l.starts_with("kvm_intel") || l.starts_with("kvm_amd")) }) .unwrap_or(false) } /// Detect running KVM-accelerated hypervisors that hold VT-x and would block a /// VirtualBox hardware VM. Pure detection — stops nothing. Empty vec = clear. pub fn vtx_holders() -> Vec { if !kvm_loaded() { return Vec::new(); } let out = match Command::new("ps").args(["-eo", "args="]).output() { Ok(o) => o, Err(_) => return Vec::new(), }; classify_vtx_holders(&String::from_utf8_lossy(&out.stdout)) } /// Pure classifier: turn `ps -eo args=` output into the VT-x holders we know how /// to (or refuse to) stop. Split out from `vtx_holders` so it can be unit-tested /// without a live process table. Only KVM-accelerated qemu lines count; Docker /// Desktop collapses to a single holder; multipass is named; anything else is an /// unknown, non-stoppable holder (we won't kill what we can't cleanly restart). fn classify_vtx_holders(text: &str) -> Vec { let mut holders = Vec::new(); let mut docker_seen = false; for args in text.lines() { let is_qemu = args.contains("qemu-system"); let uses_kvm = args.contains("--enable-kvm") || args.contains("-accel kvm") || args.contains("accel=kvm"); if !(is_qemu && uses_kvm) { continue; } if args.contains("docker-desktop") || args.contains("/.docker/") { // Docker Desktop runs one linuxkit VM; collapse to a single holder. if !docker_seen { docker_seen = true; holders.push(VtxHolder { label: "Docker Desktop".into(), kind: "docker".into(), instance: String::new(), stoppable: true, }); } } else if let Some(name) = multipass_instance(args) { holders.push(VtxHolder { label: format!("multipass VM '{name}'"), kind: "multipass".into(), instance: name, stoppable: true, }); } else { holders.push(VtxHolder { label: "an unknown KVM/QEMU virtual machine".into(), kind: "unknown".into(), instance: String::new(), stoppable: false, }); } } holders } /// Pull a multipass instance name out of a qemu command line. Multipass keeps /// each disk under `.../multipassd/vault/instances//...`. fn multipass_instance(args: &str) -> Option { if !args.contains("multipass") { return None; } let marker = "/instances/"; let i = args.find(marker)? + marker.len(); let rest = &args[i..]; let end = rest.find('/').unwrap_or(rest.len()); let name = &rest[..end]; (!name.is_empty()).then(|| name.to_string()) } /// Stop one VT-x holder with a clean, *reversible* command so it can be /// restarted later. Refuses to touch holders marked `stoppable=false`. pub fn stop_vtx_holder(h: &VtxHolder) -> Result<()> { match h.kind.as_str() { "docker" => { let out = Command::new("systemctl") .args(["--user", "stop", "docker-desktop"]) .output() .context("stopping Docker Desktop")?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); anyhow::bail!( "could not stop Docker Desktop: {}", err.lines().last().unwrap_or("").trim() ); } Ok(()) } "multipass" => { let out = Command::new("multipass") .args(["stop", &h.instance]) .output() .context("stopping multipass VM")?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); anyhow::bail!( "could not stop multipass '{}': {}", h.instance, err.lines().last().unwrap_or("").trim() ); } Ok(()) } _ => anyhow::bail!("refusing to stop {} automatically", h.label), } } /// Which sandbox to summon. Multipass = strong isolation (default for real use), /// Podman = daemonless/rootless containers (no sudo), Docker = fast, Local = no /// isolation (dev/testing only). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Backend { Local, Docker, Podman, Multipass, } impl Backend { pub fn parse(s: &str) -> Option { match s { "local" => Some(Backend::Local), "docker" => Some(Backend::Docker), "podman" => Some(Backend::Podman), "multipass" => Some(Backend::Multipass), _ => None, } } pub fn label(self) -> &'static str { match self { Backend::Local => "local-shell", Backend::Docker => "docker", Backend::Podman => "podman", Backend::Multipass => "multipass", } } /// Default image/release when the user doesn't specify one. pub fn default_image(self) -> &'static str { match self { Backend::Multipass => "24.04", // Docker defaults to Parrot OS (Security). Debian/apt-based, so the // apt-only sandbox-bootstrap.sh path works unchanged. `parrotsec/core` // is the minimal base (bootstrap fills the toolchain); swap for // `parrotsec/security` per-launch if you want the full pentest set. Backend::Docker => "parrotsec/core", // Podman defaults to Kali (rolling). Still Debian/apt-based, so the // apt-only sandbox-bootstrap.sh path works unchanged; base is minimal // (no pentest metapackages pulled by default — keep first launch fast). Backend::Podman => "kalilinux/kali-rolling", Backend::Local => "", } } /// The exec family a co-located agent uses to run a command *inside* this /// sandbox — `docker`/`podman exec`, `multipass exec`, or host `local`. /// Advertised in the `_sbx:status` frame (distinct from `label`, whose Local /// value is the cosmetic "local-shell") so the bridge can build the right /// ` exec` invocation for the backend that holds the sandbox. pub fn engine(self) -> &'static str { match self { Backend::Local => "local", Backend::Docker => "docker", Backend::Podman => "podman", Backend::Multipass => "multipass", } } } /// The container-engine binary for an OCI backend. Docker and Podman share the /// same CLI grammar (`run/exec/commit/save/rm/images`), so the container code /// path is written once and parameterised by this. Non-container backends have /// no engine binary; calling this on them is a logic error (they never reach the /// container arms), so we default to "docker" rather than panic. fn engine_bin(backend: Backend) -> &'static str { match backend { Backend::Podman => "podman", _ => "docker", } } /// One-time setup before the PTY shell is spawned. Blocking — run off the UI /// thread (Multipass boots a real VM, ~20-30s). Idempotent: reuses an instance /// that already exists. pub fn prepare( backend: Backend, name: &str, image: &str, start_daemon: bool, password: Option, ) -> Result<()> { match backend { Backend::Local => Ok(()), Backend::Multipass => { let exists = Command::new("multipass") .args(["info", name]) .output() .map(|o| o.status.success()) .unwrap_or(false); if !exists { // Capture output so it can't bleed onto the TUI surface; surface // the failure reason through the returned error instead. let out = Command::new("multipass") .args([ "launch", "--name", name, "--cpus", "1", "--memory", "1G", "--disk", "5G", image, ]) .output() .context("multipass launch (is multipass installed?)")?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); anyhow::bail!( "multipass launch failed: {}", err.lines().last().unwrap_or("").trim() ); } } else { let _ = Command::new("multipass") .args(["start", name]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); } Ok(()) } Backend::Docker | Backend::Podman => { let engine = engine_bin(backend); // Docker needs a running daemon before any call; rather than fail with // a raw connection error, start it (the caller confirmed via // `/sbx launch docker --start`). Podman is daemonless — skip the check // and the sudo modal entirely; a rootless container launches as-is. if backend == Backend::Docker && !docker_daemon_up() { if start_daemon { start_docker_daemon(password).context("starting docker daemon")?; } else { anyhow::bail!( "docker daemon is not running — retry with `/sbx launch docker --start`" ); } } // Persistent container so we can exec in to provision users + shells. let _ = Command::new(engine) .args(["rm", "-f", name]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); // Capture output so a failure can't paint over the TUI; the reason is // surfaced through the returned error (shown in the error popup). let mut run = Command::new(engine); run.args(["run", "-d", "--name", name, "--hostname", name, "-w", "/root"]); // The native harness runs the model host-side and only execs commands // into the container, so the sandbox no longer needs to reach host // Ollama. The old in-container Ollama gateway (Docker host-gateway / // Podman slirp4netns host-loopback) is therefore gone — and with it the // rootless-Podman loopback bug it used to work around. run.args([image, "sleep", "infinity"]); let out = run .output() .with_context(|| format!("{engine} run (is {engine} installed?)"))?; if !out.status.success() { let err = String::from_utf8_lossy(&out.stderr); anyhow::bail!("{engine} run failed: {}", err.lines().last().unwrap_or("").trim()); } Ok(()) } } } /// Destroy ephemeral resources after stop. Multipass instance is purged; /// the Docker container is removed; Local is a no-op. pub fn teardown(backend: Backend, name: &str) { match backend { Backend::Multipass => { // Multipass snapshots live *inside* the instance, so `delete --purge` // would destroy any saved state. If the user has snapshots worth // keeping (so `/sbx load