maf-sandbox
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-timeMafSandboxExperimentalWarning; suppress it withwarnings.filterwarnings("ignore", category=maf_sandbox.MafSandboxExperimentalWarning)once you've read the notice.
This package is not affiliated with, endorsed by, or a product of Microsoft — it is a third-party reference implementation of microsoft/agent-framework#7568, written for use with Microsoft Agent Framework but with no dependency on it in its protocol layer.
Quickstart
pip install maf-sandbox
from maf_sandbox import Isolation, SandboxKey, SandboxRouter, SandboxSpec, WorkspaceContext
# Implement SandboxBackend against your own provider — or install maf-sandbox-acas for a
# ready-made Azure Container Apps Sandboxes backend — then wire it into a router:
router = SandboxRouter([my_backend], deployed=False)
sandbox = await router.acquire(SandboxKey(scope="tenant-1", thread_id="t-1", agent_dir="devops"), SandboxSpec(kind="bicep", image="bicep-sandbox:0.46.1", egress_allow=("mcr.microsoft.com",), work_dir="/workspace"))
samples/01_acas_bicep is that wiring as a runnable program, including the part no snippet shows well: building the WorkspaceContext out of callables rather than values, which is what keeps a SandboxKey a property of the host's request.
Threat model
This package draws no isolation boundary itself — it is protocol and policy over whatever a SandboxBackend implementation actually provides. Isolation states three tiers a backend can declare, from strongest to weakest: vm (a VM boundary — the whole guest, not just a process, is untrusted), container (a shared-kernel boundary), and process (no boundary beyond the OS's own process isolation). SandboxRouter enforces the one rule below on top of that declaration; the package's job is to make an unsafe backend selection fail loudly at construction, not silently at first use. Beyond backend selection, this layer has nothing else to get wrong: it holds no credentials, executes nothing, and reaches no network — everything security-relevant about a specific sandbox lives in the backend that implements it.
The vocabulary
SandboxKey |
(scope, thread_id, agent_dir) — the one sandbox a caller may reach |
SandboxSpec |
what a sandbox of a given kind needs: image, egress allowlist, work dir |
Sandbox |
write_file + exec — all a workload gets |
SandboxBackend |
acquire / dispose / dispose_scope |
SandboxRouter |
picks the backend, enforces the deployed rule |
SandboxPurger |
duck-typed purge_scoped_thread(scope, thread_id) for a host's delete path |
SandboxKey's scope and thread come from the host's request context through WorkspaceContext, whose fields are callables read at call time rather than values. That is deliberate: a key a caller can supply is a key a model can supply, and that would let one conversation address another's sandbox.
SandboxSpec.egress_allow is an allowlist — everything not named is denied, so an empty tuple means no network. Stating it positively means a spec that forgets to mention egress gets the closed configuration rather than the open one.
The two rules that are not conveniences
DEPLOYED_ISOLATION = frozenset({Isolation.VM})
A backend declares its own isolation (vm / container / process). When the host reports it is running deployed, the router refuses to select anything weaker than a VM boundary — raising SandboxBackendNotPermitted at construction, not at first use, so a misconfigured deployment cannot start with the feature apparently enabled and quietly unsafe.
It refuses rather than degrades. Falling back to a stronger backend would hide a misconfiguration; proceeding with the weaker one would break claims the host's security posture makes about every execution surface. Neither is better than an error.
A hardened container runtime (gVisor, Kata, Firecracker) is deliberately not in the permitted set. Admitting one is a decision for whoever owns those posture claims, taken there first.
The second rule is about egress, and it exists because egress_allow was a contract nothing checked. A backend that reads it and one that ignores it have the same type, the same methods and the same passing tests, so each one declares an Egress level — allowlist (deny by default, allow the named hosts), closed (all or nothing), or unrestricted (cannot confine egress at all) — and SandboxRouter.ensure_can_serve(spec) refuses the last one where a workload attaches its tool. That is the first moment a backend and a spec are both in hand; the router is built before any workload exists.
Which direction a backend misses by decides the outcome, and it is not symmetrical. A backend that confines less than the spec asks silently widens what the workload was designed to reach. One that confines more is permitted, with a warning: the sandbox reaches nothing it should not, and the workload fails visibly at whatever it could not fetch.
Note that the two rules answer to different owners. How strong the boundary must be is the host's policy, read from deployed. What a sandbox may reach is a property of the workload, stated in its spec. Merging them into one "required capabilities" list would let a workload ask for a weaker boundary than the deployment mandates.
ensure_can_serve is also the whole of a wiring test, in your own repository, against your own backend choice:
router.ensure_can_serve(bicep_sandbox_spec())
Writing a backend
Implement name, isolation, egress, acquire, dispose, dispose_scope. Three things worth knowing before you start:
Declare egress honestly. It is read before any workload's tool is attached, and a backend that omits it is treated as unrestricted and refused: one written before the property existed cannot have been enforcing an allowlist it never read, so silence is read as enforcing nothing rather than excused.
acquire is get-or-create. A workload's fix-round loop calls it every iteration; returning a cold sandbox each time turns a seconds-long loop into a minutes-long one.
dispose_scope must not consult only your process's memory. A multi-replica host serves a conversation delete wherever it lands, so the replica that created the sandbox is usually not the one deleting it. Derive the set from the service — labels, a listing, whatever your provider offers. A backend that skips this leaves billable compute running and the bug is invisible on a single-replica dev box.
Both dispose methods are best-effort by contract: purge must never fail a delete.
Provenance
Extracted from a production agent application, where this seam was written for its first execution surface: a tool that compiles agent-authored infrastructure code in a sandbox. The deployed-isolation rule above is not a preference — it is what a security review concluded when it worked through what a shared-kernel boundary does not close for code an agent wrote.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file maf_sandbox-0.3.0.tar.gz.
File metadata
- Download URL: maf_sandbox-0.3.0.tar.gz
- Upload date:
- Size: 22.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2580d45ef8c78b6d2962cf2c5224895fd01ac14c32dac32b41e2d78b442a526c
|
|
| MD5 |
3563b545c89a50ad5dc6a8ce3eabd237
|
|
| BLAKE2b-256 |
c8a15473f92b8d8649a64ae63192c3843d4afa9e4d7ae35e1d51e3fdf63cd041
|
Provenance
The following attestation bundles were made for maf_sandbox-0.3.0.tar.gz:
Publisher:
publish-packages.yml on sokolaidev/maf-extensions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maf_sandbox-0.3.0.tar.gz -
Subject digest:
2580d45ef8c78b6d2962cf2c5224895fd01ac14c32dac32b41e2d78b442a526c - Sigstore transparency entry: 2396293507
- Sigstore integration time:
-
Permalink:
sokolaidev/maf-extensions@f5eaf4550f9a6c2748600cde800385ffc656a85e -
Branch / Tag:
refs/tags/maf-sandbox-v0.3.0 - Owner: https://github.com/sokolaidev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-packages.yml@f5eaf4550f9a6c2748600cde800385ffc656a85e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file maf_sandbox-0.3.0-py3-none-any.whl.
File metadata
- Download URL: maf_sandbox-0.3.0-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3a6f59f923a699dbad36e608d8d3b3fbbe7a4c0979b2964d9b9b4c4f4e14310
|
|
| MD5 |
98f9783d4830b3cbff196f2a08be0b7f
|
|
| BLAKE2b-256 |
c85b6a579fc04c7aa5404cd845b2cb3b3948875ac607e1583a866f91823f55e0
|
Provenance
The following attestation bundles were made for maf_sandbox-0.3.0-py3-none-any.whl:
Publisher:
publish-packages.yml on sokolaidev/maf-extensions
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maf_sandbox-0.3.0-py3-none-any.whl -
Subject digest:
d3a6f59f923a699dbad36e608d8d3b3fbbe7a4c0979b2964d9b9b4c4f4e14310 - Sigstore transparency entry: 2396294223
- Sigstore integration time:
-
Permalink:
sokolaidev/maf-extensions@f5eaf4550f9a6c2748600cde800385ffc656a85e -
Branch / Tag:
refs/tags/maf-sandbox-v0.3.0 - Owner: https://github.com/sokolaidev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-packages.yml@f5eaf4550f9a6c2748600cde800385ffc656a85e -
Trigger Event:
workflow_dispatch
-
Statement type: