Skip to main content

EU-hosted sandbox VMs for AI agents - official Python SDK

Project description

orkestr Python SDK

EU-hosted sandbox VMs for AI agents. Type-safe Python client for the api.orkestr.eu/v1/sandboxes REST API.

Status: 0.1.0 - first stable release. The client surface may still have breaking changes within the 0.x line; 1.0.0 will signal API stability.

Install

pip install orkestr

Requires Python 3.10+.

Authenticate

Mint an API token in the orkestr console with the sandboxes:write scope.

export ORKESTR_API_KEY="ork_..."

The SDK picks the key up from ORKESTR_API_KEY or you pass it explicitly.

Quick start

One-shot execution inside a fresh sandbox. The with block auto-terminates the sandbox on exit, preventing runaway costs if the caller crashes.

from orkestr import Sandbox

with Sandbox.create(template="python-3.12") as sbx:
    sbx.files.write("/workspace/main.py", "print(sum(range(1_000_000)))")
    result = sbx.exec("python /workspace/main.py")
    print(result.stdout)        # 499999500000
    print(result.duration_ms)   # ~120

API

Create a sandbox

sbx = Sandbox.create(
    template="python-3.12",          # one of the templates listed below
    size="small",                    # "small" | "medium" | "large" (tier-capped)
    network="off",                   # "off" | "restricted" | "open"
    timeout_seconds=600,             # auto-terminate after this many seconds
    env={"OPENAI_API_KEY": "sk-..."},
    metadata={"agent_run": "r_123"},
    region="fsn1",                   # region code (Sandbox.limits().regions) | None for auto
    api_key=None,                    # falls back to ORKESTR_API_KEY
)
print(sbx.id)             # "sbx_01HXYZ..."
print(sbx.status)         # "running"

Custom egress allowlist (restricted sandboxes)

A restricted sandbox reaches a default set of HTTPS hosts (package registries, GitHub, major LLM APIs). Pass allow_domains to point it at your own hosts instead - an internal API, a private package mirror, a vendor endpoint. The list replaces the default entirely (still HTTPS-only and proxy-mediated), so include any registries you still need. Sandbox.limits().default_egress_domains returns the default set to start from.

sbx = Sandbox.create(
    template="python-3.12",
    network="restricted",
    allow_domains=["pypi.org", "pythonhosted.org", "api.internal.example.com"],
)

Templates

Template Description
python-3.12 CPython 3.12 with pip and common libs
python-3.12-bare CPython 3.12 only, faster start
node-22 Node 22 with npm
debian-12 Real Debian bookworm - general-purpose base where apt-get works

Sizes

size picks from a fixed menu. Allowed sizes depend on your sandbox access tier (free = email verified, payg = card on file, enterprise).

Size vCPU RAM Tiers
small 1 1 GB free, payg, enterprise
medium 2 4 GB payg, enterprise
large 4 8 GB payg, enterprise

Check your tier limits

Sandbox.limits() reports the sizes and caps available to your API key's access tier — pick a size up front instead of discovering the limit from a PlanLimitError. Handy when the same code runs under keys on different tiers (e.g. a reseller provisioning per customer).

limits = Sandbox.limits()
limits.plan                        # "free"  (your sandbox access tier)
limits.allowed_sizes               # ["small"]
"medium" in limits.allowed_sizes   # False
limits.max_concurrent              # 1
limits.trial_credit_eur            # 10.0  (one-time trial credit, EUR; payg gets 100)
limits.trial_credit_used_eur       # 12.5  (EUR of the credit consumed)
for s in limits.sizes:             # full menu, each with .allowed
    print(s.size, s.cpu, s.memory_mb, s.allowed)

Sandbox compute is a one-time EUR trial credit that does not reset: compute is metered at the public pay-as-you-go rates and drawn against the credit. When trial_credit_used_eur >= trial_credit_eur, Sandbox.create() raises PlanLimitError with code="trial_exhausted" (free hits a hard wall; paid switches to pay-as-you-go). Metering rolls up actual cgroup CPU + working-set RAM per second, so an idle sandbox accrues only what it really uses.

Live metrics

