axio-tools-docker 0.8.0__tar.gz → 0.9.1__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.1
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
 
@@ -106,9 +115,10 @@ The running container's ID is available as `sandbox.container_id` inside the
106
115
 
107
116
  ## Named containers and reuse
108
117
 
109
- Pass `name=` to give the container a fixed name. If a running container with
110
- 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`:
118
+ Pass `name=` to give the container a fixed name. If a container with that name
119
+ already exists, the sandbox starts it if needed and attaches instead of creating
120
+ a new one. Attached containers are never removed on exit - regardless of
121
+ `remove`:
112
122
 
113
123
  ```python
114
124
  import asyncio
@@ -126,37 +136,76 @@ asyncio.run(first_session())
126
136
  asyncio.run(second_session())
127
137
  ```
128
138
 
139
+ ## Named volumes
140
+
141
+ Named volumes are managed by the Docker daemon and persist across container
142
+ restarts. Use them to share state between sandbox sessions:
143
+
144
+ <!-- name: test_readme_example; fixtures: docker -->
145
+ ```python
146
+ import asyncio
147
+ from axio_tools_docker import DockerSandbox
148
+
149
+ async def main() -> None:
150
+ async with DockerSandbox(
151
+ image="python:3.12-alpine",
152
+ named_volumes={"/data": "my-project-data"},
153
+ ) as sb:
154
+ await sb.write_file("/data/state.json", '{"count": 1}')
155
+ # Container removed, volume survives.
156
+
157
+ async with DockerSandbox(
158
+ image="python:3.12-alpine",
159
+ named_volumes={"/data": "my-project-data"},
160
+ volumes_remove=True, # delete the volume on exit
161
+ ) as sb:
162
+ raw = await sb.read_file_bytes("/data/state.json")
163
+ assert raw.decode() == '{"count": 1}'
164
+
165
+ asyncio.run(main())
166
+ ```
167
+
168
+ Docker creates the volume automatically if it does not exist yet.
169
+
129
170
  ## Configuration
130
171
 
131
- <!-- name: test_readme_config -->
172
+ <!-- name: test_readme_config; fixtures: docker -->
132
173
  ```python
174
+ import os
133
175
  from axio_tools_docker import DockerSandbox
134
176
 
135
177
  sandbox = DockerSandbox(
136
- "unix:///var/run/docker.sock", # Docker daemon URL (positional)
178
+ os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock"), # Docker daemon URL, optional
137
179
  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
180
+ memory="512m", # memory limit: "256m", "1g", …
181
+ cpus="2.0", # CPU limit
182
+ network=False, # False=none, True=default, str=explicit mode
141
183
  workdir="/workspace",
142
- volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
184
+ volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
185
+ named_volumes={"/data": "my-project-data"}, # named Docker volumes
186
+ volumes_remove=False, # remove named volumes on exit
143
187
  env={"PYTHONPATH": "/app"},
144
188
  user="nobody",
145
189
  name="my-agent-sandbox",
146
190
  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
191
+ read_only=True, # read-only root filesystem
192
+ shm_size="64m", # /dev/shm size
193
+ cap_add=["NET_ADMIN"], # add Linux capabilities
194
+ cap_drop=["ALL"], # drop Linux capabilities
151
195
  privileged=False,
152
196
  ulimits={"nofile": (1024, 65536), "nproc": 512},
153
197
  tmpfs={"/tmp": "size=128m,mode=1777"},
154
- ports={8080: 8080}, # {container_port: host_port}
198
+ ports={8080: 8080}, # {container_port: host_port}
155
199
  platform="linux/amd64",
156
200
  extra_hosts={"host.docker.internal": "host-gateway"},
157
201
  devices=["/dev/net/tun", "/dev/sda:/dev/xvda:r"],
158
202
  dns=["8.8.8.8", "1.1.1.1"],
159
203
  )
