Skip to main content

Axern Python SDK

The Axern Python SDK is the first-class programmable interface for Axern sandboxes. It exposes both the control plane client and a high-level Sandbox API for lifecycle, command execution, attached processes, file operations, directory transfer, tunnels, capability discovery, and diagnostics.

Install

Install the published package:

uv add axern-sdk

For repository development, build and run examples through the root uv workspace:

uv build --no-sources sdk/python
uv run --package axern-sdk python sdk/python/examples/sandbox_programming.py

Connect

from axern_sdk import AxernClient

client = AxernClient.from_context("~/.config/axern/config.json")

TLS-enabled local compose setups can pass certificate paths directly:

client = AxernClient(
    "127.0.0.1:25000",
    tls_ca_cert=".dev/certs/ca.crt",
    tls_cert=".dev/certs/client.crt",
    tls_key=".dev/certs/client.key",
)

AsyncAxernClient provides the same control-plane surface for asyncio code. Constructors are explicit and never read the user directory. from_context() loads a named CLI context from the supplied path; from_env() is available for environment-driven automation and reads AXERN_ENDPOINT plus the gateway TLS and proxy variables.

Sandbox Sources

Create a sandbox from exactly one source:

  • image="docker.io/library/python:3.12-slim" for an OCI image.
  • environment_id="..." for an existing environment.
from axern_sdk import AxernClient, Sandbox

client = AxernClient("127.0.0.1:25000")

with Sandbox(client=client, image="docker.io/library/python:3.12-slim") as sandbox:
    print(sandbox.metadata.allocation_id)

client.close()

Network Policies

Omitting network_policy requests unrestricted networking. Strict policies are fail-closed; deny_dns only refuses matching traditional UDP/TCP DNS queries and does not block direct IP traffic, DoH, DoT, or already-resolved addresses.

from axern_sdk import NetworkPolicy, Sandbox

with Sandbox(
    client=client,
    image="docker.io/library/python:3.12-slim",
    network_policy=NetworkPolicy.deny_dns(
        "github.com",
        "*.github.com",
        "githubusercontent.com",
        "*.githubusercontent.com",
        "gitlab.com",
        "*.gitlab.com",
        "bitbucket.org",
        "*.bitbucket.org",
    ),
) as sandbox:
    print(sandbox.metadata.allocation_id)

NetworkPolicy.allow_domains("example.com", "*.example.com") is strict: only HTTP/HTTPS traffic whose controlled DNS result and HTTP Host or TLS SNI match is allowed. Use NetworkPolicy.strict(..., cidr_rules=(CIDRRule(...),)) for explicit TCP/UDP CIDR and port grants, and NetworkPolicy.deny_all() for no egress.

Allocation-Local Files

Writable files belong to the sandbox allocation and are not preserved across replacement or node loss. Use the file APIs while the sandbox is alive and explicitly download or publish outputs that must outlive it.

from axern_sdk import Sandbox

with Sandbox(
    client=client,
    image="docker.io/library/python:3.12-slim",
) as sandbox:
    sandbox.write_text("/tmp/result.txt", "allocation-local output\n")
    sandbox.download_file("/tmp/result.txt", "result.txt")

Exec

Use exec() for command-result workflows. Set text=True to decode stdout and stderr; set check=True to raise SandboxExecError on non-zero exit.

with Sandbox(client=client, image="docker.io/library/python:3.12-slim") as sandbox:
    result = sandbox.exec("python -c \"print('hello')\"", text=True, check=True)
    print(result.stdout)

Use exec_stream() when stdout/stderr should be consumed as events:

for event in sandbox.exec_stream(["python", "-u", "-c", "print('streamed')"]):
    if event.stream == "stdout":
        print(event.text(), end="")

Attached Process

Use process() when your program needs to control stdin, observe output, wait, or terminate a running command.

with sandbox.process(["python", "-u", "-c", "import sys; print(sys.stdin.read().upper())"]) as process:
    process.write("hello process\n")
    process.close_stdin()

    for event in process.events():
        if event.stream == "stdout":
            print(event.text(), end="")

    result = process.wait()
    print(result.exit_code)

AsyncSandbox.process() returns AsyncSandboxProcess with async equivalents of write(), close_stdin(), events(), wait(), terminate(), and kill().

Files

Single-file APIs are byte-safe. Text helpers only encode/decode at the SDK boundary.

sandbox.write_text("/tmp/message.txt", "payload\n")
print(sandbox.read_text("/tmp/message.txt"))

sandbox.write_bytes("/tmp/blob.bin", b"\x00\x01")
data = sandbox.read_bytes("/tmp/blob.bin")

Platform file operations are handled by the node/runtime file service, not by SDK-side shell fallbacks:

sandbox.copy("/tmp/message.txt", "/tmp/message-copy.txt", overwrite=True)
sandbox.move("/tmp/message-copy.txt", "/tmp/message-final.txt")
sandbox.chmod("/tmp/message-final.txt", 0o600)
sandbox.touch("/tmp/message-final.txt")

