bsdkrun (Python SDK)
A Python SDK for bsdkrun — a Firecracker-style microVM launcher for BSD and Linux 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 wrapper that shells out to the bsdkrun binary, so it has zero
runtime dependencies — stdlib only, Python 3.10+.
from bsdkrun import Sandbox
box = Sandbox.create(os="linux", image="alpine")
# exec argv directly, with env / stdin / a PTY / a working dir:
print(box.exec(["uname", "-a"]).text())
box.exec(["apk", "add", "curl"])
box.run_command("curl", ["-fsSL", "https://example.com"])
box.stop()
Install
uv add bsdkrun # or: pip install bsdkrun
Or from this repo:
uv add ./sdk/python # or: pip install sdk/python
The bsdkrun binary
You need the bsdkrun binary itself. The SDK finds it via, in order:
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
Sandbox.create is keyed on os — the options change per guest kind:
# Linux OCI image (docker run-style)
Sandbox.create(
os="linux",
image="ghcr.io/owner/name:tag",
cpus=2,
mem=1024,
volume="web", # persistent CoW rootfs
mounts=["~/project:/src", "~/data:/data:ro"],
net={"ports": ["8080:80", "2222:22"]},
command=["node", "server.js"], # args after `--`
)
# FreeBSD (EFI on macOS, PVH on Linux/amd64)
Sandbox.create(os="freebsd", version="14.3", mem=2048)
# NetBSD (direct-kernel boot everywhere)
Sandbox.create(os="netbsd", version="10.1", volume="db")
# Boot a raw disk through its UEFI loader
Sandbox.create(os="firmware", firmware="KRUN_EFI.fd", disk="disk.raw")
# Boot a kernel directly, no bootloader
Sandbox.create(os="kernel", kernel="netbsd", format="elf", disk="root.raw")
Every create runs the machine detached and returns a Sandbox handle.
Running commands
Pass an argv list (no shell parsing), or a program name plus args:
box.exec(["ls", "-la", "/etc"])
box.exec(
"node",
args=["-e", "print(1)"],
env={"X": "hi"},
cwd="/app",
stdin="data on stdin",
tty=True, # allocate a PTY
throw_on_error=True, # raise CommandFailed on a non-zero exit (default: False)
)
# Vercel-Sandbox-style alias:
result = box.run_command("uname", ["-a"])
print(result.stdout, result.exit_code)
exec returns a Result with .stdout, .stderr, .exit_code, .ok, and
helpers .text(), .json(), .lines(), .throw_if_failed().
Lifecycle & inventory
box = Sandbox.create(os="linux", image="alpine", command=["sleep", "300"])
same = Sandbox.get(box.id) # reconnect (prefix ok)
rows = Sandbox.list(all=True) # list[SandboxInfo]
box.status() # SandboxInfo | None
box.is_running() # bool
box.logs() # console log (str)
box.shell() # interactive shell (inherits the terminal)
box.stop() # BSD guests clean-poweroff; Linux SIGTERM
box.start() # restart in place — resumes its own disk/rootfs (data persists)
box.update(cpus=4, mem=2048) # applies on next start
box.remove(force=True)
Host-level namespaces:
from bsdkrun import images, volumes, networks, system
system.probe() # toolchain sanity check
images.list() # list[ImageInfo]
volumes.list() # list[VolumeInfo]
volumes.remove("web", force=True)
networks.list() # list[NetworkInfo]
system.fetch_image("freebsd", version="14.3")
system.versions("netbsd")
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:
from bsdkrun import Sandbox, networks
networks.create("devnet")
db = Sandbox.create(os="linux", image="postgres", name="db", net={"network": "devnet"})
api = Sandbox.create(os="linux", image="myapi", name="api", net={"network": "devnet"})
# `api` resolves `db` to its IP on devnet and pings it by name:
api.exec(["ping", "-c1", "db"], throw_on_error=True)
# inspect + manage
networks.list() # list[NetworkInfo]
networks.members("devnet") # list[SandboxInfo] on the network
info = db.status() # info.network == "devnet", info.net_ip set
# edit membership (applies on next start — a VM's NIC is fixed at boot)
api.connect_network("devnet") # or networks.connect(api.id, "devnet")
api.disconnect_network()
api.start() # re-joins with the new membership
networks.sync("devnet") # refresh members' /etc/hosts (fixes NetBSD name lookup)
networks.remove("devnet", force=True)
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
box.ssh_setup() # install local ~/.ssh/*.pub keys
box.ssh_setup(user="tsiry", key="~/.ssh/work.pub")
# put a guest on your tailnet
box.tailscale_up(authkey="tskey-auth-...", hostname="web")
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.
from bsdkrun import Client
client = Client(url="http://vps.example.com:50052", token="9f2c...")
# or, from BSDKRUN_URL / BSDKRUN_TOKEN:
client = Client.from_env()
machines = client.list(all=True) # list[SandboxInfo] — same type Sandbox.list() returns
machine_id = client.run_linux(image="alpine", cpus=2, mem=1024, command=["sleep", "300"])
result = client.exec(machine_id, ["uname", "-a"])
print(result.output.decode(), result.exit_code)
client.stop(machine_id)
client.remove([machine_id])
Client.run_linux/run_bsd/run_nanos/run_unikraft/run_osv/run_flavor
each take the same keyword options as the corresponding GraphQL mutation
(daemon/src/graphql.rs) — run_bsd(os="freebsd", ...), etc. — and return
the new machine's id. stop/start/remove/update/commit return a
CommandResult(exit_code, stdout, stderr).
For a live terminal instead of a one-shot exec, use shell():
session = client.shell(machine_id) # or shell(machine_id, command=[...]) for a non-login command
session.on_output(lambda data: print(data.decode(), end=""))
session.on_exit(lambda code: print(f"\nexited {code}"))
session.write(b"ls -la\n")
session.resize(rows=50, cols=120)
session.close()
follow_logs(id, on_data=...) streams a machine's console live instead of
the one-shot logs(id). 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.
Like the local SDK, Client has zero runtime dependencies — the HTTP
transport is stdlib urllib, and subscriptions (used by exec/shell/
follow_logs) run over a hand-rolled graphql-transport-ws WebSocket client
on top of stdlib socket/ssl, since Python's standard library has no
WebSocket client of its own.
Client(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
All errors extend BsdkrunError:
BinaryNotFound— thebsdkrunbinary wasn't found.CommandFailed— a command exited non-zero (carriesexit_code,stdout,stderr). Raised byexecwhenthrow_on_error=True, by the lifecycle methods, and by the agent helpers.SandboxNotFound—Sandbox.getmatched no machine.GraphQLError— aClientrequest failed (carriescode, the daemon'sextensions.code, when there is one).AuthError(aGraphQLError) — the daemon rejected the bearer token.
Try it interactively
uv run console.py
Starts IPython with the SDK preloaded — Sandbox, the images / volumes /
networks / system namespaces, and a ps() shorthand for
Sandbox.list(all=True). Pass --bin ../../target/release/bsdkrun to drive a
locally built binary for the session. Falls back to the stdlib REPL if IPython
isn't installed.
Development
The SDK is developed with uv. From sdk/python:
uv sync # create .venv and install the dev group
uv run pytest # tests
uv run ruff check # lint
uv run ruff format # format
uv run mypy # type-check (strict)
The package itself has no runtime dependencies — pytest, ruff, and
mypy live in the dev dependency group and are never installed for consumers.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bsdkrun-0.2.0.tar.gz.
File metadata
- Download URL: bsdkrun-0.2.0.tar.gz
- Upload date:
- Size: 72.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce722503a0dbf84f24d0e8cf4d468b07919bdfb44caca9b106bf47213fd1dc7b
|
|
| MD5 |
bfa776df474e933cbd3413527fa4a8af
|
|
| BLAKE2b-256 |
f212636c6cee94f0064ff9883fdef1c7692687f3be1062e1ecc3ed22a077d238
|
File details
Details for the file bsdkrun-0.2.0-py3-none-any.whl.
File metadata
- Download URL: bsdkrun-0.2.0-py3-none-any.whl
- Upload date:
- Size: 34.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f377674fefae78b508f8cc7aca717096205dc78fb551173cc51a888ea8b0417
|
|
| MD5 |
477748144219050a93113ff406217aa8
|
|
| BLAKE2b-256 |
07ce5d438ffc18e5fc855911bac28a1109e3a78f8729530a954eae1e7e5ae7f7
|