Client SDK for sandkiln — a compute primitive for safely running untrusted or AI-generated code in hardware-isolated Firecracker microVMs. Each sandbox is a real microVM: its own kernel, its own filesystem, its own network.
This package is the client. It talks to a sandkilnd daemon over HTTP —
you need one running somewhere reachable (see the main repo for how to
run one; there is no hosted service). Zero runtime dependencies —
urllib from the standard library is all it needs.
Not published to PyPI yet — install from this repo:
pip install ./packages/python
from sandkiln import Sandbox
sandbox = Sandbox.create(tags={"env": "ci"})
result = sandbox.run_command("python3", ["analyze.py"])
print(result.stdout, result.exit_code)
sandbox.write_file("/tmp/config.json", '{"ok": true}')
data = sandbox.read_file("/tmp/config.json")
running = Sandbox.list(tags={"env": "ci"})
sandbox.stop()- Daemon URL: pass
base_urltoSandbox.create()/Sandbox.list(), or setSANDKILN_DAEMON_URL. Defaults tohttp://127.0.0.1:7777. - Auth: pass
auth_token, or setSANDKILN_AUTH_TOKEN, if the daemon hasSANDKILN_AUTH_TOKENset. Omit entirely for an unauthenticated local daemon.
Sandbox.create(name=None, tags=None, base_url=None, auth_token=None, vcpu_count=None, mem_size_mib=None, image_id=None)— boots a sandbox.nameis a caller-given identity, unique among live sandboxes and held snapshots (409 if already taken) — seeby_name/get_or_createbelow to find it again later.vcpu_count/mem_size_miboverride the daemon's configured defaults for this one sandbox, subject to the daemon's configured ceiling.image_idboots from a registered image (seeImage.registerbelow) instead of the daemon's configured default rootfs.Sandbox.attach(id, base_url=None, auth_token=None)— wraps an existing sandbox id without a network round-trip.Sandbox.by_name(name, base_url=None, auth_token=None)— resolves a name to a live sandbox and returns a handle to it. RaisesSandkilnApiError(409) if the name currently belongs to a stopped (snapshotted) sandbox instead — useget_or_createif you want that resumed automatically.Sandbox.get_or_create(name, tags=None, base_url=None, auth_token=None, vcpu_count=None, mem_size_mib=None)— resolvesnameto a sandbox in one race-safe call: a live sandbox with this name is returned as-is, a stopped one is resumed, otherwise a fresh one is created and given this name. Returns(sandbox, created).Sandbox.list(tags=None, base_url=None, auth_token=None)— lists sandboxes;tagsfilters by exact match on every given key.sandbox.run_command(command, args=None)— returns anExecResult(stdout,stderr,exit_code).sandbox.read_file(path)— returns file contents asbytes.sandbox.write_file(path, content)—contentisstrorbytes.sandbox.preview_url(port, path="/")— the URL a browser can open directly to reach a server listening onportinside this sandbox, proxied through the daemon.sandbox.stop(keep=None)— stops the sandbox. By default (keepomitted orTrue) this preserves its state as a resumable snapshot, same assnapshot(), and returns aStopResult(kept, snapshot_id). Passkeep=Falsefor the old "just destroy it" behavior — no snapshot, nothing left to resume.sandbox.snapshot()— saves the sandbox's full state to disk and stops it; returns a snapshot id. The daemon can also do this on its own, for an idle sandbox, if the operator hasSANDKILN_AUTO_SUSPEND_TIMEOUT_SECSconfigured — seeSandbox.list_snapshotsbelow for how to notice it and find the resulting snapshot.Sandbox.resume(snapshot_id, base_url=None, auth_token=None)— boots a new sandbox from a snapshot, consuming it (the snapshot is gone afterward).Sandbox.fork(snapshot_id, base_url=None, auth_token=None)— boots a new sandbox from a snapshot without consuming it, so it can be forked or resumed again later. Only one live fork of a given snapshot may run at a time — a second concurrentfork()raisesSandkilnApiErrorwith status 409 until the first is stopped; seeROADMAP.md's "Persistence and snapshotting" section for why.Sandbox.list_snapshots(source_sandbox_id=None, base_url=None, auth_token=None)— lists snapshots.source_sandbox_idnarrows this to the (at most one) snapshot taken from that original sandbox id — the way to find out whether a sandbox id that dropped out ofSandbox.list()turned into a snapshot (via a manualsnapshot()or the daemon's auto-suspend) and what its new id is.Image.register(id, path, base_url=None, auth_token=None)— registers an already-built ext4 rootfs file atpathon the daemon's own host filesystem underid, forSandbox.create(image_id=...)to boot from. Not a file upload — the daemon can't verify the guest agent is baked in without root access to loop-mount it (ImageInfo.guest_agent_verifiedis alwaysFalse); runscripts/preflight-check.sh --root-checks --rootfs-image <path>out of band first.Image.list(base_url=None, auth_token=None)/Image.delete(id, base_url=None, auth_token=None)— list registered images, or delete one (refused with 409 while any live sandbox, in-flight boot, or held snapshot still references it).
This mirrors the JS/TS SDK
exactly — same daemon, same operations, Python-idiomatic naming
(run_command not runCommand, snake_case fields).
This SDK matches the daemon's current HTTP API exactly — no more, no less. Still open: publishing to PyPI, streamed command output, and attaching persistent drives at create time (supported by the daemon and CLI, not yet exposed here); see the roadmap.
MIT