maf-sandbox-docker 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maf_sandbox_docker-0.1.0/LICENSE +21 -0
- maf_sandbox_docker-0.1.0/PKG-INFO +104 -0
- maf_sandbox_docker-0.1.0/README.md +79 -0
- maf_sandbox_docker-0.1.0/pyproject.toml +76 -0
- maf_sandbox_docker-0.1.0/pyproject.toml.orig +75 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/__init__.py +79 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/_backend.py +1044 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/_config.py +75 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/_proxy/Dockerfile +8 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/_proxy/__init__.py +21 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/_proxy/proxy.py +179 -0
- maf_sandbox_docker-0.1.0/src/maf_sandbox_docker/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SOKOLAI BV
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maf-sandbox-docker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plain Docker containers as a local and CI sandbox backend for Microsoft Agent Framework agents — any Docker-compatible engine, zero dependencies beyond maf-sandbox.
|
|
5
|
+
Keywords: docker,containers,sandbox,agent-framework,microsoft-agent-framework,isolation
|
|
6
|
+
Author: SOKOLAI BV
|
|
7
|
+
Author-email: SOKOLAI BV <info@sokolai.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Dist: maf-sandbox>=0.6.0,<0.7
|
|
20
|
+
Requires-Python: >=3.12, <3.15
|
|
21
|
+
Project-URL: Homepage, https://www.sokol.ai
|
|
22
|
+
Project-URL: Source, https://github.com/sokolaidev/maf-extensions
|
|
23
|
+
Project-URL: Issues, https://github.com/sokolaidev/maf-extensions/issues
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# maf-sandbox-docker
|
|
27
|
+
|
|
28
|
+
> **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.
|
|
29
|
+
|
|
30
|
+
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](https://aka.ms/AgentFramework).
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
app -> maf_sandbox -> maf_sandbox_docker -> the container
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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`](https://github.com/sokolaidev/maf-extensions/tree/main/packages/maf-sandbox) itself. A workload written against the protocol runs here unchanged, which is what makes it a workload rather than an integration.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install maf-sandbox-docker
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from maf_sandbox import Isolation, SandboxRouter
|
|
46
|
+
from maf_sandbox_docker import DockerSandboxBackend, DockerSandboxConfig
|
|
47
|
+
|
|
48
|
+
router = SandboxRouter([DockerSandboxBackend(DockerSandboxConfig())], min_isolation=Isolation.CONTAINER)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
[`samples/06_docker_codeact`](https://github.com/sokolaidev/maf-extensions/tree/main/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.
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
**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.
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
**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.
|
|
60
|
+
|
|
61
|
+
## What this backend declares
|
|
62
|
+
|
|
63
|
+
**`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.
|
|
64
|
+
|
|
65
|
+
**`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.
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from maf_sandbox_docker import proxy_build_context, DockerSandboxConfig
|
|
71
|
+
|
|
72
|
+
print(f"docker build -t maf-egress-proxy:local {proxy_build_context()}") # run this once
|
|
73
|
+
config = DockerSandboxConfig(egress_proxy_image="maf-egress-proxy:local")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`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.
|
|
77
|
+
|
|
78
|
+
**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 `/`. Anything in that chain that is not a real directory is refused. 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.
|
|
79
|
+
|
|
80
|
+
## The backend
|
|
81
|
+
|
|
82
|
+
`DockerSandboxBackend` implements `maf_sandbox.SandboxBackend`:
|
|
83
|
+
|
|
84
|
+
| | |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `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 |
|
|
87
|
+
| `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 |
|
|
88
|
+
| `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 |
|
|
89
|
+
| `dispose(key)` | `rm -f` on every kind's container the key names, with the proxy and network of an allowlisted one |
|
|
90
|
+
| `dispose_scope(scope, thread)` | delete every container for a conversation — **by label, read back from docker**, not from process memory |
|
|
91
|
+
| `isolation` | `container`, unconditionally |
|
|
92
|
+
| `egress` | `closed`, or `allowlist` when `egress_proxy_image` is set |
|
|
93
|
+
| `capabilities` | `{EXEC, FILES_IN, FILES_OUT}` |
|
|
94
|
+
| `limits` | the transfer ceilings a spec may not exceed, per direction |
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
`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.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Maintained by [SOKOLAI BV](https://www.sokol.ai).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# maf-sandbox-docker
|
|
2
|
+
|
|
3
|
+
> **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.
|
|
4
|
+
|
|
5
|
+
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](https://aka.ms/AgentFramework).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
app -> maf_sandbox -> maf_sandbox_docker -> the container
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
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`](https://github.com/sokolaidev/maf-extensions/tree/main/packages/maf-sandbox) itself. A workload written against the protocol runs here unchanged, which is what makes it a workload rather than an integration.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install maf-sandbox-docker
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from maf_sandbox import Isolation, SandboxRouter
|
|
21
|
+
from maf_sandbox_docker import DockerSandboxBackend, DockerSandboxConfig
|
|
22
|
+
|
|
23
|
+
router = SandboxRouter([DockerSandboxBackend(DockerSandboxConfig())], min_isolation=Isolation.CONTAINER)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
[`samples/06_docker_codeact`](https://github.com/sokolaidev/maf-extensions/tree/main/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.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
**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.
|
|
31
|
+
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
**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.
|
|
35
|
+
|
|
36
|
+
## What this backend declares
|
|
37
|
+
|
|
38
|
+
**`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.
|
|
39
|
+
|
|
40
|
+
**`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.
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from maf_sandbox_docker import proxy_build_context, DockerSandboxConfig
|
|
46
|
+
|
|
47
|
+
print(f"docker build -t maf-egress-proxy:local {proxy_build_context()}") # run this once
|
|
48
|
+
config = DockerSandboxConfig(egress_proxy_image="maf-egress-proxy:local")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**`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.
|
|
52
|
+
|
|
53
|
+
**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 `/`. Anything in that chain that is not a real directory is refused. 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.
|
|
54
|
+
|
|
55
|
+
## The backend
|
|
56
|
+
|
|
57
|
+
`DockerSandboxBackend` implements `maf_sandbox.SandboxBackend`:
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `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 |
|
|
62
|
+
| `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 |
|
|
63
|
+
| `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 |
|
|
64
|
+
| `dispose(key)` | `rm -f` on every kind's container the key names, with the proxy and network of an allowlisted one |
|
|
65
|
+
| `dispose_scope(scope, thread)` | delete every container for a conversation — **by label, read back from docker**, not from process memory |
|
|
66
|
+
| `isolation` | `container`, unconditionally |
|
|
67
|
+
| `egress` | `closed`, or `allowlist` when `egress_proxy_image` is set |
|
|
68
|
+
| `capabilities` | `{EXEC, FILES_IN, FILES_OUT}` |
|
|
69
|
+
| `limits` | the transfer ceilings a spec may not exceed, per direction |
|
|
70
|
+
|
|
71
|
+
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.
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
`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.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
Maintained by [SOKOLAI BV](https://www.sokol.ai).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maf-sandbox-docker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Plain Docker containers as a local and CI sandbox backend for Microsoft Agent Framework agents — any Docker-compatible engine, zero dependencies beyond maf-sandbox."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.15"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"docker",
|
|
11
|
+
"containers",
|
|
12
|
+
"sandbox",
|
|
13
|
+
"agent-framework",
|
|
14
|
+
"microsoft-agent-framework",
|
|
15
|
+
"isolation",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
]
|
|
28
|
+
dependencies = ["maf-sandbox>=0.6.0,<0.7"]
|
|
29
|
+
|
|
30
|
+
[[project.authors]]
|
|
31
|
+
name = "SOKOLAI BV"
|
|
32
|
+
email = "info@sokolai.com"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://www.sokol.ai"
|
|
36
|
+
Source = "https://github.com/sokolaidev/maf-extensions"
|
|
37
|
+
Issues = "https://github.com/sokolaidev/maf-extensions/issues"
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources.maf-sandbox]
|
|
40
|
+
workspace = true
|
|
41
|
+
|
|
42
|
+
[tool.uv.build-backend]
|
|
43
|
+
module-name = "maf_sandbox_docker"
|
|
44
|
+
module-root = "src"
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py312"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
extend-select = [
|
|
52
|
+
"I",
|
|
53
|
+
"UP",
|
|
54
|
+
"D100",
|
|
55
|
+
"D101",
|
|
56
|
+
"D103",
|
|
57
|
+
"D104",
|
|
58
|
+
]
|
|
59
|
+
ignore = ["UP037"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.per-file-ignores]
|
|
62
|
+
"tests/**" = [
|
|
63
|
+
"D101",
|
|
64
|
+
"D103",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.pyright]
|
|
68
|
+
include = ["src"]
|
|
69
|
+
typeCheckingMode = "strict"
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
|
|
74
|
+
[build-system]
|
|
75
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
76
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maf-sandbox-docker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Plain Docker containers as a local and CI sandbox backend for Microsoft Agent Framework agents — any Docker-compatible engine, zero dependencies beyond maf-sandbox."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.15"
|
|
7
|
+
authors = [{ name = "SOKOLAI BV", email = "info@sokolai.com" }]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
keywords = [
|
|
11
|
+
"docker",
|
|
12
|
+
"containers",
|
|
13
|
+
"sandbox",
|
|
14
|
+
"agent-framework",
|
|
15
|
+
"microsoft-agent-framework",
|
|
16
|
+
"isolation",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Topic :: Software Development :: Testing",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
# The backend protocol and the router this backend registers with — the only dependency
|
|
31
|
+
# there is. Everything else is the standard library driving the `docker` CLI. The floor is
|
|
32
|
+
# 0.6.0 because that release carries the FILES_OUT pull surface this backend declares and
|
|
33
|
+
# implements from day one; upper-bounded because maf-sandbox has not yet reached a stable
|
|
34
|
+
# API, so every release before 1.0.0 may include breaking changes (see its CHANGELOG);
|
|
35
|
+
# moving either end of this range has an order to it — see RELEASING.md.
|
|
36
|
+
"maf-sandbox>=0.6.0,<0.7",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://www.sokol.ai"
|
|
41
|
+
Source = "https://github.com/sokolaidev/maf-extensions"
|
|
42
|
+
Issues = "https://github.com/sokolaidev/maf-extensions/issues"
|
|
43
|
+
|
|
44
|
+
[tool.uv.sources]
|
|
45
|
+
maf-sandbox = { workspace = true }
|
|
46
|
+
|
|
47
|
+
[tool.uv.build-backend]
|
|
48
|
+
module-name = "maf_sandbox_docker"
|
|
49
|
+
module-root = "src"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py312"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
extend-select = ["I", "UP", "D100", "D101", "D103", "D104"]
|
|
57
|
+
ignore = ["UP037"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
# ruff resolves [tool.ruff] per file by the NEAREST ancestor pyproject.toml that has one, so
|
|
61
|
+
# this package carries its own copy rather than inheriting one from the workspace root.
|
|
62
|
+
"tests/**" = ["D101", "D103"]
|
|
63
|
+
|
|
64
|
+
# Self-contained type checking, scoped to this package — strict. `tests/` stays out (fakes
|
|
65
|
+
# for the docker seam, not signal for a strict checker).
|
|
66
|
+
[tool.pyright]
|
|
67
|
+
include = ["src"]
|
|
68
|
+
typeCheckingMode = "strict"
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
|
|
73
|
+
[build-system]
|
|
74
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
75
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Plain Docker containers as a sandbox backend for Microsoft Agent Framework agents.
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
app -> maf_sandbox -> maf_sandbox_docker -> the container
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
:class:`DockerSandboxBackend` implements :class:`maf_sandbox.SandboxBackend` by driving the
|
|
8
|
+
``docker`` command-line client as a subprocess: a container on any machine with a
|
|
9
|
+
Docker-compatible engine — macOS, Linux, Windows with WSL 2, and every GitHub Actions
|
|
10
|
+
``ubuntu-latest`` runner — with no dependency beyond ``maf-sandbox`` itself.
|
|
11
|
+
|
|
12
|
+
It declares :data:`~maf_sandbox.Isolation.CONTAINER`, below the router's default
|
|
13
|
+
:data:`~maf_sandbox.Isolation.MICROVM` floor — a host opts down explicitly with
|
|
14
|
+
``min_isolation=Isolation.CONTAINER``, and with nothing passed construction refuses this
|
|
15
|
+
backend. Egress is :data:`~maf_sandbox.Egress.CLOSED` by default — ``--network none`` on
|
|
16
|
+
every container — and becomes :data:`~maf_sandbox.Egress.ALLOWLIST` when the config names a
|
|
17
|
+
:func:`proxy_build_context`-built image, which places each sandbox on its own internal network
|
|
18
|
+
behind a filtering proxy. It declares :data:`~maf_sandbox.Capability.FILES_OUT` — stat from
|
|
19
|
+
the first tar header of ``docker cp``, read from the same stream — and never
|
|
20
|
+
:data:`~maf_sandbox.Capability.FILES_LIST`, the enumeration capability Docker has no
|
|
21
|
+
engine-level primitive for.
|
|
22
|
+
|
|
23
|
+
This package is the backend only. The sandbox kinds that run on it live in sibling packages
|
|
24
|
+
and are written against the router's protocol, not against this backend, so they never import
|
|
25
|
+
it.
|
|
26
|
+
|
|
27
|
+
This package imports no host application and no agent framework.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from ._backend import DockerSandboxBackend
|
|
33
|
+
from ._config import DockerSandboxConfig
|
|
34
|
+
from ._proxy import build_context as proxy_build_context
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DockerSandboxBackend",
|
|
38
|
+
"DockerSandboxConfig",
|
|
39
|
+
"MafSandboxDockerExperimentalWarning",
|
|
40
|
+
"proxy_build_context",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# --- Experimental-package notice ---------------------------------------------------------
|
|
44
|
+
# This package is early-stage ("Development Status :: 4 - Beta"). Mirrors `agent_framework`'s
|
|
45
|
+
# own experimental-feature idiom (see its `_feature_stage` module and `ExperimentalWarning`, a
|
|
46
|
+
# `FutureWarning` subclass) but deliberately subclasses `UserWarning` instead: a host that runs
|
|
47
|
+
# under `python -W error` (many CI/production launchers do) would have importing this package
|
|
48
|
+
# alone raise before any of its own code runs if the category were a `FutureWarning`.
|
|
49
|
+
# `UserWarning` keeps the notice informational-by-default while staying a real, catchable,
|
|
50
|
+
# filterwarnings-suppressible category — see the try/except below for how `-W error` is
|
|
51
|
+
# handled anyway.
|
|
52
|
+
#
|
|
53
|
+
# Duplicated (not imported from a shared module) in each of the maf-sandbox* packages on
|
|
54
|
+
# purpose — a shared warnings module would be a cross-package dependency this split is
|
|
55
|
+
# designed to avoid.
|
|
56
|
+
import warnings as _warnings
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MafSandboxDockerExperimentalWarning(UserWarning):
|
|
60
|
+
"""Warning category for maf-sandbox-docker's experimental-package notice."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _warn_experimental() -> None:
|
|
64
|
+
message = (
|
|
65
|
+
"maf_sandbox_docker is experimental and may change or be removed in future versions "
|
|
66
|
+
"without notice."
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
_warnings.warn(message, category=MafSandboxDockerExperimentalWarning, stacklevel=2)
|
|
70
|
+
except MafSandboxDockerExperimentalWarning:
|
|
71
|
+
# A host running under `python -W error` (or with a blanket `filterwarnings("error")`
|
|
72
|
+
# active) turns the warning above into an exception at the call site. Importing a
|
|
73
|
+
# package must never fail because of an informational notice, so it is swallowed here
|
|
74
|
+
# — this is the one piece of state a `-W error` host is allowed to change: whether the
|
|
75
|
+
# notice was printed, never whether the import succeeded.
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_warn_experimental()
|