Skip to content

Python SDK reference ​

opendsec is OpenDsec's asynchronous Python SDK, built on gRPC and asyncio. This page lists all of its classes and methods. The concepts behind them (backends, states, timeouts, sessions, permissions) are in Concepts.

  • Python 3.10 or newer; depends on grpcio>=1.73.1, protobuf>=6.31,<8 and googleapis-common-protos>=1.60.
  • Licensed Apache-2.0: programs that call OpenDsec are not bound by the server's AGPL.
  • Every method is a coroutine: await it inside an async function.

Install ​

sh
# from the source repository
python3 -m venv .venv && .venv/bin/pip install ./sdk/python
# from a wheel your administrator gave you
pip install opendsec-0.11.0-py3-none-any.whl

Offline: the offline bundle's wheels/ directory holds the SDK and all its dependencies; install with pip install --no-index --find-links wheels opendsec.

Quick example ​

python
import asyncio
from opendsec import DSecClient, DSecRunArgs, DSecError

async def main():
    async with DSecClient() as client:            # reads the OPENDSEC_* variables
        sb = await client.run_sandbox(DSecRunArgs(
            backend="microvm", image="root/py:3.12",
            cpu_cores_limit=1, memory_limit_mb=1024,
            ttl_running_stop=600,                   # stop after 10 idle minutes
            network_rules={"pypi": True}))          # PyPI only
        try:
            r = await sb.run_shell("pip install requests && python3 -c 'import requests; print(requests.__version__)'")
            print(r.exit_code, r.stdout, r.stderr)
        finally:
            await sb.stop()

asyncio.run(main())

Connecting: DSecClient ​

python
DSecClient(endpoint=None, token=None, project=None, ca_file=None,
           server_name=None, retry_timeout=60)
ParameterEnvironment variableDefaultMeaning
endpointOPENDSEC_ADDR127.0.0.1:7100Apiserver address host:port. Several addresses, comma separated (10.0.0.11:7100,10.0.0.12:7100), spread calls over every reachable apiserver and move off one that goes down
tokenOPENDSEC_TOKENemptyThe caller's credential dsk.<id>.<secret>, sent with every call
projectOPENDSEC_PROJECTrootDefault project for creations, listings, task actions and image listings
ca_fileOPENDSEC_CAnoneThe cluster's CA certificate. Set, the client uses TLS; unset, plaintext, for development clusters only
server_name—none; api.opendsec.internal with several addressesThe name the server certificate is checked against. Every apiserver certificate carries api.opendsec.internal
retry_timeout—60How long (seconds) calls keep retrying while a server is unreachable (unavailable: a restart, a failover). 0 disables retries
python
async with DSecClient() as client:      # recommended: connects on entry, closes on exit
    ...

client = DSecClient(endpoint="10.0.0.10:7100", token=tok, project="root/team-a", ca_file="ca.crt")
await client.open()                      # or open and close it yourself
...
await client.close()

Connection settings (automatic): reconnects start after 100 ms and back off to at most 2 s; a keepalive every 10 s detects a silently lost connection within about 15 s; messages may be up to 80 MB.

Creation arguments: DSecRunArgs ​

python
@dataclass
class DSecRunArgs:
    backend: str = "sim"
    image: str = ""
    cpu_cores_limit: float = 1.0
    memory_limit_mb: int = 2048
    gpus: int = 0
    gpu_mode: str = ""
    disk_mb: int = 0
    ttl_running_stop: int = 0
    ttl_max: int = 0
    network_rules: dict[str, bool] = {}
    init_user: str = "root"
    task_id: str = ""
    qos: str = "LS"
    labels: dict[str, str] = {}
    project: str | None = None
FieldMeaning
backendmicrovm, container, fncall, fullvm or sim. The default, sim, only simulates and runs nothing: always set it
imageImage name <project>/<name>:<tag>, e.g. root/py:3.12. Needs layer.use on the image's project
cpu_cores_limitCPU cores; may be fractional
memory_limit_mbMemory (MB). Creating from a snapshot with memory, it must equal the snapshot's
gpus, gpu_modeNumber of GPUs; gpu_mode is exclusive or shared (default: the node decides)
disk_mbWritable disk size (MB); 0 means the backend's default
ttl_running_stopIdle timeout (seconds); 0 means the cluster default (usually 1 hour)
ttl_maxLifetime (seconds) from creation, paused time included; 0 means the cluster default (usually 1 day); usually at most 7 days
network_rulesNetwork rules, e.g. {"pypi": True}, {"*": True}, {"github.com:443": True}. See Network policy
init_userThe sandbox's default user, also the sessions' default user
task_idTask name, for task actions such as pause_task
qosLS (latency-sensitive, default) or BE (best effort)
labelsArbitrary key-value labels, kept in the sandbox's spec
projectThe project to create in; default: the client's project