sbx.metrics() returns this sandbox's live CPU and memory - the latest reading, a rolling ~60s sample window for sparklines, and its lifetime totals. Use it to watch a workload for saturation or memory pressure without instrumenting the workload itself.

m = sbx.metrics()
m.cpu.usage_percent     # 47.0  (% of allocated cores; pegged 1-core = 100)
m.cpu.usage_cores       # 0.94  (cores in use of m.cpu.cores)
m.memory.usage_bytes    # 1879048192  (working set, excludes reclaimable cache)
m.memory.usage_percent  # 43.7  (% of m.memory.limit_bytes)
m.lifetime.cpu_seconds  # 1284.31  (on-CPU seconds since the sandbox started)
for s in m.samples:     # oldest first; s.t is a datetime
    print(s.t, s.cpu_percent, s.mem_bytes)

It is telemetry, not a state change: a paused or terminated sandbox returns a result with null live usage (check m.sandbox_status) and an empty samples window - lifetime is still populated. Poll no faster than m.sample_interval_seconds; pass since=<datetime> to fetch only samples newer than your last poll.

Expose a port (preview URLs)

sbx.get_host(port) returns a public hostname for an HTTP port a process in the sandbox is serving. Build the URL as f"https://{sbx.get_host(port)}" and open it in a browser or embed it in your app: a live preview of a dev server running inside the sandbox.

sbx = Sandbox.create(template="node-22", network="open")

# Start something that listens on a port (non-blocking, in the background):
sbx.exec("nohup python3 -m http.server 3000 >/tmp/srv.log 2>&1 &")
# ...or your framework's dev server, e.g. `npm run dev -- --port 3000`.

host = sbx.get_host(3000)          # "3000-01hxyz....sbx.orkestr.run"
url = f"https://{host}"            # open in a browser or embed it in your app

The URL is public - its only capability is the unguessable sandbox id in the hostname, since an embedded preview cannot send an Authorization header. It is stable for the sandbox's lifetime but only serves traffic while the sandbox is running (a paused or terminated sandbox returns nothing). WebSockets ride through, so dev-server HMR works.

get_host requires a card on file (paid tier) and a networked sandbox (network="restricted" or "open"); an off sandbox has no port to expose. It raises in either case. The sandbox GET/list payloads also carry preview_host_base (the port-less <id>.<base> suffix, or None) for building these URLs in a UI.

Run a command

result = sbx.exec("python /workspace/main.py", timeout_seconds=60)
result.stdout       # str
result.stderr       # str
result.exit_code    # int
result.duration_ms  # int

Stream a long command

for chunk in sbx.exec_stream("python long_task.py"):
    if chunk.stream == "stdout":
        print(chunk.data, end="", flush=True)
    else:
        print(chunk.data, end="", flush=True, file=sys.stderr)

Files

sbx.files.write("/workspace/data.json", '{"x": 1}')
sbx.files.write_bytes("/workspace/blob.bin", b"\x00\x01\x02")

content = sbx.files.read("/workspace/out.txt")       # returns str
raw = sbx.files.read_bytes("/workspace/blob.bin")     # returns bytes

for entry in sbx.files.list("/workspace"):
    print(entry.name, entry.is_dir, entry.size)

sbx.files.delete("/workspace/out.txt")

Persistent volumes

/workspace is fast scratch space, but it is RAM-backed and gone when the sandbox terminates. To keep data across sandboxes, attach a persistent volume - a named disk mounted at /persist. Anything written under /persist survives termination and comes back when a later sandbox attaches the same volume.

# First run: write results to the volume.
sbx = Sandbox.create(template="python-3.12", volume="my-cache")
sbx.run('echo "trained-weights-v1" > /persist/model.txt')
sbx.terminate()

# Hours or days later, a brand-new sandbox re-attaches the same volume:
sbx = Sandbox.create(template="python-3.12", volume="my-cache")
print(sbx.run("cat /persist/model.txt").stdout)   # -> trained-weights-v1
print(sbx.volume)                                   # -> "my-cache"

