bsdkrun-sdk (Rust SDK)
A Rust SDK for bsdkrun — a Firecracker-style microVM launcher for BSD, Linux, and unikernel guests on macOS and Linux, built on libkrun. Boot and drive microVMs programmatically, inspired by the Vercel and Deno Sandbox SDKs.
It's a thin, blocking wrapper that shells out to the bsdkrun binary — no
async runtime, no tokio. The API is fluent: consuming builders you chain and
finish with one terminal call.
use Sandbox;
let sandbox = linux.create?;
// exec argv directly, or build a command with env / stdin / a PTY / a cwd:
println!;
sandbox.exec?;
sandbox
.command
.args
.run?;
sandbox.stop?;
Install
Or from this repo:
[]
= { = "../bsdkrun/sdk/rust" }
The bsdkrun binary
You need the bsdkrun binary itself. The SDK finds it via, in order:
bsdkrun_sdk::set_binary_path("/path/to/bsdkrun")- the
BSDKRUN_BINenvironment variable bsdkrunon yourPATH- an in-repo
target/release/bsdkrunortarget/debug/bsdkrunbuild
See the bsdkrun README for installing the binary (Homebrew on macOS, or build from source on Linux/KVM). This SDK assumes libkrun is already provisioned — it does not auto-install it.
Creating a sandbox
One builder per guest kind — each exposes only the options its kind takes, and
every create() runs the machine detached and returns a Sandbox handle:
use Sandbox;
// Linux OCI image (docker run-style)
linux
.cpus
.mem
.volume // persistent CoW rootfs
.mount
.mount
.port
.forward // same thing, from numbers
.command // args after `--`
.create?;
// FreeBSD (EFI on macOS, PVH on Linux/amd64)
freebsd.version.mem.create?;
// NetBSD (direct-kernel boot everywhere)
netbsd.version.volume.create?;
// Boot a raw disk through its UEFI loader
firmware.create?;
// Boot a kernel directly, no bootloader
kernel.format.disk.create?;
// Unikernels: Unikraft, Solo5 (MirageOS), Nanos, OSv
unikraft.cmdline.create?;
solo5.args.create?;
nanos.mem.create?;
osv.cmdline.create?;
Every builder also has .to_args(), returning the exact argv create() would
run — handy for debugging and what the unit tests assert on.
Running commands
Pass an argv (no shell parsing), or build a command fluently:
sandbox.exec?;
let result = sandbox
.command
.args
.env
.cwd
.stdin
.tty // allocate a PTY
.run?;
println!;
run() (and the exec shorthand) return an ExecResult with stdout,
stderr, exit_code, and helpers .ok(), .text(), .json::<T>(),
.lines(). A non-zero exit is data, not an error — chain .ok_or_err()
to turn it into Error::CommandFailed:
sandbox.exec?.ok_or_err?;
let info: Info = sandbox.exec?.json?;
Lifecycle & inventory
let sandbox = linux.command.create?;
let same = get?; // reconnect (prefix ok) — SandboxNotFound otherwise
let rows = list?; // Vec<SandboxInfo>, exited included
sandbox.status?; // Option<SandboxInfo>
sandbox.is_running?; // bool
sandbox.logs?; // console log (String); boot_logs() for bsdkrun's own
sandbox.shell?; // interactive shell (inherits the terminal)
sandbox.stop?; // BSD guests clean-poweroff; Linux SIGTERM
sandbox.start?; // restart in place — same id, disk/rootfs, network
sandbox.update.cpus.mem.apply?; // applies on next start
sandbox.remove?; // force: stop first if running
Host-level namespaces:
use ;
probe?; // toolchain sanity check
list?; // Vec<ImageInfo>
list?; // Vec<VolumeInfo>
remove?;
list?; // Vec<NetworkInfo>
fetch_image.version.run?;
versions?;
Global networks — reach machines by name
Opt machines into a shared network so they get distinct IPs on one subnet and reach each other by IP and by name (docker-compose style), with internal DNS:
use ;
create?;
let db = linux.name.network.create?;
let api = linux.name.network.create?;
// `api` resolves `db` to its IP on devnet and pings it by name:
api.exec?.ok_or_err?;
// inspect + manage
list?; // Vec<NetworkInfo>
members?; // Vec<SandboxInfo> on the network
let info = db.status?.unwrap; // info.network == Some("devnet"), info.net_ip set
// edit membership (applies on next start — a VM's NIC is fixed at boot)
api.connect_network?; // or networks::connect(api.id(), "devnet")
api.disconnect_network?;
api.start?; // re-joins with the new membership
sync?; // refresh members' /etc/hosts (fixes NetBSD name lookup)
remove?;
Names resolve on Linux and FreeBSD via the network's DNS; NetBSD resolves
via a synced /etc/hosts block — joins auto-sync, and networks::sync
refreshes an existing network without restarting members.
SSH & Tailscale
// agent-managed key-based SSH
sandbox.ssh_setup.run?; // install local ~/.ssh/*.pub keys
sandbox.ssh_setup.user.key.run?;
// put a guest on your tailnet — the authkey rides in TS_AUTHKEY, never on the argv
sandbox.tailscale_up.authkey.hostname.run?;
Connecting to a remote daemon
Everything above talks to a local bsdkrun binary. Client is the network
sibling: it drives the same operations against a remote
bsdkrund over its GraphQL API — no local binary
needed, just a URL and a bearer token.
use Client;
let client = new?;
// or, from BSDKRUN_URL / BSDKRUN_TOKEN:
let client = from_env?;
let machines = client.list?; // Vec<SandboxInfo> — same type Sandbox::list returns
let machine_id = client
.run_linux
.image
.cpus
.mem
.command
.launch?;
let result = client.exec?;
println!;
client.stop?;
client.remove?;
run_linux() / run_bsd(BsdOs::Freebsd) / run_nanos() / run_unikraft() /
run_solo5() / run_osv() / run_flavor("name") each build the
corresponding GraphQL mutation's input (daemon/src/graphql.rs) and
launch() returns the new machine's id. run_solo5 boots a MirageOS
unikernel under the solo5-hvt tender rather than libkrun:
client
.run_solo5
.path
.args
.launch?;
stop/start/remove/update/commit return a
CommandResult { exit_code, stdout, stderr } — a non-zero exit there is a
state to report, not an error.
For a live terminal instead of a one-shot exec, use shell():
let mut session = client.shell.rows.cols.open?;
session.on_output;
session.on_exit;
session.write?;
session.resize?;
session.close;
Output that arrives before on_output is registered is buffered and flushed
the moment the callback is set, so no frame is ever lost to the race between
opening the session and wiring it up.
follow_logs streams a machine's console live instead of the one-shot
logs(id, boot):
let sub = client
.follow_logs
.on_data
.on_complete
.start?;
// ... later:
sub.unsubscribe;
Both exec/shell and follow_logs are built on the same
openShell/shellOutput shell-session protocol the daemon uses for every
interactive terminal — see
daemon/README.md
for the wire-level story.
Not every GraphQL operation has a typed method yet (flavor/network/volume
management, for instance) — client.request(query, variables) runs any raw
query or mutation, and client.subscribe(query, variables, on_next) runs any
raw subscription, for anything not wrapped above.
The transport is deliberately small and fully synchronous: queries and
mutations are one blocking ureq POST each, and subscriptions run over a
single shared graphql-transport-ws WebSocket (tungstenite) with one
background reader thread — no async runtime anywhere.
Client::new(url, token) and from_env() both reject a URL configured
without a token rather than silently making an unauthenticated request — set
both BSDKRUN_URL and BSDKRUN_TOKEN, or pass both explicitly.
Errors
Everything returns bsdkrun_sdk::Result<T> with one Error enum:
BinaryNotFound— thebsdkrunbinary wasn't found (carries every location searched).CommandFailed— a command exited non-zero (carriesexit_code,stdout,stderr). Produced byok_or_err(), the lifecycle methods, and the agent helpers.SandboxNotFound—Sandbox::getmatched no machine.GraphQL— aClientrequest failed (carriescode, the daemon'sextensions.code, when there is one).Auth— the daemon rejected the bearer token;err.code()always answersUNAUTHENTICATED.InvalidInput,Io,Json— a refused option combination, a host-side process failure, unparseable JSON output.
Development
From sdk/rust:
BSDKRUN_SDK_E2E=1
The tests never require the real bsdkrun binary or a live daemon: the client
suites run against an in-process fake GraphQL server (HTTP + WebSocket,
including the graphql-transport-ws connection_init/connection_ack
handshake and a scripted shell session), and the local Sandbox suites run
against a stub shell script that records the exact argv produced.
License
MIT