DSecContainerRunArgs is a subclass with backend="container" by default and a container_image field (as the paper writes it) that replaces image when set:

python
DSecContainerRunArgs(container_image="root/py:3.12", memory_limit_mb=4096, cpu_cores_limit=4)

Creating and querying ​

run_sandbox(args, timeout=120) -> Sandbox ​

Creates a sandbox and returns once it is RUNNING.

  • timeout: limit for the whole creation (seconds); None for none.
  • While the server is unreachable, the call retries within retry_timeout. Each creation carries a random idempotency key: even if the first answer was lost, a retry returns the sandbox the first attempt created, never a second one.
  • Needs sandbox.create on the project and layer.use on the image's project.
  • Common errors: quota_exceeded, no_capacity, invalid (no such image or backend, a timeout over the limit), forbidden.

run_container(args, timeout=120) -> Sandbox ​

The same as run_sandbox, usually with DSecContainerRunArgs.

run_sandboxes(args, count, timeout=600) -> list[Sandbox | DSecError] ​

Creates count sandboxes of one spec, up to 32768 per call.

  • The list is in index order: Sandbox for each success, DSecError for each failure (not raised).
  • The server queues creations at the rate limit, so rate limiting never fails part of a large batch.
  • A lost connection is retried; only what is still missing gets created.
  • If the batch request itself fails (no permission, say), DSecError is raised.
python
res = await client.run_sandboxes(DSecRunArgs(backend="microvm", image="root/py:3.12", task_id="job-7"), 500)
ok = [x for x in res if not isinstance(x, DSecError)]
failed = [x for x in res if isinstance(x, DSecError)]

run_containers(args, count, timeout=600) ​

The same as run_sandboxes.

get_sandbox(sandbox_id) -> Sandbox ​

Looks a sandbox up by id. Needs sandbox.operate on its project. Ended sandboxes stay on their node for 10 minutes; after that, not_found.

list_sandboxes(task_id="", include_ended=False, project=None) -> list[Sandbox] ​

Lists the sandboxes of a project and all its subprojects. task_id narrows to one task; include_ended=True includes recently ended ones. Needs sandbox.operate.

cluster_status() -> Snapshot ​

The cluster snapshot: each edge's address, health, draining state, backends, capacity, allocated resources, sandboxes per backend, free memory and disk, and pressure (protobuf Snapshot; fields in proto/opendsec/v1/cluster.proto and EdgeState in types.proto).

Sandbox handle: Sandbox ​

run_sandbox, get_sandbox and the others return Sandbox objects.

AttributeMeaning
idSandbox id
stateState name, e.g. "RUNNING". The last seen state; refresh() updates it
edge_idThe node it runs on
infoThe full record (protobuf Sandbox): project, principal, spec, created_at, last_active_at, ended_at, message

Lifecycle ​

Every lifecycle method returns the Sandbox itself (info updated) and retries while the server is unreachable. They need sandbox.operate.

MethodMeaning
refresh()Fetch the record again
stop()Stop and clean up. The quota comes back at once
pause()Pause. A microVM is snapshotted and its process ends, freeing all its memory; a container's processes are frozen
resume()Resume. Optional: a runtime call to a paused sandbox resumes it
touch()Reset the idle timer; resumes a paused sandbox
update_network(rules, replace=False)Change network rules: merged into the current ones by default, replaced with replace=True. Taking an allowance back applies to new connections at once and cuts existing ones too

Running commands ​

python
run_shell(cmd, timeout=None, session="default", env=None, cwd=None, user=None) -> ShellResult

Runs a shell command in a session and returns its result when it ends.

ParameterMeaning
cmdA whole shell command, interpreted by the session's shell: pipes, && and so on work
timeoutSeconds. When it expires, the processes the command started are killed, the session stays, and ShellResult.timed_out is True
sessionSession name. Within a session, cd and export carry over between commands; sessions are independent of each other
env, cwdEnvironment variables and working directory for this command only
userThe session's user, fixed by its first command: a name or uid[:gid] (e.g. "1000"). Later values for the same session have no effect