info = sandbox.stat("/tmp/message-final.txt")
entries = sandbox.list_dir("/tmp")
exists = sandbox.exists("/tmp/message-final.txt")

Directory Transfer

Directory upload/download uses archive streaming. The SDK packages local directories with tarfile and safely extracts downloaded archives; remote file semantics remain owned by the platform file service.

from pathlib import Path

source = Path("example-upload")
source.mkdir(exist_ok=True)
source.joinpath("data.txt").write_text("directory payload\n")

sandbox.upload_dir(source, "/tmp/example-upload", overwrite=True)
sandbox.download_dir("/tmp/example-upload", "example-download", overwrite=True)

Local symlinks are rejected during upload. Download extraction rejects absolute paths, parent traversal, symlinks, and hardlinks.

Tunnel

Pass upstream to expose a local TCP service to code running inside the sandbox. The SDK owns the tunnel connector and renews finite tunnel TTLs while the sandbox is active.

from axern_sdk import Sandbox

with Sandbox(
    client=client,
    image="docker.io/library/python:3.12-slim",
    upstream="127.0.0.1:8080",
    remote_port=8786,
) as sandbox:
    print(sandbox.bound_addr)

For lower-level use, create the Run, wait for its public allocation_id, create a TunnelSession, then construct TunnelConnector(client=client, ...). The connector inherits the Axern mTLS transport from the client; callers never import a private transport type. wait_closed() provides bounded cleanup/revocation observation. Tunnel authority is finite, does not renew the Run or ExecutionLease, and is revalidated online within 20 seconds (15-second interval plus a 5-second validation deadline).

Keep caller-local service credentials outside the sandbox and tunnel only the raw TCP endpoint. Never copy the Axern mTLS key or Tunnel client token into the sandbox, Run metadata, or declared output.

Metadata

Sandbox.state is the lightweight runtime state. Sandbox.metadata is stable for logs and diagnostics:

metadata = sandbox.metadata
print(metadata.environment_id, metadata.run_id, metadata.allocation_id)
print(metadata.run_id, metadata.tunnel_session_id)

Capabilities

Use capability_status() to discover baseline and optional sandboxd-backed providers before calling Computer Use APIs:

status = sandbox.capability_status()
print(status.ready, status.capabilities)

for provider in status.providers:
    print(provider.name, provider.state, provider.available, provider.reason)

Errors

SDK exceptions expose fields for programmatic handling:

from axern_sdk import (
    SandboxConnectionError,
    SandboxPermissionError,
    SandboxPreconditionError,
    SandboxRpcError,
)

try:
    sandbox.exec("python -V", check=True)
except SandboxConnectionError as exc:
    if exc.retryable:
        print("temporary node/control-plane connectivity issue")
    raise
except SandboxPermissionError as exc:
    print("credentials do not permit this operation", exc.operation)
    raise
except SandboxPreconditionError as exc:
    if exc.capability:
        print(
            exc.capability.capability,
            exc.capability.provider,
            exc.capability.provider_state,
            exc.capability.missing_dependencies,
        )
    raise
except SandboxRpcError as exc:
    print(exc.operation, exc.code, exc.details, exc.allocation_id)
    raise

Common error classes:

  • SandboxNotStartedError: operation requires an active sandbox.
  • SandboxExecError: exec(..., check=True) observed non-zero exit.
  • SandboxConnectionError: transport or connectivity failure.
  • SandboxPermissionError: authentication or authorization failure.
  • SandboxRpcError: gRPC status mapped from node/runtime APIs.
  • SandboxTimeoutError: SDK-side timeout.

Sandboxd-backed capability failures keep their normal SDK exception class and also expose exc.capability when the node returns provider diagnostics. That object contains capability, provider, provider_state, reason, and missing_dependencies, so callers can branch on missing Computer Use dependencies without parsing the full error string.

Async

Async APIs mirror the synchronous shape:

from axern_sdk import AsyncAxernClient, AsyncSandbox

async with AsyncAxernClient("127.0.0.1:25000") as client:
    async with AsyncSandbox(client=client, image="python:3.12-slim") as sandbox:
        result = await sandbox.exec("python -c \"print('hello async')\"", text=True, check=True)
        print(result.stdout)

        async with await sandbox.process(["python", "-u", "-c", "import sys; print(sys.stdin.read())"]) as process:
            await process.write("async input\n")
            await process.close_stdin()
            async for event in process.events():
                if event.stream == "stdout":
                    print(event.text(), end="")

Examples

Runnable examples live in examples:

Examples expect a reachable Axern gateway control edge at 127.0.0.1:25000.

Validation

make test-py
make lint-py
make sdk-python-verify
make local-compose-python-sdk-e2e

Read-only Image Mounts And Secret Projections

Image mounts and Secret projections are immutable Run inputs. Image mounts are always read-only and therefore expose only an image reference and target path. Secret inputs carry references (secret_id and key), never plaintext values. They belong to the Run that declares them and are not inherited by another Run.

from axern_sdk import ImageMount, Sandbox, SecretFile

