feat(sandbox): VM library — pointer catalog + local build via /sbx vmlib
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run

Add an installable-VM catalog that ships pointers only (no multi-GB images
in the repo). When a VM is chosen it is BUILT LOCALLY on the caller's own
machine — nothing is relayed to the room.

- scripts/vbox-library.json: 7-entry manifest (Windows 11, macOS Sonoma,
  Kali, Parrot, Ubuntu 24.04, Fedora 41, Debian 12) with download
  pointers, ostype, cpu/mem/disk, and build kind.
- scripts/vbox-library.sh: --list / --info / --plan / --install. Build
  kinds: iso (download/--iso + createvm + boot installer; EFI+TPM for
  Win11), cloudimg (delegate to vbox-new.sh, unattended), ova (import),
  manual (pointer-only, e.g. macOS per Apple licensing). Detect-first:
  --plan changes nothing, install refuses to clobber and rolls back
  half-built VMs, direct URLs fall back to the page + --iso <path>.
- sbx.rs: vbox_library()/library_vm()/vbox_library_install() loaders +
  running_vms() for live-state markers.
- app.rs: /sbx vmlib (catalog ✓installed/↓available), /sbx vmlib <id>
  (pointer/notes), /sbx vmlib <id> install [--iso path] (local build);
  /sbx vms now flags running VMs (▶). Registered in SBX_SUBCOMMANDS +
  usage.
