maf-sandbox-docker-sbx
Experimental. Releases before 1.0 may change or remove APIs. Importing this package emits
MafSandboxDockerSbxExperimentalWarning.
Run sandbox commands in Docker Sandboxes, one microVM with its own Linux kernel per sandbox, on your own Windows, macOS or Linux machine. The backend drives Docker's sbx CLI and has no Python dependency beyond maf-sandbox.
This is an independent package, not a Docker or Microsoft product.
Quickstart
pip install maf-sandbox-docker-sbx
from maf_sandbox import SandboxRouter
from maf_sandbox_docker_sbx import SbxSandboxBackend, SbxSandboxConfig
backend = SbxSandboxBackend(SbxSandboxConfig())
router = SandboxRouter([backend])
The backend clears the router's default MICROVM minimum, so no min_isolation is needed.
The Python event loop must support subprocesses. Windows' default Proactor loop does; WindowsSelectorEventLoopPolicy does not.
Requirements
Install sbx and sign in once:
- Windows 11 with the Windows Hypervisor Platform:
winget install -h Docker.sbx. It lands in%LOCALAPPDATA%\DockerSandboxes\bin, which may not be onPATH; pass that path assbx_path. - macOS 14 or later on Apple silicon:
brew install docker/tap/sbx. - Ubuntu 24.04 or later with KVM, and your user in the
kvmgroup: thedocker-sbxapt package.
Then prepare the host once:
sbx login
sbx policy init deny-all
sbx settings set ssh.agentForwardingEnabled false
sbx daemon restart
The backend checks two host-wide settings at every acquire and refuses with SbxHostNotConfined when either fails. It reads them and never writes them. Because the SSH setting takes effect only after sbx daemon restart, each new sandbox is also checked for the forwarded agent socket before any workload runs. That check runs in the template's own sh, so it catches a daemon that was not restarted, not a template that lies: use templates you trust.
- SSH agent forwarding must be off. It is on by default, and it gives every sandbox a socket to the host's SSH agent.
- No MCP server may be registered (
sbx mcp ls). The host's MCP gateway answers a sandbox even under a deny-all rule.
A lapsed login fails every sbx command until a person signs in again; the backend raises SbxLoginRequired. In CI, pipe an access token to sbx login --username <user> --password-stdin.
sbx collects telemetry by default. Set SBX_NO_TELEMETRY=1 in the host's environment to opt out.
Supported operations
| Setting | Behavior |
|---|---|
| Isolation | MICROVM |
| Capabilities | EXEC, FILES_IN, FILES_OUT, FILES_LIST, FILES_DELETE, RECLAIM |
| Guest OS | POSIX |
| Network | CLOSED |
| Isolation scope | CONVERSATION |
| Egress observation | No |
| Command output | 8 MiB of stdout and stderr together; more kills the command and refuses the call |
RUN_CODE is not declared, because any image is accepted and the runtime is the image's. SNAPSHOT is not declared. HOST_TOOLS is not declared yet: an idle sandbox stops 30 seconds after its last sbx session and kills every process, and the host-tool transport has not been measured against that.
spec.image_id, or else spec.image, is passed to sbx create --template, and a warm acquire refuses a spec that changes either. Without it the sandbox uses Docker's shell template, where commands run as agent (uid 1000). An image without that user runs commands as root. Commands run in a user namespace of their own, so sudo does not work in them, even in Docker's template. The namespace maps only the command's own uid and gid. Files owned by anyone else show as uid 65534 (nobody), and chown to another user fails, also in a template that runs as root. Permission checks still use the real owners.
Any Linux image works as a template if it has /bin/sh and /bin/bash and the tools listed under Commands. Without /bin/bash, sbx cannot start the sandbox. An image built locally reaches sbx through a tar:
docker build -t my-image:local .
docker save -o my-image.tar my-image:local
sbx template load my-image.tar
Then pass image="my-image:local".
Files: a workspace answered by the host
Each sandbox mounts one fresh, private host directory, <workspace_root>/<sandbox name>/ws-<random>, and nothing else. Every create gets a new one, so a new sandbox never mounts an earlier one's files, and sbx ls tells which create made a sandbox. Every command binds that mount again at the storage base's parent, inside a user and mount namespace of its own, and then runs as the image's user. With the default storage base /maf-sandbox/work, that parent is /maf-sandbox. sbx lets only its own templates mount as root, and the namespace needs no such capability, so any template works. At create, root only makes the empty directory the mount goes over.
Stats, reads, listings and writes act on the host side of the mount. No path check is answered inside the guest.
- On macOS and Linux each path component is opened relative to its parent with
O_NOFOLLOW, so a link the guest creates between a check and an operation makes the operation fail rather than follow it. On Linux the guest can create links in its workspace, and the plane refused to read or write through each one, measured withsbxv0.45.1. - On Windows there are no descriptor-relative calls. The backend refuses reparse points, and relies on the guest being unable to create a link in its workspace, measured with
sbxv0.45.1. - Writes land in a temporary file and are renamed into place, so a write never goes through what stood at the destination.
removeandreclaimboth remove in the guest, at the guest's own authority.removefirst runs the host-side path check, thenrm -f, which refuses a directory swapped in after the check, orrm -rfwhen recursive.reclaimchecks placement, then runs the host-side check on the target, thenrm -rf: a relative target must name a child of the working directory, and an absolute one may be anywhere strictly inside the workspace. A guest keeps seeing a name for seconds after a host-side delete, so a host-side removal would leave it acting on a file that is gone.- Names the host would change, hide or merge are refused: a name that stats but is not listed verbatim, such as a case variant on a case-insensitive host. On Windows, reserved characters, reserved device names and a trailing dot or space are refused too. The name check and the operation are separate calls, and neither NTFS nor APFS offers an exact-case open, so a guest racing them can make an alias appear in between. That redirects the operation only to another name inside the guest's own workspace, which the guest could reach anyway.
File methods reach only paths under the storage base's parent. Any other absolute path is refused with ValueError. work_dir must have a parent other than /, and that parent must not already exist in the image.
workspace_root defaults to a per-user state directory: %LOCALAPPDATA%\maf-sandbox-docker-sbx\workspaces, ~/Library/Application Support/maf-sandbox-docker-sbx or $XDG_STATE_HOME/maf-sandbox-docker-sbx/workspaces. It is never the temporary directory, which a host may clean.
Commands
exec runs argv verbatim, with separate, byte-exact streams and the command's own exit code. Every command runs through a small sh wrapper. The image needs sh, base64, setsid, mount, unshare from util-linux 2.38 or later, mkdir, cat, rm and sleep, and acquire checks for all of them:
- argv is base64-encoded, because
sbxrefuses an empty argument; - a nonce on stderr marks where the command's own stderr starts, so a missing sandbox is never read as a command that exited 1;
- the command runs in its own process group. When
timeoutexpires, the backend kills that group with a secondsbx exec, bounded byexec_cleanup_timeout_seconds, and then raisesTimeoutError. Killing thesbxclient alone would leave the command running. The same command first leaves a cancel file, which the wrapper checks after recording its group, so a command that has not started yet never will. Only the expired command is touched; a sibling call in the same sandbox keeps running. If the kill cannot run at all, the sandbox is retired: every handle to it refuses further use, and the next acquire replaces it. The retirement is written to the sandbox's record, so a backend in another process, or after a restart, replaces it too. The kill reaches only the command's own process group. A command that starts its own session, or rewrites the pid file its user can write, outlives it; only disposal ends every process in the sandbox. - output past 8 MiB, stdout and stderr together, ends the command the same way and raises
SandboxExecOutputLimitExceeded, so a guest printing without end cannot fill host memory. The sandbox is kept unless the kill fails.
A missing working directory exits 125 with the shell's message on stderr.
Network access
Every sandbox is created with --deny-network "**". A per-sandbox deny beats every global allow rule and every allow added later, so the sandbox has no network whatever the host's global policy says. A denied raw TCP connection still connects to Docker's proxy and then carries no data.
ALLOWLIST is not declared. Global allow rules apply to every sandbox, including running ones, so an exact allowlist would depend on host state that can change after acquire.
Ownership, cleanup and retention
sbx has no labels, so ownership is in the name: name_prefix, then digests of the conversation, the whole key and the kind. Two creates racing one name get one sandbox and a conflict.
dispose and dispose_scope run sbx rm --force on every name with the matching prefix, taken from sbx ls and from the workspace directories. Afterwards they delete only what was on the host before the removal, never a workspace made after it, since the name is then free for another process to create again. sbx rm accepts only a name, so disposing of one instance checks its id in sbx ls first, then removes by name: another process that replaces the instance in between loses the replacement. The directories are a second record because the daemon can lose its engine and then report no sandboxes at all. That failure surfaces as SbxDaemonFault, which names sbx daemon restart, and a disposal reports it as unreachable rather than as a sandbox that is gone.
Nothing expires on its own. A sandbox left behind by a crashed host keeps its cpus and memory until a disposal or sbx rm removes it. An idle sandbox stops after 30 seconds and keeps its files; the next command starts it again, in 0.5 to 1.2 seconds on the ubuntu-24.04 runner.
Verification
The live suite in tests/test_sbx_e2e.py runs the shared storage-base, FILES_IN, FILES_OUT, FILES_DELETE, RECLAIM, reach and EXEC conformance suites against a real sandbox, and checks by response content that the network is closed. Set MAF_SANDBOX_SBX_E2E=1, and MAF_SANDBOX_SBX_PATH when sbx is not on PATH.
tests/test_sbx_e2e_host.py checks the refusals and faults that depend on host-wide state: SSH agent forwarding, a registered MCP server, a lapsed login, and a daemon that has lost its engine (its docker.sock hidden). Most of these tests change that state and put it back, so they also need MAF_SANDBOX_SBX_E2E_HOST=1. Set it only on a host no one else is using. The login test logs back in with DOCKER_USERNAME and DOCKER_PAT.
Tested with sbx v0.45.1 on Windows 11 and on GitHub's ubuntu-24.04 runner, where the live suite runs nightly. Not tested on macOS. sbx is closed source and in early access, and its behaviour has changed between releases, so re-run the live suite on every sbx version you install.
Metadata
Release files for maf-sandbox-docker-sbx 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| maf_sandbox_docker_sbx-0.2.1.tar.gz | 27.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| maf_sandbox_docker_sbx-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 57.2 kB
Release files / maf_sandbox_docker_sbx-0.2.1.tar.gz
| Download URL | maf_sandbox_docker_sbx-0.2.1.tar.gz |
|---|---|
| Size | 27.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ac7ac90c25b216e6437a7972371322fca465e4ee7b574c6b3c73029d27dd6e75
|
|
BLAKE2b-256 checksum How to use checksums |
04621cbcf49f0a5dd5e1acd906b84cae72d3090d4b270d34da4c571e4a1c8a68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / maf_sandbox_docker_sbx-0.2.1-py3-none-any.whl
| Download URL | maf_sandbox_docker_sbx-0.2.1-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
11b71e635eb28edc8dcd3b70de88d32c7e365196712bb985dae119fa8bcee3ad
|
|
BLAKE2b-256 checksum How to use checksums |
368b2c4ff93d3e80c3a2f686cd9935a6cdbab5d22015322eae493b975b36871e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log