204
+
205
+ assert sandbox.image == "python:3.12-slim"
206
+ assert sandbox.memory == "512m"
207
+ assert sandbox.cpus == "2.0"
208
+ assert sandbox.network == False
160
209
  ```
161
210
 
162
211
  | Parameter | Type | Default | Description |
@@ -168,6 +217,8 @@ sandbox = DockerSandbox(
168
217
  | `network` | `bool \| str` | `False` | Network mode. `False` → `none`. `True` → Docker default. String → explicit `NetworkMode` (e.g. `"host"`, `"bridge"`, `"my-net"`). |
169
218
  | `workdir` | `str` | `"/workspace"` | Working directory inside the container. |
170
219
  | `volumes` | `dict[str, str]` | `{}` | Bind mounts as `{container_path: host_path}`. |
220
+ | `named_volumes` | `dict[str, str]` | `{}` | Named Docker volumes as `{container_path: volume_name}`. Created automatically if absent. |
221
+ | `volumes_remove` | `bool` | `False` | Remove named volumes on exit. No effect when attached to an existing container. |
171
222
  | `env` | `dict[str, str]` | `{}` | Environment variables passed to all commands. |
172
223
  | `user` | `str` | `""` | User to run as (e.g. `"nobody"`, `"1000"`). |
173
224
  | `name` | `str` | `""` | Container name. Attaches to existing container if found; creates new one otherwise. |
@@ -176,7 +227,7 @@ sandbox = DockerSandbox(
176
227
  | `shm_size` | `str` | `""` | `/dev/shm` size (e.g. `"64m"`). Useful for PyTorch / shared-memory IPC. |
177
228
  | `cap_add` | `list[str]` | `[]` | Linux capabilities to add (e.g. `["NET_ADMIN", "SYS_PTRACE"]`). |
178
229
  | `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. |
230
+ | `privileged` | `bool` | `False` | Extended privileges - full capability set and device access. Use with care. |
180
231
  | `ulimits` | `dict[str, int \| tuple[int, int]]` | `{}` | Resource limits. `{"nofile": 1024}` → soft=hard=1024. `{"nofile": (1024, 65536)}` → soft/hard split. |
181
232
  | `tmpfs` | `dict[str, str]` | `{}` | Tmpfs mounts as `{path: options}` (e.g. `{"/tmp": "size=128m,mode=1777"}`). |
182
233
  | `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
 
@@ -92,9 +101,10 @@ The running container's ID is available as `sandbox.container_id` inside the
92
101
 
93
102
  ## Named containers and reuse
94
103
 
95
- Pass `name=` to give the container a fixed name. If a running container with
96
- 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`:
104
+ Pass `name=` to give the container a fixed name. If a container with that name
105
+ already exists, the sandbox starts it if needed and attaches instead of creating
106
+ a new one. Attached containers are never removed on exit - regardless of
107
+ `remove`:
98
108
 
99
109
  ```python
100
110
  import asyncio
@@ -112,37 +122,76 @@ asyncio.run(first_session())
112
122
  asyncio.run(second_session())
113
123
  ```
114
124
 
125
+ ## Named volumes
126
+
127
+ Named volumes are managed by the Docker daemon and persist across container
128
+ restarts. Use them to share state between sandbox sessions:
129
+
130
+ <!-- name: test_readme_example; fixtures: docker -->
131
+ ```python
132
+ import asyncio
133
+ from axio_tools_docker import DockerSandbox
134
+
135
+ async def main() -> None:
136
+ async with DockerSandbox(
137
+ image="python:3.12-alpine",
138
+ named_volumes={"/data": "my-project-data"},
139
+ ) as sb:
140
+ await sb.write_file("/data/state.json", '{"count": 1}')
141
+ # Container removed, volume survives.
142
+
143
+ async with DockerSandbox(
144
+ image="python:3.12-alpine",
145
+ named_volumes={"/data": "my-project-data"},
146
+ volumes_remove=True, # delete the volume on exit
147
+ ) as sb:
148
+ raw = await sb.read_file_bytes("/data/state.json")
149
+ assert raw.decode() == '{"count": 1}'
150
+
151
+ asyncio.run(main())
152
+ ```
153
+
154
+ Docker creates the volume automatically if it does not exist yet.
155
+
115
156
  ## Configuration
116
157
 
