Skip to content

Latest commit

 

History

History
411 lines (305 loc) · 16.9 KB

File metadata and controls

411 lines (305 loc) · 16.9 KB

BandSox documentation

Python library for managing Firecracker microVMs. Create, manage, and snapshot sandboxes from Docker images.

Table of contents

Quick start

from bandsox.core import BandSox

# 1. Initialize the manager
#    (Default storage at /var/lib/bandsox, requires write permissions)
manager = BandSox()

# 2. Create a VM from a Docker image
#    (Requires internet to pull the image if not cached)
vm = manager.create_vm("alpine:latest", vcpu=2, mem_mib=1024)

print(f"VM Created with ID: {vm.vm_id}")

# 3. Run a command inside the VM
result = vm.exec_command("echo Hello from Firecracker!")
print(f"Command exit code: {result}")

# 4. Cleanup
vm.stop()
vm.delete()

Core concepts

  • BandSox -- the main controller. Manages storage, networking, and VM lifecycle.
  • MicroVM -- a single running Firecracker instance. Talks to the guest agent and the VMM.
  • Agent -- a small process inside the guest OS that runs commands and handles file ops on behalf of the host.
  • Rootfs -- the VM filesystem, built from a Docker image.

Usage guide

Initialization

Start with the BandSox class.

from bandsox.core import BandSox

manager = BandSox(storage_dir="/path/to/storage")

The storage directory holds images, sockets, and metadata -- set permissions accordingly. Large artifacts (kernel, CNI plugins, rootfs images) are not in git; run bandsox init to download them.

To use a running BandSox server instead of managing Firecracker locally, pass server_url and include auth headers:

from bandsox.core import BandSox

manager = BandSox(
    server_url="http://localhost:8000",
    headers={"Authorization": "Bearer bsx_your_key_here"},
)
vm = manager.create_vm("python:3.11-slim", enable_networking=False)

result = vm.exec_python_capture("print('hello from the remote server')")
print(result["stdout"])

vm.stop()

You can also pass the URL as the first argument: BandSox("http://localhost:8000").

Creating VMs

From a Docker image:

vm = manager.create_vm(
    docker_image="python:3.9-slim",
    name="my-python-sandbox",
    vcpu=2,
    mem_mib=512,
    enable_networking=True
)

From a Dockerfile:

vm = manager.create_vm_from_dockerfile(
    dockerfile_path="./Dockerfile",
    tag="custom-image:v1",
    name="my-custom-vm"
)

With environment variables and MCP servers (Claude Code use case):

vm = manager.create_vm(
    "ghcr.io/bandsox/claude-code:latest",
    env_vars={"ANTHROPIC_API_KEY": "..."},
    mcp={
        "browserbase": {"apiKey": "...", "projectId": "..."},
        "github":      {"token": "ghp_..."},
        # Custom server not in the built-in registry:
        "my-custom":   {"spec": {"command": "uvx", "args": ["my-mcp"]}},
    },
)

BandSox resolves each mcp= entry against the MCP server registry, merges the resulting env vars into the VM's environment (caller-supplied env_vars win on collisions), and stages /workspace/.mcp.json in the rootfs before the VM boots. MCP-derived secrets show as <redacted> in metadata.json on disk; the running VM gets the real values. Unknown server names and malformed spec blocks raise ValueError instead of silently no-opping. The Claude Code cookbook walks through a full end-to-end use.

Executing commands

Several ways to run commands:

1. Blocking (exec_command) Waits for the command to finish.

code = vm.exec_command("ls -la /", on_stdout=lambda line: print(f"OUT: {line}"), timeout=10)

2. Background (start_session) Returns a session ID for long-running processes.

session_id = vm.start_session("sleep 100")
# ... do other things ...
vm.kill_session(session_id)

3. Interactive PTY (start_pty_session) Allocates a pseudo-terminal. Use when the program expects a TTY (shells, interactive CLIs).

session_id = vm.start_pty_session("/bin/sh", cols=80, rows=24)
vm.send_session_input(session_id, "echo Interactive\n")

4. Streaming exec (exec-stream WebSocket) Streams stdout/stderr incrementally over the serial agent path (not the vsock bulk-upload path used by POST /exec). Use for progress output (git clone, npm install, etc.). The TypeScript SDK exposes this as execStream().

