Developer docs / AI Fabric SDK / Primitives

Sandboxes

Imperative GPU/CPU container primitive: create a sandbox, exec commands, stream output, read and write files, open tunnels, and terminate.

Sandbox.create

bladekit.Sandbox is an imperative container you drive from your own code — the escape hatch for agents and interactive workflows that need to run arbitrary commands on a GPU without defining an @app.function. Sandbox.create() schedules the container, pulls the image, and (by default) blocks until it is running, then returns a handle.

The default runtime is runsc (gVisor) — the safe sandbox for untrusted, LLM-generated code. Pass runtime="runc" only for trusted internal debugging. There is no silent fallback: if no node announces the requested runtime the sandbox stays creating and a RUNTIME_UNAVAILABLE warning is surfaced on the create response.

A sandbox is confined from the host, its neighbours and the control plane by default — but it can still reach the Internet. Pass block_network=True for code that must not be able to phone home: the container gets no network at all (loopback only, no interface, no address, no route, no DNS). exec, file I/O and tunnels keep working, since none of them goes through the sandbox's network stack; what stops working is anything the code inside reaches for, so bake dependencies into the image or mount them from a volume. Same no-silent-fallback rule as runtime: it is only scheduled on a node that enforces it, and otherwise stays creating with a NETWORK_BLOCK_UNAVAILABLE warning rather than starting with a network.

Signature
Sandbox.create(
    image,
    *,
    gpu=None,
    cpu=None,
    memory=None,
    volumes=None,
    secrets=None,
    allow_secrets=False,
    timeout=3600,
    runtime="runsc",
    block_network=False,
    tunnel_port=None,
    namespace=None,
    priority=5,
    wait=True,
    client=None,
) -> Sandbox

image str | bladekit.Image

Image alias (fabric/base-cuda12), a full registry ref, or a bladekit.Image DSL instance (built on the fly, keyed by hash). Required, positional.

gpu str | NoneDefault: None

GPU type string such as RTX-A4500, or RTX-A4500:4 for a multi-GPU sandbox. None requests a CPU-only sandbox.

cpu int | NoneDefault: None

CPU millicores. Falls back to the server default when omitted.

memory int | NoneDefault: None

Memory in MiB. Falls back to the server default when omitted.

volumes list | NoneDefault: None

Volume mounts — the same format as runs: mount-spec dicts or bladekit.Volume mount objects.

secrets list | NoneDefault: None

Secret names (or bladekit.Secret instances) to inject as environment, same as runs. A sandbox exists to run untrusted code, so this is refused unless allow_secrets=True is passed as well; every sandbox created with secrets is recorded as a sandbox.secrets_attached audit event, and a function's runner token can never attach secrets.

allow_secrets boolDefault: False

Explicit acknowledgement that the code inside the sandbox will be able to read the values of secrets. Emits a RuntimeWarning.

timeout intDefault: 3600

Global sandbox lifetime in seconds (max 86400). After it elapses the sandbox is force-terminated — the anti-zombie fence.

runtime strDefault: "runsc"

runsc (gVisor, untrusted default) or runc (trusted debug only). Any other value raises InvalidArgumentError.

block_network boolDefault: False

True gives the sandbox no network at all — loopback only, no DNS, no Internet. Enforced by the node, which simply never wires the container, so it cannot be undone from inside. Raises if the control plane is too old to confirm it, rather than returning a sandbox that still has a network.

tunnel_port int | NoneDefault: None

Optionally pre-register a tunnel port at creation time.

namespace str | NoneDefault: None

Namespace name for quota tracking.

priority intDefault: 5

Scheduler priority, 0–9. Higher is more urgent.

wait boolDefault: True

Block until the sandbox is running. Pass False to return the handle immediately while it is still creating (advanced/async use).

client Client | NoneDefault: None

Override the default client. Mainly for tests.

Create a GPU sandbox and run a command
import bladekit

sb = bladekit.Sandbox.create(
    image="fabric/base",
    gpu="RTX-A4500",
    timeout=300,
)
proc = sb.exec("python", "-c", "print('hello')")
proc.wait()
print(proc.returncode)      # 0
print(proc.stdout.read())   # b"hello\n"
sb.terminate()

Build the image from the DSL
img = bladekit.Image.from_catalog("fabric/torch")
sb = bladekit.Sandbox.create(image=img, gpu="RTX-A4500")

Sandbox.exec

Run a command inside the sandbox and get a SandboxProcess handle. In one-shot mode (stream=False, the default) the call is synchronous — it blocks server-side until the command exits and buffers up to 1 MiB per stream. In streaming mode (stream=True) a WebSocket is opened for interactive stdin/stdout/stderr.

Signature
sb.exec(
    *cmd,
    stdin=None,
    stream=False,
    env=None,
    workdir=None,
    tty=False,
    timeout=None,
) -> SandboxProcess

*cmd str

The command and its arguments, passed as separate positional strings (the program name followed by each argument). At least one is required.

stdin bytes | NoneDefault: None

Bytes to feed as stdin. One-shot mode only — ignored when stream=True.

stream boolDefault: False

Open a WebSocket for interactive streaming stdin/stdout/stderr instead of buffering the result.

env dict | NoneDefault: None

Extra environment variables for this exec, merged over the sandbox environment.

workdir str | NoneDefault: None

Working directory inside the container for this exec.

tty boolDefault: False

Allocate a pseudo-TTY; stdout and stderr are merged into a single output stream.

timeout int | NoneDefault: None

Per-exec timeout in seconds. On expiry the exec process (not the sandbox) is SIGKILLed and reports exit code 124; the sandbox keeps running.

Stream stdout line by line
proc = sb.exec("python", "train.py", stream=True, env={"EPOCHS": "3"})
for line in proc.stdout:          # yields bytes, blocking as needed
    print(line.decode(), end="")
