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",                   # "fsn1" (DE) | "hel1" (FI) | 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.
  • Attaching a volume pins the sandbox to the volume's host, so pass the same (or no) region.
  • Manage volumes explicitly if you want a custom size or to clean up:
from orkestr import Volume

vol = Volume.create("datasets", size_gb=50)
for v in Volume.list():
    print(v.name, v.size_mb, v.status)
Volume.delete(vol.id)     # rejected while attached to a running sandbox

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.2.2.tar.gz (33.6 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.2.2-py3-none-any.whl (24.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for orkestr-0.2.2.tar.gz
Algorithm Hash digest
SHA256 26bdba17413d30acdad77d3f32ce3c0cf6951d03169f1621e0430f9b7b150677
MD5 19889ef1923d53021657e56a58929483
BLAKE2b-256 7bec5a4fa35b016ebc7cdc19518bf283413c6bdd58f62366431582caf1580a18

See more details on using hashes here.

File details

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

File metadata

  • Download URL: orkestr-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 24.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.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 7ce40d9d0f3a6acb67afad4dfceca38a9fb933e893be29b6ee57d3dad14a38af
MD5 effd103a1d0c35fdd5ddff0e8c3953b7
BLAKE2b-256 417b8656b40a760f7b6dfac37dbde1f61c4866cf303612cafdcb6871dd1a6d46

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