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.3.0.tar.gz (43.3 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.3.0-py3-none-any.whl (31.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for orkestr-0.3.0.tar.gz
Algorithm Hash digest
SHA256 56bef552dd4874b45e8a7e9bc29df43162a33c140aa50b04497392433a7c3ddc
MD5 46194fb44e7260d60f2d24c8ee1667da
BLAKE2b-256 c12b0a7d2e72eb9f58904ab8e06cb592ac5c79de081ab6a5bba4df2f7c04a56d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: orkestr-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 31.5 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.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 abd5a3f1dcf4fc0dbf0df686c736089e3b1a85214d3fb9aa3c90e183aa8c548f
MD5 b5c52ba74726b88749d31799cec5dcfe
BLAKE2b-256 b4d3340781e0f8d5080fd3c646f154e169c9422b3df612a242e0888940f8e465

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