Skip to main content

maf-sandbox-docker

PyPI Python License

Experimental. This package is early-stage (pre-1.0, Development Status :: 4 - Beta) — its API may change or be removed in a future release without notice. Importing it emits a one-time MafSandboxDockerExperimentalWarning; suppress it with warnings.filterwarnings("ignore", category=maf_sandbox_docker.MafSandboxDockerExperimentalWarning) once you've read the notice.

This package is not affiliated with, endorsed by, or a product of Docker Inc. or Microsoft — it is a third-party sandbox backend for Microsoft Agent Framework.

app  ->  maf_sandbox  ->  maf_sandbox_docker  ->  the container

The sandbox backend for everyone wslc leaves out: plain Docker containers, driven through the docker command-line client, on any machine with a Docker-compatible engine — macOS, Linux, Windows with WSL 2, and every GitHub Actions ubuntu-latest runner. No subscription, no login, and no dependency but maf-sandbox itself. A workload written against the protocol runs here unchanged, which is what makes it a workload rather than an integration.

Quickstart

pip install maf-sandbox-docker
from maf_sandbox import Isolation, SandboxRouter
from maf_sandbox_docker import DockerSandboxBackend, DockerSandboxConfig

router = SandboxRouter([DockerSandboxBackend(DockerSandboxConfig())], min_isolation=Isolation.CONTAINER)

samples/06_docker_codeact runs those two lines end to end: an agent that executes model-written Python in a container and reads the result back out. Its siblings 03_acas_codeact and 04_wslc_codeact are the same program on a microVM-isolated Azure backend and on wslc, and the diff between any two of them is two imports and one constructor.

Requirements

