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.
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,
) -> Sandboximagestr | bladekit.ImageImage alias (
fabric/base-cuda12), a full registry ref, or abladekit.ImageDSL instance (built on the fly, keyed by hash). Required, positional.gpustr | NoneDefault:NoneGPU type string such as
RTX-A4500, orRTX-A4500:4for a multi-GPU sandbox.Nonerequests a CPU-only sandbox.cpuint | NoneDefault:NoneCPU millicores. Falls back to the server default when omitted.
memoryint | NoneDefault:NoneMemory in MiB. Falls back to the server default when omitted.
volumeslist | NoneDefault:NoneVolume mounts — the same format as runs: mount-spec dicts or
bladekit.Volumemount objects.secretslist | NoneDefault:NoneSecret names (or
bladekit.Secretinstances) to inject as environment, same as runs. A sandbox exists to run untrusted code, so this is refused unlessallow_secrets=Trueis passed as well; every sandbox created with secrets is recorded as asandbox.secrets_attachedaudit event, and a function's runner token can never attach secrets.allow_secretsboolDefault:FalseExplicit acknowledgement that the code inside the sandbox will be able to read the values of
secrets. Emits aRuntimeWarning.timeoutintDefault:3600Global sandbox lifetime in seconds (max 86400). After it elapses the sandbox is force-terminated — the anti-zombie fence.
runtimestrDefault:"runsc"runsc(gVisor, untrusted default) orrunc(trusted debug only). Any other value raisesInvalidArgumentError.block_networkboolDefault:FalseTruegives 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_portint | NoneDefault:NoneOptionally pre-register a tunnel port at creation time.
namespacestr | NoneDefault:NoneNamespace name for quota tracking.
priorityintDefault:5Scheduler priority, 0–9. Higher is more urgent.
waitboolDefault:TrueBlock until the sandbox is
running. PassFalseto return the handle immediately while it is stillcreating(advanced/async use).clientClient | NoneDefault:NoneOverride the default client. Mainly for tests.
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()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.
sb.exec(
*cmd,
stdin=None,
stream=False,
env=None,
workdir=None,
tty=False,
timeout=None,
) -> SandboxProcess*cmdstrThe command and its arguments, passed as separate positional strings (the program name followed by each argument). At least one is required.
stdinbytes | NoneDefault:NoneBytes to feed as stdin. One-shot mode only — ignored when
stream=True.streamboolDefault:FalseOpen a WebSocket for interactive streaming stdin/stdout/stderr instead of buffering the result.
envdict | NoneDefault:NoneExtra environment variables for this exec, merged over the sandbox environment.
workdirstr | NoneDefault:NoneWorking directory inside the container for this exec.
ttyboolDefault:FalseAllocate a pseudo-TTY; stdout and stderr are merged into a single output stream.
timeoutint | NoneDefault:NonePer-exec timeout in seconds. On expiry the exec process (not the sandbox) is SIGKILLed and reports exit code 124; the sandbox keeps running.
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_idstrThe
sx_identifier for this exec.stdoutSandboxIOCaptured stdout.
read()blocks until exit and returns all bytes; iterating yields lines as bytes.stderrSandboxIOCaptured stderr, same interface as
stdout.stdinSandboxStdin | NoneWrite side of stdin — only present for streaming execs.
write(bytes)sends data;close()sends EOF.returncodeint | NoneRaw exit code,
Noneuntil the process exits.
Methods
proc.wait(timeout=None) -> int # block until exit, return raw exit code
proc.poll() -> int | None # non-blocking exit code (None if running)proc = sb.exec("bash", "-c", "exit 3")
code = proc.wait()
if code != 0:
print("command failed with", code) # command failed with 3File 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.
sb.write_file(path, data) -> None # data: bytes
sb.read_file(path) -> bytes
sb.ls(path="/", *, limit=200, cursor=None) -> list[dict]pathstrAbsolute path inside the sandbox rootfs. For
ls, defaults to/.databytesFile contents to write (
write_fileonly).limitintDefault:200Max directory entries per page (
lsonly).cursorstr | NoneDefault:NonePagination cursor for the next page of a large directory (
lsonly).
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.
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.
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_ tunnelportintContainer port to expose (
tunnel).tunnel_idstrThe
tn_id of the tunnel to close (close_tunnel).
url = sb.tunnel(8080)
# https://<sb>-8080.sandbox.forge.internalLifecycle — 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.
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() -> Nonepoll()-> str | NoneCurrent state — one of
creating,running,terminating,terminated,failed— orNoneif unreachable. Does not block.wait()-> strBlock until a terminal state (
terminatedorfailed) and return it. RaisesTimeoutErroriftimeoutseconds elapse first.wait_for_running()-> NoneBlock until
running. RaisesSandboxFailedErrorif it transitions tofailed,SandboxTerminatedErrorif it is terminated first, orTimeoutErroraftertimeout(default 120s).terminate()-> NoneTerminate 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.
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 automaticallySandbox exceptions
Sandbox operations raise typed subclasses of FabricError. Full hierarchy and retry guidance live in the errors reference.
| Exception | Code / trigger |
|---|---|
SandboxNotRunningError | SANDBOX_NOT_RUNNING (409) — exec/file/tunnel on a sandbox that is not running. |
SandboxExecLimitError | SANDBOX_EXEC_LIMIT (429) — more than max_concurrent_execs (16) execs in flight. |
SandboxFileNotFoundError | SANDBOX_FILE_NOT_FOUND (404) — read_file path does not exist. |
SandboxFileTooLargeError | SANDBOX_FILE_TOO_LARGE (413) — a single write exceeds the 5 GiB per-file limit. |
SandboxDiskQuotaExceededError | SANDBOX_DISK_QUOTA_EXCEEDED (413) — write exceeds the sandbox's cumulative disk quota. |
SandboxPathInvalidError | SANDBOX_PATH_INVALID (400) — non-absolute path, .., or symlink escape. |
SandboxPortInUseError | SANDBOX_PORT_IN_USE (409) — the port is bound by a different tunnel. |
SandboxTerminatedError | The sandbox died while an exec was in flight; raised from proc.wait() (no exit code). |
SandboxFailedError | The sandbox transitioned to failed before running (e.g. IMAGE_PULL_FAILED, GPU_QUOTA_EXCEEDED). |
RuntimeUnavailableWarning | RUNTIME_UNAVAILABLE — surfaced as a warning on create(), not raised; the sandbox waits for a matching node. |
Reference from the Blade console · Updated .