117
- <!-- name: test_readme_config -->
158
+ <!-- name: test_readme_config; fixtures: docker -->
118
159
  ```python
160
+ import os
119
161
  from axio_tools_docker import DockerSandbox
120
162
 
121
163
  sandbox = DockerSandbox(
122
- "unix:///var/run/docker.sock", # Docker daemon URL (positional)
164
+ os.getenv("DOCKER_HOST", "unix:///var/run/docker.sock"), # Docker daemon URL, optional
123
165
  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
166
+ memory="512m", # memory limit: "256m", "1g", …
167
+ cpus="2.0", # CPU limit
168
+ network=False, # False=none, True=default, str=explicit mode
127
169
  workdir="/workspace",
128
- volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
170
+ volumes={"/workspace": "/tmp/host-dir"}, # {container_path: host_path}
171
+ named_volumes={"/data": "my-project-data"}, # named Docker volumes
172
+ volumes_remove=False, # remove named volumes on exit
129
173
  env={"PYTHONPATH": "/app"},
130
174
  user="nobody",
131
175
  name="my-agent-sandbox",
132
176
  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
177
+ read_only=True, # read-only root filesystem
178
+ shm_size="64m", # /dev/shm size
179
+ cap_add=["NET_ADMIN"], # add Linux capabilities
180
+ cap_drop=["ALL"], # drop Linux capabilities
137
181
  privileged=False,
138
182
  ulimits={"nofile": (1024, 65536), "nproc": 512},
139
183
  tmpfs={"/tmp": "size=128m,mode=1777"},
140
- ports={8080: 8080}, # {container_port: host_port}
184
+ ports={8080: 8080}, # {container_port: host_port}
141
185
  platform="linux/amd64",
142
186
  extra_hosts={"host.docker.internal": "host-gateway"},
143
187
  devices=["/dev/net/tun", "/dev/sda:/dev/xvda:r"],
144
188
  dns=["8.8.8.8", "1.1.1.1"],
145
189
  )
190
+
191
+ assert sandbox.image == "python:3.12-slim"
192
+ assert sandbox.memory == "512m"
193
+ assert sandbox.cpus == "2.0"
194
+ assert sandbox.network == False
146
195
  ```
147
196
 
148
197
  | Parameter | Type | Default | Description |
@@ -154,6 +203,8 @@ sandbox = DockerSandbox(
154
203
  | `network` | `bool \| str` | `False` | Network mode. `False` → `none`. `True` → Docker default. String → explicit `NetworkMode` (e.g. `"host"`, `"bridge"`, `"my-net"`). |
155
204
  | `workdir` | `str` | `"/workspace"` | Working directory inside the container. |
156
205
  | `volumes` | `dict[str, str]` | `{}` | Bind mounts as `{container_path: host_path}`. |
206
+ | `named_volumes` | `dict[str, str]` | `{}` | Named Docker volumes as `{container_path: volume_name}`. Created automatically if absent. |
207
+ | `volumes_remove` | `bool` | `False` | Remove named volumes on exit. No effect when attached to an existing container. |
157
208
  | `env` | `dict[str, str]` | `{}` | Environment variables passed to all commands. |
158
209
  | `user` | `str` | `""` | User to run as (e.g. `"nobody"`, `"1000"`). |
159
210
  | `name` | `str` | `""` | Container name. Attaches to existing container if found; creates new one otherwise. |
@@ -162,7 +213,7 @@ sandbox = DockerSandbox(
162
213
  | `shm_size` | `str` | `""` | `/dev/shm` size (e.g. `"64m"`). Useful for PyTorch / shared-memory IPC. |
163
214
  | `cap_add` | `list[str]` | `[]` | Linux capabilities to add (e.g. `["NET_ADMIN", "SYS_PTRACE"]`). |
164
215
  | `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. |
216
+ | `privileged` | `bool` | `False` | Extended privileges - full capability set and device access. Use with care. |
166
217
  | `ulimits` | `dict[str, int \| tuple[int, int]]` | `{}` | Resource limits. `{"nofile": 1024}` → soft=hard=1024. `{"nofile": (1024, 65536)}` → soft/hard split. |
167
218
  | `tmpfs` | `dict[str, str]` | `{}` | Tmpfs mounts as `{path: options}` (e.g. `{"/tmp": "size=128m,mode=1777"}`). |
168
219
  | `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.1"
4
4
  description = "Docker sandbox tools for Axio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -24,6 +24,7 @@ testpaths = ["tests", "README.md"]
24
24
 
25
25
  [tool.ruff]
26
26
  line-length = 119
27
+ output-format = "concise"
27
28
  target-version = "py312"
28
29
 
29
30
  [tool.ruff.lint]
@@ -33,6 +34,10 @@ select = ["E", "F", "I", "UP"]
33
34
  strict = true
34
35
  python_version = "3.12"
35
36
 
37
+ [[tool.mypy.overrides]]
38
+ module = "aiodocker.*"
39
+ follow_imports = "skip"
40
+
36
41
  [dependency-groups]
37
42
  dev = [
38
43
  "pytest>=8",
@@ -43,4 +48,3 @@ dev = [
43
48
  "markdown-pytest>=0.6.0",
44
49
  "aiodocker>=0.26",
45
50
  ]
46
-