A Docker-compatible engine, reachable through the docker client. Docker Desktop (macOS, Linux, Windows with WSL 2) and Docker Engine (Linux, rootful or rootless) are what this backend supports. The client's own configuration — DOCKER_HOST, the active context, TLS settings — is inherited, because every call is a subprocess that inherits this process's environment; point DockerSandboxConfig.docker_path at a different client binary to use another one. Colima, OrbStack, Rancher Desktop and Podman expose Docker-compatible sockets and may work through the same client (Podman's default outbound network is called podman, so set outbound_network="podman" in allowlist mode), but they are not officially supported and nothing here is verified against them.

Every call spawns the docker client, so the host's event loop has to be one that can start subprocesses — asyncio's default Proactor loop on Windows does, and a host that installs WindowsSelectorEventLoopPolicy has to undo that first, or every acquire fails with a message saying so.

Hosts this backend does not serve: Windows without WSL (Docker Desktop's Hyper-V backend is documented by Docker but not its default, needs Pro or Enterprise, and is not verified here; Windows Home has no route at all), GitHub Actions' windows-latest (Windows containers only) and macos-latest (no Docker, no nested virtualization). For WSL-less Windows the eventual answer is a separate backend over Docker's "Docker Sandboxes" micro-VM product.

What this backend declares

Isolation.CONTAINER. A container shares the host kernel, below SandboxRouter's default min_isolation=Isolation.MICROVM floor — construct the router with min_isolation=Isolation.CONTAINER and it admits this backend; leave the floor at its default and construction raises SandboxBackendNotPermitted. A Docker Desktop or Colima VM does not lift the rung: one shared VM kernel serves every container, the same shape wslc's WSL 2 utility VM has, and the ladder classifies that at container. The declaration is a constant — no configuration raises it, because a security level the backend cannot verify must not become one the router repeats.

Egress.CLOSED by default, Egress.ALLOWLIST on request. With no proxy configured every container is created --network none: a network namespace with only loopback, enforced by whichever kernel runs the container, so a spec's allowlist is honoured by denying everything — confining more than a workload asked for, which the router permits with a warning precisely because the failure is loud.

Set egress_proxy_image and the declaration becomes ALLOWLIST: each sandbox gets its own internal network and a dual-homed filtering proxy, and the spec's allowlist is enforced by topology — the container has no route out except the proxy, which opens a CONNECT tunnel only to the hosts the spec names. The HTTP_PROXY/HTTPS_PROXY variables set on the workload are how ordinary clients find the proxy, not what enforces the allowlist; the topology is. TLS is not decrypted, and the sandbox never resolves an external name itself. The proxy is shipped as source, not as an image you must trust: build it from the packaged recipe, whose only pinned dependency is its Azure Linux base.

from maf_sandbox_docker import proxy_build_context, DockerSandboxConfig

print(f"docker build -t maf-egress-proxy:local {proxy_build_context()}")  # run this once
config = DockerSandboxConfig(egress_proxy_image="maf-egress-proxy:local")

Capability.FILES_OUT, never Capability.FILES_LIST. This backend reads declared outputs back out — docker cp <container>:<path> - streams a tar whose first 512-byte header carries the size, the entry type and any link target, so a file is statted and read from one stream with no stat command and no shell in the image. It does not enumerate directories: Docker has no engine-level primitive for it, which is exactly why the protocol splits enumeration into FILES_LIST. A kind that cannot name its outputs in advance requires that capability and is refused here — served instead by a backend, like ACAS, that has native listing.

Every path component is checked, not just the last one. A symlink is refused on the tar entry's type bit only when it is the entry being tarred; the engine resolves the path daemon-side, so a guest that points out at /etc gets a stat of out/hostname describing a regular file with the parent link nowhere in it. stat_file and read_file therefore stat every parent component from the filesystem root down — not from the working directory, whose own ancestors the guest can replace just as easily: with /acas -> / unchecked, /acas/work stats as a real directory and serves /. The walk itself is maf_sandbox.paths.refuse_symlinked_parents, not a copy living here: this backend passes it the unconfined tar-header stat above. A link is refused as a confinement failure and any other non-directory as an ordinary ENOTDIR — the entry comes back as EntryKind.SYMLINK or EntryKind.OTHER, so a caller can tell an escape from a guest tripping over its own fifo. One residual stays open: the walk and the read are separate calls and docker cp has no no-follow form, so a guest that swaps a stat-ed component for a link in between is followed.

Whether that is actually enforced is not this package's own claim either. maf_sandbox.conformance is the shared suite every backend serving FILES_OUT answers, and this is the one backend that answers it against a real engine on every pull request — a container on the runner, a hostile layout planted in it through the public surface, and the probes attacking that.

The backend

DockerSandboxBackend implements maf_sandbox.SandboxBackend:

acquire(key, spec) get-or-create, keyed (scope, thread, agent, kind). A running container is reused, a stopped one started, a missing one created; an absent image is pulled explicitly first so a cold pull does not ride the lifecycle timeout
write_file(path, content) a one-entry tar on stdin to cp - <container>:/, which creates the parent directories from the entry name; str is UTF-8, bytes is written as given
stat_file / read_file the FILES_OUT pull surface — stat from the first tar header of docker cp, read from the same stream; symlinks and other non-regular entries refused on the header type, every parent component refused unless it is a real directory, a body over the caller's cap refused rather than truncated
dispose(key) rm -f on every kind's container the key names, with the proxy and network of an allowlisted one
dispose_scope(scope, thread) delete every container for a conversation — by label, read back from docker, not from process memory
isolation container, unconditionally
egress closed, or allowlist when egress_proxy_image is set
capabilities {EXEC, FILES_IN, FILES_OUT}
limits the transfer ceilings a spec may not exceed, per direction

Container names are derived from the key and kind rather than remembered, so acquire and dispose agree on one without a registry to keep in sync. Labels are the durable record dispose_scope selects on, and their values are digested when they are long or carry a separator — the same mapping on both sides, because transforming one and not the other makes a purge quietly select nothing.

No bind mounts, no host paths, and never the Docker socket cross into a sandbox — files go in and out only through docker cp. The hardening flags --security-opt no-new-privileges and --pids-limit go on every container; --cap-drop ALL, --memory and --cpus are opt-in through the config.

stop is never used. A container whose init process ignores SIGTERM takes ten seconds to stop and a fraction of a second to remove, and there is nothing in a sandbox worth waiting for.


Maintained by SOKOLAI BV.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

maf_sandbox_docker-0.2.1.tar.gz (26.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

maf_sandbox_docker-0.2.1-py3-none-any.whl (29.2 kB view details)

Uploaded Python 3

File details

Details for the file maf_sandbox_docker-0.2.1.tar.gz.

File metadata

  • Download URL: maf_sandbox_docker-0.2.1.tar.gz
  • Upload date:
  • Size: 26.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for maf_sandbox_docker-0.2.1.tar.gz
Algorithm Hash digest
SHA256 96d8e1c761280c479c7680e248091887d95ba7daa864e59baa7f2b47cf8fa2c9
MD5 8a9ccc753d92d776ad4397e6864017c2
BLAKE2b-256 2e5a8f37e79ddbf4ae0e0cdd0f6caea2f6fdb4881b6f3b32e2bc1a721e8e3d93

See more details on using hashes here.

Provenance

The following attestation bundles were made for maf_sandbox_docker-0.2.1.tar.gz:

Publisher: publish-packages.yml on sokolaidev/maf-extensions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file maf_sandbox_docker-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for maf_sandbox_docker-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d062146a22788ab709cfcf69d46665bde8c44ea3b60fac5b1a7deae23a61c441
MD5 59c3a4fea680bc72264a84713ab96e92
BLAKE2b-256 27a05df66eaeda6c2538c3a81b97c8ee5fc99dc237b6697c1f728d8046e2ef41

See more details on using hashes here.

Provenance

The following attestation bundles were made for maf_sandbox_docker-0.2.1-py3-none-any.whl:

Publisher: publish-packages.yml on sokolaidev/maf-extensions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.15.2

2 files

0.15.1

2 files

0.15.0

2 files

0.14.1

2 files

0.14.0

2 files

0.13.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.2

2 files

0.8.1

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 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