A lost connection (edge or apiserver restart) is retried without running the command twice.

python
r = await sb.run_shell("cd /srv && export X=1")
r = await sb.run_shell("pwd; echo $X")          # /srv 1
r = await sb.run_shell("pytest -q", timeout=600, cwd="/srv/repo")
if not r.ok:
    print(r.exit_code, r.stderr)

ShellResult fields:

FieldMeaning
cmd_idThe command's number in its session
exit_codeExit code
stdout, stderrOutput decoded as UTF-8 (undecodable bytes become �); the raw bytes are stdout_bytes, stderr_bytes
doneWhether the command has ended (background commands may not have)
timed_outWhether it was killed by its timeout
durationRun time (seconds)
truncatedThe output exceeds what one call returns (16 MB per stream) and was not all returned; the full output (up to 64 MB per stream) is still recorded: fetch the rest with get_output(cmd_id, stdout_offset=...)
dropped_bytesBytes dropped past the 64 MB per-stream record limit
okdone and exit_code == 0 and not timed_out

Background commands ​

python
cmd_id = await sb.run_shell_background("python3 train.py", session="train")
...
r = await sb.get_output(cmd_id, session="train", wait=30)   # wait up to 30 s
print(r.done, r.stdout)
  • run_shell_background(cmd, timeout=None, session="default", env=None, cwd=None, user=None) -> int starts the command and returns its number at once.
  • get_output(cmd_id, session="default", wait=0, stdout_offset=0, stderr_offset=0) -> ShellResult returns the recorded output, waiting up to wait seconds for the command to end; stdout_offset and stderr_offset start from a byte position, for reading incrementally.

Streaming output ​

python
async for kind, data in sb.stream_shell("make test", timeout=1800):
    if kind == "stdout":
        sys.stdout.buffer.write(data)
    elif kind == "stderr":
        sys.stderr.buffer.write(data)
    elif kind == "exit":
        print("exit code", data.exit_code)     # data is a ShellResult without output

stream_shell(cmd, timeout=None, session="default", env=None, cwd=None, user=None) yields ("stdout", bytes) and ("stderr", bytes) chunks as they come, then ("exit", ShellResult). A broken connection resumes where it broke: the command keeps running in the sandbox and nothing is repeated or lost.

Sessions ​

  • list_sessions() -> list[str]: the sandbox's sessions.
  • close_session(session): end a session, killing every process it started.

Files ​

Paths are absolute paths in the sandbox; file calls run as the session's user (session parameter, default default).

MethodMeaning
read_file(path, offset=0, length=0) -> bytesRead a file; length=0 reads to the end
write_file(path, data, mode=0, append=False) -> FileInfoWrite a file; data is bytes or str (encoded as UTF-8). mode sets the permissions (e.g. 0o755; 0 for the default); append=True appends
list_dir(path) -> list[FileInfo]List a directory
stat(path) -> FileInfoFile information
remove(path, recursive=False)Remove a file; recursive=True removes a directory and its contents

FileInfo fields: name, type (file, dir, symlink, other), size, mode, mtime.

Size limits:

  • one read_file returns at most 64 MB; read larger files in pieces with offset and length;
  • keep one write_file under 64 MB (a message may be up to 80 MB); write larger files in pieces with append=True;
  • in older code, a message through the apiserver was limited to 4 MB, and going over it reported quota_exceeded (really a message that was too large). The first entry under "Fixed" in CHANGELOG 0.11.0 raised the limit to 80 MB on the servers, the Go client and the Python SDK; such errors now report invalid.
python
# Read a large file in pieces. Past the end, read_file returns b"".
chunks, off = [], 0
while True:
    part = await sb.read_file("/data/big.bin", offset=off, length=32 << 20)
    if not part:
        break
    chunks.append(part)
    off += len(part)

HTTP services in the sandbox ​

python
r = await sb.http("http://127.0.0.1:8000/health", method="GET", headers=None, body=b"", timeout=30)
print(r.status, r.headers, r.body)

Makes an HTTP request from inside the sandbox's network namespace, to reach services running in it. Returns HttpResult(status, headers, body). For a direct TCP connection (WebSocket, say), use the API's ConnectPort, or reach the port through the E2B gateway.

Fork, snapshots and images ​

