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")
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
- Sandboxes
- Public API docs
- Questions and bug reports: code@orkestr.eu
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file orkestr-0.2.1.tar.gz.
File metadata
- Download URL: orkestr-0.2.1.tar.gz
- Upload date:
- Size: 31.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edc909b648075ff38462f9c723e0b6e0078796073a82ebb357ab1012ac88ef8c
|
|
| MD5 |
cb00af90babb497e86bfedcf19c16b4e
|
|
| BLAKE2b-256 |
214991a973ff51d62395b7f17ac3143faec047bdb6bb77fffb3ada190e171642
|
File details
Details for the file orkestr-0.2.1-py3-none-any.whl.
File metadata
- Download URL: orkestr-0.2.1-py3-none-any.whl
- Upload date:
- Size: 23.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b03645e9ec90ee7066a1bb0cc73b7be3ea7da64ef301379bcab979bd81da410
|
|
| MD5 |
b3ad45b5fe118bad973a63a4c4950236
|
|
| BLAKE2b-256 |
f1bb58e46b60543ae5088019a04bddc6cd428d9118601054b441af61462bb4ea
|