axio-tools-docker 0.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axio-tools-docker
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Docker sandbox tools for Axio
5
5
  Project-URL: Homepage, https://github.com/mosquito/axio-agent
6
6
  Project-URL: Repository, https://github.com/mosquito/axio-agent
@@ -22,7 +22,7 @@ Docker sandbox tools for [axio](https://github.com/mosquito/axio-agent).
22
22
 
23
23
  `DockerSandbox` is an async context manager that spins up an isolated Docker
24
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`,
25
+ `axio` tools - the same `shell`, `write_file`, `read_file`, `list_files`,
26
26
  `run_python`, and `patch_file` as `axio-tools-local`, but every operation runs
27
27
  inside the container, never on the host.
28
28
 
@@ -35,7 +35,7 @@ docker info # should succeed
35
35
  ```
36
36
 
37
37
  The package talks to the Docker Engine API directly via
38
- [aiodocker](https://aiodocker.readthedocs.io/) the `docker` CLI is not
38
+ [aiodocker](https://aiodocker.readthedocs.io/) - the `docker` CLI is not
39
39
  required.
40
40
 
41
41
  ## Installation
@@ -46,7 +46,7 @@ pip install axio-tools-docker
46
46
 
47
47
  ## Quick start
48
48
 
49
- <!-- name: test_readme_usage; mark: docker -->
49
+ <!-- name: test_readme_usage; fixtures: docker -->
50
50
  ```python
51
51
  import asyncio
52
52
  from axio.agent import Agent
@@ -71,7 +71,7 @@ asyncio.run(main())
71
71
 
72
72
  ## Sandbox tools
73
73
 
74
- These mirror `axio-tools-local` exactly same names and field schemas:
74
+ These mirror `axio-tools-local` exactly - same names and field schemas:
75
75
 
76
76
  | Tool | Description |
77
77
  |---|---|
@@ -88,9 +88,18 @@ The container is created on `__aenter__` and removed on `__aexit__` (`docker rm
88
88
  Cleanup runs even when the body raises an exception:
89
89
 
90
90
  ```python
91
+ from axio_tools_docker import DockerSandbox
92
+ from axio import Agent
93
+
91
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
+
92
100
  async with DockerSandbox(image="alpine:latest") as sandbox:
93
101
  await agent.run("...", ctx)
102
+
94
103
  # container removed here (unless remove=False)
95
104
  ```
96
105
 
@@ -108,7 +117,7 @@ The running container's ID is available as `sandbox.container_id` inside the
108
117
 
109
118
  Pass `name=` to give the container a fixed name. If a running container with
110
119
  that name already exists, the sandbox attaches to it instead of creating a new
111
- one, and never removes it on exit regardless of `remove`:
120
+ one, and never removes it on exit - regardless of `remove`:
112
121
 
113
122
  ```python
114
123
  import asyncio
@@ -126,37 +135,76 @@ asyncio.run(first_session())
126
135
  asyncio.run(second_session())
127
136
  ```
128
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
+
129
169
  ## Configuration
130
170
 
131
- <!-- name: test_readme_config -->
171
+ <!-- name: test_readme_config; fixtures: docker -->
132
172
  ```python
173
+ import os
133
174
  from axio_tools_docker import DockerSandbox
134
175
 
135
176
  sandbox = DockerSandbox(
136
- "unix:///var/run/docker.sock", # Docker daemon URL (positional)
177
+ os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock"), # Docker daemon URL, optional
137
178
  image="python:3.12-slim",
138
- memory="512m", # memory limit: "256m", "1g", …
139
- cpus="2.0", # CPU limit
140
- network=False, # False=none, True=default, str=explicit mode
179
+ memory="512m", # memory limit: "256m", "1g", …
180
+ cpus="2.0", # CPU limit
181
+ network=False, # False=none, True=default, str=explicit mode
141
182
  workdir="/workspace",
142
- volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
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
143
186
  env={"PYTHONPATH": "/app"},
144
187
  user="nobody",
145
188
  name="my-agent-sandbox",
146
189
  remove=False,
147
- read_only=True, # read-only root filesystem
148
- shm_size="64m", # /dev/shm size
149
- cap_add=["NET_ADMIN"], # add Linux capabilities
150
- cap_drop=["ALL"], # drop Linux capabilities
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
151
194
  privileged=False,
152
195
  ulimits={"nofile": (1024, 65536), "nproc": 512},
153
196
  tmpfs={"/tmp": "size=128m,mode=1777"},
154
- ports={8080: 8080}, # {container_port: host_port}
197
+ ports={8080: 8080}, # {container_port: host_port}
155
198
  platform="linux/amd64",
156
199
  extra_hosts={"host.docker.internal": "host-gateway"},
157
200
  devices=["/dev/net/tun", "/dev/sda:/dev/xvda:r"],
158
201
  dns=["8.8.8.8", "1.1.1.1"],
159
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
160
208
  ```
161
209
 
162
210
  | Parameter | Type | Default | Description |
@@ -168,6 +216,8 @@ sandbox = DockerSandbox(
168
216
  | `network` | `bool \| str` | `False` | Network mode. `False` → `none`. `True` → Docker default. String → explicit `NetworkMode` (e.g. `"host"`, `"bridge"`, `"my-net"`). |
169
217
  | `workdir` | `str` | `"/workspace"` | Working directory inside the container. |
170
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. |
171
221
  | `env` | `dict[str, str]` | `{}` | Environment variables passed to all commands. |
172
222
  | `user` | `str` | `""` | User to run as (e.g. `"nobody"`, `"1000"`). |
173
223
  | `name` | `str` | `""` | Container name. Attaches to existing container if found; creates new one otherwise. |
@@ -176,7 +226,7 @@ sandbox = DockerSandbox(
176
226
  | `shm_size` | `str` | `""` | `/dev/shm` size (e.g. `"64m"`). Useful for PyTorch / shared-memory IPC. |
177
227
  | `cap_add` | `list[str]` | `[]` | Linux capabilities to add (e.g. `["NET_ADMIN", "SYS_PTRACE"]`). |
178
228
  | `cap_drop` | `list[str]` | `[]` | Linux capabilities to drop (e.g. `["ALL"]`). |
179
- | `privileged` | `bool` | `False` | Extended privileges full capability set and device access. Use with care. |
229
+ | `privileged` | `bool` | `False` | Extended privileges - full capability set and device access. Use with care. |
180
230
  | `ulimits` | `dict[str, int \| tuple[int, int]]` | `{}` | Resource limits. `{"nofile": 1024}` → soft=hard=1024. `{"nofile": (1024, 65536)}` → soft/hard split. |
181
231
  | `tmpfs` | `dict[str, str]` | `{}` | Tmpfs mounts as `{path: options}` (e.g. `{"/tmp": "size=128m,mode=1777"}`). |
182
232
  | `ports` | `dict[int, int]` | `{}` | Port bindings as `{container_port: host_port}`. Only meaningful when `network != False`. |
@@ -8,7 +8,7 @@ Docker sandbox tools for [axio](https://github.com/mosquito/axio-agent).
8
8
 
9
9
  `DockerSandbox` is an async context manager that spins up an isolated Docker
10
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`,
11
+ `axio` tools - the same `shell`, `write_file`, `read_file`, `list_files`,
12
12
  `run_python`, and `patch_file` as `axio-tools-local`, but every operation runs
13
13
  inside the container, never on the host.
14
14
 
@@ -21,7 +21,7 @@ docker info # should succeed
21
21
  ```
22
22
 
23
23
  The package talks to the Docker Engine API directly via
24
- [aiodocker](https://aiodocker.readthedocs.io/) the `docker` CLI is not
24
+ [aiodocker](https://aiodocker.readthedocs.io/) - the `docker` CLI is not
25
25
  required.
26
26
 
27
27
  ## Installation
@@ -32,7 +32,7 @@ pip install axio-tools-docker
32
32
 
33
33
  ## Quick start
34
34
 
35
- <!-- name: test_readme_usage; mark: docker -->
35
+ <!-- name: test_readme_usage; fixtures: docker -->
36
36
  ```python
37
37
  import asyncio
38
38
  from axio.agent import Agent
@@ -57,7 +57,7 @@ asyncio.run(main())
57
57
 
58
58
  ## Sandbox tools
59
59
 
60
- These mirror `axio-tools-local` exactly same names and field schemas:
60
+ These mirror `axio-tools-local` exactly - same names and field schemas:
61
61
 
62
62
  | Tool | Description |
63
63
  |---|---|
@@ -74,9 +74,18 @@ The container is created on `__aenter__` and removed on `__aexit__` (`docker rm
74
74
  Cleanup runs even when the body raises an exception:
75
75
 
76
76
  ```python
77
+ from axio_tools_docker import DockerSandbox
78
+ from axio import Agent
79
+
77
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
+
78
86
  async with DockerSandbox(image="alpine:latest") as sandbox:
79
87
  await agent.run("...", ctx)
88
+
80
89
  # container removed here (unless remove=False)
81
90
  ```
82
91
 
@@ -94,7 +103,7 @@ The running container's ID is available as `sandbox.container_id` inside the
94
103
 
95
104
  Pass `name=` to give the container a fixed name. If a running container with
96
105
  that name already exists, the sandbox attaches to it instead of creating a new
97
- one, and never removes it on exit regardless of `remove`:
106
+ one, and never removes it on exit - regardless of `remove`:
98
107
 
99
108
  ```python
100
109
  import asyncio
@@ -112,37 +121,76 @@ asyncio.run(first_session())
112
121
  asyncio.run(second_session())
113
122
  ```
114
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
+
115
155
  ## Configuration
116
156
 
117
- <!-- name: test_readme_config -->
157
+ <!-- name: test_readme_config; fixtures: docker -->
118
158
  ```python
159
+ import os
119
160
  from axio_tools_docker import DockerSandbox
120
161
 
121
162
  sandbox = DockerSandbox(
122
- "unix:///var/run/docker.sock", # Docker daemon URL (positional)
163
+ os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock"), # Docker daemon URL, optional
123
164
  image="python:3.12-slim",
124
- memory="512m", # memory limit: "256m", "1g", …
125
- cpus="2.0", # CPU limit
126
- network=False, # False=none, True=default, str=explicit mode
165
+ memory="512m", # memory limit: "256m", "1g", …
166
+ cpus="2.0", # CPU limit
167
+ network=False, # False=none, True=default, str=explicit mode
127
168
  workdir="/workspace",
128
- volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
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
129
172
  env={"PYTHONPATH": "/app"},
130
173
  user="nobody",
131
174
  name="my-agent-sandbox",
132
175
  remove=False,
133
- read_only=True, # read-only root filesystem
134
- shm_size="64m", # /dev/shm size
135
- cap_add=["NET_ADMIN"], # add Linux capabilities
136
- cap_drop=["ALL"], # drop Linux capabilities
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
137
180
  privileged=False,
138
181
  ulimits={"nofile": (1024, 65536), "nproc": 512},
139
182
  tmpfs={"/tmp": "size=128m,mode=1777"},
140
- ports={8080: 8080}, # {container_port: host_port}
183
+ ports={8080: 8080}, # {container_port: host_port}
141
184
  platform="linux/amd64",
142
185
  extra_hosts={"host.docker.internal": "host-gateway"},
143
186
  devices=["/dev/net/tun", "/dev/sda:/dev/xvda:r"],
144
187
  dns=["8.8.8.8", "1.1.1.1"],
145
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
146
194
  ```
147
195
 
148
196
  | Parameter | Type | Default | Description |
@@ -154,6 +202,8 @@ sandbox = DockerSandbox(
154
202
  | `network` | `bool \| str` | `False` | Network mode. `False` → `none`. `True` → Docker default. String → explicit `NetworkMode` (e.g. `"host"`, `"bridge"`, `"my-net"`). |
155
203
  | `workdir` | `str` | `"/workspace"` | Working directory inside the container. |
156
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. |
157
207
  | `env` | `dict[str, str]` | `{}` | Environment variables passed to all commands. |
158
208
  | `user` | `str` | `""` | User to run as (e.g. `"nobody"`, `"1000"`). |
159
209
  | `name` | `str` | `""` | Container name. Attaches to existing container if found; creates new one otherwise. |
@@ -162,7 +212,7 @@ sandbox = DockerSandbox(
162
212
  | `shm_size` | `str` | `""` | `/dev/shm` size (e.g. `"64m"`). Useful for PyTorch / shared-memory IPC. |
163
213
  | `cap_add` | `list[str]` | `[]` | Linux capabilities to add (e.g. `["NET_ADMIN", "SYS_PTRACE"]`). |
164
214
  | `cap_drop` | `list[str]` | `[]` | Linux capabilities to drop (e.g. `["ALL"]`). |
165
- | `privileged` | `bool` | `False` | Extended privileges full capability set and device access. Use with care. |
215
+ | `privileged` | `bool` | `False` | Extended privileges - full capability set and device access. Use with care. |
166
216
  | `ulimits` | `dict[str, int \| tuple[int, int]]` | `{}` | Resource limits. `{"nofile": 1024}` → soft=hard=1024. `{"nofile": (1024, 65536)}` → soft/hard split. |
167
217
  | `tmpfs` | `dict[str, str]` | `{}` | Tmpfs mounts as `{path: options}` (e.g. `{"/tmp": "size=128m,mode=1777"}`). |
168
218
  | `ports` | `dict[int, int]` | `{}` | Port bindings as `{container_port: host_port}`. Only meaningful when `network != False`. |
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio-tools-docker"
3
- version = "0.8.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"