exit_code = proc.wait()

SandboxProcess

The handle returned by exec(). Its streams are file-like SandboxIO buffers; returncode is the raw exit code with no mapping.

Attributes

exec_id str

The sx_ identifier for this exec.

stdout SandboxIO

Captured stdout. read() blocks until exit and returns all bytes; iterating yields lines as bytes.

stderr SandboxIO

Captured stderr, same interface as stdout.

stdin SandboxStdin | None

Write side of stdin — only present for streaming execs. write(bytes) sends data; close() sends EOF.

returncode int | None

Raw exit code, None until the process exits.

Methods

Signature
proc.wait(timeout=None) -> int   # block until exit, return raw exit code
proc.poll() -> int | None        # non-blocking exit code (None if running)

Inspect the raw exit code
proc = sb.exec("bash", "-c", "exit 3")
code = proc.wait()
if code != 0:
    print("command failed with", code)   # command failed with 3

File I/O — write_file, read_file, ls

Move data in and out of the sandbox rootfs. Paths must be absolute and are resolved symlink-safely server-side (openat2 RESOLVE_BENEATH); a non-absolute path or one containing .. raises SandboxPathInvalidError. Files up to 1 MiB travel inline (base64); larger payloads are streamed through a presigned URL automatically.

Signature
sb.write_file(path, data) -> None            # data: bytes
sb.read_file(path) -> bytes
sb.ls(path="/", *, limit=200, cursor=None) -> list[dict]

path str

Absolute path inside the sandbox rootfs. For ls, defaults to /.

data bytes

File contents to write (write_file only).

limit intDefault: 200

Max directory entries per page (ls only).

cursor str | NoneDefault: None

Pagination cursor for the next page of a large directory (ls only).

ls() returns a list of dicts with keys name, path, type (file / dir / symlink), size_bytes (files) and link_target (symlinks). Symlinks are listed but never followed.

Round-trip a file and list a directory
sb.write_file("/work/data.txt", b"hello")
content = sb.read_file("/work/data.txt")   # b"hello"

for entry in sb.ls("/work"):
    print(entry["name"], entry["type"])

Tunnels — tunnel, list_tunnels, close_tunnel

Expose a port from the sandbox container through the AI Fabric ingress to reach a server (Jupyter, an HTTP API, a debugger) running inside it.

Signature
sb.tunnel(port) -> str                 # returns the public URL
sb.list_tunnels() -> list[dict]        # active tunnel objects
sb.close_tunnel(tunnel_id) -> None     # close a specific tn_ tunnel

port int

Container port to expose (tunnel).

tunnel_id str

The tn_ id of the tunnel to close (close_tunnel).

Open a tunnel
url = sb.tunnel(8080)
# https://<sb>-8080.sandbox.forge.internal

Lifecycle — poll, wait, wait_for_running, terminate

Inspect state, block on transitions, and tear the sandbox down. sandbox_id is exposed as a read-only property. All exec/file/tunnel operations raise SandboxNotRunningError when the sandbox is not running.

Signature
sb.poll() -> str | None                    # current state, non-blocking
sb.wait(*, timeout=None) -> str            # block until terminal state
sb.wait_for_running(*, timeout=120.0) -> None
sb.terminate() -> None

poll() -> str | None

Current state — one of creating, running, terminating, terminated, failed — or None if unreachable. Does not block.

wait() -> str

Block until a terminal state (terminated or failed) and return it. Raises TimeoutError if timeout seconds elapse first.

wait_for_running() -> None

Block until running. Raises SandboxFailedError if it transitions to failed, SandboxTerminatedError if it is terminated first, or TimeoutError after timeout (default 120s).

terminate() -> None

Terminate the sandbox (SIGTERM → grace → SIGKILL). Idempotent; frees the GPU and any tunnels.

Sandbox is a context manager — terminate() is called on __exit__, so a with block guarantees cleanup even on error.

Context manager — auto-terminate
with bladekit.Sandbox.create(image="fabric/base", gpu="RTX-A4500") as sb:
    proc = sb.exec("nvidia-smi")
    print(proc.stdout.read().decode())
    proc.wait()
# sb.terminate() runs here automatically

Sandbox exceptions

Sandbox operations raise typed subclasses of FabricError. Full hierarchy and retry guidance live in the errors reference.

ExceptionCode / trigger
SandboxNotRunningErrorSANDBOX_NOT_RUNNING (409) — exec/file/tunnel on a sandbox that is not running.
SandboxExecLimitErrorSANDBOX_EXEC_LIMIT (429) — more than max_concurrent_execs (16) execs in flight.
SandboxFileNotFoundErrorSANDBOX_FILE_NOT_FOUND (404) — read_file path does not exist.
SandboxFileTooLargeErrorSANDBOX_FILE_TOO_LARGE (413) — a single write exceeds the 5 GiB per-file limit.
SandboxDiskQuotaExceededErrorSANDBOX_DISK_QUOTA_EXCEEDED (413) — write exceeds the sandbox's cumulative disk quota.
SandboxPathInvalidErrorSANDBOX_PATH_INVALID (400) — non-absolute path, .., or symlink escape.
SandboxPortInUseErrorSANDBOX_PORT_IN_USE (409) — the port is bound by a different tunnel.
SandboxTerminatedErrorThe sandbox died while an exec was in flight; raised from proc.wait() (no exit code).
SandboxFailedErrorThe sandbox transitioned to failed before running (e.g. IMAGE_PULL_FAILED, GPU_QUOTA_EXCEEDED).
RuntimeUnavailableWarningRUNTIME_UNAVAILABLE — surfaced as a warning on create(), not raised; the sandbox waits for a matching node.

Reference from the Blade console · Updated .

Contact the team

Loading the form…