5. Python execution (exec_python) Run Python code with isolated dependencies (uses uv for package installation).

# Simple
vm.exec_python("print('Hello from Python!')")

# With dependencies
vm.exec_python(
    code="import requests; print(requests.get('https://example.com').status_code)",
    packages=["requests"]
)

6. Python execution with capture (exec_python_capture) Captures output and returns a result dict. Does not raise on errors.

result = vm.exec_python_capture("print('hello')")
if result['success']:
    print(f"Output: {result['output']}")
else:
    print(f"Error: {result['error']}")

File operations

File transfers go through the guest agent (static Go binary by default) with a vsock fast path and automatic serial fallback.

# Upload a file (timeout scales with file size: min 30s + 10s per MiB)
vm.upload_file("./local_script.py", "/app/script.py")

# Download a file
vm.download_file("/app/result.txt", "./result.txt")

# Read file contents as a string
content = vm.get_file_contents("/etc/hostname")

# Read a slice with formatting applied on the host
partial = vm.get_file_contents(
    "/var/log/app.log",
    offset=200,
    limit=50,
    show_line_numbers=True,
    show_header=True,
    show_footer=True,
)

Performance note (single VM on typical laptop/desktop): in our benchmark (tests/benchmark_go_agent.py) we see ~2.3ms mean latency for exec_command("true"), ~190 MiB/s upload for an 8 MiB file, and up to ~1 GiB/s download for an 8 MiB file. (Exact numbers vary by host I/O + CPU.)

Snapshots

Save the memory and disk state of a running VM, then restore it later.

# Create a snapshot (VM pauses briefly)
snapshot_id = manager.snapshot_vm(vm, snapshot_name="checkpoint-1")

# Restore into a new VM
restored_vm = manager.restore_vm(snapshot_id)

Authentication

