Skip to main content

Python SDK for the Isola sandbox platform

Project description

Isola Python SDK

Isola is an open-source sandbox platform for running untrusted and AI-generated code securely on your own Kubernetes cluster. This SDK lets you create sandboxes, execute commands, read and write files, and snapshot environments programmatically.

The SDK provides both synchronous and asynchronous clients.

Install

pip install isola-run

Requires Python 3.10 or later.

How it works

Before diving into code, here is a quick overview of the object model:

  • Isola (or AsyncIsola) is the client. It holds the connections to your Isola instance.
  • client.sandboxes.create() returns a Sandbox, where you run code.
  • A Sandbox exposes .commands for executing processes and .filesystem for reading and writing files.
  • Commands can be blocking (run, waits for completion) or non-blocking (spawn, streams output as it arrives).
  • Rootfs snapshots are separate resources managed through RootfsSnapshot. They let you capture and restore a container's root filesystem changes.

Quick start

ISOLA_URL must point at your Isola api-gateway. There is no default - set it for every deployment:

# Local development (kubectl port-forward)
export ISOLA_URL=http://localhost:8080

# In-cluster (same namespace)
export ISOLA_URL=http://isola-api-gateway

# In-cluster (cross-namespace)
export ISOLA_URL=http://isola-api-gateway.isola.svc.cluster.local

# External (ingress or load balancer)
export ISOLA_URL=https://isola.example.com

Or pass it directly: Isola(url="http://isola-api-gateway.isola.svc.cluster.local")

from isola import Isola

with Isola() as client:
    sandbox = client.sandboxes.create(image="alpine:3.21")
    result = sandbox.commands.run("echo", "hello world")
    print(result.stdout)    # "hello world\n"
    print(result.exit_code) # 0
    sandbox.delete()

The with block closes the HTTP client automatically when it exits. You can also call client.close() instead.

Call sandbox.delete() when you are done with a sandbox. Two alternatives: use with sandbox: to delete automatically on exit, or set timeout_seconds on create() to let the server delete the sandbox after a fixed duration.

Async client

Import AsyncIsola instead of Isola and use await with each call:

from isola import AsyncIsola

async with AsyncIsola() as client:
    sandbox = await client.sandboxes.create(image="alpine:3.21")
    result = await sandbox.commands.run("echo", "hello async")
    print(result.stdout)
    await sandbox.delete()

The async API is identical to the sync API. All examples below use the sync client.

Sandbox options

Customize resources, environment variables, and the startup command:

sandbox = client.sandboxes.create(
    image="python:3.12-slim",
    command=["python", "-m", "http.server", "8080"],
    env={"PORT": "8080", "DEBUG": "1"},
    cpu=0.5,            # CPU cores
    memory=256,         # MiB
    ephemeral_storage=1024,  # MiB
    timeout_seconds=3600,   # auto-delete after 1 hour
)

Skip waiting for the sandbox to be ready:

sandbox = client.sandboxes.create(image="alpine:3.21", max_wait_seconds=0)
print(sandbox.status)  # might be SandboxStatus.PENDING

Sandboxes have no network access by default. See Network configuration to enable it.

Timeouts

Timeout Side Default What it controls
max_wait_seconds Client 65s How long create() polls before returning. Set to 0 to return immediately. Raises IsolaTimeoutError if it expires. The sandbox keeps running on the server regardless.
startup_timeout_seconds Server 60s How long the server gives the sandbox to start. If it expires, the sandbox is marked Failed. Omit to use the server default.
timeout_seconds Server No limit Maximum lifetime of the sandbox. The server begins the termination process after this duration.

Commands

Run (blocking)

run() executes a command and waits for it to finish:

result = sandbox.commands.run("echo", "hello world")
print(result.stdout)      # "hello world\n"
print(result.stderr)      # ""
print(result.exit_code)   # 0

run() does not raise on non-zero exit codes. Always check result.exit_code.

result = sandbox.commands.run(
    "ls", "-la",
    cwd="/tmp",                   # working directory for this command
    env={"LANG": "en_US.UTF-8"},  # merged with sandbox env
    timeout_seconds=30,           # SIGKILL after 30s
)

Running scripts

Shell scripts, pass to sh -c:

script = """
for f in /tmp/*.txt; do
  echo "$f: $(wc -l < "$f") lines"
done
"""
result = sandbox.commands.run("sh", "-c", script)