python
fork(count=1, ttl_running_stop=0, ttl_max=0, labels=None, task_id="", timeout=600) -> list[Sandbox | DSecError]

Copies a running sandbox into count (1 to 256) running sandboxes.

  • The source pauses briefly and carries on, its id and timeouts unchanged.
  • Copies inherit the source's spec; labels are added to theirs, task_id replaces the task (default: the source's), and ttl_running_stop and ttl_max are the copies' timeouts (0 for the cluster default).
  • microVM copies include memory, so processes continue from the fork; container copies get the files only.
  • Results are in index order, failures as DSecError. Each copy counts against quota. A retry after a lost connection creates only the copies still missing.
  • Needs sandbox.create on the project and layer.use on the source image's project.
python
create_snapshot(image, memory=False, overwrite=False) -> ImageInfo

Saves the sandbox as image image (<project>/<name>:<tag>) exactly as it is; nothing is cleaned up.

  • memory=True (microVM only) includes memory: sandboxes created from the image resume from the snapshot, with its processes running. Their memory_limit_mb must equal the snapshot's.
  • microVMs require memory=True. For a files-only image, build on container and use pack_diff.
  • overwrite=True replaces an existing image of that name.
  • Needs layer.publish on the image's project.
python
pack_diff(image, scrub=(), overwrite=False) -> ImageInfo

Saves the sandbox's changes against the image it started from as a new image image, stacked on that image (container-type backends only).

  • Removed by default: /tmp/*, /var/tmp/*, /root/.cache, /root/.bash_history, /home/*/.bash_history, /root/.python_history. scrub adds more (globs relative to /, e.g. ["srv/repo/.git"]).
  • The image works for both container and microVM.
  • Needs layer.publish on the image's project.
python
sb = await client.run_container(DSecContainerRunArgs(container_image="root/rl/python:3.12",
                                                     network_rules={"pypi": True, "github": True}))
await sb.run_shell("git clone https://github.com/org/repo /srv/repo && pip install -e /srv/repo")
img = await sb.pack_diff("root/rl/repo-env:v1")
await sb.stop()
# from now on, create from root/rl/repo-env:v1 instead of installing again

ImageInfo fields: name, layers (bottom first), env, workdir, user, parent, source, created_at, created_by, snapshot, memory.

Task actions ​

MethodMeaning
pause_task(task_id, project=None)Pause every sandbox of the task
resume_task(task_id, project=None)Resume every sandbox of the task
stop_task(task_id, project=None)Stop every sandbox of the task