- ui.rs: help entry for the VM library.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-08 12:09:54 -07:00
parent 15aa2027c4
commit f93c8c5e4f
5 changed files with 612 additions and 6 deletions
+110 -6
View File
@@ -2291,15 +2291,23 @@ fn handle_command(
"VirtualBox isn't installed — install it with `/sbx gui <vm> --install`, or run ./scripts/ensure-vbox.sh".to_string()
})?;
let vms = sbx::list_vms().map_err(|e| e.to_string())?;
Ok::<_, String>((ver, vms))
let running = sbx::running_vms();
Ok::<_, String>((ver, vms, running))
})
.await;
let _ = match res {
Ok(Ok((ver, v))) if !v.is_empty() => tx.send(Net::Sys(format!(
Ok(Ok((ver, v, running))) if !v.is_empty() => tx.send(Net::Sys(format!(
"VirtualBox {ver} detected · VMs: {}",
v.join(", ")
v.iter()
.map(|n| if running.iter().any(|r| r == n) {
format!("{n}")
} else {
n.clone()
})
.collect::<Vec<_>>()
.join(", ")
))),
Ok(Ok((ver, _))) => tx.send(Net::Sys(format!(
Ok(Ok((ver, _, _))) => tx.send(Net::Sys(format!(
"VirtualBox {ver} detected · no VMs registered"
))),
Ok(Err(e)) => tx.send(Net::Err(e)),
@@ -2401,6 +2409,102 @@ fn handle_command(
}
}
}
Some("vmlib") => {
// VM library: a catalog of pointers to installable VMs (no
// images are bundled). Bare `/sbx vmlib` lists the catalog
// marking which are already installed locally; `/sbx vmlib <id>`
// shows the pointer/notes; `/sbx vmlib <id> install [--iso path]`
// builds the chosen VM locally on the caller's own machine.
let args: Vec<&str> = p.collect();
let iso = args
.iter()
.position(|a| *a == "--iso")
.and_then(|i| args.get(i + 1).copied())
.map(str::to_string);
let mut pos = args.iter().copied().filter(|a| !a.starts_with('-'));
match pos.next() {
None => {
// Catalog listing
let tx = app_tx.clone();
tokio::spawn(async move {
let res =
tokio::task::spawn_blocking(sbx::vbox_library).await;
let _ = match res {
Ok(Ok(vms)) => {
let body = vms
.iter()
.map(|v| {
let mark = if v.installed { "" } else { "" };
format!("{mark} {}{} ({})", v.id, v.name, v.os)
})
.collect::<Vec<_>>()
.join("\n");
tx.send(Net::Sys(format!(
"VM library (✓ installed · ↓ available) — `/sbx vmlib <id>` for details, `/sbx vmlib <id> install` to build locally:\n{body}"
)))
}
Ok(Err(e)) => tx.send(Net::Err(format!("vmlib: {e}"))),
Err(e) => tx.send(Net::Err(format!("vmlib task: {e}"))),
};
});
}
Some(id) => {
let id = id.to_string();
let action = pos.next();
if matches!(action, Some("install")) {
app.sys(format!(
"building VM '{id}' locally{}… (this downloads + provisions on your own machine; watch for a VirtualBox window)",
iso.as_deref().map(|p| format!(" from {p}")).unwrap_or_default()
));
let (tx, iso) = (app_tx.clone(), iso.clone());
tokio::spawn(async move {
let res = tokio::task::spawn_blocking(move || {
sbx::vbox_library_install(&id, iso)
})
.await;
let _ = match res {
Ok(Ok(desc)) => tx.send(Net::Sys(format!("{desc}"))),
Ok(Err(e)) => tx.send(Net::Err(format!("vmlib install failed: {e}"))),
Err(e) => tx.send(Net::Err(format!("vmlib install task: {e}"))),
};
});
} else {
// Info / pointer for one entry
let tx = app_tx.clone();
tokio::spawn(async move {
let res = tokio::task::spawn_blocking(move || {
sbx::library_vm(&id).ok_or(id)
})
.await;
let _ = match res {
Ok(Ok(v)) => {
let state = if v.installed {
"already installed".to_string()
} else {
"available — `/sbx vmlib <id> install` to build locally".to_string()
};
let mut body = format!(
"{} ({}) · {} · {}\n{}",
v.name, v.id, v.os, v.size, state
);
if !v.notes.is_empty() {
body.push_str(&format!("\n{}", v.notes));
}
if !v.page.is_empty() {
body.push_str(&format!("\nsource: {}", v.page));
}
tx.send(Net::Sys(body))
}
Ok(Err(bad)) => tx.send(Net::Err(format!(
"no VM library entry '{bad}' — `/sbx vmlib` lists the catalog"
))),
Err(e) => tx.send(Net::Err(format!("vmlib task: {e}"))),
};
});
}
}
}
}
Some("gui") => {
// Convenience alias for `/sbx vbox gui <vm> [yes]` — opens a
// local VirtualBox VM's GUI on your own machine. Bare `/sbx gui`
@@ -2416,7 +2520,7 @@ fn handle_command(
}
}
other => {
let usage = "usage: /sbx <type> <option> — /sbx docker|podman|multipass|local [image] · /sbx vbox [gui] <vm> [yes] (host opens directly; non-host appends yes) · /sbx vbox new [name] (fresh VM) · vms · stop · save [label] [--local] · load <label> · snaps · vmsave <vm> [label] [--local] · vmload <vm> [label] · vmsnaps <vm>";
let usage = "usage: /sbx <type> <option> — /sbx docker|podman|multipass|local [image] · /sbx vbox [gui] <vm> [yes] (host opens directly; non-host appends yes) · /sbx vbox new [name] (fresh VM) · vmlib [<id> [install [--iso path]]] (VM library — build locally) · vms · stop · save [label] [--local] · load <label> · snaps · vmsave <vm> [label] [--local] · vmload <vm> [label] · vmsnaps <vm>";
// Bare `/sbx` → usage. An unrecognised subcommand → suggest the
// closest documented one before the usage line.
match other.and_then(|bad| closest(bad, SBX_SUBCOMMANDS).map(|s| (bad, s))) {
@@ -2686,7 +2790,7 @@ const KNOWN_COMMANDS: &[&str] = &[
/// omitted so suggestions point at the documented form.
const SBX_SUBCOMMANDS: &[&str] = &[
"docker", "podman", "multipass", "local", "vbox", "stop", "save", "load", "snaps", "vms",
"vmsave", "vmload", "vmsnaps",
"vmsave", "vmload", "vmsnaps", "vmlib",
];
/// Classic Levenshtein edit distance (insert/delete/substitute, each cost 1).