Metadata-Version: 2.4
Name: akernel-sdk
Version: 0.2.0rc1
Summary: Python SDK and CLI for AKernel remote sandboxes
Author: AKernel Authors
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/inclusionAI/AKernel
Project-URL: Documentation, https://github.com/inclusionAI/AKernel/tree/develop/sdk/python
Project-URL: Repository, https://github.com/inclusionAI/AKernel.git
Project-URL: Issues, https://github.com/inclusionAI/AKernel/issues
Keywords: ai-agents,sandbox,gvisor,remote-execution
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Distributed Computing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: adx-environment==0.1.0rc1
Requires-Dist: idna<4,>=3
Requires-Dist: websockets>=10.0
Requires-Dist: dockerfile-parse>=2.0.1
Provides-Extra: openyuanrong-sandbox
Provides-Extra: all
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == "dev"
Requires-Dist: mypy<2,>=1.10; extra == "dev"
Requires-Dist: ruff<1,>=0.11; extra == "dev"

# AKernel Python SDK

`akernel-sdk` is the Python interface for creating and managing remote AKernel
sandboxes. Applications use one stable API for commands, files, interactive
PTYs, port forwarding, and reverse tunnels.

## Navigation

- [AKernel Python SDK](#akernel-python-sdk)
  - [Navigation](#navigation)
  - [Install and configure](#install-and-configure)
  - [Create a sandbox](#create-a-sandbox)
    - [Experimental GPU and writable storage](#experimental-gpu-and-writable-storage)
    - [Network ACLs](#network-acls)
  - [Sandbox runtimes](#sandbox-runtimes)
  - [Commands](#commands)
  - [Filesystem](#filesystem)
  - [Interactive PTYs](#interactive-ptys)
  - [Port forwarding](#port-forwarding)
  - [Local failover and reload](#local-failover-and-reload)
  - [Reverse tunnels](#reverse-tunnels)
  - [Rootfs and mounts](#rootfs-and-mounts)
  - [Launch from a Dockerfile](#launch-from-a-dockerfile)
  - [Resources and lifecycle](#resources-and-lifecycle)
  - [CLI](#cli)
  - [Examples and tests](#examples-and-tests)
  - [Public value types](#public-value-types)

## Install and configure

AKernel SDK requires Python 3.10 or newer.

Install the stable SDK from PyPI:

```bash
python -m pip install akernel-sdk
```

To include published release candidates:

```bash
python -m pip install --pre akernel-sdk
```

Choose an SDK version compatible with the deployed backend and pin its exact
published version for reproducible installations. For an unpublished source
revision, install from the repository root:

```bash
python -m pip install ./sdk/python
```

The SDK's ADX dependency is declared in `pyproject.toml`.
The ADX adapter translates the public AKernel `Sandbox` API
to `adx_environment.Environment`. Deploy a matching ADX release exposing
`/api/environment/v1` for lifecycle, resources and command subscriptions.

Source installs and CI resolve the declared ADX dependency from the package index.
CI checks AKernel wheel and source distribution installs in separate clean
environments, including `pip check`, imports, version checks, and the `ak` CLI.

Configure the public AKernel entrypoint and the deployment token:

```bash
export AKERNEL_SERVER_ADDRESS="akernel.example.com"
export AKERNEL_TOKEN="<token>"
```

Address behavior is deterministic:

- A host or IP without a port uses HTTPS/WSS on 443 for the frontend and HTTP
  on 80 for public sandbox port URLs.
- `host:port` preserves shared-port behavior: control uses HTTPS/WSS and
  public sandbox URLs use HTTP/WS on that explicit port.
- `AKERNEL_GATEWAY_ADDRESS` overrides only the port-forwarding and reverse
  tunnel data endpoint for nonstandard ports or TLS gateways. An override
  without a scheme uses HTTP/WS. Exec and file transfer continue to use
  `AKERNEL_SERVER_ADDRESS`.

### TLS certificate verification

`AKERNEL_VERIFY_TLS` controls certificate-chain and hostname verification for
the SDK's HTTPS control, command and file requests, command-watch WebSockets,
PTYs, CLI inventory/deletion and resource queries. It defaults to `0` to retain
the current self-signed deployment behavior. HTTPS still encrypts the connection
with this default, but does not authenticate the server certificate.

For an entrypoint with a certificate trusted by the default certificate stores:

```bash
export AKERNEL_VERIFY_TLS=1
```

For a private CA or a standalone self-signed certificate, additionally configure
the trusted PEM bundle before the first SDK operation:

```bash
export AKERNEL_VERIFY_TLS=1
export SSL_CERT_FILE="/path/to/trusted-ca-or-certificate.pem"
```

The standalone operator can provide the existing
`<data-directory>/adx/tls/ingress-public.pem` file. For Kubernetes, the operator
can provide `public.pem` from the configured ADX TLS Secret. This is public
certificate material; clients do not need the server's private key or the
administrator API key to establish certificate trust. Use the hostname or IP
listed in the certificate's subject alternative names. The default standalone
certificate covers `localhost` and `127.0.0.1`; replace it with a certificate
matching the public hostname for remote access.

The CLI accepts global options before the subcommand:

```bash
ak --verify-tls list
ak --ca-file /path/to/trusted-ca-or-certificate.pem resources
ak --ca-file /path/to/trusted-ca-or-certificate.pem exec <sandbox-id>
```

`--ca-file` configures `SSL_CERT_FILE` and enables verification. The explicit
`--no-verify-tls` option selects the existing unverified mode and cannot be
combined with `--ca-file`. Environment boolean values accept `1/true/yes/on`
and `0/false/no/off`; invalid values fail rather than disabling verification.

CA loading follows the underlying transport when verification is enabled:

| Transport | Trust-source contract |
|---|---|
| SDK control, command and file HTTP requests (HTTPX) | A nonempty `SSL_CERT_FILE` takes precedence over `SSL_CERT_DIR` and is loaded explicitly. A missing or malformed file raises an error during client construction, even if the directory contains a usable CA. If no file is selected, HTTPX uses `SSL_CERT_DIR` when set, otherwise its default CA bundle. |
| CLI inventory/deletion, SDK resource queries, PTY and command-watch (Python SSL/OpenSSL) | Use OpenSSL's default trust paths, including `SSL_CERT_FILE` and `SSL_CERT_DIR`. A missing or malformed file can be ignored while a usable CA directory still validates the server. |

`--ca-file` sets the process trust-file variable; it does not restrict all
transports to that file as their sole trust source or guarantee an error for
an invalid path. Check the selected PEM file before use. For a private CA,
configure a valid file and unset `SSL_CERT_DIR` to avoid an additional
environment-selected directory; OpenSSL can still use its default trust paths.
These loading differences do not disable certificate-chain or hostname checks
in verified mode. When verification is off, these service transports do not
load the CA sources to authenticate the server.

Verification failures do not fall back to unverified TLS. The process backend
and dependency HTTP pools retain their configuration, so set trust and
verification before first use; restart the client after changing CA material.

Certificate issuance and renewal remain deployment responsibilities. Registry
TLS, reverse-tunnel upstream HTTPS trust and SSH host keys have separate
configuration; this setting controls the AKernel service connection.

## Create a sandbox

```python
from akernel_sdk import Sandbox

with Sandbox(cpu=1000, memory=2048) as sandbox:
    result = sandbox.commands.run("printf hello")
    print(result.stdout)
```

The constructor accepts:

```python
Sandbox(
    image: str | None = None,
    rootfs: S3Config | None = None,
    runtime: str = "runsc",
    cpu: int = 1000,
    memory: int = 4096,
    cpu_limit: int = 0,
    mem_limit: int = 0,
    idle_timeout: int = 300,
    schedule_timeout: int = 30,
    env: dict[str, str] | None = None,
    name: str | None = None,
    cwd: str | None = None,
    port_forwardings: list[int] | None = None,
    mounts: list[Mount] | None = None,
    reverse_tunnel: HttpReverseTunnel | None = None,
    detached: bool = False,
    node_id: str | None = None,
    *,
    xpu: str | None = None,
    storage_mb: int | None = None,
    storage_limit_mb: int = 0,
    network_policy: NetworkPolicy | None = None,
    dockerfile: DockerfileLaunch | None = None,
    extra_config: Mapping[str, object] | None = None,
)
```

### Experimental GPU and writable storage

Request a whole NVIDIA GPU by type, exact product model, and count:

```python
with Sandbox(xpu="gpu:l20:1") as sandbox:
    print(sandbox.commands.run("nvidia-smi -L").stdout)
```

The `type:model:count` value is case-insensitive and canonicalized to lower
case. The model is required and matched exactly; wildcard models are not
supported. The bundled backend currently requires the gVisor `runsc` runtime
and a node configured for gVisor nvproxy. Runtime compatibility is validated
by the backend rather than the SDK.

Set the storage scheduling quota and actual writable root filesystem hard limit
in MiB:

```python
with Sandbox(storage_mb=4 * 1024, storage_limit_mb=20 * 1024) as sandbox:
    print(sandbox.commands.run("df -h /").stdout)
```

`storage_mb` is the scheduling quota: the storage amount reserved when placing
the sandbox. `storage_limit_mb` is the actual hard limit on writable-root
capacity. In this example, scheduling reserves 4 GiB while the writable root
can grow up to 20 GiB. Reserving less than the hard limit permits storage
overcommit; the limit does not guarantee that the node has enough physical
space for all sandboxes to reach their limits simultaneously.

`storage_limit_mb=0` follows `storage_mb`, or the cluster default when the
scheduling quota is omitted. If only a positive limit is given, the backend
also reserves that amount for scheduling. A positive limit must be at least
the explicit scheduling quota. Limits must be nonnegative integers; booleans
are rejected.

Explicit writable storage quotas are supported by `runsc` and Firecracker
and use sandboxd's disk-backed filestore (ext4 in the bundled standalone
deployment, XFS in the Terraform-managed Aliyun profile). Runtime compatibility
is validated by the backend. If `storage_limit_mb` is not supported by the
selected backend, a nonzero value fails explicitly. See
[`examples/gpu_sandbox.py`](./examples/gpu_sandbox.py) and
[`examples/storage_sandbox.py`](./examples/storage_sandbox.py).

### Network ACLs

Omit `network_policy` to leave all sandbox networking unrestricted. An empty
`NetworkPolicy()` is equivalent and is omitted from the creation request:

```python
from akernel_sdk import NetworkPolicy, Sandbox

with Sandbox() as unrestricted:
    print(unrestricted.commands.run("python3 -c 'import socket; "
                                    "socket.getaddrinfo(\"github.com\", 443)'"))
```

Block new sandbox flows except the YuanRong control proxy and published
sandbox-port routes with the compatibility helper:

```python
with Sandbox(network_policy=NetworkPolicy.block()) as sandbox:
    result = sandbox.commands.run("printf 'control plane still works'")
    assert result.exit_code == 0
```

Commands and lifecycle operations continue to work in block mode. The policy
also publishes the sandbox targets required by direct filesystem I/O, reverse
tunnels, and explicit `port_forwardings`. Replies on those allowed paths are
stateful. Other new network flows remain denied.

Deny conventional DNS lookups for exact names or leading `*.` suffix
patterns:

```python
policy = NetworkPolicy.deny_dns("github.com", "*.github.com")
with Sandbox(network_policy=policy) as sandbox:
    blocked = sandbox.commands.run(
        "python3 -c 'import socket; socket.getaddrinfo(\"github.com\", 443)'"
    )
    assert blocked.exit_code != 0
```

An exact pattern matches only that name. For example, `github.com` does not
match `api.github.com`, while `*.github.com` matches descendants but not
the apex. Supply both when both should be denied. Patterns are normalized to
lower-case ASCII without a trailing dot; international names are converted to
punycode. Each pattern may be an exact name or begin with one leading
`*.`. The remaining name is at most 253 characters; each dot-separated label
is 1-63 letters, digits, underscores, or hyphens, and a hyphen cannot start or
end a label. Other wildcard placements and `?` are rejected.

Create an egress allowlist with the high-level helper. Rules may select an
IPv4 address or CIDR, an exact or leading-wildcard DNS name, TCP or UDP peer
ports, and a priority:

```python
from akernel_sdk import NetworkRule, PortRange

policy = NetworkPolicy.allowlist(
    [
        NetworkRule(
            domain="*.example.com",
            protocol="tcp",
            port_range=PortRange(443),
            priority=200,
        ),
        NetworkRule(
            cidr="192.0.2.10",
            protocol="tcp",
            port_range=PortRange(8000, 8010),
        ),
    ]
)
with Sandbox(network_policy=policy) as sandbox:
    print(sandbox.commands.run("curl https://api.example.com").stdout)
```
Replace the complete policy of a running sandbox atomically with
`update_network_policy`. Passing `None` or an empty `NetworkPolicy()`
clears the policy and restores unrestricted networking:

```python
with Sandbox() as sandbox:
    sandbox.update_network_policy(NetworkPolicy.block())
    sandbox.update_network_policy(
        NetworkPolicy.deny_dns("github.com", "*.github.com")
    )
    sandbox.update_network_policy(None)
```

The desired policy survives sandboxd restarts, explicit reloads, and same-node
failover. Dynamic replacement is supported by the default
bundled backend.

For independent ingress and egress defaults, deny rules, sandbox-side port
ranges, DNS allowlists, or stateless matching, construct the schema v2 model
directly with `TrafficPolicy`, `NetworkRule`, `DNSPolicy`, and `DNSRule`.
Traffic rules are evaluated by highest priority first; an equal-priority deny
wins. Stateful mode is the default and permits reply traffic. Priority
`4294967295` is reserved for control-plane and published-port rules, so user
priorities are limited to `1..4294967294`.
In stateless mode, a protected published-port rule covers ingress only; add an
explicit egress rule for the matching sandbox source port when the application
must send a reply. This avoids turning a published port into an unrestricted
egress escape hatch.

Domain traffic rules authorize IPv4 addresses learned from an allowed
original DNS query, following its complete CNAME chain. The authorization
uses the answer TTL, clamped to 1..3600 seconds, and is replaced when the name
is resolved again with an IPv4 A or ANY query. Existing connections that
depended on an expired or replaced authorization are removed. Parallel AAAA
queries do not revoke IPv4 grants. DNS names not covered by a domain traffic
rule can still resolve when the DNS policy allows the query, but their answers
do not grant packet access. While a DNS policy or domain traffic rule is
active, ordinary TCP and UDP DNS is accepted only through sandboxd's managed
resolver; a traffic rule for another port-53 resolver does not bypass it.
The resulting enforcement is at IPv4 and transport layers. Another virtual
host sharing an authorized address and port is not distinguishable; use an
application proxy when hostname-level isolation is required.

Network policy replacement uses whole-policy semantics. The legacy
`block_network` and `dns_blacklist` fields cannot be combined with schema v2
sections. DNS policies cover ordinary UDP and TCP DNS and return a refused
response for denied queries; DNS-over-HTTPS and connections to a known IP are
outside DNS filtering. Packet rules are IPv4. sandboxd accepts 256 combined
traffic rules; the backend reserves entries from that limit for the Function
Proxy and each distinct published sandbox port. IPv6 traffic is dropped
whenever a traffic or DNS policy is active, preventing an alternate resolver
from bypassing the IPv4 policy. Arbitrary non-IP Ethernet protocols are
outside the portable ACL contract. Domain and DNS patterns are normalized
through IDNA.

See [`examples/network_policy.py`](./examples/network_policy.py) for the
compatibility modes and generic allowlist. Deployment nodes must have network
ACL support enabled; the bundled standalone, Helm, and Terraform
configurations enable it. Drain existing sandboxes before upgrading a node to
an ACL-enabled sandboxd configuration, as described in the
[deployment guide](../../deploy/README.md#network-acls).

## Sandbox runtimes

AKernel uses the gVisor `runsc` runtime when `runtime` is omitted. Runtime
identifiers and optional `extra_config` are forwarded to the selected backend
instead of being restricted or interpreted by an SDK-owned registry. Values in
`extra_config` must be JSON-compatible. The bundled deployment advertises
`runsc`, Kata Containers, and Firecracker by default when their host
prerequisites are available.

Kata and Firecracker require at least one cluster node whose sandboxd instance
successfully initialized the requested runtime with a usable `/dev/kvm`
device. Nodes without KVM remain available for runsc workloads and do not
advertise either VM runtime. The bundled Firecracker configuration enables
read-only virtio-fs for OCI/Nydus image roots and read-only host directories,
while retaining EROFS image roots and mounts. OCI image mounts, writable host
binds, GPUs, and nested KVM remain unsupported.

The all-in-one image can optionally package a native Linux `runc` backend.
Operators build it with `AKERNEL_ENABLE_RUNC=true` and enable it explicitly
because it provides host-kernel container isolation, not the user-space kernel
boundary of runsc. Guided cloud profiles use
`make config ENABLE_RUNC=true`; standalone deployments use
`AKERNEL_ENABLE_RUNC=true`, and direct Helm deployments use
`node.config.sandboxd.enableRunc=true`. After it is advertised:

```python
with Sandbox(runtime="runc") as sandbox:
    print(sandbox.commands.run("uname -s").stdout)

with Sandbox(
    runtime="runc",
    extra_config={"enableKVM": True},
) as sandbox_with_kvm:
    print(sandbox_with_kvm.commands.run("test -c /dev/kvm").exit_code)
```

`enableKVM` is owned by the runc backend and requires a usable `/dev/kvm` on
the selected node. Runc supports OCI/EROFS root filesystems, read-only mounts,
networking, command execution, and the default writable overlay. Experimental
GPU requests remain runsc-only; explicit `storage_mb` quotas are supported by
runsc and Firecracker. See the
[sandbox runtime comparison](../../src/sandboxd/doc/runtime.md) for the
runtime capability boundaries.

See [`examples/sandbox_runtime.py`](./examples/sandbox_runtime.py) for a runnable example.

## Commands

Run a foreground command:

```python
result = sandbox.commands.run(
    "printf $GREETING",
    envs={"GREETING": "hello"},
    cwd="/tmp",
    timeout=60,
)
print(result.stdout, result.stderr, result.exit_code)
```

Run and control a background command:

```python
handle = sandbox.commands.run("sleep 30", background=True)
print(handle.pid)

for process in sandbox.commands.list():
    print(process.pid, process.command, process.running)

handle.kill()
```

Enable stdin only when it is needed:

```python
handle = sandbox.commands.run("wc -l", background=True, stdin=True)
handle.send_stdin("one\ntwo\n")
handle.close_stdin()
result = handle.wait(timeout=15)
```

Foreground commands return a backend-neutral `CommandResult`. Background
commands return an AKernel `CommandHandle`; its lifecycle operations are
delegated to the selected backend.

With the ADX backend, `handle.wait(timeout)` returns
`CommandResult(exit_code=None, status="RUNNING", error_code="WAIT_TIMEOUT")`
when the wait deadline expires. The command keeps running; a later `wait()` can
observe its completion. `handle.kill()` and `sandbox.commands.kill(pid)` return
`False` when the command does not exist or has already finished.

PTY session wrappers are not retained by the factory after the caller releases
them. A successful `wait()` also removes the completed session from its tracked
sessions. A wait timeout keeps the session active; use `close()` or a context
manager to terminate an interactive session explicitly.

## Filesystem

```python
sandbox.files.write("/tmp/message.txt", "hello")
print(sandbox.files.read("/tmp/message.txt"))

sandbox.files.write("/tmp/data.bin", b"\x00\x01")
print(sandbox.files.read("/tmp/data.bin", format="bytes"))

for entry in sandbox.files.list("/tmp"):
    print(entry.path, entry.type, entry.size)

sandbox.files.make_dir("/workspace")
sandbox.files.rename("/tmp/message.txt", "/workspace/message.txt")
sandbox.files.remove("/workspace/message.txt")
```

Copy local files or directories through the frontend exec WebSocket:

```python
sandbox.files.copy_from_local("./project", "/workspace/project")
sandbox.files.copy_to_local("/workspace/result.json", "./result.json")
```

## Interactive PTYs

Use `sandbox.pty` for an interactive byte stream with stdin, streaming output, terminal resizing, and an exit status:

```python
import sys

from akernel_sdk import Sandbox


def write_output(data: bytes) -> None:
    sys.stdout.buffer.write(data)
    sys.stdout.buffer.flush()


with Sandbox() as sandbox:
    with sandbox.pty.create(on_data=write_output) as session:
        session.send_stdin(b"echo hello from PTY\n")
        session.resize(rows=40, cols=120)
        session.send_stdin(b"exit 7\n")
        print(session.wait())
```

PTY output remains bytes so the SDK does not guess the terminal encoding. Use `session.close_stdin()` to signal end-of-input while continuing to receive output. A session belongs to its WebSocket connection: closing it terminates the remote interactive process, and reconnecting to an existing session is not supported.

Use `sandbox.commands` instead when the caller needs separate stdout and stderr, a complete `CommandResult`, or a controllable background process. The former `Shell` API and its actor `bash_*` methods were removed before the v0.1.0 public API was released.

## Port forwarding

Declare each sandbox port at creation time:

```python
from akernel_sdk import Sandbox

with Sandbox(port_forwardings=[8080]) as sandbox:
    server = sandbox.commands.run(
        "python3 -m http.server 8080 --bind 0.0.0.0",
        background=True,
    )
    print(sandbox.get_port_url(8080))
    server.kill()
```

`get_port_url()` rejects undeclared ports. Pass `internal=True` only when a
deployment operator explicitly wants the direct ADX Ingress address instead of the
public gateway.

## Local failover and reload

`Sandbox(failover=True)` opts into same-node recovery of the same logical
sandbox after its physical runtime fails. `sandbox.reload()` requests the same
rollback explicitly. It returns `False` whenever the rollback is not completed,
including when no usable local anonymous checkpoint exists, the sandbox is
already closed, or the backend reports an operational failure. A successful
reload preserves `sandbox.id` and the existing commands, filesystem, and PTY
facades. The SDK also confirms the data route with a read-only process
listing before reporting success. A temporary route conflict is retried within
a 10-second window; it never reissues the rollback. Other errors, explicit
non-retryable errors, or an expired wait return `False`.

Recovery points are local and follow the source sandbox lifecycle. They are
created by sandbox workloads through Execd's internal `POST /checkpoint`
endpoint on `/run/akernel/execd.sock`. A successful request returns
`{"status":"completed"}`; a concurrent checkpoint request returns HTTP 409.
This Unix-socket protocol is experimental and is not a stable public AKernel
SDK interface. The SDK deliberately does not expose checkpoint identifiers,
restore, list, or delete operations.

The bundled runsc and Firecracker runtimes support this recovery flow. The
maintained example validates it with runsc and installs curl in the sandbox
before calling the internal Unix-socket endpoint, so the default RRT runtime
profile is sufficient:

```bash
AKERNEL_TEST_RUNTIME=runsc python examples/failover_reload.py
```

See [`examples/failover_reload.py`](./examples/failover_reload.py) for the
internal trigger used during integration.

## Reverse tunnels

A reverse tunnel lets sandbox code call an HTTP or HTTPS service reachable
from the machine running the SDK:

```python
from akernel_sdk import HttpReverseTunnel, Sandbox

tunnel = HttpReverseTunnel(
    target="https://service.example.com",
    reverse_port=8765,
    listen_port=8766,
    connect_timeout=60,
)

with Sandbox(reverse_tunnel=tunnel) as sandbox:
    result = sandbox.commands.run(
        f"curl {sandbox.reverse_tunnel.url}/health"
    )
```

`reverse_port` carries the WebSocket tunnel through ADX Ingress. `listen_port` is
the loopback HTTP listener used inside the sandbox. Consequently,
`sandbox.reverse_tunnel.url` is always
`http://127.0.0.1:<listen_port>`, even when `target` uses HTTPS.

For an HTTPS target, the SDK-side tunnel client performs the TLS handshake and
certificate verification. The sandbox application talks only to its loopback
HTTP listener. AKernel supports one HTTP/HTTPS reverse tunnel per sandbox and
does not expose a general TCP tunnel.

The bundled backend supports custom internal tunnel
ports. Its frontend derives the WebSocket port from the HTTP listener, so
`reverse_port` must equal `listen_port - 1`. Both ports are reserved inside
that sandbox while the tunnel is active and must not also appear in
`port_forwardings`; they do not occupy ports on the SDK host.

## Rootfs and mounts

Use a public OCI image:

```python
with Sandbox(image="ubuntu:24.04") as sandbox:
    print(sandbox.commands.run("cat /etc/os-release").stdout)
```

To start the effective OCI image `ENTRYPOINT` and `CMD` as the managed sandbox
workload, enable entrypoint inheritance and wait for its exit status:

```python
with Sandbox(image="example/worker:latest", inherit_entrypoint=True) as sandbox:
    exit_code = sandbox.wait_entrypoint()
    print(exit_code, sandbox.entrypoint_exit_info)
```

For this image-inheritance path, `sandbox.startup_command` is `None`.
`wait_entrypoint()` returns an integer exit code; `entrypoint_exit_info`
contains the structured exit details collected by that call. Waiting observes
process exit, not application readiness. An inherited process exiting after
successful sandbox creation does not by itself terminate the sandbox.
For a long-running service, check its application health endpoint separately.

`Sandbox(cwd=...)` sets the default working directory for subsequent
`sandbox.commands.run()` calls that omit `cwd`. With `inherit_entrypoint=True`,
the image process starts in the image's OCI `WORKDIR`; the constructor's
`cwd` does not override it. For example, if the image declares `WORKDIR /app`:

```python
with Sandbox(
    image="example/worker:latest", inherit_entrypoint=True, cwd="/tmp"
) as sandbox:
    # The inherited image entrypoint starts in /app.
    print(sandbox.commands.run("pwd").stdout)  # /tmp
    print(sandbox.commands.run("pwd", cwd="/").stdout)  # /
```

Or use an object in S3-compatible storage as the rootfs:

```python
from akernel_sdk import S3Config, Sandbox

rootfs = S3Config(
    endpoint="https://s3.example.com",
    bucket="akernel-rootfs",
    object="ubuntu-24.04/rootfs.img",
    access_key="<optional>",
    secret_key="<optional>",
)

with Sandbox(rootfs=rootfs) as sandbox:
    print(sandbox.commands.run("cat /etc/os-release").stdout)
```

`image` and `rootfs` are mutually exclusive. The SDK generates the backend
wire representation; callers do not pass raw rootfs JSON or override the
runtime inside an S3 object. When neither source is supplied, AKernel sends
only the selected isolation runtime and openYuanRong overlays it onto the
rootfs configured by the deployed service.

Firecracker supports `Sandbox(runtime="firecracker", image="ubuntu:24.04")`
with the bundled virtio-fs configuration. Both OCI and Nydus roots use the
image manager's directory directly, without conversion to EROFS. The shared
root stays read-only; sandbox writes use its private ext4 overlay. The deployed
default and explicit `rootfs` S3 objects continue to use raw EROFS images.
The bundled distill-fs supports Nydus RAFS v5. When preparing Nydus images with
`nydusify convert`, select `--fs-version 5` explicitly.
Custom deployments must enable `plugin.runtime.firecracker.virtiofs_enabled`
and install the matching Firecracker stack and virtiofsd.

The same `S3Config` type can be used as a read-only mount source:

```python
from akernel_sdk import Mount

mount = Mount(target="/models", type="erofs", s3_config=rootfs)
with Sandbox(mounts=[mount]) as sandbox:
    print(sandbox.commands.run("ls /models").stdout)
```

OCI images can also be mounted read-only:

```python
mount = Mount(target="/opt/tools", image_url="ubuntu:24.04")
```

Firecracker mounts must instead use `type="erofs"` with an S3 object containing
a raw EROFS image; it rejects `image_url` and directory-backed mounts.

## Launch from a Dockerfile

Dockerfile direct launch is a supported AKernel SDK capability and will remain
available. Its documented strict subset evolves incrementally with production
experience; unsupported inputs continue to fail closed. The specific API surface
may evolve, with documentation and migration guidance for material changes. It
is not a general-purpose Docker build.

`FROM` supplies only the root filesystem; inherited OCI configuration is not
applied. Precheck the context, then pass its launch configuration to `Sandbox`:

```python
from akernel_sdk import DockerfileLaunch, LocalDockerContext, Sandbox, check_direct_launch
context = LocalDockerContext("Dockerfile", context_dir=".")
if check_direct_launch(context).direct_launchable:
    with Sandbox(dockerfile=DockerfileLaunch(context, run_timeout=300)) as sandbox:
        startup = sandbox.startup_command
        if startup is not None:
            # For a finite CMD/ENTRYPOINT, collect its exit code and output.
            result = startup.wait(timeout=60)
            print(result.exit_code, result.stdout, result.stderr)
```

`startup_command` is a `CommandHandle` for the Dockerfile's background
`CMD`/`ENTRYPOINT`. It is `None` when `auto_start_cmd=False` or no startup
command is declared. The handle supports `wait(timeout=...)` and `kill()`;
construction confirms dispatch, while application readiness requires a
separate health check. For long-running services, perform that check instead
of waiting for exit during startup.

`wait_entrypoint()` is exclusive to image launches with
`inherit_entrypoint=True`; calling it on a Dockerfile launch raises
`RuntimeError`, and `entrypoint_exit_info` is `None`. See the
[image launch examples](#rootfs-and-mounts) for that path.

`RUN`, `COPY`, and `ADD` run on every launch without a snapshot or cache;
unsupported Dockerfiles must be built externally. Read the complete contract,
security boundaries, and supported syntax in
[the Dockerfile launch guide](./docs/launch-from-dockerfile.md). See the
[runnable example](./examples/dockerfile_launch.py).

## Resources and lifecycle

`resources()` returns stable `NodeInfo` values rather than backend objects:

```python
from akernel_sdk import resources

for node in resources():
    print(node.id, node.status, node.capacity, node.allocatable, node.labels)
```

Accelerators appear under keys such as `GPU/l20`. Capacity is the total card
count and allocatable is the currently free count. `ak resources` renders the
same information as, for example, `gpu/l20 1/4`. Its status column renders only
normal nodes as `OK`; unavailable states remain explicit as `EVICTING`,
`RECOVERING`, or `TO_BE_DELETED`.

Use a context manager or call `kill()` in a `finally` block. The SDK does not
delete sandboxes during garbage collection or interpreter exit. Cleanup failures
propagate and can be retried explicitly; existing workload exceptions are
preserved on context exit. Server `idle_timeout` is a fallback, not a maximum
lifetime.

For a named detached sandbox, explicitly delete it when it is no longer needed:

```python
sandbox = Sandbox(name="worker", detached=True)
sandbox.kill()             # closes local clients; remote sandbox remains
Sandbox.delete("worker")   # terminates the named remote sandbox
```

`sandbox.id` is the physical ID shown by `ak list`. `get_info()` returns a
`SandboxInfo` containing `id`, state, requested CPU, memory, XPU and storage,
and the OCI image when one was configured.

## CLI

The `ak` CLI is installed with the SDK package:

```bash
ak resources
ak list
ak list --quiet
ak exec <sandbox-id>
ak exec <sandbox-id> -- /bin/sh
ak delete <sandbox-id> [<sandbox-id> ...]
```

It uses the same `AKERNEL_SERVER_ADDRESS` and `AKERNEL_TOKEN` environment as
the Python API.

`ak list` displays `ID`, `TENANT`, and `STATUS`. Administrator keys can see
instances across tenants; tenant keys only see their own instances. `ak list
--quiet` still prints only IDs. A server that omits `tenant_id` is displayed
with `-` in the tenant column; the CLI does not guess the owner.

`ak resources` displays per-node CPU, memory, disk, and accelerator resources.
`DISK` is scheduling capacity, `DISK ALLOC` is allocated capacity, and
`DISK%` is the allocated fraction. Disk values use GiB (`G`) after converting the API's MiB values.
The cluster summary totals disk capacity and allocation.
CPU/MEM used values and disk allocation describe the scheduling ledger, not
measured process usage or filesystem occupancy. Missing disk values are
shown as `-` and excluded from disk totals.

## Examples and tests

Maintained examples are under [`examples/`](./examples):

- `basic_usage.py`
- `command_stdin.py`
- `custom_image.py`
- `dockerfile_launch.py`
- `failover_reload.py`
- `gpu_sandbox.py`
- `named_sandbox.py`
- `network_policy.py`
- `pty.py`
- `port_forwarding.py`
- `reverse_tunnel.py`
- `s3_rootfs_and_mounts.py`
- `storage_sandbox.py`

Run unit tests without a deployment:

```bash
PYTHONPATH=sdk/python \
  python -m unittest discover -s sdk/python/tests/unit -t sdk/python -v
```

Run the integration suite against a configured deployment:

```bash
export AKERNEL_RUN_INTEGRATION=1
PYTHONPATH=sdk/python \
  python -m unittest discover -s sdk/python/tests/integration -t sdk/python -v
```

Set `AKERNEL_TEST_RUNTIME=firecracker` and `AKERNEL_TEST_IMAGE=ubuntu:24.04`
to exercise the same suite, including checkpoint/reload, against a virtio-fs
image root. `AKERNEL_TEST_IMAGE` also accepts a Nydus image reference and
enables a check that writes stay private to each sandbox sharing the image.
The test image must provide an Ubuntu/Debian userspace with `apt-get` for the
checkpoint test's curl and CA certificate installation. Omit
`AKERNEL_TEST_IMAGE` to test the deployed default EROFS root.

Load and transfer benchmarks live under [`benchmarks/`](./benchmarks) and are
not part of the default test suite.

## Public value types

| Type | Fields |
|---|---|
| `CommandResult` | `stdout`, `stderr`, `exit_code` (optional), `status`, `error_code`, `error_message` (optional) |
| `CommandInfo` | `pid`, `command`, `running` |
| `EntryInfo` | `name`, `path`, `type`, `size`, `permissions`, `modified_time` |
| `SandboxInfo` | `id`, `state`, `cpu`, `memory`, `image`, `xpu`, `storage_mb` |
| `NodeInfo` | `id`, `status`, `capacity`, `allocatable`, `labels` |
| `S3Config` | `endpoint`, `bucket`, `object`, optional credentials |
| `Mount` | `target`, one source, and `type` |
| `HttpReverseTunnel` | `target`, `reverse_port`, `listen_port`, `connect_timeout` |
| `PortRange` | `first`, `last` |
| `NetworkRule` | action, direction, protocol, peer and sandbox port selectors, priority |
| `TrafficPolicy` | independent ingress/egress defaults, rules, mode |
| `DNSRule` | `pattern`, `action` |
| `DNSPolicy` | `default_action`, `rules` |
| `NetworkPolicy` | legacy fields or schema v2 `traffic` and `dns` sections |
| `DockerfileLaunch` | `context`, `auto_start_cmd`, `run_timeout` |
| `DockerContext` | Abstract Dockerfile and build-context source |
| `DockerContextEntry` | `path`, `kind`, `mode` |
| `LocalDockerContext` | Local Dockerfile and context implementation |