They return a TaskActionResult: affected (succeeded), failed, errors (the failures' reasons). They need sandbox.operate.

Images ​

MethodMeaningPermission
list_images(project=None) -> list[ImageInfo]Images of the project and its subprojectslayer.use
get_image(name) -> ImageInfoOne imagelayer.use
compose_image(name, parts, env=None, workdir="", user="", overwrite=False) -> ImageInfoStack the parts bottom first into a new image without copying data. env adds environment variables; workdir and user set the defaultslayer.publish on the new image's project, layer.use on each part's
delete_image(name)Delete the image's registration. opendsecctl image gc reclaims its layerslayer.publish
python
await client.compose_image("root/rl/swe-4821:v1",
                           ["root/rl/python:3.12", "root/rl/django-ws:a1b2", "root/tools/agent-kit:3"])

Importing from a registry is CLI-only (opendsecctl image import): it writes into the store on a machine that mounts it.

Projects, principals and tokens ​

These methods use the same API as opendsecctl, for people and agents alike, under the same permission rules (Projects, principals and permissions).

MethodMeaningNeeds
whoami() -> strThe principal of the current token—
create_project(parent, name, quota=None) -> ProjectCreate a subproject of parent. quota e.g. {"sandboxes": 100, "cpu_cores": 400}; keys left out take the parent's limitproject.admin on parent
get_project(project=None) -> Projectpath, quota, usage (current), policies (each principal's permissions), childrenany permission on the project
set_quota(project, quota) -> ProjectSet the quota (replaced as a whole); may not exceed any ancestor'sproject.admin on the parent project
delete_project(project)Delete an empty project (no subprojects, no usage, no principals of its own)project.admin on the parent project
create_principal(project, principal_id, kind="agent", ttl=0) -> strCreate a principal in the project and return its first token (returned only this once). ttl: the token's lifetime in seconds, 0 for the server defaultproject.admin
delete_principal(principal)Delete a principal and all its tokensproject.admin on the principal's project
grant(project, principal, perms) -> ProjectGrant permissions, e.g. ["sandbox.create", "sandbox.operate"]. Only permissions you hold yourselfproject.admin and each permission granted
revoke(project, principal, perms=()) -> ProjectRevoke permissions; all of them when perms is emptyproject.admin
create_token(principal=None, ttl=0, description="") -> (str, TokenInfo)Issue a token, by default for yourselfyourself: nothing; others: project.admin on their project
list_tokens(principal=None) -> list[TokenInfo]Tokens without secrets: id, created_at, expires_at (0: never), description, created_byas above
revoke_token(principal, token_id)Revoke a token; it stops working on every apiserver within about 1 sas above
get_usage(project=None, start="", end="") -> UsageReportUsage of the project and its subprojects per UTC day; start and end are YYYY-MM-DD, inclusiveproject.admin

Each row of UsageReport.rows: project, day, sandboxes_started, sandbox_hours, cpu_core_hours, memory_gb_hours, gpu_hours.

Errors: DSecError ​

Every failure raises DSecError, except single items of a batch creation or fork, which go in the result list.

python
from opendsec import DSecError, QUOTA_EXCEEDED, NO_CAPACITY

try:
    sb = await client.run_sandbox(args)
except DSecError as e:
    if e.reason in (QUOTA_EXCEEDED, NO_CAPACITY):
        await asyncio.sleep(5)        # try again later
    else:
        raise
  • e.reason: a stable reason string, with a constant of the same name in the module: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, QUOTA_EXCEEDED, RATE_LIMITED, NO_CAPACITY, INVALID, UNAVAILABLE, TIMEOUT, INTERNAL. What each means and what to do: Concepts §9.
  • e.message: the server's explanation.

What is retried automatically: only unavailable (server unreachable), within retry_timeout, for creation, batch creation, fork, lifecycle calls, commands, file reads, directory listings, sessions and streams. write_file, remove, http, update_network, pack_diff, create_snapshot and the management methods are not retried; on failure the caller decides.

Patterns ​

RL rollouts ​

python
async def rollout(client, step_image, episode):
    sb = await client.run_sandbox(DSecRunArgs(backend="microvm", image=step_image,
                                              cpu_cores_limit=2, memory_limit_mb=2048,
                                              task_id="ppo-run-42", ttl_max=3600))
    try:
        for action in episode:
            r = await sb.run_shell(action, session="agent", user="1000", timeout=120)
            ...
        score = await sb.run_shell("python3 /grader/grade.py", session="grader")   # a root session
        return float(score.stdout)
    finally:
        await sb.stop()

# training is preempted
await client.pause_task("ppo-run-42")
# afterwards, the next call to each sandbox resumes it; or explicitly:
await client.resume_task("ppo-run-42")

Branching from one state ​

python
base = await client.run_sandbox(DSecRunArgs(backend="microvm", image="root/rl/swe-4821:v1"))
await base.run_shell("cd /srv/repo && git apply /tmp/partial.patch")
branches = await base.fork(count=8, task_id="search-17")
results = await asyncio.gather(*(b.run_shell(f"python3 try.py --variant {i}")
                                 for i, b in enumerate(branches) if not isinstance(b, DSecError)))

A warmed-up environment ​

python
sb = await client.run_sandbox(DSecRunArgs(backend="microvm", image="root/rl/model-server:v1",
                                          memory_limit_mb=4096))
# start a long-running service that loads a model (slow), then waits for requests on port 8000
await sb.run_shell("(nohup python3 /srv/serve.py > /tmp/serve.log 2>&1 &)")
await sb.run_shell("until curl -sf http://127.0.0.1:8000/ready; do sleep 1; done", timeout=600)
await sb.create_snapshot("root/rl/model-server:warm", memory=True)
await sb.stop()
# created from the snapshot, the service is already running with the model loaded: ready in about 55 ms
fast = await client.run_sandbox(DSecRunArgs(backend="microvm", image="root/rl/model-server:warm",
                                            memory_limit_mb=4096))

More examples in the repository's sdk/python/examples/: quickstart.py, test_features.py (checks each feature in turn), load_test.py (load test), decoupled_rollout.py (decoupled rollouts).

Implements the paper DeepSeek Elastic Compute (DSec) (arXiv:2609.22978). An independent project, not affiliated with DeepSeek.