See AUTHENTICATION.md for the full auth model (API keys, session cookies, WebSocket tokens), CLI commands, SDK examples, and /api/auth/* endpoint reference.

CLI reference

The bandsox CLI wraps the server and API.

  • bandsox init -- download required artifacts.

    • Flags: --kernel-url, --kernel-output, --skip-kernel, --cni-url, --cni-dir, --skip-cni, --rootfs-url, --rootfs-output, --skip-rootfs, --force

    • Downloads vmlinux, CNI plugins (tgz), and optionally a base .ext4 rootfs. Skips existing files unless --force is set.

      bandsox init --rootfs-url ./bandsox-base.ext4
  • bandsox serve [--host 0.0.0.0] [--port 8000] [--storage /var/lib/sandbox] -- run the FastAPI server. Auth is off unless auth.json exists.

  • bandsox create <image> [--name NAME] [--vcpu N] [--mem MiB] [--disk-size MiB] [--host HOST] [--port PORT] -- create a VM from a Docker image via the server API.

  • bandsox vm list|stop|pause|resume|delete|save|rename ... -- manage VMs.

  • bandsox snapshot list|delete|restore|rename ... -- manage snapshots.

  • bandsox terminal <vm_id> [--host HOST] [--port PORT] -- connect to a VM's terminal over WebSocket.

  • bandsox auth init|set-password|create-key|list-keys|revoke-key ... -- manage authentication (off by default).

  • bandsox cleanup -- remove stale TAP devices.

HTTP API

Base URL: http://HOST:PORT

When auth is enabled (auth.json exists), all /api/ endpoints except auth login/logout/check require a Bearer token or session cookie. When auth is disabled, all endpoints are open.

Auth

  • POST /api/auth/login -- log in, get session cookie.
  • POST /api/auth/logout -- log out, clear session.
  • GET /api/auth/check -- check if authenticated.
  • GET /api/auth/keys -- list API keys.
  • POST /api/auth/keys -- create an API key.
  • DELETE /api/auth/keys/{key_id} -- revoke an API key.

VMs

  • GET /api/vms -- list VMs.
  • POST /api/vms -- create a VM from an image.
    • Body: { "image": "alpine:latest", "name": "...", "vcpu": 1, "mem_mib": 128, "enable_networking": true, "force_rebuild": false, "disk_size_mib": 4096, "env_vars": {"KEY": "value"}, "mcp": {"github": {"token": "ghp_..."}} }
    • env_vars is optional and gets forwarded into every exec_command and session call.
    • mcp is optional. It's resolved against the MCP registry, its derived env vars are merged into env_vars, and secrets are redacted before write to disk. Unknown server names or malformed spec entries come back as a 500 carrying the ValueError message.
  • POST /api/vms/from-dockerfile -- build an image from an uploaded Dockerfile and create a VM. Multipart form with dockerfile file plus optional fields (tag, name, vcpu, mem_mib, disk_size_mib, force_rebuild, env_vars JSON, metadata JSON, mcp JSON).
  • GET /api/vms/{vm_id} -- get VM details.
  • POST /api/vms/{vm_id}/stop|pause|resume -- lifecycle operations.
  • DELETE /api/vms/{vm_id} -- delete a VM.
  • POST /api/vms/{vm_id}/snapshot -- snapshot a running VM.
    • Body: { "name": "snap-name" }
  • POST /api/vms/{vm_id}/exec -- run a blocking command (may buffer output via vsock when available).
    • Body: { "command": "echo hello", "timeout": 30 }
  • WS /api/vms/{vm_id}/exec-stream -- stream one command's stdout/stderr, then exit.
    • Client sends: {"command": "git clone ...", "timeout": 600} (timeout 1–3600 seconds, default 600).
    • Server pushes: {"type":"stdout"|"stderr","data":"..."} frames, then {"type":"exit","exit_code": int}.
    • Auth: same as terminal (session cookie, token= query param, or bandsox.auth.<base64url> WebSocket subprotocol).
  • POST /api/vms/{vm_id}/exec-python -- run Python and return captured output.
  • GET /api/vms/{vm_id}/files?path=/ -- list files inside the VM.
  • GET /api/vms/{vm_id}/read-file?path=/etc/hosts -- read a UTF-8 file.
  • POST /api/vms/{vm_id}/write-file -- write a UTF-8 or base64-encoded file.
  • POST /api/vms/{vm_id}/append-file -- append UTF-8 or base64-encoded content to a file.
  • GET /api/vms/{vm_id}/file-info?path=/etc/hosts -- get file metadata.
  • POST /api/vms/{vm_id}/upload -- upload a multipart file.
  • GET /api/vms/{vm_id}/download?path=/etc/hosts -- download a file.
  • POST /api/vms/{vm_id}/http -- proxy an HTTP request to a service inside the VM.
  • WS /api/vms/{vm_id}/terminal?cols=80&rows=24&token=<session_or_api_key> -- interactive terminal (WebSocket).
  • WS /api/vms/{vm_id}/exec-stream -- streaming exec (see above).

Snapshots

  • GET /api/snapshots -- list snapshots.
  • DELETE /api/snapshots/{snapshot_id} -- delete a snapshot.
  • POST /api/snapshots/{snapshot_id}/restore -- restore into a new VM.
    • Body: { "name": "optional-name", "enable_networking": true }

Static pages

All pages redirect to /login if not authenticated.

  • GET / -- dashboard.
  • GET /login -- login page.
  • GET /terminal -- web terminal page.
  • GET /vm_details -- VM details page.
  • GET /markdown_viewer -- markdown viewer.

Class reference

BandSox

Method Description
create_vm(docker_image, name=None, vcpu=1, mem_mib=128, env_vars=None, mcp=None, ...) Create a new VM. env_vars is forwarded to every exec inside the VM. mcp is resolved against bandsox.mcp_registry and staged as /workspace/.mcp.json.
create_vm_from_dockerfile(dockerfile_path, tag, env_vars=None, mcp=None, ...) Build an image and create a VM. Same env_vars / mcp semantics as create_vm.
restore_vm(snapshot_id, enable_networking=True) Restore a VM from a snapshot.
snapshot_vm(vm, snapshot_name=None) Snapshot a running VM.
delete_vm(vm_id) Stop and delete a VM and its resources.
list_vms() List all known VMs.
get_owner(vm_id) Return the MicroVM instance.

MicroVM

Method Description
start(), stop(), pause(), resume() Lifecycle control.
exec_command(cmd, on_stdout=None, timeout=30) Run a command (blocking).
exec_python(code, cwd, packages, ...) Run Python code with isolated env using uv.
exec_python_capture(code, packages, ...) Run Python and return output dict.
start_session(cmd) Run a command (background).
write_text(remote, content, timeout=None, append=False) Write UTF-8 text directly without creating a host temp file.
upload_file(local, remote, timeout=None, append=False) Upload a file. Prefers vsock (fast), falls back to chunked serial. Timeout scales with file size (min 30s + 10s/MiB).
download_file(remote, local) Download a file. Prefers vsock (fast), falls back to chunked serial.
get_file_contents(remote, offset=0, limit=0, show_line_numbers=False, show_header=True, show_footer=True) Read file content with optional host-side formatting.

Caveats and troubleshooting

1. Root privileges and networking

Networking (enable_networking=True) uses privileged host commands.

  • The library runs sudo ip ... and sudo iptables ... to configure TAP devices and NAT; you will be prompted for a password when needed.
  • Passwordless sudo for those commands avoids prompts in automation.
  • If you don't have sudo access, create VMs with enable_networking=False.

2. File operations and VM pausing

By default, file operations use the guest agent (vsock/serial) and do not pause the VM.

BandSox has an emergency fallback path using debugfs (direct ext4 reads) when agent_ready is false but the host still has access to the rootfs image. That fallback may pause/resume the VM to reduce the risk of reading a mutating filesystem.

3. Kernel dependencies

VMs need a compatible Linux kernel binary (vmlinux).

  • By default it looks at /var/lib/bandsox/vmlinux.
  • Make sure this file exists, or pass kernel_path to create_vm.

4. Boot latency

A warm create_vm (rootfs already built) reaches agent-ready in ~150ms. Most of that speed comes from DEFAULT_BOOT_ARGS, which suppresses two things the microVM doesn't need:

  • i8042.noaux i8042.nomux i8042.nopnp i8042.dumbkbd — stops the kernel probing the emulated PS/2 keyboard controller. Without these the i8042 driver polls a device that isn't there and stalls boot ~750ms.
  • quiet loglevel=1 — silences the kernel's ~190-line printk stream. Those lines otherwise trickle out over the emulated serial UART one byte at a time while the host reads them, adding ~45ms. The agent still uses console=ttyS0 for its own I/O; only kernel chatter is suppressed. This also hides kernel panic detail on boot failures — set BANDSOX_VERBOSE_BOOT=1 to drop quiet and get full kernel logs when debugging a boot regression.

The first create_vm for a given image is much slower — it pulls the image and builds the ext4 rootfs (build_rootfs). That cost is paid once and cached; every subsequent VM from the same image hits the ~150ms warm path.

5. Entropy and HTTPS on fresh boot

The Firecracker quickstart kernel that bandsox init downloads doesn't have RANDOM_TRUST_CPU=y enabled, so on a fresh microVM crng_init can take tens of seconds to complete. While it's not done, every TLS handshake blocks. Two things help here:

  • DEFAULT_BOOT_ARGS passes random.trust_cpu=on. The bundled kernel ignores it, but it kicks in automatically the moment you swap vmlinux for any build with RANDOM_TRUST_CPU=y.
  • The /init shim runs haveged (or rng-tools) if the image has it installed. The templates/claude-code/Dockerfile does this. If you build your own image and need TLS in the first second of boot, apt-get install -y haveged (or equivalent for your distro).

On snapshot restore, BandSox always mixes a per-restore host seed into /dev/urandom and /dev/random via printf | base64 -d. That diverges the CRNG of two VMs restored from the same snapshot immediately, even on images without python3. If python3 is available too, the kernel also credits entropy through RNDADDENTROPY.

6. Image size

The rootfs size is fixed at build time (Docker export size + overhead). If you need more space, adjust the image generation logic in image.py.

7. Snapshot compatibility

Restoring a snapshot requires the same kernel and a compatible network config.

  • If you move the storage directory, move metadata and snapshots together.
  • Snapshots are tied to the exact kernel binary used when they were created.

8. Authentication

See AUTHENTICATION.md.