Python code, pass to python3 -c:

code = """
import json, os
print(json.dumps({"cwd": os.getcwd(), "files": os.listdir(".")}))
"""
result = sandbox.commands.run("python3", "-c", code)

This is the natural pattern when executing LLM-generated code blocks. Both work with multi-line strings, newlines are preserved and interpreted by the shell or Python interpreter as statement separators.

For commands you control, prefer separate args (run("python3", "analyze.py", "--input", filename)), it keeps data separate from the command itself.

Spawn (non-blocking)

spawn() starts a command and returns immediately. Stream output as it arrives:

cmd = sandbox.commands.spawn("sh", "-c", "for i in 1 2 3; do echo line$i; sleep 1; done")
for chunk in cmd.stdout:
    print(chunk, end="")
exit_code = cmd.wait()

Stdin

For simple cases, pass input to run():

result = sandbox.commands.run("cat", input="hello from stdin\n")
print(result.stdout)  # "hello from stdin\n"

For interactive control, use write_stdin() and close_stdin() on a spawned command:

cmd = sandbox.commands.spawn("cat")
cmd.write_stdin("hello\n")
cmd.close_stdin()
cmd.wait()
print(cmd.stdout.read())  # "hello\n"

Command control

cmd = sandbox.commands.spawn("sleep", "60")
cmd.exit_code()  # None (still running)
cmd.kill()
cmd.wait()       # returns exit code

File I/O

# Write text
sandbox.filesystem.write("/tmp/hello.txt", "Hello, World!")

# Write binary data
sandbox.filesystem.write("/tmp/data.bin", b"\x00\x01\x02")

# Upload a local file
with open("local.tar.gz", "rb") as f:
    sandbox.filesystem.write("/tmp/archive.tar.gz", f)

# Read a file
data = sandbox.filesystem.read("/tmp/hello.txt")
print(data.decode())  # "Hello, World!"

Parent directories are created automatically on uploads.

Sandbox management

# List sandboxes
summaries = client.sandboxes.list()
for s in summaries:
    print(s.id, s.status)

# Get a sandbox by ID
sandbox = client.sandboxes.get("sandbox-id")
print(sandbox.status)              # SandboxStatus.RUNNING
print(sandbox.creation_timestamp)  # datetime

# Delete explicitly (instead of using a context manager)
sandbox.delete()

Network configuration

Sandboxes have no network access by default. Pass a Network object to create() to open things up.

Allow full internet access:

from isola import Network

sandbox = client.sandboxes.create(
    image="alpine:3.21",
    network=Network(allow_internet_egress=True),
)

When internet egress or custom CIDRs are enabled without cluster DNS, the server automatically configures public nameservers (8.8.8.8, 1.1.1.1) so DNS resolution works out of the box.

Other network options:

Network(
    allow_internet_egress=False,             # block outbound internet traffic (default)
    allowed_egress_cidrs=["104.16.0.0/12"],  # fine-grained CIDR allowlist
    allow_cluster_dns=False,                 # use the cluster's DNS service
    nameservers=["8.8.8.8"],                 # custom DNS nameservers
    allow_ipv6_egress=False,                 # extend egress config to IPv6
)

Rootfs snapshots

Requires rootfs snapshots to be enabled and a storage bucket configured in your Helm values (operator.sandboxRuntime.rootfssnapshot).

Rootfs snapshots capture one container's root filesystem changes so you can restore them later in a new sandbox. This is useful for pre-warming environments: install dependencies once, snapshot, then spin up fresh sandboxes from that snapshot.

Create a snapshot

snapshot = client.rootfs_snapshots.create(
    sandbox_id=sandbox.id,
    snapshot_name="my-snapshot",
)
print(snapshot.status)  # RootfsSnapshotStatus.SUCCEEDED

create() blocks max_wait_seconds until the snapshot completes. Pass max_wait_seconds=0 to return immediately.

Restore from a snapshot

restored = client.sandboxes.create(
    image="alpine:3.21",
    rootfs_snapshot_name="my-snapshot",
)

Full round-trip example

from isola import Isola, Network

client = Isola()

