boxd-inspect 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.
Files changed (34) hide show
  1. boxd_inspect-0.1.0/.gitignore +7 -0
  2. boxd_inspect-0.1.0/CHANGELOG.md +24 -0
  3. boxd_inspect-0.1.0/LICENSE +21 -0
  4. boxd_inspect-0.1.0/PKG-INFO +314 -0
  5. boxd_inspect-0.1.0/README.md +284 -0
  6. boxd_inspect-0.1.0/examples/docker_in_sample.py +51 -0
  7. boxd_inspect-0.1.0/examples/docker_task/Dockerfile +3 -0
  8. boxd_inspect-0.1.0/examples/docker_task/task.py +34 -0
  9. boxd_inspect-0.1.0/examples/from_snapshot.py +89 -0
  10. boxd_inspect-0.1.0/examples/quickstart.py +25 -0
  11. boxd_inspect-0.1.0/pyproject.toml +88 -0
  12. boxd_inspect-0.1.0/src/boxd_inspect/__init__.py +7 -0
  13. boxd_inspect-0.1.0/src/boxd_inspect/_commands.py +203 -0
  14. boxd_inspect-0.1.0/src/boxd_inspect/_compose.py +157 -0
  15. boxd_inspect-0.1.0/src/boxd_inspect/_config.py +148 -0
  16. boxd_inspect-0.1.0/src/boxd_inspect/_environment.py +731 -0
  17. boxd_inspect-0.1.0/src/boxd_inspect/_registry.py +5 -0
  18. boxd_inspect-0.1.0/src/boxd_inspect/_version.py +1 -0
  19. boxd_inspect-0.1.0/src/boxd_inspect/py.typed +0 -0
  20. boxd_inspect-0.1.0/tests/__init__.py +0 -0
  21. boxd_inspect-0.1.0/tests/compose/nonroot/Dockerfile +2 -0
  22. boxd_inspect-0.1.0/tests/compose/nonroot/compose.yaml +9 -0
  23. boxd_inspect-0.1.0/tests/compose/nonroot-alpine/Dockerfile +2 -0
  24. boxd_inspect-0.1.0/tests/compose/nonroot-alpine/compose.yaml +9 -0
  25. boxd_inspect-0.1.0/tests/conftest.py +19 -0
  26. boxd_inspect-0.1.0/tests/fakes.py +184 -0
  27. boxd_inspect-0.1.0/tests/test_commands.py +169 -0
  28. boxd_inspect-0.1.0/tests/test_compose.py +89 -0
  29. boxd_inspect-0.1.0/tests/test_config.py +104 -0
  30. boxd_inspect-0.1.0/tests/test_environment.py +577 -0
  31. boxd_inspect-0.1.0/tests/test_eval.py +51 -0
  32. boxd_inspect-0.1.0/tests/test_live_lifecycle.py +178 -0
  33. boxd_inspect-0.1.0/tests/test_live_self_check.py +110 -0
  34. boxd_inspect-0.1.0/tests/test_registry.py +19 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ dist/
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-11)
4
+
5
+ First release.
6
+
7
+ - The `boxd` sandbox type for Inspect AI, registered through the `inspect_ai` entry point:
8
+ one boxd machine per sample, deleted when the sample ends.
9
+ - Tasks with a `compose.yaml` or `Dockerfile` run unchanged with `--sandbox boxd`: Docker
10
+ runs them inside the machine, and several services share one machine.
11
+ - `BoxdSandboxEnvironmentConfig`: `region`, `org`, `vcpu`, `memory`, `from_snapshot`,
12
+ `isolated` (on by default), `working_dir`, `name_prefix`, `labels`, `delete_after`,
13
+ `ready_timeout` and `compose_timeout`. Also accepted as a JSON file.
14
+ - `exec` with `input`, `cwd`, `env`, `user` and `timeout`; `read_file` and `write_file`
15
+ with `user`; `connection()`; `inspect sandbox cleanup boxd [id]`.
16
+ - `inspect sandbox cleanup boxd` covers all regions of the org and keeps machines that are
17
+ in use, unless `BOXD_INSPECT_CLEANUP_RUNNING=1`.
18
+ - Each eval keeps its own record of the machines it made, so two evals in one process
19
+ only delete their own.
20
+ - Passes Inspect's portable sandbox checks on the machine and in Debian and Alpine
21
+ compose services.
22
+ - Four examples: a quick start, a Docker task run unchanged, Docker inside each sample,
23
+ and samples started from a snapshot.
24
+ - Ships `py.typed`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 boxd
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,314 @@
1
+ Metadata-Version: 2.5
2
+ Name: boxd-inspect
3
+ Version: 0.1.0
4
+ Summary: boxd sandbox for Inspect AI: each sample runs on its own boxd machine, with Docker inside for compose tasks.
5
+ Project-URL: Homepage, https://boxd.sh
6
+ Project-URL: Documentation, https://docs.boxd.sh/?ref=boxd-inspect
7
+ Project-URL: Repository, https://github.com/boxd-sh/boxd-inspect
8
+ Project-URL: Issues, https://github.com/boxd-sh/boxd-inspect/issues
9
+ Project-URL: Changelog, https://github.com/boxd-sh/boxd-inspect/blob/main/CHANGELOG.md
10
+ Author-email: boxd <contact@boxd.sh>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,boxd,evals,inspect,inspect-ai,microvm,sandbox
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: boxd>=0.3.1
25
+ Requires-Dist: inspect-ai>=0.3.289
26
+ Requires-Dist: pydantic>=2
27
+ Requires-Dist: pyyaml>=6
28
+ Requires-Dist: typing-extensions>=4.5
29
+ Description-Content-Type: text/markdown
30
+
31
+ ![boxd sandboxes for Inspect AI](https://raw.githubusercontent.com/boxd-sh/boxd-inspect/main/assets/banner.png)
32
+
33
+ # boxd-inspect
34
+
35
+ A [boxd](https://boxd.sh/?ref=boxd-inspect) sandbox for [Inspect AI](https://inspect.aisi.org.uk/).
36
+ Each sample runs on its own boxd machine, a full Ubuntu 24.04 VM, and the machine is
37
+ deleted when the sample ends. Tasks written for Inspect's Docker sandbox run unchanged:
38
+ their `compose.yaml` or `Dockerfile` runs with Docker inside the machine.
39
+
40
+ A boxd machine is a KVM microVM with its own kernel, `sudo`, Docker, Python, Node and Go.
41
+ It boots in about a second, so you can run many samples in parallel without a Docker host of
42
+ your own. Because each sample is a whole machine, the agent can build images, start
43
+ containers and install system packages, and you can start every sample from a snapshot
44
+ that already has your tools and images in it.
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ pip install boxd-inspect
50
+ ```
51
+
52
+ You'll need a boxd API key ([sign up](https://boxd.sh/?ref=boxd-inspect), then create a key
53
+ in the console or with `boxd auth keys create inspect`):
54
+
55
+ ```sh
56
+ export BOXD_API_KEY=bxd_...
57
+ ```
58
+
59
+ When Inspect itself runs on a boxd machine you can skip the key: the machine's own
60
+ credential works. Python 3.11 or later.
61
+
62
+ Installing the package registers the sandbox type `boxd` with Inspect. There is nothing to
63
+ import in your task.
64
+
65
+ ## Quick start
66
+
67
+ ```python
68
+ # kernel.py
69
+ from inspect_ai import Task, task
70
+ from inspect_ai.dataset import Sample
71
+ from inspect_ai.scorer import includes
72
+ from inspect_ai.solver import generate, use_tools
73
+ from inspect_ai.tool import bash
74
+
75
+
76
+ @task
77
+ def kernel():
78
+ return Task(
79
+ dataset=[
80
+ Sample(
81
+ input="Run `uname -s` with the bash tool and tell me its output.",
82
+ target="Linux",
83
+ )
84
+ ],
85
+ solver=[use_tools(bash(timeout=60)), generate()],
86
+ scorer=includes(),
87
+ sandbox="boxd",
88
+ )
89
+ ```
90
+
91
+ ```sh
92
+ pip install anthropic
93
+ export ANTHROPIC_API_KEY=...
94
+ inspect eval kernel.py --model anthropic/claude-haiku-5-5
95
+ ```
96
+
97
+ Any model Inspect supports works.
98
+
99
+ ## Run an existing Docker task on boxd
100
+
101
+ Override the sandbox type, and the task's own `compose.yaml` or `Dockerfile` is used:
102
+
103
+ ```sh
104
+ inspect eval my_task.py --sandbox boxd --model anthropic/claude-haiku-5-5
105
+ ```
106
+
107
+ Or name the file in the task: `sandbox=("boxd", "compose.yaml")`. The compose file's
108
+ directory (all of it except `.git`) goes to the machine, so `build:` contexts next to it work.
109
+ Services named `default` or marked `x-default: true` are the default sandbox, as in Docker.
110
+ Several services share one machine.
111
+
112
+ Without a compose file, commands run on the machine itself: Ubuntu 24.04, user `boxd` with
113
+ passwordless `sudo`, Docker, Python, Node, Go and git preinstalled. The working directory is
114
+ `/home/boxd`.
115
+
116
+ ## Configuration
117
+
118
+ Pass a `BoxdSandboxEnvironmentConfig`, or a JSON file with the same fields:
119
+ `sandbox=("boxd", "boxd.json")`.
120
+
121
+ ```python
122
+ from inspect_ai.util import SandboxEnvironmentSpec
123
+
124
+ from boxd_inspect import BoxdSandboxEnvironmentConfig
125
+
126
+ sandbox = SandboxEnvironmentSpec(
127
+ "boxd",
128
+ BoxdSandboxEnvironmentConfig(region="fra", vcpu=4, delete_after=3600),
129
+ )
130
+ ```
131
+
132
+ | Field | Default | Means |
133
+ | --- | --- | --- |
134
+ | `compose` | none | A `compose.yaml` or `Dockerfile` to run with Docker inside the machine. |
135
+ | `region` | `BOXD_REGION`, then your account's | `fra` or `nyc`. |
136
+ | `org` | `BOXD_ORG`, then the key's org | The org the machines belong to. |
137
+ | `vcpu` | the org's machine size | 1, 2, 4 or 8. Memory follows: 4, 8, 16 or 32 GiB. |
138
+ | `memory` | follows `vcpu` | `4G`, `8G`, `16G` or `32G`. |
139
+ | `from_snapshot` | none | Start each sample from this snapshot, for example one with images already pulled. |
140
+ | `isolated` | `true` | The agent's commands get no peers on your org's private network and none of the in-machine boxd credentials, CLI or connected integrations your other machines have. Outbound internet still works. Turn it off only for code you trust. |
141
+ | `working_dir` | `/home/boxd`, or the container's | Where commands run and relative paths resolve. |
142
+ | `name_prefix` | `inspect` | Machine names are `<prefix>-<task>-<sample>-<random>`. |
143
+ | `labels` | none | Extra labels on every machine. Each machine also has `inspect-sandbox=boxd`, `inspect-run` and `inspect-task`. |
144
+ | `delete_after` | none | Seconds after its creation at which boxd deletes a machine, whatever it is doing. A safety net for evals that are killed. Keep it above your longest sample. |
145
+ | `ready_timeout` | `180` | Seconds to wait for a new machine. |
146
+ | `compose_timeout` | `900` | Seconds `docker compose up` may take, pulls and builds included. |
147
+
148
+ The API key is read from `BOXD_API_KEY` (or `BOXD_TOKEN`) only. A key in the configuration
149
+ would end up in the eval log.
150
+
151
+ | Environment variable | Means |
152
+ | --- | --- |
153
+ | `BOXD_API_KEY` | The API key. |
154
+ | `BOXD_REGION`, `BOXD_ORG` | The defaults for `region` and `org`. `inspect sandbox cleanup boxd` lists the machines of `BOXD_ORG`. |
155
+ | `BOXD_INSPECT_CLEANUP_RUNNING` | Set to `1` and `inspect sandbox cleanup boxd` also deletes machines that are in use. |
156
+
157
+ ## What is supported
158
+
159
+ | Inspect | On boxd |
160
+ | --- | --- |
161
+ | `exec` with `input`, `cwd`, `env`, `timeout` | Yes. |
162
+ | `exec(user=...)`, `read_file(user=...)`, `write_file(user=...)` | Yes. On the machine through `sudo`, in a service through `docker compose exec --user`. |
163
+ | `read_file`, `write_file` | Yes. Missing directories are created. Permissions apply. |
164
+ | Sample `files` and `setup` | Yes. |
165
+ | Several sandboxes per sample | Yes, as services of one compose file in one machine. |
166
+ | `connection()` (for example the human agent) | The `boxd connect <machine>` command, and the service as `container`. |
167
+ | `inspect sandbox cleanup boxd [id]` | With an id: deletes that machine, if this package made it. Without: lists every machine with `inspect-sandbox=boxd` in the org, in all regions, and deletes the ones that are not in use. See [Clean up](#clean-up). |
168
+
169
+ Every check in Inspect's portable sandbox suite (`inspect_ai.util._sandbox.self_check`)
170
+ passes on the machine and in Debian and Alpine compose services. A container running as root
171
+ passes all but the three checks that Inspect's own Docker sandbox also expects root to fail.
172
+
173
+ `timeout_retry` and `concurrency` have no effect. Limit parallel machines with
174
+ `--max-sandboxes`.
175
+
176
+ ## Errors
177
+
178
+ Configuration and key problems stop the eval before the first sample. Everything else
179
+ fails the sample it happens in, and its machine is still deleted.
180
+
181
+ | What happened | What you get |
182
+ | --- | --- |
183
+ | A configuration value boxd doesn't accept (region, size, label, prefix, path) | `ValueError` when the task starts, before any machine is made |
184
+ | The compose file's directory packs to more than 512 MiB | `ValueError` when the task starts |
185
+ | A `config` that is not a `BoxdSandboxEnvironmentConfig`, `ComposeConfig`, compose file, `Dockerfile` or JSON file | `TypeError` when the task starts |
186
+ | No API key, or boxd rejected it | the boxd SDK's error (`boxd.AuthenticationError` for a rejected key) when the task starts |
187
+ | The machine didn't answer within `ready_timeout` | `TimeoutError`; the sample fails and its machine is deleted |
188
+ | The compose project's default service didn't start | `RuntimeError` with the end of the `docker compose up` output; the machine is deleted |
189
+ | A command ran past its `timeout` | `TimeoutError`. The command and its child processes are stopped. |
190
+ | A command printed more than 10 MiB | `OutputLimitExceededError`, and the command is stopped |
191
+ | A command that exists but can't be run | `PermissionError` |
192
+ | `read_file` of a file over 100 MiB | `OutputLimitExceededError` |
193
+ | `read_file` or `write_file` on a missing file, a directory, or without permission | `FileNotFoundError`, `IsADirectoryError` or `PermissionError` |
194
+ | `user=` names a user that doesn't exist | `SandboxUserUnsupportedError` |
195
+ | The machine was deleted while the sample ran | `SandboxUnavailableError` |
196
+ | Deleting a machine failed | a warning in the log; the machine is tried again when the task ends |
197
+ | `inspect sandbox cleanup boxd <id>` with an id that doesn't exist, or of a machine this package didn't make | a message, exit code 1, nothing deleted |
198
+ | `inspect sandbox cleanup boxd` couldn't list a region, or a delete failed | a message naming the region or machine, exit code 1; the rest is still cleaned up |
199
+
200
+ A command that exits non-zero is not an error: `exec` returns its exit code, stdout and
201
+ stderr, as in every Inspect sandbox.
202
+
203
+ ## Limits
204
+
205
+ | | Limit |
206
+ | --- | --- |
207
+ | Machines | one per sample, shared by all services of its compose file |
208
+ | Machine size | 1, 2, 4 or 8 vCPUs with 4 GiB of memory each, up to your organization's quota |
209
+ | Machine ready | waits up to 180 seconds (`ready_timeout`); it usually takes about a second |
210
+ | `docker compose up` | up to 900 seconds (`compose_timeout`), image pulls and builds included |
211
+ | Command output | 10 MiB per stream, Inspect's limit for every sandbox |
212
+ | `read_file` | 100 MiB, Inspect's limit for every sandbox |
213
+ | `read_file`, `write_file` | 600 seconds each |
214
+ | Compose directory | 512 MiB, sent to the machine as one archive |
215
+ | Machine name prefix | 1 to 20 lowercase letters, digits or `-`, starting with a letter |
216
+ | Regions | `fra` and `nyc` |
217
+ | `inspect sandbox cleanup boxd` | one org (`BOXD_ORG`, or the key's), all its regions; machines that are running or starting are kept unless `BOXD_INSPECT_CLEANUP_RUNNING=1` |
218
+
219
+ ## How it works
220
+
221
+ - Installing the package adds an `inspect_ai` entry point. Inspect loads it at startup, and
222
+ it registers `BoxdSandboxEnvironment` as the sandbox type `boxd`.
223
+ - When the task starts, the configuration is checked and one API call checks the key, so a
224
+ mistake shows up before any machine exists.
225
+ - For each sample, `sample_init` creates a machine with the boxd Python SDK, waits until it
226
+ takes commands, and makes a private temp directory on it. Auto-suspend is off for these
227
+ machines, so a sample that thinks for a long time keeps its machine running.
228
+ - Commands stream through the SDK's `machines.exec`. `read_file` and `write_file` run as
229
+ small shell commands too, as the requested user, so the file's permissions apply. A write
230
+ creates missing directories and writes through the existing file, so its owner and mode
231
+ stay. Stdin and very long command lines go up as files first.
232
+ - With a compose file, its directory is uploaded as one archive, and `docker compose up
233
+ --wait` starts it inside the machine. Each running service becomes one sandbox, reached
234
+ with `docker compose exec`.
235
+ - When the sample ends, fails or is cancelled, its machine is deleted. Every machine id the
236
+ eval made is recorded, and any left over is deleted by id when the eval ends. The record
237
+ belongs to one eval: two evals in the same process never delete each other's machines.
238
+
239
+ ## Examples
240
+
241
+ Each one runs against real boxd. They use Claude (`pip install anthropic`,
242
+ `ANTHROPIC_API_KEY`); pass any other model with `--model`.
243
+
244
+ - [`examples/quickstart.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/quickstart.py): the task above, one sample on one fresh
245
+ machine.
246
+ - [`examples/docker_task/`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/docker_task/task.py): a task written for Inspect's Docker
247
+ sandbox with its own `Dockerfile`, run unchanged with `--sandbox boxd`.
248
+ - [`examples/docker_in_sample.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/docker_in_sample.py): three samples in parallel,
249
+ each on its own machine, where the agent builds a Docker image, runs it with nginx and
250
+ fetches the page.
251
+ - [`examples/from_snapshot.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/from_snapshot.py): prepare a machine once (install a
252
+ tool, pull an image), save it as a snapshot, and start every sample from it.
253
+
254
+ ```sh
255
+ inspect eval examples/quickstart.py --model anthropic/claude-haiku-5-5
256
+ inspect eval examples/docker_task/task.py --sandbox boxd --model anthropic/claude-haiku-5-5
257
+ inspect eval examples/docker_in_sample.py --model anthropic/claude-haiku-5-5
258
+ python examples/from_snapshot.py
259
+ ```
260
+
261
+ ## Clean up
262
+
263
+ A sample's machine is deleted when the sample ends, also when it fails or is cancelled.
264
+ Machines whose samples did not clean up are deleted, by id, when the task ends. With
265
+ `--no-sandbox-cleanup` the machines stay and their ids are printed. If the eval process is
266
+ killed, its machines stay. Set `delete_after` so boxd deletes them for you, or delete them
267
+ yourself:
268
+
269
+ ```sh
270
+ inspect sandbox cleanup boxd <id> # one machine
271
+ inspect sandbox cleanup boxd # every machine that is not in use
272
+ BOXD_INSPECT_CLEANUP_RUNNING=1 inspect sandbox cleanup boxd # the ones in use too
273
+ ```
274
+
275
+ Without an id, the command lists every machine with the label `inspect-sandbox=boxd` in the
276
+ org, in all regions. That includes machines of evals other people in the org are running
277
+ right now. So it only deletes machines that are stopped, hibernated, in standby or failed,
278
+ and keeps the ones that are running or starting. A machine of a killed eval is still
279
+ running: delete it by id, or set `BOXD_INSPECT_CLEANUP_RUNNING=1` when you know no eval is
280
+ running in the org. In a terminal the command asks before it deletes. In a script or in CI
281
+ it doesn't ask.
282
+
283
+ New machines start fresh, so a compose service's images are pulled for each sample. For big
284
+ images, prepare a machine once, save it as a snapshot and pass `from_snapshot`
285
+ (see [`examples/from_snapshot.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/from_snapshot.py)).
286
+
287
+ ## Development
288
+
289
+ Needs [uv](https://docs.astral.sh/uv/).
290
+
291
+ ```sh
292
+ uv sync
293
+ uv run pytest # offline, the boxd client is faked
294
+ uv run ruff check . && uv run ruff format --check .
295
+ uv run mypy src tests examples
296
+ BOXD_API_KEY=bxd_... uv run pytest -m live # real machines
297
+ ```
298
+
299
+ The live tests run Inspect's portable sandbox checks on a machine and in three compose
300
+ setups, plus lifecycle and error tests. They create machines named `t-inspect-*`, labelled
301
+ `boxd-inspect=live-test`, each with a one-hour `delete_after`, and delete them again.
302
+ `BOXD_INSPECT_TEST_REGION` (default `nyc`) picks the region. Set
303
+ `BOXD_INSPECT_TEST_OTHER_REGION` to a second region to also test cleanup across regions.
304
+ They are skipped when `BOXD_API_KEY` is not set. No live test runs the cleanup without an id.
305
+
306
+ ## Resources
307
+
308
+ - [boxd docs](https://docs.boxd.sh/?ref=boxd-inspect)
309
+ - [boxd Python SDK](https://pypi.org/project/boxd/)
310
+ - [Inspect sandboxing docs](https://inspect.aisi.org.uk/sandboxing.html)
311
+
312
+ ## License
313
+
314
+ MIT
@@ -0,0 +1,284 @@
1
+ ![boxd sandboxes for Inspect AI](https://raw.githubusercontent.com/boxd-sh/boxd-inspect/main/assets/banner.png)
2
+
3
+ # boxd-inspect
4
+
5
+ A [boxd](https://boxd.sh/?ref=boxd-inspect) sandbox for [Inspect AI](https://inspect.aisi.org.uk/).
6
+ Each sample runs on its own boxd machine, a full Ubuntu 24.04 VM, and the machine is
7
+ deleted when the sample ends. Tasks written for Inspect's Docker sandbox run unchanged:
8
+ their `compose.yaml` or `Dockerfile` runs with Docker inside the machine.
9
+
10
+ A boxd machine is a KVM microVM with its own kernel, `sudo`, Docker, Python, Node and Go.
11
+ It boots in about a second, so you can run many samples in parallel without a Docker host of
12
+ your own. Because each sample is a whole machine, the agent can build images, start
13
+ containers and install system packages, and you can start every sample from a snapshot
14
+ that already has your tools and images in it.
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ pip install boxd-inspect
20
+ ```
21
+
22
+ You'll need a boxd API key ([sign up](https://boxd.sh/?ref=boxd-inspect), then create a key
23
+ in the console or with `boxd auth keys create inspect`):
24
+
25
+ ```sh
26
+ export BOXD_API_KEY=bxd_...
27
+ ```
28
+
29
+ When Inspect itself runs on a boxd machine you can skip the key: the machine's own
30
+ credential works. Python 3.11 or later.
31
+
32
+ Installing the package registers the sandbox type `boxd` with Inspect. There is nothing to
33
+ import in your task.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ # kernel.py
39
+ from inspect_ai import Task, task
40
+ from inspect_ai.dataset import Sample
41
+ from inspect_ai.scorer import includes
42
+ from inspect_ai.solver import generate, use_tools
43
+ from inspect_ai.tool import bash
44
+
45
+
46
+ @task
47
+ def kernel():
48
+ return Task(
49
+ dataset=[
50
+ Sample(
51
+ input="Run `uname -s` with the bash tool and tell me its output.",
52
+ target="Linux",
53
+ )
54
+ ],
55
+ solver=[use_tools(bash(timeout=60)), generate()],
56
+ scorer=includes(),
57
+ sandbox="boxd",
58
+ )
59
+ ```
60
+
61
+ ```sh
62
+ pip install anthropic
63
+ export ANTHROPIC_API_KEY=...
64
+ inspect eval kernel.py --model anthropic/claude-haiku-5-5
65
+ ```
66
+
67
+ Any model Inspect supports works.
68
+
69
+ ## Run an existing Docker task on boxd
70
+
71
+ Override the sandbox type, and the task's own `compose.yaml` or `Dockerfile` is used:
72
+
73
+ ```sh
74
+ inspect eval my_task.py --sandbox boxd --model anthropic/claude-haiku-5-5
75
+ ```
76
+
77
+ Or name the file in the task: `sandbox=("boxd", "compose.yaml")`. The compose file's
78
+ directory (all of it except `.git`) goes to the machine, so `build:` contexts next to it work.
79
+ Services named `default` or marked `x-default: true` are the default sandbox, as in Docker.
80
+ Several services share one machine.
81
+
82
+ Without a compose file, commands run on the machine itself: Ubuntu 24.04, user `boxd` with
83
+ passwordless `sudo`, Docker, Python, Node, Go and git preinstalled. The working directory is
84
+ `/home/boxd`.
85
+
86
+ ## Configuration
87
+
88
+ Pass a `BoxdSandboxEnvironmentConfig`, or a JSON file with the same fields:
89
+ `sandbox=("boxd", "boxd.json")`.
90
+
91
+ ```python
92
+ from inspect_ai.util import SandboxEnvironmentSpec
93
+
94
+ from boxd_inspect import BoxdSandboxEnvironmentConfig
95
+
96
+ sandbox = SandboxEnvironmentSpec(
97
+ "boxd",
98
+ BoxdSandboxEnvironmentConfig(region="fra", vcpu=4, delete_after=3600),
99
+ )
100
+ ```
101
+
102
+ | Field | Default | Means |
103
+ | --- | --- | --- |
104
+ | `compose` | none | A `compose.yaml` or `Dockerfile` to run with Docker inside the machine. |
105
+ | `region` | `BOXD_REGION`, then your account's | `fra` or `nyc`. |
106
+ | `org` | `BOXD_ORG`, then the key's org | The org the machines belong to. |
107
+ | `vcpu` | the org's machine size | 1, 2, 4 or 8. Memory follows: 4, 8, 16 or 32 GiB. |
108
+ | `memory` | follows `vcpu` | `4G`, `8G`, `16G` or `32G`. |
109
+ | `from_snapshot` | none | Start each sample from this snapshot, for example one with images already pulled. |
110
+ | `isolated` | `true` | The agent's commands get no peers on your org's private network and none of the in-machine boxd credentials, CLI or connected integrations your other machines have. Outbound internet still works. Turn it off only for code you trust. |
111
+ | `working_dir` | `/home/boxd`, or the container's | Where commands run and relative paths resolve. |
112
+ | `name_prefix` | `inspect` | Machine names are `<prefix>-<task>-<sample>-<random>`. |
113
+ | `labels` | none | Extra labels on every machine. Each machine also has `inspect-sandbox=boxd`, `inspect-run` and `inspect-task`. |
114
+ | `delete_after` | none | Seconds after its creation at which boxd deletes a machine, whatever it is doing. A safety net for evals that are killed. Keep it above your longest sample. |
115
+ | `ready_timeout` | `180` | Seconds to wait for a new machine. |
116
+ | `compose_timeout` | `900` | Seconds `docker compose up` may take, pulls and builds included. |
117
+
118
+ The API key is read from `BOXD_API_KEY` (or `BOXD_TOKEN`) only. A key in the configuration
119
+ would end up in the eval log.
120
+
121
+ | Environment variable | Means |
122
+ | --- | --- |
123
+ | `BOXD_API_KEY` | The API key. |
124
+ | `BOXD_REGION`, `BOXD_ORG` | The defaults for `region` and `org`. `inspect sandbox cleanup boxd` lists the machines of `BOXD_ORG`. |
125
+ | `BOXD_INSPECT_CLEANUP_RUNNING` | Set to `1` and `inspect sandbox cleanup boxd` also deletes machines that are in use. |
126
+
127
+ ## What is supported
128
+
129
+ | Inspect | On boxd |
130
+ | --- | --- |
131
+ | `exec` with `input`, `cwd`, `env`, `timeout` | Yes. |
132
+ | `exec(user=...)`, `read_file(user=...)`, `write_file(user=...)` | Yes. On the machine through `sudo`, in a service through `docker compose exec --user`. |
133
+ | `read_file`, `write_file` | Yes. Missing directories are created. Permissions apply. |
134
+ | Sample `files` and `setup` | Yes. |
135
+ | Several sandboxes per sample | Yes, as services of one compose file in one machine. |
136
+ | `connection()` (for example the human agent) | The `boxd connect <machine>` command, and the service as `container`. |
137
+ | `inspect sandbox cleanup boxd [id]` | With an id: deletes that machine, if this package made it. Without: lists every machine with `inspect-sandbox=boxd` in the org, in all regions, and deletes the ones that are not in use. See [Clean up](#clean-up). |
138
+
139
+ Every check in Inspect's portable sandbox suite (`inspect_ai.util._sandbox.self_check`)
140
+ passes on the machine and in Debian and Alpine compose services. A container running as root
141
+ passes all but the three checks that Inspect's own Docker sandbox also expects root to fail.
142
+
143
+ `timeout_retry` and `concurrency` have no effect. Limit parallel machines with
144
+ `--max-sandboxes`.
145
+
146
+ ## Errors
147
+
148
+ Configuration and key problems stop the eval before the first sample. Everything else
149
+ fails the sample it happens in, and its machine is still deleted.
150
+
151
+ | What happened | What you get |
152
+ | --- | --- |
153
+ | A configuration value boxd doesn't accept (region, size, label, prefix, path) | `ValueError` when the task starts, before any machine is made |
154
+ | The compose file's directory packs to more than 512 MiB | `ValueError` when the task starts |
155
+ | A `config` that is not a `BoxdSandboxEnvironmentConfig`, `ComposeConfig`, compose file, `Dockerfile` or JSON file | `TypeError` when the task starts |
156
+ | No API key, or boxd rejected it | the boxd SDK's error (`boxd.AuthenticationError` for a rejected key) when the task starts |
157
+ | The machine didn't answer within `ready_timeout` | `TimeoutError`; the sample fails and its machine is deleted |
158
+ | The compose project's default service didn't start | `RuntimeError` with the end of the `docker compose up` output; the machine is deleted |
159
+ | A command ran past its `timeout` | `TimeoutError`. The command and its child processes are stopped. |
160
+ | A command printed more than 10 MiB | `OutputLimitExceededError`, and the command is stopped |
161
+ | A command that exists but can't be run | `PermissionError` |
162
+ | `read_file` of a file over 100 MiB | `OutputLimitExceededError` |
163
+ | `read_file` or `write_file` on a missing file, a directory, or without permission | `FileNotFoundError`, `IsADirectoryError` or `PermissionError` |
164
+ | `user=` names a user that doesn't exist | `SandboxUserUnsupportedError` |
165
+ | The machine was deleted while the sample ran | `SandboxUnavailableError` |
166
+ | Deleting a machine failed | a warning in the log; the machine is tried again when the task ends |
167
+ | `inspect sandbox cleanup boxd <id>` with an id that doesn't exist, or of a machine this package didn't make | a message, exit code 1, nothing deleted |
168
+ | `inspect sandbox cleanup boxd` couldn't list a region, or a delete failed | a message naming the region or machine, exit code 1; the rest is still cleaned up |
169
+
170
+ A command that exits non-zero is not an error: `exec` returns its exit code, stdout and
171
+ stderr, as in every Inspect sandbox.
172
+
173
+ ## Limits
174
+
175
+ | | Limit |
176
+ | --- | --- |
177
+ | Machines | one per sample, shared by all services of its compose file |
178
+ | Machine size | 1, 2, 4 or 8 vCPUs with 4 GiB of memory each, up to your organization's quota |
179
+ | Machine ready | waits up to 180 seconds (`ready_timeout`); it usually takes about a second |
180
+ | `docker compose up` | up to 900 seconds (`compose_timeout`), image pulls and builds included |
181
+ | Command output | 10 MiB per stream, Inspect's limit for every sandbox |
182
+ | `read_file` | 100 MiB, Inspect's limit for every sandbox |
183
+ | `read_file`, `write_file` | 600 seconds each |
184
+ | Compose directory | 512 MiB, sent to the machine as one archive |
185
+ | Machine name prefix | 1 to 20 lowercase letters, digits or `-`, starting with a letter |
186
+ | Regions | `fra` and `nyc` |
187
+ | `inspect sandbox cleanup boxd` | one org (`BOXD_ORG`, or the key's), all its regions; machines that are running or starting are kept unless `BOXD_INSPECT_CLEANUP_RUNNING=1` |
188
+
189
+ ## How it works
190
+
191
+ - Installing the package adds an `inspect_ai` entry point. Inspect loads it at startup, and
192
+ it registers `BoxdSandboxEnvironment` as the sandbox type `boxd`.
193
+ - When the task starts, the configuration is checked and one API call checks the key, so a
194
+ mistake shows up before any machine exists.
195
+ - For each sample, `sample_init` creates a machine with the boxd Python SDK, waits until it
196
+ takes commands, and makes a private temp directory on it. Auto-suspend is off for these
197
+ machines, so a sample that thinks for a long time keeps its machine running.
198
+ - Commands stream through the SDK's `machines.exec`. `read_file` and `write_file` run as
199
+ small shell commands too, as the requested user, so the file's permissions apply. A write
200
+ creates missing directories and writes through the existing file, so its owner and mode
201
+ stay. Stdin and very long command lines go up as files first.
202
+ - With a compose file, its directory is uploaded as one archive, and `docker compose up
203
+ --wait` starts it inside the machine. Each running service becomes one sandbox, reached
204
+ with `docker compose exec`.
205
+ - When the sample ends, fails or is cancelled, its machine is deleted. Every machine id the
206
+ eval made is recorded, and any left over is deleted by id when the eval ends. The record
207
+ belongs to one eval: two evals in the same process never delete each other's machines.
208
+
209
+ ## Examples
210
+
211
+ Each one runs against real boxd. They use Claude (`pip install anthropic`,
212
+ `ANTHROPIC_API_KEY`); pass any other model with `--model`.
213
+
214
+ - [`examples/quickstart.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/quickstart.py): the task above, one sample on one fresh
215
+ machine.
216
+ - [`examples/docker_task/`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/docker_task/task.py): a task written for Inspect's Docker
217
+ sandbox with its own `Dockerfile`, run unchanged with `--sandbox boxd`.
218
+ - [`examples/docker_in_sample.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/docker_in_sample.py): three samples in parallel,
219
+ each on its own machine, where the agent builds a Docker image, runs it with nginx and
220
+ fetches the page.
221
+ - [`examples/from_snapshot.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/from_snapshot.py): prepare a machine once (install a
222
+ tool, pull an image), save it as a snapshot, and start every sample from it.
223
+
224
+ ```sh
225
+ inspect eval examples/quickstart.py --model anthropic/claude-haiku-5-5
226
+ inspect eval examples/docker_task/task.py --sandbox boxd --model anthropic/claude-haiku-5-5
227
+ inspect eval examples/docker_in_sample.py --model anthropic/claude-haiku-5-5
228
+ python examples/from_snapshot.py
229
+ ```
230
+
231
+ ## Clean up
232
+
233
+ A sample's machine is deleted when the sample ends, also when it fails or is cancelled.
234
+ Machines whose samples did not clean up are deleted, by id, when the task ends. With
235
+ `--no-sandbox-cleanup` the machines stay and their ids are printed. If the eval process is
236
+ killed, its machines stay. Set `delete_after` so boxd deletes them for you, or delete them
237
+ yourself:
238
+
239
+ ```sh
240
+ inspect sandbox cleanup boxd <id> # one machine
241
+ inspect sandbox cleanup boxd # every machine that is not in use
242
+ BOXD_INSPECT_CLEANUP_RUNNING=1 inspect sandbox cleanup boxd # the ones in use too
243
+ ```
244
+
245
+ Without an id, the command lists every machine with the label `inspect-sandbox=boxd` in the
246
+ org, in all regions. That includes machines of evals other people in the org are running
247
+ right now. So it only deletes machines that are stopped, hibernated, in standby or failed,
248
+ and keeps the ones that are running or starting. A machine of a killed eval is still
249
+ running: delete it by id, or set `BOXD_INSPECT_CLEANUP_RUNNING=1` when you know no eval is
250
+ running in the org. In a terminal the command asks before it deletes. In a script or in CI
251
+ it doesn't ask.
252
+
253
+ New machines start fresh, so a compose service's images are pulled for each sample. For big
254
+ images, prepare a machine once, save it as a snapshot and pass `from_snapshot`
255
+ (see [`examples/from_snapshot.py`](https://github.com/boxd-sh/boxd-inspect/blob/main/examples/from_snapshot.py)).
256
+
257
+ ## Development
258
+
259
+ Needs [uv](https://docs.astral.sh/uv/).
260
+
261
+ ```sh
262
+ uv sync
263
+ uv run pytest # offline, the boxd client is faked
264
+ uv run ruff check . && uv run ruff format --check .
265
+ uv run mypy src tests examples
266
+ BOXD_API_KEY=bxd_... uv run pytest -m live # real machines
267
+ ```
268
+
269
+ The live tests run Inspect's portable sandbox checks on a machine and in three compose
270
+ setups, plus lifecycle and error tests. They create machines named `t-inspect-*`, labelled
271
+ `boxd-inspect=live-test`, each with a one-hour `delete_after`, and delete them again.
272
+ `BOXD_INSPECT_TEST_REGION` (default `nyc`) picks the region. Set
273
+ `BOXD_INSPECT_TEST_OTHER_REGION` to a second region to also test cleanup across regions.
274
+ They are skipped when `BOXD_API_KEY` is not set. No live test runs the cleanup without an id.
275
+
276
+ ## Resources
277
+
278
+ - [boxd docs](https://docs.boxd.sh/?ref=boxd-inspect)
279
+ - [boxd Python SDK](https://pypi.org/project/boxd/)
280
+ - [Inspect sandboxing docs](https://inspect.aisi.org.uk/sandboxing.html)
281
+
282
+ ## License
283
+
284
+ MIT