Skip to main content
The SDK is LithosBox (import lithosbox; class LithosBox; Nanovm and import nanovm remain aliases). Sync and async (AsyncNanovm) with identical surfaces. Constructor: api_url, token — or auth_url/client_id/client_secret — and timeout; all read NANOVM_* env vars when omitted. The whole surface is twelve names. Each is one operation whose arguments select the behavior — there is one way to run a command, one way to read metrics, one way to put a sandbox away. Every argument listed here is the whole contract; there are no other public methods worth learning (older names are silent aliases).

nv.sandboxes.create(...)

create(image=None, template=None, snapshot_id=None, cpus=None, memory_mb=None, disable_internet=False, runtime=None, vm_id=None, wait_for_template=True, warm_timeout=300) One source: an OCI image ref, a template (name, name:version, or id), or a snapshot_id to restore into a NEW sandbox; none = default sandbox (Python 3.12 + Node 22). Shape: cpus 1–4, memory_mb 128–8192 (defaults 2/1024). runtime="vm" gives full-VM semantics (real /dev, dockerd works). vm_id (a UUID you mint) makes the create retry-safe. Returns a Sandbox; context-manager deletes on exit. Also get(id), list(), delete(id).

sb.run(command, background=False, stdin=None) -> ExecResult

THE command verb. command str = shell (sh -c: pipes, globs, redirects). command list = argv, no shell — the only safe way to pass untrusted values. Blocks until the command and its children finish (~110 s wall at the edge): .exit_code/.stdout/.stderr (+ .stdout_truncated past the 16 MiB stream cap). background=True (shell string only) returns immediately and the process outlives the call, keeping the sandbox awake. stdin bytes/str ≤ 4 MiB (the way to ship files/binary data — argv strings cap at 128 KiB and cannot hold NUL).

sb.session(session_id=None) -> Session

A shell where cd, export, venvs and background jobs persist across .run(cmd) calls (256 concurrent per sandbox; same id from any handle = same shell; .close() ends it for everyone). Use for multi-step shell work; use plain run() for everything else.

sb.files.*

write(path, data) / read(path) / read_bytes(path) / list(path) / remove(path) / rename(src, dst) / mkdir(path) / exists(path) — all file work (4 MiB per write, 16 MiB per read; loop for bigger). remove is recursive. write creates parents.

sb.expose(guest_port, public_port=0) -> IngressInfo

Public HTTPS URL (.url) for a port the guest listens on. Live once the server answers; survives sleep/wake; re-installs across archive/unarchive (may 404 for seconds). sb.ingress() lists, nv.ingress.delete(id) removes.

sb.archive() / sb.unarchive()

Put the sandbox away: memory+disk to object storage, host freed, compute billing stopped, survives node loss; composes from any rest state. Unarchive restores it running, processes intact (seconds). A later run() on an archived sandbox raises — unarchive first.

sb.snapshot(leave_paused=False, wait_durable=False) -> Snapshot

Durable checkpoint of memory+disk (~50 ms; durable seconds later — wait_durable=True blocks for it). Restore into a NEW sandbox: nv.snapshots.restore(snap) or create(snapshot_id=...). Snapshots outlive their sandbox. nv.snapshots.list/get/delete.

sb.fork(n=1) -> Sandbox | list[Sandbox]

Live copies from this instant — running processes and open connections included (n ≤ 4 per call; fork between commands, not mid-command). One checkpoint, n parallel restores.

sb.events(limit=200) -> list[dict]

The lifecycle timeline, newest first: {kind, detail, node, at} for created/paused/resumed/slept/archived/unarchived/rebooted/snapshotted/forked/deleted. Survives deletion.

sb.logs(tail_bytes=65536) -> str

Serial-console tail (boot output, kernel messages; ≤ 256 KiB per read). Host-side: never wakes a paused/slept guest. Archived sandboxes have no live console (their events() and metrics(hours=…) still answer).

sb.metrics(hours=0, start="", end="") -> dict

No arguments: {"samples": [...], "cpus": n} — live 30 s host samples, last ~2 h (cpu_ms, mem_mb, rx/tx_bytes, interval_s; CPU fraction = cpu_ms/(interval_s*1000*cpus)). With hours= or start=/end= (RFC3339, ≤ 15 days back): {"points": [...]} — per-minute ledger history, including archived and deleted sandboxes; a point with measured=false is a gap, never a zero (normalize by its util_seconds).

nv.usage(start=None, end=None) / nv.audit(limit=200, before=None)

Org metering aggregates (per-state vm/cpu/memory seconds + snapshot byte-seconds) and the org’s audit trail of mutating API calls ({id, key_id, action, resource, outcome, request_id, at}; page with before=<last id>).
That is the product. Everything below is for when you need it — nothing there changes how the twelve behave.
Older method names from 0.4.x (exec, exec_background, stop/start, branch, files.ls) keep working forever as aliases of the twelve — existing code never breaks — but new code and these docs use one name per operation.

Errors

AuthError (401/403 → key), NotFoundError (404), ConflictError (409: template not ready / sandbox stopped), TemplateBuildingError (503: first-use build, create() waits), ServerError (5xx: retry idempotent calls). A 503 whose text says “connection error” is a transient control-plane restart: retry in a few seconds. SandboxUnhealthyError (503: the guest stopped answering — a fork bomb or OOM inside it): reboot() or sb.delete() and create a new one; archiving cannot checkpoint a starved guest, so it fails. TransportError: the connection failed before a response — idempotent calls were already retried; for a create the outcome is UNKNOWN, so nv.sandboxes.list() before retrying (never double-create blindly). QuotaError (429/403) is one of two things — read .kind: "rate" = the organization’s per-minute request budget is spent; wait .retry_after seconds (from Retry-After) and retry. Creates, snapshots, forks, exposes, templates and exports share one budget; exec and other operations a much larger one; DELETE is never rate limited, so releasing resources always works. "cap" = the concurrent-sandbox cap; delete or archive a sandbox, then retry — waiting does not help. Never loop on a 429 without waiting. Full status-code table: HTTP API.