# 1. Install a heavy stack once, with internet connectivity
with client.sandboxes.create(
    image="python:3.12-slim",
    network=Network(allow_internet_egress=True),
    ephemeral_storage=4096,
) as sandbox:
    sandbox.commands.run("pip", "install", "numpy", "pandas", "scikit-learn")
    snapshot = client.rootfs_snapshots.create(
        sandbox_id=sandbox.id,
        snapshot_name="datascience-base",
    )

# 2. Restore from the snapshotted rootfs, packages already installed, no internet needed
with client.sandboxes.create(
    image="python:3.12-slim",
    ephemeral_storage=4096,
    rootfs_snapshot_name="datascience-base",
) as sandbox:
    result = sandbox.commands.run("python3", "-c", """
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
X, y = load_iris(return_X_y=True)
print(RandomForestClassifier(random_state=0).fit(X, y).score(X, y))
""")
    print(result.stdout)

Automatic snapshots on termination

You can also configure a sandbox to snapshot automatically before it terminates:

from isola import SnapshotRootfs

sandbox = client.sandboxes.create(
    image="alpine:3.21",
    termination_policy=SnapshotRootfs(snapshot_name="on-exit-snapshot"),
)

Checking snapshot status

snapshot = client.rootfs_snapshots.get(snapshot.id)
print(snapshot.status)  # RootfsSnapshotStatus.SUCCEEDED

Multi-container sandboxes

For advanced use cases, you can run multiple containers in a single sandbox. Use the containers parameter instead of image:

from isola import Container

sandbox = client.sandboxes.create(
    containers=[
        Container(
            name="app",
            image="python:3.12-slim",
            command=["python", "-m", "http.server", "8080"],
        ),
        Container(
            name="worker",
            image="alpine:3.21",
        ),
    ],
)

Target a specific container when running commands or writing files:

result = sandbox.commands.run("wget", "-qO-", "http://127.0.0.1:8080", container="worker")
sandbox.filesystem.write("/tmp/data.txt", "hello", container="app")

Error handling

API and SDK errors inherit from IsolaError:

IsolaError
├── APIError
│   ├── BadRequestError
│   ├── NotFoundError
│   ├── ConflictError
│   ├── ValidationError
│   ├── InternalError
│   └── BadGatewayError
├── IsolaTimeoutError
└── APIConnectionError
from isola import IsolaError, IsolaTimeoutError, NotFoundError, APIConnectionError

try:
    sandbox = client.sandboxes.get("nonexistent")
except NotFoundError as e:
    print(e.status_code)  # 404
    print(e.message)
except IsolaTimeoutError:
    print("Timed out waiting")
except APIConnectionError:
    print("Could not reach the API")
except IsolaError:
    print("Something else went wrong")

The SDK automatically retries on transient errors.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

isola_run-0.5.0rc0.tar.gz (18.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

isola_run-0.5.0rc0-py3-none-any.whl (26.6 kB view details)

Uploaded Python 3

File details

Details for the file isola_run-0.5.0rc0.tar.gz.

File metadata

  • Download URL: isola_run-0.5.0rc0.tar.gz
  • Upload date:
  • Size: 18.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for isola_run-0.5.0rc0.tar.gz
Algorithm Hash digest
SHA256 d100da0fb5ca25fc0384063aea6b9b6f9676cecbc6742bbb5ccec4aad2abb37c
MD5 638f780156320adc0847e33c698b7246
BLAKE2b-256 1d505dcb7daeafa00db2865ebbda8002206d8daf5dcc6935e1218c63153b2c85

See more details on using hashes here.

Provenance

The following attestation bundles were made for isola_run-0.5.0rc0.tar.gz:

Publisher: release.yml on isola-run/isola

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file isola_run-0.5.0rc0-py3-none-any.whl.

File metadata

  • Download URL: isola_run-0.5.0rc0-py3-none-any.whl
  • Upload date:
  • Size: 26.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for isola_run-0.5.0rc0-py3-none-any.whl
Algorithm Hash digest
SHA256 81bccfab7711a533390afe5c32e0c75942d55c924c41a64e152b997d3a6be8a3
MD5 c0fac5ab4ece72111f220cf81ffe94e1
BLAKE2b-256 93e211d50d962b2fda9db6d8f18c721686ec4dd2076f31cebfcb9eedfc618127

See more details on using hashes here.

Provenance

The following attestation bundles were made for isola_run-0.5.0rc0-py3-none-any.whl:

Publisher: release.yml on isola-run/isola

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page