with Sandbox(
    client=client,
    image="docker.io/library/python:3.12-slim",
    image_mounts=[
        ImageMount(
            "registry.example/claude-code@sha256:<digest>",
            "/__claude_code",
        )
    ],
    secret_files=[
        SecretFile(
            "/run/secrets/workload-config",
            "secret-workload",
            "settings.json",
            mode=0o400,
        )
    ],
) as sandbox:
    sandbox.exec(["/__claude_code/bin/claude"], check=True)

Targets, environment names, duplicate projections, file modes, and Secret namespace ownership are validated by the control plane. Secret files default to 0400; explicit modes must contain no write bits, and pseudo-filesystems, executable/library trees, Axern's runtime state, and critical host-identity files are protected targets. Optional projections may be absent, but an existing Secret in another Namespace is never eligible.

Secret projection is for a least-privilege workload credential that must exist inside that Run. Credentials for a caller-local service remain outside the sandbox and are not projected through this API.

Declared And Stream Output

Declare bounded result paths when creating a Run or Sandbox. Axern quiesces the Allocation and seals each regular file or directory-as-tar before runtime cleanup. Query the manifest and download by the persisted run_id; full downloads verify the declared size and SHA-256 digest.

from axern_sdk import DeclaredOutput, DeclaredOutputFormat, Sandbox

with Sandbox(
    client=client,
    image="docker.io/library/python:3.12-slim",
    declared_outputs=[DeclaredOutput("/tmp/result.json", DeclaredOutputFormat.FILE, "application/json")],
) as sandbox:
    sandbox.write_text("/tmp/result.json", '{"ok":true}\n')
    run_id = sandbox.metadata.run_id

run = client.wait_run(run_id, timeout=60)
output = next(item for item in client.get_sealed_output_manifest(run_id) if item.path == "/tmp/result.json")
with open("result.json", "wb") as destination:
    client.download_sealed_output(run_id, output.output_id, destination)

Declared output is retained on the Node for 15 minutes after cleanup starts. It survives axnoded restart, but not Node-disk loss, and must be copied to caller-owned durable storage. Limits are 16 paths, 64 MiB per file, 256 MiB per tar archive, and 256 MiB total. Missing, unsafe, wrong-kind, oversized, capture-failed, and node-unavailable results are explicit manifest states. This is not a persistent workspace, Artifact service, or object store.

Reusable Rootfs Environment

Use a finite Run when successful preparation should become the immutable input for later fresh Allocations:

prepared = client.create_run(
    environment_id=base_environment_id,
    argv=["/bin/sh", "-lc", "./compile.sh"],
    rootfs_snapshot=True,
)
snapshot = client.wait_rootfs_snapshot(prepared.id, timeout=600)
fresh = client.create_run(
    environment_id=snapshot.environment_id,
    argv=["./test.sh"],
)

The result is a normal Environment backed by a content-addressed OCI image, not a separate Snapshot object. Only writable rootfs changes are captured; bind and image mounts, Secret projections, kernel filesystems, active processes, sockets, and sessions are excluded. Failed or cancelled Runs produce no Environment. Sandbox does not expose this option because its close path cancels the long-lived Run.

stdout/stderr use the same node-local retention lifecycle and have a combined 64 MiB readable limit. Python exec() collects at most 1 MiB per stream while continuing to drain the RPC and reports truncation; use exec_stream() or process() for larger output. Sandbox.close() requests Run cancellation and reports cleanup failures, but does not wait for the durable terminal state; call wait_run() explicitly when terminal confirmation matters.

Metadata

Release files for axern-sdk 0.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for axern-sdk 0.11.0
File Size Uploaded
axern_sdk-0.11.0.tar.gz 126.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for axern-sdk 0.11.0
File Interpreter ABI Platform
axern_sdk-0.11.0-py3-none-any.whl Python 3 none any Details

Total release size: 288.1 kB

Release files / axern_sdk-0.11.0.tar.gz

Download URL axern_sdk-0.11.0.tar.gz
Size 126.4 kB
Tags Source
SHA-256 checksum
How to use checksums
b442cdf18c465a0bbfc86a98a39c98c6c92fdb0251843f3048658a7e8d9accdb
BLAKE2b-256 checksum
How to use checksums
05968b0c66a8b4f3eec234b55f0a8e15bb644d88a4f486bf75eedb10717a7145
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / axern_sdk-0.11.0-py3-none-any.whl

Download URL axern_sdk-0.11.0-py3-none-any.whl
Size 161.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
499d3475bf6945492e7b34e3c14e3aa429d8e0e140306ebd4136d776e6f20340
BLAKE2b-256 checksum
How to use checksums
2d676710cbf443435bdecffd6a531b5a87b5355524a0cd970a5350c6307c11d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.12.1

2 release files

0.12.0

2 release files

0.11.4

2 release files

0.11.3

2 release files

0.11.2

2 release files

0.11.1

2 release files

This release

0.11.0 This release

2 release files

0.10.0

2 release files

0.9.1

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.2.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page