feat(sbx,ft): share VirtualBox VMs — vbox launch grammar, picker & streaming transfer

Unify sandbox summoning under `/sbx launch <docker|multipass|vbox>`, each
running on the invoker's own machine. For vbox:
- `/sbx launch vbox` opens an arrow-navigable VM picker (↑↓/Enter/Tab/Esc).
- `/sbx launch vbox [gui] <vm>` boots frictionlessly for a host that already
  has VirtualBox + the VM imported; a non-host appends `yes` to install
  VirtualBox, import the shared appliance, and/or free VT-x first.
- `/sbx gui <vm> [yes]` kept as an alias.

Add `vm_registered()` + `import_appliance()` (VBoxManage import) in sbx.rs.

Make file transfer stream disk-to-disk so multi-GB VM images can be shared
(the old 50 MB in-memory cap blocked them): `STREAM_MAX` = 16 GiB for `/send`
(`MAX_SIZE` now guards only the in-memory tar_path for sandbox injection).
`prepare_send` stat+stream-hashes off the UI thread; `Sink`/`commit` write
incoming chunks straight to a temp `.part` file and verify SHA before moving
into place. Wire frames (offer/accept/chunk/done, 64 KB, base64) unchanged.

A received `.ova`/`.ovf` auto-imports so the recipient can immediately
`/sbx launch vbox gui <vm>`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-06 19:45:45 -07:00
parent 1fa8c332ed
commit 98e202e0fe
4 changed files with 821 additions and 402 deletions
+39
View File
@@ -108,6 +108,45 @@ pub fn list_vms() -> Result<Vec<String>> {
.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<String> {
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")