Skip to content
sistemo.io beta

Sandbox API (Python)

from sistemo import Sandbox

Sandbox(...)

Creating a Sandbox provisions a real microVM and blocks until it boots.

Sandbox(
    *,
    client=None,            # reuse a Client (advanced)
    api_key=None,           # defaults to SISTEMO_API_KEY
    base_url=None,          # defaults to SISTEMO_BASE_URL or https://api.sistemo.io
    name="",                # optional label
    vcpus=1,
    memory_mb=1024,
    rootfs_size_gb=10,
    stack="base",           # base | python | node | ai | debian13 | ubuntu26.04
    metadata=None,          # dict of arbitrary key/values
    root_volume_id="",      # boot an existing detached root disk
    idempotency_key=None,   # defaults to a fresh UUID
)
Argument Default Notes
api_key SISTEMO_API_KEY Must be a full-scope key to create/run
base_url https://api.sistemo.io Override the API endpoint
vcpus 1 Virtual CPUs
memory_mb 1024 RAM in MB
rootfs_size_gb 10 Root disk in GB
stack "base" Base image; see Images
metadata {} Stored with the machine
root_volume_id "" Boot a disk kept by an earlier close(preserve_storage=True). Size and stack come from the volume, so the rootfs_size_gb and stack arguments are ignored
idempotency_key random UUID See below

Attributes: sb.id (machine UUID), sb.state (e.g. "running"), sb.root_volume_id.

The defaults fit the free quota

1 vCPU / 1024 MB / 10 GB fits a free account (up to 2 vCPU / 2 GB per machine, 3 machines). That is what keeps a bare Sandbox() working on a new account — and it means raising a default breaks first-time users unless their quota is raised too.

sb.run(script, timeout=30)

Run a shell command/script inside the sandbox.

r = sb.run("ls -la /", timeout=60)

Returns an ExecResult:

Field Type Meaning
exit_code int Process exit code (-1 if unknown)
stdout str Standard output
stderr str Standard error
ok bool True when exit_code == 0

timeout is seconds (default 120, ceiling 24 hours), enforced in the machine. This is start + wait on POST /execs. For streaming or reconnect use start() — long-running.

sb.start()

Start a command that outlives this request. Returns a Job immediately.

job = sb.start("npm ci && npm run build", timeout=3600)
for chunk in job.stream():
    print(chunk, end="")
result = job.wait()
print(result.state, result.exit_code)

timeout is the job's lifetime in the machine (default 1 hour, max 24). The SDK mints exec_id if you omit it — pass your own to reconnect after a crash.

Job
id / state / exit_code Handle, status, code (None until known)
done / ok Terminal? Exited 0?
truncated Output hit the 32 MiB cap
wait(timeout=None) Block until terminal. Client timeout does not cancel the job
stream(stream="stdout") Yield output as it appears
output(stream="stdout") Bytes so far
cancel() SIGTERM, then SIGKILL
sb.job(id) / sb.jobs() Re-attach / list

If start raises ExecStartUnconfirmed, the command may be running. The exception carries exec_id — poll sb.job(...), do not start again.

sb.close(preserve_storage=False)

Destroy the sandbox. Pass preserve_storage=True to keep its disk.

sb.close()                        # destroy disk too
sb.close(preserve_storage=True)   # keep the volume

Context manager

with Sandbox() as sb:
    sb.run("...")
# sb.close() called automatically, even on exception

Idempotency — why a retry cannot double-boot

Creating a machine sends an Idempotency-Key header. If the response is lost to a dropped connection, the retry returns the machine that was already created instead of booting a second one you would be billed for and never see.

The SDK generates a fresh UUID per Sandbox() call, which is right for almost everyone. Pass your own only when your code may re-enter the same logical create — for example a job runner that retries a task:

sb = Sandbox(idempotency_key=f"job-{job_id}")   # same job → same machine

Timeouts and retries

from sistemo import Client, Sandbox

c = Client(
    timeout=120.0,          # per-request seconds; create blocks until boot
    max_retries=2,          # extra attempts after the first → up to 3 tries
    retry_backoff=0.4,      # base backoff seconds, exponential + jitter
    retry_max_backoff=8.0,
)
sb = Sandbox(client=c)

What gets retried:

Retried on 408/429/5xx? Retried on a dropped connection?
GET, DELETE Yes Yes
POST with an idempotency key (create) Yes Yes
POST without one — including run() No Yes

run() is never retried on an HTTP error, on purpose

A replayed exec runs your script a second time. Since the SDK cannot know whether your script is safe to repeat, it does not decide for you: an HTTP error from run() is yours to handle. Connection failures before a response are still retried, because nothing ran.

Low-level Client (advanced)

For endpoints the Sandbox helper doesn't cover — stop, start, volumes, snapshots, quotas, billing, the Activity feed — use the raw client:

from sistemo import Client

c = Client()                                   # reads SISTEMO_API_KEY / SISTEMO_BASE_URL

machines = c.request("GET", "/v1/machines")
c.request("POST", f"/v1/machines/{mid}/stop")
c.request("POST", f"/v1/machines/{mid}/start")
quotas   = c.request("GET", "/v1/quotas")
activity = c.request("GET", "/v1/activity?category=machine")

Signature:

Client(api_key=None, base_url=None, timeout=120.0, *,
       max_retries=2, retry_backoff=0.4, retry_max_backoff=8.0)

c.request(method, path, body=None, *, headers=None,
          idempotency_key=None, max_retries=None)

Pass idempotency_key= on a POST you may safely repeat, and max_retries=0 to disable retries for a single call. See the API reference for paths and response shapes.

Volume helpers

Client wraps the volume endpoints so you do not have to hand-write the paths:

from sistemo import Client

c = Client()
volumes = c.list_volumes()                      # list[dict] — [] when you have none
vol     = c.create_volume("agent-data", 10)     # name, size_gb
c.attach_volume(machine_id, vol["id"])          # machine must be STOPPED
c.detach_volume(machine_id, vol["id"])
c.delete_volume(vol["id"])
Method Returns
list_volumes() list[dict] — root and data volumes
create_volume(name, size_gb) dict — the new volume
attach_volume(machine_id, volume_id) dict
detach_volume(machine_id, volume_id) dict
delete_volume(volume_id) dict

create_volume, attach_volume and detach_volume each send their own Idempotency-Key, so a dropped connection cannot create or move a volume twice.

list_volumes() returns a list here, an object in JS

Python gives you list[dict] directly; @sistemo/sdk gives you { volumes: Volume[] }. Both normalise the API's null to empty, so neither ever hands you None/undefined — but the unwrapping differs, and that is the line to change when porting an example across.

A detached root volume can be booted onto a new sandbox with root_volume_id — that is how you keep a disk and come back to it. See Persist data with volumes.