axio-tools-docker 0.7.0__tar.gz → 0.9.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.
- axio_tools_docker-0.9.0/PKG-INFO +244 -0
- axio_tools_docker-0.9.0/README.md +230 -0
- axio_tools_docker-0.9.0/conftest.py +28 -0
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/pyproject.toml +3 -7
- axio_tools_docker-0.9.0/src/axio_tools_docker/__init__.py +5 -0
- axio_tools_docker-0.9.0/src/axio_tools_docker/py.typed +0 -0
- axio_tools_docker-0.9.0/src/axio_tools_docker/sandbox.py +535 -0
- axio_tools_docker-0.9.0/tests/__init__.py +0 -0
- axio_tools_docker-0.9.0/tests/test_integration.py +381 -0
- axio_tools_docker-0.9.0/tests/test_sandbox.py +976 -0
- axio_tools_docker-0.7.0/PKG-INFO +0 -142
- axio_tools_docker-0.7.0/README.md +0 -129
- axio_tools_docker-0.7.0/src/axio_tools_docker/__init__.py +0 -7
- axio_tools_docker-0.7.0/src/axio_tools_docker/config.py +0 -46
- axio_tools_docker-0.7.0/src/axio_tools_docker/handler.py +0 -73
- axio_tools_docker-0.7.0/src/axio_tools_docker/manager.py +0 -158
- axio_tools_docker-0.7.0/src/axio_tools_docker/plugin.py +0 -87
- axio_tools_docker-0.7.0/src/axio_tools_docker/settings.py +0 -5
- axio_tools_docker-0.7.0/src/axio_tools_docker/tui/__init__.py +0 -15
- axio_tools_docker-0.7.0/src/axio_tools_docker/tui/docker.py +0 -152
- axio_tools_docker-0.7.0/tests/test_config.py +0 -54
- axio_tools_docker-0.7.0/tests/test_handler.py +0 -100
- axio_tools_docker-0.7.0/tests/test_manager.py +0 -258
- axio_tools_docker-0.7.0/tests/test_plugin.py +0 -85
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/.github/workflows/publish.yml +0 -0
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/.github/workflows/tests.yml +0 -0
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/.gitignore +0 -0
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/LICENSE +0 -0
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/Makefile +0 -0
- {axio_tools_docker-0.7.0 → axio_tools_docker-0.9.0}/uv.lock +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axio-tools-docker
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Docker sandbox tools for Axio
|
|
5
|
+
Project-URL: Homepage, https://github.com/mosquito/axio-agent
|
|
6
|
+
Project-URL: Repository, https://github.com/mosquito/axio-agent
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,ai,docker,llm,sandbox,tools
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: aiodocker>=0.26
|
|
12
|
+
Requires-Dist: axio
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# axio-tools-docker
|
|
16
|
+
|
|
17
|
+
[](https://pypi.org/project/axio-tools-docker/)
|
|
18
|
+
[](https://pypi.org/project/axio-tools-docker/)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
|
|
21
|
+
Docker sandbox tools for [axio](https://github.com/mosquito/axio-agent).
|
|
22
|
+
|
|
23
|
+
`DockerSandbox` is an async context manager that spins up an isolated Docker
|
|
24
|
+
container on entry and removes it on exit. Inside the context it exposes six
|
|
25
|
+
`axio` tools - the same `shell`, `write_file`, `read_file`, `list_files`,
|
|
26
|
+
`run_python`, and `patch_file` as `axio-tools-local`, but every operation runs
|
|
27
|
+
inside the container, never on the host.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
Docker must be installed and running:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
docker info # should succeed
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The package talks to the Docker Engine API directly via
|
|
38
|
+
[aiodocker](https://aiodocker.readthedocs.io/) - the `docker` CLI is not
|
|
39
|
+
required.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install axio-tools-docker
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
<!-- name: test_readme_usage; fixtures: docker -->
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
from axio.agent import Agent
|
|
53
|
+
from axio.context import MemoryContextStore
|
|
54
|
+
from axio.testing import StubTransport, make_text_response
|
|
55
|
+
from axio_tools_docker import DockerSandbox
|
|
56
|
+
|
|
57
|
+
async def main() -> None:
|
|
58
|
+
transport = StubTransport([make_text_response("Done.")])
|
|
59
|
+
async with DockerSandbox(image="python:3.12-alpine") as sandbox:
|
|
60
|
+
agent = Agent(
|
|
61
|
+
system="You are a coding assistant. Use the sandbox tools to run code safely.",
|
|
62
|
+
tools=sandbox.tools,
|
|
63
|
+
transport=transport,
|
|
64
|
+
)
|
|
65
|
+
ctx = MemoryContextStore()
|
|
66
|
+
result = await agent.run("Print hello from Python.", ctx)
|
|
67
|
+
print(result)
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Sandbox tools
|
|
73
|
+
|
|
74
|
+
These mirror `axio-tools-local` exactly - same names and field schemas:
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `shell` | Run a shell command; returns stdout + stderr. Supports `timeout`, `cwd`, `stdin`. |
|
|
79
|
+
| `write_file` | Create or overwrite a file. Parent directories are created automatically. |
|
|
80
|
+
| `read_file` | Read a file with optional `start_line`/`end_line`, `line_numbers`, `max_chars`. |
|
|
81
|
+
| `list_files` | List a directory; directories first with a trailing `/`. |
|
|
82
|
+
| `run_python` | Execute a Python snippet in a subprocess. Supports `timeout`, `cwd`, `stdin`. |
|
|
83
|
+
| `patch_file` | Replace lines `from_line`..`to_line` (1-indexed, inclusive). `to_line = from_line - 1` inserts. |
|
|
84
|
+
|
|
85
|
+
## Container lifecycle
|
|
86
|
+
|
|
87
|
+
The container is created on `__aenter__` and removed on `__aexit__` (`docker rm -f`).
|
|
88
|
+
Cleanup runs even when the body raises an exception:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from axio_tools_docker import DockerSandbox
|
|
92
|
+
from axio import Agent
|
|
93
|
+
|
|
94
|
+
async def run(ctx):
|
|
95
|
+
agent = Agent(
|
|
96
|
+
system="You are a coding assistant. Use the sandbox tools to run code safely.",
|
|
97
|
+
tools=ctx.sandbox.tools,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async with DockerSandbox(image="alpine:latest") as sandbox:
|
|
101
|
+
await agent.run("...", ctx)
|
|
102
|
+
|
|
103
|
+
# container removed here (unless remove=False)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The image is pulled automatically if not present locally. If the Docker daemon
|
|
107
|
+
is unreachable, `__aenter__` raises immediately:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
RuntimeError: Docker daemon not available at 'unix:///var/run/docker.sock': ...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The running container's ID is available as `sandbox.container_id` inside the
|
|
114
|
+
`async with` block.
|
|
115
|
+
|
|
116
|
+
## Named containers and reuse
|
|
117
|
+
|
|
118
|
+
Pass `name=` to give the container a fixed name. If a running container with
|
|
119
|
+
that name already exists, the sandbox attaches to it instead of creating a new
|
|
120
|
+
one, and never removes it on exit - regardless of `remove`:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import asyncio
|
|
124
|
+
from axio_tools_docker import DockerSandbox
|
|
125
|
+
|
|
126
|
+
async def first_session() -> None:
|
|
127
|
+
async with DockerSandbox(image="python:3.12-slim", name="my-sandbox", remove=False) as sb:
|
|
128
|
+
await sb.exec("pip install requests")
|
|
129
|
+
|
|
130
|
+
async def second_session() -> None:
|
|
131
|
+
async with DockerSandbox(name="my-sandbox") as sb:
|
|
132
|
+
result = await sb.exec("python3 -c 'import requests; print(requests.__version__)'")
|
|
133
|
+
|
|
134
|
+
asyncio.run(first_session())
|
|
135
|
+
asyncio.run(second_session())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Named volumes
|
|
139
|
+
|
|
140
|
+
Named volumes are managed by the Docker daemon and persist across container
|
|
141
|
+
restarts. Use them to share state between sandbox sessions:
|
|
142
|
+
|
|
143
|
+
<!-- name: test_readme_example; fixtures: docker -->
|
|
144
|
+
```python
|
|
145
|
+
import asyncio
|
|
146
|
+
from axio_tools_docker import DockerSandbox
|
|
147
|
+
|
|
148
|
+
async def main() -> None:
|
|
149
|
+
async with DockerSandbox(
|
|
150
|
+
image="python:3.12-alpine",
|
|
151
|
+
named_volumes={"/data": "my-project-data"},
|
|
152
|
+
) as sb:
|
|
153
|
+
await sb.write_file("/data/state.json", '{"count": 1}')
|
|
154
|
+
# Container removed, volume survives.
|
|
155
|
+
|
|
156
|
+
async with DockerSandbox(
|
|
157
|
+
image="python:3.12-alpine",
|
|
158
|
+
named_volumes={"/data": "my-project-data"},
|
|
159
|
+
volumes_remove=True, # delete the volume on exit
|
|
160
|
+
) as sb:
|
|
161
|
+
raw = await sb.read_file_bytes("/data/state.json")
|
|
162
|
+
assert raw.decode() == '{"count": 1}'
|
|
163
|
+
|
|
164
|
+
asyncio.run(main())
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Docker creates the volume automatically if it does not exist yet.
|
|
168
|
+
|
|
169
|
+
## Configuration
|
|
170
|
+
|
|
171
|
+
<!-- name: test_readme_config; fixtures: docker -->
|
|
172
|
+
```python
|
|
173
|
+
import os
|
|
174
|
+
from axio_tools_docker import DockerSandbox
|
|
175
|
+
|
|
176
|
+
sandbox = DockerSandbox(
|
|
177
|
+
os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock"), # Docker daemon URL, optional
|
|
178
|
+
image="python:3.12-slim",
|
|
179
|
+
memory="512m", # memory limit: "256m", "1g", …
|
|
180
|
+
cpus="2.0", # CPU limit
|
|
181
|
+
network=False, # False=none, True=default, str=explicit mode
|
|
182
|
+
workdir="/workspace",
|
|
183
|
+
volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
|
|
184
|
+
named_volumes={"/data": "my-project-data"}, # named Docker volumes
|
|
185
|
+
volumes_remove=False, # remove named volumes on exit
|
|
186
|
+
env={"PYTHONPATH": "/app"},
|
|
187
|
+
user="nobody",
|
|
188
|
+
name="my-agent-sandbox",
|
|
189
|
+
remove=False,
|
|
190
|
+
read_only=True, # read-only root filesystem
|
|
191
|
+
shm_size="64m", # /dev/shm size
|
|
192
|
+
cap_add=["NET_ADMIN"], # add Linux capabilities
|
|
193
|
+
cap_drop=["ALL"], # drop Linux capabilities
|
|
194
|
+
privileged=False,
|
|
195
|
+
ulimits={"nofile": (1024, 65536), "nproc": 512},
|
|
196
|
+
tmpfs={"/tmp": "size=128m,mode=1777"},
|
|
197
|
+
ports={8080: 8080}, # {container_port: host_port}
|
|
198
|
+
platform="linux/amd64",
|
|
199
|
+
extra_hosts={"host.docker.internal": "host-gateway"},
|
|
200
|
+
devices=["/dev/net/tun", "/dev/sda:/dev/xvda:r"],
|
|
201
|
+
dns=["8.8.8.8", "1.1.1.1"],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
assert sandbox.image == "python:3.12-slim"
|
|
205
|
+
assert sandbox.memory == "512m"
|
|
206
|
+
assert sandbox.cpus == "2.0"
|
|
207
|
+
assert sandbox.network == False
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Parameter | Type | Default | Description |
|
|
211
|
+
|-----------|------|---------|-------------|
|
|
212
|
+
| `url` | `str` | `"unix:///var/run/docker.sock"` | Docker daemon URL (unix socket or TCP). Positional. |
|
|
213
|
+
| `image` | `str` | `"python:latest"` | Container image. Pulled automatically if not present. |
|
|
214
|
+
| `memory` | `str` | `"256m"` | Memory limit. Accepts `k`/`m`/`g` suffixes. |
|
|
215
|
+
| `cpus` | `str` | `"1.0"` | CPU limit as a decimal string. |
|
|
216
|
+
| `network` | `bool \| str` | `False` | Network mode. `False` → `none`. `True` → Docker default. String → explicit `NetworkMode` (e.g. `"host"`, `"bridge"`, `"my-net"`). |
|
|
217
|
+
| `workdir` | `str` | `"/workspace"` | Working directory inside the container. |
|
|
218
|
+
| `volumes` | `dict[str, str]` | `{}` | Bind mounts as `{container_path: host_path}`. |
|
|
219
|
+
| `named_volumes` | `dict[str, str]` | `{}` | Named Docker volumes as `{container_path: volume_name}`. Created automatically if absent. |
|
|
220
|
+
| `volumes_remove` | `bool` | `False` | Remove named volumes on exit. No effect when attached to an existing container. |
|
|
221
|
+
| `env` | `dict[str, str]` | `{}` | Environment variables passed to all commands. |
|
|
222
|
+
| `user` | `str` | `""` | User to run as (e.g. `"nobody"`, `"1000"`). |
|
|
223
|
+
| `name` | `str` | `""` | Container name. Attaches to existing container if found; creates new one otherwise. |
|
|
224
|
+
| `remove` | `bool` | `True` | Remove container on exit. No effect when attached to an existing container. |
|
|
225
|
+
| `read_only` | `bool` | `False` | Read-only root filesystem. Combine with `tmpfs` for writable scratch paths. |
|
|
226
|
+
| `shm_size` | `str` | `""` | `/dev/shm` size (e.g. `"64m"`). Useful for PyTorch / shared-memory IPC. |
|
|
227
|
+
| `cap_add` | `list[str]` | `[]` | Linux capabilities to add (e.g. `["NET_ADMIN", "SYS_PTRACE"]`). |
|
|
228
|
+
| `cap_drop` | `list[str]` | `[]` | Linux capabilities to drop (e.g. `["ALL"]`). |
|
|
229
|
+
| `privileged` | `bool` | `False` | Extended privileges - full capability set and device access. Use with care. |
|
|
230
|
+
| `ulimits` | `dict[str, int \| tuple[int, int]]` | `{}` | Resource limits. `{"nofile": 1024}` → soft=hard=1024. `{"nofile": (1024, 65536)}` → soft/hard split. |
|
|
231
|
+
| `tmpfs` | `dict[str, str]` | `{}` | Tmpfs mounts as `{path: options}` (e.g. `{"/tmp": "size=128m,mode=1777"}`). |
|
|
232
|
+
| `ports` | `dict[int, int]` | `{}` | Port bindings as `{container_port: host_port}`. Only meaningful when `network != False`. |
|
|
233
|
+
| `platform` | `str` | `""` | Platform override (e.g. `"linux/amd64"`, `"linux/arm64"`). |
|
|
234
|
+
| `extra_hosts` | `dict[str, str]` | `{}` | Extra `/etc/hosts` entries as `{hostname: ip}`. |
|
|
235
|
+
| `devices` | `list[str]` | `[]` | Host devices to expose. Format: `"/dev/sda"`, `"/dev/sda:/dev/xvda"`, `"/dev/sda:/dev/xvda:r"`. |
|
|
236
|
+
| `dns` | `list[str]` | `[]` | DNS servers (e.g. `["8.8.8.8"]`). Only meaningful when `network != False`. |
|
|
237
|
+
|
|
238
|
+
## Part of the axio ecosystem
|
|
239
|
+
|
|
240
|
+
[axio](https://github.com/mosquito/axio-agent) · [axio-tools-local](https://github.com/mosquito/axio-agent) · [axio-tools-mcp](https://github.com/mosquito/axio-agent) · [axio-tui](https://github.com/mosquito/axio-agent)
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# axio-tools-docker
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/axio-tools-docker/)
|
|
4
|
+
[](https://pypi.org/project/axio-tools-docker/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Docker sandbox tools for [axio](https://github.com/mosquito/axio-agent).
|
|
8
|
+
|
|
9
|
+
`DockerSandbox` is an async context manager that spins up an isolated Docker
|
|
10
|
+
container on entry and removes it on exit. Inside the context it exposes six
|
|
11
|
+
`axio` tools - the same `shell`, `write_file`, `read_file`, `list_files`,
|
|
12
|
+
`run_python`, and `patch_file` as `axio-tools-local`, but every operation runs
|
|
13
|
+
inside the container, never on the host.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
Docker must be installed and running:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
docker info # should succeed
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The package talks to the Docker Engine API directly via
|
|
24
|
+
[aiodocker](https://aiodocker.readthedocs.io/) - the `docker` CLI is not
|
|
25
|
+
required.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install axio-tools-docker
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
<!-- name: test_readme_usage; fixtures: docker -->
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
from axio.agent import Agent
|
|
39
|
+
from axio.context import MemoryContextStore
|
|
40
|
+
from axio.testing import StubTransport, make_text_response
|
|
41
|
+
from axio_tools_docker import DockerSandbox
|
|
42
|
+
|
|
43
|
+
async def main() -> None:
|
|
44
|
+
transport = StubTransport([make_text_response("Done.")])
|
|
45
|
+
async with DockerSandbox(image="python:3.12-alpine") as sandbox:
|
|
46
|
+
agent = Agent(
|
|
47
|
+
system="You are a coding assistant. Use the sandbox tools to run code safely.",
|
|
48
|
+
tools=sandbox.tools,
|
|
49
|
+
transport=transport,
|
|
50
|
+
)
|
|
51
|
+
ctx = MemoryContextStore()
|
|
52
|
+
result = await agent.run("Print hello from Python.", ctx)
|
|
53
|
+
print(result)
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Sandbox tools
|
|
59
|
+
|
|
60
|
+
These mirror `axio-tools-local` exactly - same names and field schemas:
|
|
61
|
+
|
|
62
|
+
| Tool | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `shell` | Run a shell command; returns stdout + stderr. Supports `timeout`, `cwd`, `stdin`. |
|
|
65
|
+
| `write_file` | Create or overwrite a file. Parent directories are created automatically. |
|
|
66
|
+
| `read_file` | Read a file with optional `start_line`/`end_line`, `line_numbers`, `max_chars`. |
|
|
67
|
+
| `list_files` | List a directory; directories first with a trailing `/`. |
|
|
68
|
+
| `run_python` | Execute a Python snippet in a subprocess. Supports `timeout`, `cwd`, `stdin`. |
|
|
69
|
+
| `patch_file` | Replace lines `from_line`..`to_line` (1-indexed, inclusive). `to_line = from_line - 1` inserts. |
|
|
70
|
+
|
|
71
|
+
## Container lifecycle
|
|
72
|
+
|
|
73
|
+
The container is created on `__aenter__` and removed on `__aexit__` (`docker rm -f`).
|
|
74
|
+
Cleanup runs even when the body raises an exception:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from axio_tools_docker import DockerSandbox
|
|
78
|
+
from axio import Agent
|
|
79
|
+
|
|
80
|
+
async def run(ctx):
|
|
81
|
+
agent = Agent(
|
|
82
|
+
system="You are a coding assistant. Use the sandbox tools to run code safely.",
|
|
83
|
+
tools=ctx.sandbox.tools,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async with DockerSandbox(image="alpine:latest") as sandbox:
|
|
87
|
+
await agent.run("...", ctx)
|
|
88
|
+
|
|
89
|
+
# container removed here (unless remove=False)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The image is pulled automatically if not present locally. If the Docker daemon
|
|
93
|
+
is unreachable, `__aenter__` raises immediately:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
RuntimeError: Docker daemon not available at 'unix:///var/run/docker.sock': ...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The running container's ID is available as `sandbox.container_id` inside the
|
|
100
|
+
`async with` block.
|
|
101
|
+
|
|
102
|
+
## Named containers and reuse
|
|
103
|
+
|
|
104
|
+
Pass `name=` to give the container a fixed name. If a running container with
|
|
105
|
+
that name already exists, the sandbox attaches to it instead of creating a new
|
|
106
|
+
one, and never removes it on exit - regardless of `remove`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import asyncio
|
|
110
|
+
from axio_tools_docker import DockerSandbox
|
|
111
|
+
|
|
112
|
+
async def first_session() -> None:
|
|
113
|
+
async with DockerSandbox(image="python:3.12-slim", name="my-sandbox", remove=False) as sb:
|
|
114
|
+
await sb.exec("pip install requests")
|
|
115
|
+
|
|
116
|
+
async def second_session() -> None:
|
|
117
|
+
async with DockerSandbox(name="my-sandbox") as sb:
|
|
118
|
+
result = await sb.exec("python3 -c 'import requests; print(requests.__version__)'")
|
|
119
|
+
|
|
120
|
+
asyncio.run(first_session())
|
|
121
|
+
asyncio.run(second_session())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Named volumes
|
|
125
|
+
|
|
126
|
+
Named volumes are managed by the Docker daemon and persist across container
|
|
127
|
+
restarts. Use them to share state between sandbox sessions:
|
|
128
|
+
|
|
129
|
+
<!-- name: test_readme_example; fixtures: docker -->
|
|
130
|
+
```python
|
|
131
|
+
import asyncio
|
|
132
|
+
from axio_tools_docker import DockerSandbox
|
|
133
|
+
|
|
134
|
+
async def main() -> None:
|
|
135
|
+
async with DockerSandbox(
|
|
136
|
+
image="python:3.12-alpine",
|
|
137
|
+
named_volumes={"/data": "my-project-data"},
|
|
138
|
+
) as sb:
|
|
139
|
+
await sb.write_file("/data/state.json", '{"count": 1}')
|
|
140
|
+
# Container removed, volume survives.
|
|
141
|
+
|
|
142
|
+
async with DockerSandbox(
|
|
143
|
+
image="python:3.12-alpine",
|
|
144
|
+
named_volumes={"/data": "my-project-data"},
|
|
145
|
+
volumes_remove=True, # delete the volume on exit
|
|
146
|
+
) as sb:
|
|
147
|
+
raw = await sb.read_file_bytes("/data/state.json")
|
|
148
|
+
assert raw.decode() == '{"count": 1}'
|
|
149
|
+
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Docker creates the volume automatically if it does not exist yet.
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
<!-- name: test_readme_config; fixtures: docker -->
|
|
158
|
+
```python
|
|
159
|
+
import os
|
|
160
|
+
from axio_tools_docker import DockerSandbox
|
|
161
|
+
|
|
162
|
+
sandbox = DockerSandbox(
|
|
163
|
+
os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock"), # Docker daemon URL, optional
|
|
164
|
+
image="python:3.12-slim",
|
|
165
|
+
memory="512m", # memory limit: "256m", "1g", …
|
|
166
|
+
cpus="2.0", # CPU limit
|
|
167
|
+
network=False, # False=none, True=default, str=explicit mode
|
|
168
|
+
workdir="/workspace",
|
|
169
|
+
volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
|
|
170
|
+
named_volumes={"/data": "my-project-data"}, # named Docker volumes
|
|
171
|
+
volumes_remove=False, # remove named volumes on exit
|
|
172
|
+
env={"PYTHONPATH": "/app"},
|
|
173
|
+
user="nobody",
|
|
174
|
+
name="my-agent-sandbox",
|
|
175
|
+
remove=False,
|
|
176
|
+
read_only=True, # read-only root filesystem
|
|
177
|
+
shm_size="64m", # /dev/shm size
|
|
178
|
+
cap_add=["NET_ADMIN"], # add Linux capabilities
|
|
179
|
+
cap_drop=["ALL"], # drop Linux capabilities
|
|
180
|
+
privileged=False,
|
|
181
|
+
ulimits={"nofile": (1024, 65536), "nproc": 512},
|
|
182
|
+
tmpfs={"/tmp": "size=128m,mode=1777"},
|
|
183
|
+
ports={8080: 8080}, # {container_port: host_port}
|
|
184
|
+
platform="linux/amd64",
|
|
185
|
+
extra_hosts={"host.docker.internal": "host-gateway"},
|
|
186
|
+
devices=["/dev/net/tun", "/dev/sda:/dev/xvda:r"],
|
|
187
|
+
dns=["8.8.8.8", "1.1.1.1"],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert sandbox.image == "python:3.12-slim"
|
|
191
|
+
assert sandbox.memory == "512m"
|
|
192
|
+
assert sandbox.cpus == "2.0"
|
|
193
|
+
assert sandbox.network == False
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
| Parameter | Type | Default | Description |
|
|
197
|
+
|-----------|------|---------|-------------|
|
|
198
|
+
| `url` | `str` | `"unix:///var/run/docker.sock"` | Docker daemon URL (unix socket or TCP). Positional. |
|
|
199
|
+
| `image` | `str` | `"python:latest"` | Container image. Pulled automatically if not present. |
|
|
200
|
+
| `memory` | `str` | `"256m"` | Memory limit. Accepts `k`/`m`/`g` suffixes. |
|
|
201
|
+
| `cpus` | `str` | `"1.0"` | CPU limit as a decimal string. |
|
|
202
|
+
| `network` | `bool \| str` | `False` | Network mode. `False` → `none`. `True` → Docker default. String → explicit `NetworkMode` (e.g. `"host"`, `"bridge"`, `"my-net"`). |
|
|
203
|
+
| `workdir` | `str` | `"/workspace"` | Working directory inside the container. |
|
|
204
|
+
| `volumes` | `dict[str, str]` | `{}` | Bind mounts as `{container_path: host_path}`. |
|
|
205
|
+
| `named_volumes` | `dict[str, str]` | `{}` | Named Docker volumes as `{container_path: volume_name}`. Created automatically if absent. |
|
|
206
|
+
| `volumes_remove` | `bool` | `False` | Remove named volumes on exit. No effect when attached to an existing container. |
|
|
207
|
+
| `env` | `dict[str, str]` | `{}` | Environment variables passed to all commands. |
|
|
208
|
+
| `user` | `str` | `""` | User to run as (e.g. `"nobody"`, `"1000"`). |
|
|
209
|
+
| `name` | `str` | `""` | Container name. Attaches to existing container if found; creates new one otherwise. |
|
|
210
|
+
| `remove` | `bool` | `True` | Remove container on exit. No effect when attached to an existing container. |
|
|
211
|
+
| `read_only` | `bool` | `False` | Read-only root filesystem. Combine with `tmpfs` for writable scratch paths. |
|
|
212
|
+
| `shm_size` | `str` | `""` | `/dev/shm` size (e.g. `"64m"`). Useful for PyTorch / shared-memory IPC. |
|
|
213
|
+
| `cap_add` | `list[str]` | `[]` | Linux capabilities to add (e.g. `["NET_ADMIN", "SYS_PTRACE"]`). |
|
|
214
|
+
| `cap_drop` | `list[str]` | `[]` | Linux capabilities to drop (e.g. `["ALL"]`). |
|
|
215
|
+
| `privileged` | `bool` | `False` | Extended privileges - full capability set and device access. Use with care. |
|
|
216
|
+
| `ulimits` | `dict[str, int \| tuple[int, int]]` | `{}` | Resource limits. `{"nofile": 1024}` → soft=hard=1024. `{"nofile": (1024, 65536)}` → soft/hard split. |
|
|
217
|
+
| `tmpfs` | `dict[str, str]` | `{}` | Tmpfs mounts as `{path: options}` (e.g. `{"/tmp": "size=128m,mode=1777"}`). |
|
|
218
|
+
| `ports` | `dict[int, int]` | `{}` | Port bindings as `{container_port: host_port}`. Only meaningful when `network != False`. |
|
|
219
|
+
| `platform` | `str` | `""` | Platform override (e.g. `"linux/amd64"`, `"linux/arm64"`). |
|
|
220
|
+
| `extra_hosts` | `dict[str, str]` | `{}` | Extra `/etc/hosts` entries as `{hostname: ip}`. |
|
|
221
|
+
| `devices` | `list[str]` | `[]` | Host devices to expose. Format: `"/dev/sda"`, `"/dev/sda:/dev/xvda"`, `"/dev/sda:/dev/xvda:r"`. |
|
|
222
|
+
| `dns` | `list[str]` | `[]` | DNS servers (e.g. `["8.8.8.8"]`). Only meaningful when `network != False`. |
|
|
223
|
+
|
|
224
|
+
## Part of the axio ecosystem
|
|
225
|
+
|
|
226
|
+
[axio](https://github.com/mosquito/axio-agent) · [axio-tools-local](https://github.com/mosquito/axio-agent) · [axio-tools-mcp](https://github.com/mosquito/axio-agent) · [axio-tui](https://github.com/mosquito/axio-agent)
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Pytest configuration for axio-tools-docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import aiodocker
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope="session")
|
|
13
|
+
def docker() -> str:
|
|
14
|
+
docker_url = os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock")
|
|
15
|
+
|
|
16
|
+
async def _probe() -> bool:
|
|
17
|
+
# noinspection PyBroadException
|
|
18
|
+
try:
|
|
19
|
+
async with aiodocker.Docker(url=docker_url) as client:
|
|
20
|
+
await client.system.info()
|
|
21
|
+
return True
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
if not asyncio.run(_probe()):
|
|
26
|
+
pytest.skip("Skipping due to missing Docker daemon")
|
|
27
|
+
|
|
28
|
+
return docker_url
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "axio-tools-docker"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Docker sandbox tools for Axio"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = {text = "MIT"}
|
|
8
8
|
keywords = ["llm", "agent", "ai", "docker", "sandbox", "tools"]
|
|
9
|
-
dependencies = ["axio"]
|
|
10
|
-
|
|
11
|
-
[project.entry-points."axio.tools.settings"]
|
|
12
|
-
docker = "axio_tools_docker.plugin:DockerPlugin"
|
|
13
|
-
|
|
9
|
+
dependencies = ["axio", "aiodocker>=0.26"]
|
|
14
10
|
|
|
15
11
|
[project.urls]
|
|
16
12
|
Homepage = "https://github.com/mosquito/axio-agent"
|
|
@@ -45,6 +41,6 @@ dev = [
|
|
|
45
41
|
"ruff>=0.9",
|
|
46
42
|
"pytest-cov>=7.1.0",
|
|
47
43
|
"markdown-pytest>=0.6.0",
|
|
48
|
-
"
|
|
44
|
+
"aiodocker>=0.26",
|
|
49
45
|
]
|
|
50
46
|
|
|
File without changes
|