DockerBackend (v0.5.1+)¶
DockerBackend runs a Python function inside an ephemeral Docker
container. It is one of the SandboxBackend implementations in
agent_airlock.sandbox_backend (E2B, Docker, Local, Modal, and a
Managed Agents stub).
What v0.5.1 actually ships¶
- Timeout enforced.
DockerBackend.execute(..., timeout=60)callscontainer.wait(timeout=...)and kills+removes the container on timeout. Prior to v0.5.1 thetimeoutkwarg was a TODO, so a runaway function would hang forever. See CHANGELOG v0.5.1. no-new-privilegesalways on. The container runs withsecurity_opt=["no-new-privileges:true"]so a child process cannot regain privileges viasetuid.- All capabilities dropped by default.
cap_drop=["ALL"]— noNET_ADMIN,SYS_ADMIN, nothing. Tools that legitimately need a capability must be explicitly allow-listed via your own wrapping backend. - Pass-through
security_opt. Supply a seccomp profile via thesecurity_opt=["seccomp=/path/to/profile.json"]constructor parameter. No default profile is shipped — Docker's own default seccomp profile applies when you don't override. - Network isolation default.
network_mode="none"unless you explicitly opt out. If your tool needs the network, use agent- airlock'sEndpointPolicyat the app layer rather than opening the sandbox. - Integration tests. Five tests behind the
pytest -m dockermarker prove availability, success, an async function awaited, timeout, and network isolation. Defaultpytestruns exclude them; CI'sdocker-sandboxjob builds the repoDockerfileand requires all five to pass.
Usage¶
@Airlock(sandbox=True) does not take a backend: its sandbox path
always runs through E2B, and AirlockConfig has no backend setting.
Call the backend's execute() yourself. It returns a SandboxResult
instead of raising, and the timeout is a per-call argument, not a
constructor parameter:
from agent_airlock.sandbox_backend import DockerBackend
backend = DockerBackend(
image="airlock-sandbox:py3.11", # must have cloudpickle installed; see below
memory_limit="256m",
cpu_limit=0.5,
security_opt=["seccomp=/etc/docker/airlock-seccomp.json"],
)
def risky_thing(arg: str) -> str:
return arg.upper()
result = backend.execute(risky_thing, args=("hello",), kwargs={}, timeout=30)
if result.success:
print(result.result) # HELLO
else:
print(result.error)
The host needs the docker Python package, which no extra installs,
and cloudpickle (in the [sandbox] extra). The image needs
cloudpickle too: the script the backend runs starts with
import cloudpickle, and with network_mode="none" the container
cannot install it, so with the default python:3.11-slim image
execute() fails with Container exited with status 1. Use the
host's Python version in the image, because cloudpickle sends a
function defined in __main__ as bytecode. A function it can import
by name is sent by reference instead, so its module must be installed
in the image as well. A minimal image:
The repo's Dockerfile builds the image the integration tests use.
To pick a backend by availability, get_default_backend(config=None)
(exported from agent_airlock) returns E2BBackend when an E2B API
key is set (on config or in E2B_API_KEY) and the SDK is installed,
else DockerBackend() with the default image when a Docker daemon
answers, else raises SandboxNotAvailableError. Pass
allow_unsafe_local=True to get LocalBackend(allow_unsafe=True)
instead, which runs the function in your process with no isolation.
Until 0.10.20 that fallback happened unasked, with only a
no_sandbox_available warning in the log.
v0.7.0 hardening flags (#37, #38)¶
Two opt-in fail-closed flags shipped together in v0.7.0:
require_rootless=True— onis_available(), inspectsdocker info'sSecurityOptionsand refuses to report available unless the daemon advertisesrootless(legacy form) orname=rootless(current form). Some threat models (multi-tenant CI, shared dev hosts) want the call to fail-closed when the daemon runs as root rather than silently downgrading.
backend = DockerBackend(image="python@sha256:...", require_rootless=True)
if not backend.is_available():
raise RuntimeError("daemon is not rootless — refusing to spawn")
require_digest_pin=True— refuses tag-only image strings at construction time. Closes the floating-tag supply-chain risk where a tag's identity can change under you. The accepted form is<name>@sha256:<64-hex>(validated by an explicit regex).
# OK
DockerBackend(image="python@sha256:" + "0" * 64, require_digest_pin=True)
# raises ValueError
DockerBackend(image="python:3.11-slim", require_digest_pin=True)
Discover the digest of a tag from the Digest: line docker pull <name>:<tag> prints,
or with docker inspect <name>:<tag> --format='{{.RepoDigests}}'.
Known gaps (still tracked)¶
- No user-namespace remap helper. Users who want
--userns=host-uid-remapstyle isolation must configure Docker themselves; we don't offer a one-liner.
Relationship to E2B and Managed backends¶
| Backend | Isolation | Per call | Dependency |
|---|---|---|---|
E2BBackend |
Firecracker MicroVM | Takes a sandbox from its config's E2B pool, or creates one, and kills it after the call | E2B cloud |
DockerBackend (this page) |
Container, cap_drop=ALL |
Starts, then removes, a new container | local Docker daemon |
ModalBackend |
Modal sandbox (gVisor) | Creates a new Modal sandbox | Modal ([modal] extra) |
LocalBackend |
None | Calls the function in-process | none (dev only) |
ManagedSandboxBackend |
None: execute() never runs the function |
Returns a failed SandboxResult |
anthropic SDK and API key |
Only the E2B path is used by @Airlock(sandbox=True), and
get_default_backend() never returns ModalBackend or
ManagedSandboxBackend.
If you're running in air-gapped / on-prem environments where E2B
is not an option, DockerBackend is the recommended choice, called
directly as in Usage.