A volume is created on first reference (default 10 GB), so you usually just name it in create(). Notes:

  • A volume can back one running sandbox at a time - attaching a volume that's already in use raises a 409 (volume_in_use); terminate the holder first.
  • A volume lives in one region, and a sandbox attaching it runs there. A new volume homes wherever its first sandbox lands (so a region on that first Sandbox.create places both); after that, passing a different region is a clear 400 - omit it, or move the volume first.
  • Manage volumes explicitly to pick a size or region up front, relocate one, or clean up:
from orkestr import Volume

vol = Volume.create("datasets", size_gb=50, region="rbx")
for v in Volume.list():
    print(v.name, v.size_mb, v.status, v.region)
Volume.move(vol.id, "hel1")   # detached only; data follows via its checkpoint
Volume.delete(vol.id)     # rejected while attached to a running sandbox

Valid region ids come from Sandbox.limits().regions - the live fleet, not a fixed list.

Pause + resume

Pausing snapshots the sandbox memory + disk and stops the compute meter. Resume restores it on the same or a different host. pause() returns the sandbox id; persist it across processes and pass to Sandbox.resume.

sbx = Sandbox.create(template="node-22", network="restricted")
sandbox_id = sbx.pause()
# ... minutes or hours later, from any worker:
sbx = Sandbox.resume(sandbox_id)

Snapshot retention is tier-capped (free: 1, payg: 10, enterprise: 50). Calling pause() over the cap raises SnapshotCapReached.

Terminate

Context-manager exit calls terminate(). Outside with:

sbx.terminate()

After terminate() the sandbox row stays in your account history but the VM and any in-memory state are gone.

List your sandboxes

for sbx in Sandbox.list(status="running"):
    print(sbx.id, sbx.template, sbx.created_at)

Errors

All SDK errors inherit from orkestr.OrkestrError.

Exception When
AuthError Missing / invalid / expired API key, or scope mismatch
RateLimitError Plan rate limit hit
PlanLimitError Sandbox limit, concurrent limit, snapshot cap
FleetFull Fleet momentarily at capacity (auto-retried first)
SandboxNotFound sandbox_id doesn't exist or isn't yours
SandboxNotReady Operation called on a paused / terminated sandbox
ExecTimeout exec() exceeded timeout_seconds
NetworkPolicyError Command tried to reach a host the policy blocks
OrkestrError Any other API-level error

When the fleet is momentarily full, create() and resume() back off and retry automatically (honouring the server's Retry-After), so a transient capacity blip doesn't kill a long-running agent. They only raise FleetFull once the retry budget is spent - tune it per call with max_retries / retry_max_seconds, or pass max_retries=0 to handle the backoff yourself.

Async support

The MVP SDK is sync only. Async (AsyncSandbox) lands in v0.2.0 if a design partner blocks on it; the wire format is identical.

Versioning

Follows Semantic Versioning. The SDK targets the /v1/sandboxes API; bumps to v1 of the API are non-breaking for SDK callers. v2 of the API will require an SDK major.

Links

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

orkestr-0.4.0.tar.gz (44.9 kB view details)

Uploaded Source

Built Distribution

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

orkestr-0.4.0-py3-none-any.whl (32.7 kB view details)

Uploaded Python 3

File details

Details for the file orkestr-0.4.0.tar.gz.

File metadata

  • Download URL: orkestr-0.4.0.tar.gz
  • Upload date:
  • Size: 44.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for orkestr-0.4.0.tar.gz
Algorithm Hash digest
SHA256 21c33a5e81c8845e3a78b9d774b9d79693c20163a2a62d2cdcf56553f87ce9f0
MD5 7d8e1414ec256555f0649c9359e46b1c
BLAKE2b-256 8e869f0de4333241b8b9244223b6f558656d2acfb2e32044f9a9c9a640b6b9b5

See more details on using hashes here.

File details

Details for the file orkestr-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: orkestr-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 32.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for orkestr-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e2cce0e621f1fa8ae96c4bb0369ff6d2ba6150f0ca958b101fde3df7e77d5f15
MD5 1af391f8af2ef03ca65be6719e366f88
BLAKE2b-256 f022eb97848cb9ece1fc4e89c7dc227e7576e2f19d3edc0e14f4c380564315b9

See more details on using hashes here.

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