feat(sbx): VirtualBox detect-first install + local GUI VM launch

Integrate VirtualBox as a local facility rather than a shared-PTY backend: a
Windows guest has no shell to relay, so the honest fit is launching the VM's
GUI on the caller's own machine (the "share a VM, run it locally" path) — no
display is relayed to the room, so zero-knowledge is untouched.

- ensure-vbox.sh: detect-first installer mirroring ensure-docker.sh; --check,
  --plan (real apt --simulate download plan, no changes), --yes; apt/dnf/
  pacman/brew/winget; Secure Boot MOK warning. HH_VBOX_FORCE_MISSING lets a
  demo exercise the missing->install path without uninstalling.
- sbx.rs: vbox_installed/vbox_version/list_vms/vm_running/gui_launch +
  ensure_vbox_install.
- app.rs: /sbx vms (detect + list) and /sbx gui <vm> [--install] (detect-first
  then startvm --type gui); /sbx launch virtualbox steers to /sbx gui.
- ui.rs help: /sbx vms and /sbx gui entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-03 10:41:32 -07:00
parent 60168a4341
commit 7519df1695
4 changed files with 299 additions and 2 deletions
+68 -2
View File
@@ -1191,6 +1191,9 @@ fn handle_command(
let start_daemon = args
.iter()
.any(|a| matches!(*a, "--start" | "--start-daemon" | "-y"));
// VirtualBox isn't a shared-PTY backend — a GUI VM (a Windows
// guest especially) has no shell to relay. Steer to /sbx gui.
let wants_vbox = args.iter().any(|a| matches!(*a, "virtualbox" | "vbox"));
let mut pos = args.iter().copied().filter(|a| !a.starts_with('-'));
let backend = pos
.next()
@@ -1201,7 +1204,11 @@ fn handle_command(
.map(str::to_string)
.unwrap_or_else(|| backend.default_image().to_string());
if backend == sbx::Backend::Docker && !start_daemon && !sbx::docker_daemon_up()
if wants_vbox {
app.sys("VirtualBox VMs run locally in their own GUI — use `/sbx gui <vm>` (list them with `/sbx vms`)");
} else if backend == sbx::Backend::Docker
&& !start_daemon
&& !sbx::docker_daemon_up()
{
app.err("docker daemon is not running — retry with `/sbx launch docker --start` to boot it (sudo), or run ./ensure-docker.sh in a terminal first");
} else {
@@ -1305,8 +1312,67 @@ fn handle_command(
};
});
}
Some("vms") => {
let tx = app_tx.clone();
tokio::spawn(async move {
let res = tokio::task::spawn_blocking(|| {
let ver = sbx::vbox_version().ok_or_else(|| {
"VirtualBox isn't installed — install it with `/sbx gui <vm> --install`, or run ./ensure-vbox.sh".to_string()
})?;
let vms = sbx::list_vms().map_err(|e| e.to_string())?;
Ok::<_, String>((ver, vms))
})
.await;
let _ = match res {
Ok(Ok((ver, v))) if !v.is_empty() => tx.send(Net::Sys(format!(
"VirtualBox {ver} detected · VMs: {}",
v.join(", ")
))),
Ok(Ok((ver, _))) => tx.send(Net::Sys(format!(
"VirtualBox {ver} detected · no VMs registered"
))),
Ok(Err(e)) => tx.send(Net::Err(e)),
Err(e) => tx.send(Net::Err(format!("vms task: {e}"))),
};
});
}
Some("gui") => {
let gargs: Vec<&str> = p.collect();
let install = gargs.iter().any(|a| *a == "--install");
let name = gargs
.iter()
.copied()
.find(|a| !a.starts_with('-'))
.map(str::to_string);
match name {
None => app.sys("usage: /sbx gui <vm> [--install] (list VMs with /sbx vms)"),
Some(vm) => {
app.sys(format!("launching {vm} in the VirtualBox GUI…"));
let tx = app_tx.clone();
tokio::spawn(async move {
let res = tokio::task::spawn_blocking(move || {
if !sbx::vbox_installed() {
if install {
sbx::ensure_vbox_install()
.map_err(|e| format!("install failed: {e}"))?;
} else {
return Err("VirtualBox isn't installed — retry with `/sbx gui <vm> --install` (needs sudo), or run ./ensure-vbox.sh first".to_string());
}
}
sbx::gui_launch(&vm).map_err(|e| e.to_string())
})
.await;
let _ = match res {
Ok(Ok(desc)) => tx.send(Net::Sys(format!("{desc}"))),
Ok(Err(e)) => tx.send(Net::Err(e)),
Err(e) => tx.send(Net::Err(format!("gui task: {e}"))),
};
});
}
}
}
_ => app.sys(
"usage: /sbx launch [local|docker|multipass] [image] · stop · save [label] · load <label> · snaps",
"usage: /sbx launch [local|docker|multipass] [image] · gui <vm> · vms · stop · save [label] · load <label> · snaps",
),
}
} else if let Some(rest) = line.strip_prefix("/unsudo") {
+98
View File
@@ -14,6 +14,8 @@ use std::sync::mpsc;
/// Helper that ensures the Docker daemon is running (ships beside this source).
const ENSURE_DOCKER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ensure-docker.sh");
/// Detect-first VirtualBox installer (ships beside this source).
const ENSURE_VBOX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ensure-vbox.sh");
/// Is the Docker daemon accepting connections? (`docker info` succeeds.)
pub fn docker_daemon_up() -> bool {
@@ -45,6 +47,102 @@ fn start_docker_daemon() -> Result<()> {
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<String> {
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).
/// Returns the script's last error line on failure (e.g. needs sudo).
pub fn ensure_vbox_install() -> Result<()> {
let out = Command::new("bash")
.arg(ENSURE_VBOX)
.arg("--yes")
.output()
.context("running ensure-vbox.sh")?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
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<Vec<String>> {
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 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 <name> --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<String> {
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)"))
}
/// Which sandbox to summon. Multipass = strong isolation (default for real use),
/// Docker = fast, Local = no isolation (dev/testing only).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+2
View File
@@ -154,6 +154,8 @@ fn help_clusters(theme: &Theme) -> Vec<HelpCluster> {
kv("/sbx save [label]", "snapshot state (docker image; survives stop)"),
kv("/sbx load <label>", "launch a fresh sandbox from a saved snapshot"),
kv("/sbx snaps", "list saved snapshots"),
kv("/sbx vms", "list local VirtualBox VMs"),
kv("/sbx gui <vm>", "boot a VirtualBox VM locally in its GUI"),
kv("/drive · F2", "type into the shared shell (Esc releases)"),
],
},