marin-shellbox 0.2.127.dev36173526241__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 (43) hide show
  1. marin_shellbox-0.2.127.dev36173526241/.gitignore +253 -0
  2. marin_shellbox-0.2.127.dev36173526241/PKG-INFO +283 -0
  3. marin_shellbox-0.2.127.dev36173526241/README.md +266 -0
  4. marin_shellbox-0.2.127.dev36173526241/benchmarks/tasktrove/compare.py +204 -0
  5. marin_shellbox-0.2.127.dev36173526241/benchmarks/tasktrove/sample.py +83 -0
  6. marin_shellbox-0.2.127.dev36173526241/pyproject.toml +32 -0
  7. marin_shellbox-0.2.127.dev36173526241/src/shellbox/__init__.py +4 -0
  8. marin_shellbox-0.2.127.dev36173526241/src/shellbox/agent.py +162 -0
  9. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/__init__.py +2 -0
  10. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/daytona/__init__.py +7 -0
  11. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/daytona/machine.py +179 -0
  12. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/docker/__init__.py +2 -0
  13. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/docker/machine.py +157 -0
  14. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/gvisor/__init__.py +7 -0
  15. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/gvisor/machine.py +22 -0
  16. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/iris/__init__.py +7 -0
  17. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/iris/machine.py +271 -0
  18. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/__init__.py +2 -0
  19. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/bundle.py +249 -0
  20. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/environment.py +218 -0
  21. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/guest/disk-init +12 -0
  22. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/guest/init +30 -0
  23. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/guest/pty-agent.c +261 -0
  24. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/image.py +70 -0
  25. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/qemu/machine.py +455 -0
  26. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/shellsim/__init__.py +2 -0
  27. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/shellsim/environment.py +148 -0
  28. marin_shellbox-0.2.127.dev36173526241/src/shellbox/backends/shellsim/machine.py +183 -0
  29. marin_shellbox-0.2.127.dev36173526241/src/shellbox/image.py +207 -0
  30. marin_shellbox-0.2.127.dev36173526241/src/shellbox/machine.py +132 -0
  31. marin_shellbox-0.2.127.dev36173526241/tests/harbor_smoke.py +139 -0
  32. marin_shellbox-0.2.127.dev36173526241/tests/manual/README.md +9 -0
  33. marin_shellbox-0.2.127.dev36173526241/tests/manual/check_machine.py +46 -0
  34. marin_shellbox-0.2.127.dev36173526241/tests/manual/check_prepared_image.py +63 -0
  35. marin_shellbox-0.2.127.dev36173526241/tests/manual/check_shell.py +53 -0
  36. marin_shellbox-0.2.127.dev36173526241/tests/manual/task/instruction.md +1 -0
  37. marin_shellbox-0.2.127.dev36173526241/tests/manual/task/task.toml +12 -0
  38. marin_shellbox-0.2.127.dev36173526241/tests/manual/task/tests/test.sh +7 -0
  39. marin_shellbox-0.2.127.dev36173526241/tests/manual/tasktrove_nl2bash_command.sh +7 -0
  40. marin_shellbox-0.2.127.dev36173526241/tests/test_daytona_machine.py +90 -0
  41. marin_shellbox-0.2.127.dev36173526241/tests/test_harbor_shellsim.py +26 -0
  42. marin_shellbox-0.2.127.dev36173526241/tests/test_iris_machine.py +71 -0
  43. marin_shellbox-0.2.127.dev36173526241/tests/test_shellsim_machine.py +55 -0
@@ -0,0 +1,253 @@
1
+ # redundant, but Ray looks for this otherwise.
2
+ .git
3
+
4
+ logs/
5
+
6
+ # CPU profiles
7
+ prof/
8
+
9
+ # Downloaded build tools (zig, etc.)
10
+ .tools/
11
+
12
+ tests/snapshots/outputs
13
+ tests/snapshots/diffs
14
+
15
+ # don't log data/MD outputs to git
16
+ data/*
17
+ output/*
18
+ outputs/*
19
+
20
+ # Snapshot diffs and outputs
21
+ tests/snapshots/*/outputs/*
22
+ tests/snapshots/*/diffs/*
23
+
24
+ # This is mainly for Ray and using submodule
25
+ */**/.git
26
+
27
+ ### Python template
28
+ # Byte-compiled / optimized / DLL files
29
+ __pycache__/
30
+ *.py[cod]
31
+ *$py.class
32
+
33
+ # C extensions
34
+ *.so
35
+
36
+ # pypa/gh-action-pypi-publish caches its Docker action manifest here.
37
+ .github/.tmp/
38
+
39
+ # Distribution / packaging
40
+ .Python
41
+ build/
42
+ develop-eggs/
43
+ dist/
44
+ downloads/
45
+ eggs/
46
+ .eggs/
47
+ lib64/
48
+ parts/
49
+ sdist/
50
+ local_store/
51
+ wheels/
52
+ share/python-wheels/
53
+ *.egg-info/
54
+ .installed.cfg
55
+ *.egg
56
+ MANIFEST
57
+
58
+ # PyInstaller
59
+ # Usually these files are written by a python script from a template
60
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
61
+ *.manifest
62
+ *.spec
63
+
64
+ # Installer logs
65
+ pip-log.txt
66
+ pip-delete-this-directory.txt
67
+
68
+ # Unit test / coverage reports
69
+ htmlcov/
70
+ .tox/
71
+ .nox/
72
+ .coverage
73
+ .coverage.*
74
+ .cache
75
+ nosetests.xml
76
+ coverage.xml
77
+ *.cover
78
+ *.py,cover
79
+ .hypothesis/
80
+ .pytest_cache/
81
+ cover/
82
+
83
+ # Translations
84
+ *.mo
85
+ *.pot
86
+
87
+ # Django stuff:
88
+ *.log
89
+ local_settings.py
90
+ db.sqlite3
91
+ db.sqlite3-journal
92
+
93
+ # Flask stuff:
94
+ instance/
95
+ .webassets-cache
96
+
97
+ # Scrapy stuff:
98
+ .scrapy
99
+
100
+ # Sphinx documentation
101
+ docs/_build/
102
+
103
+ # PyBuilder
104
+ .pybuilder/
105
+ target/
106
+
107
+ # Jupyter Notebook
108
+ .ipynb_checkpoints
109
+
110
+ # IPython
111
+ profile_default/
112
+ ipython_config.py
113
+
114
+ # pyenv
115
+ # For a library or package, you might want to ignore these files since the code is
116
+ # intended to run in multiple environments; otherwise, check them in:
117
+ # .python-version
118
+
119
+ # pipenv
120
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
121
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
122
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
123
+ # install all needed dependencies.
124
+ #Pipfile.lock
125
+
126
+ # poetry
127
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
128
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
129
+ # commonly ignored for libraries.
130
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
131
+ #poetry.lock
132
+
133
+ # pdm
134
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
135
+ #pdm.lock
136
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
137
+ # in version control.
138
+ # https://pdm.fming.dev/#use-with-ide
139
+ .pdm.toml
140
+
141
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
142
+ __pypackages__/
143
+
144
+ # Celery stuff
145
+ celerybeat-schedule
146
+ celerybeat.pid
147
+
148
+ # SageMath parsed files
149
+ *.sage.py
150
+
151
+ # Environments
152
+ .env
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # Ruff
179
+ .ruff_cache/
180
+
181
+ # pytype static type analyzer
182
+ .pytype/
183
+
184
+ # Cython debug symbols
185
+ cython_debug/
186
+
187
+ # PyCharm
188
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
189
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
190
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
191
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
192
+ .idea/
193
+ *.iml
194
+
195
+ # IDE Config
196
+ .vscode/
197
+
198
+ # Mac OS
199
+ .DS_Store
200
+
201
+ # Secrets
202
+ credentials.json
203
+ marin/crawl/bigquery-gcs-key.json
204
+
205
+ # Archive
206
+ archive/
207
+
208
+ # Caches and Outputs
209
+ !/scripts/web/output/
210
+ !/output/
211
+
212
+ # csv
213
+ *.csv
214
+
215
+ # wandb logs
216
+ wandb
217
+ artifacts
218
+
219
+ # Ignore generated credentials from google-github-actions/auth
220
+ gha-creds-*.json
221
+
222
+ .aider*
223
+ .git/*
224
+
225
+ *.jsonl
226
+ **/*.jsonl
227
+ # Echo's judged search corpus is source, not a run artifact.
228
+ !infra/marina/apps/echo/benchmarks/*.jsonl
229
+ scr/*
230
+ .weaver/
231
+
232
+ # Local host Marin config
233
+ .marin.yaml
234
+
235
+ /scratch
236
+
237
+ .forge
238
+ .claude/*
239
+ !.claude/skills
240
+ !.claude/agents
241
+ !.claude/settings.json
242
+ .agents/tmp/
243
+ .codex
244
+ .entire
245
+ .beads
246
+
247
+ .worktrees
248
+ .obsidian
249
+ .cw_env
250
+ .localbin/
251
+
252
+ # Stamped into built artifacts by lib/iris/hatch_build.py; never tracked.
253
+ lib/iris/src/iris/_build_info.py
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.5
2
+ Name: marin-shellbox
3
+ Version: 0.2.127.dev36173526241
4
+ Summary: Shell execution machines with local and remote backends
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: httpx>=0.27
7
+ Provides-Extra: daytona
8
+ Requires-Dist: daytona==0.200.2; extra == 'daytona'
9
+ Provides-Extra: gvisor
10
+ Provides-Extra: iris
11
+ Requires-Dist: marin-iris==0.2.127.dev36173526241; extra == 'iris'
12
+ Provides-Extra: qemu
13
+ Requires-Dist: quicksand-qemu<0.6,>=0.5.12; extra == 'qemu'
14
+ Provides-Extra: shellsim
15
+ Requires-Dist: shellsim<0.2,>=0.1.17; extra == 'shellsim'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Marin Shellbox (0.1)
19
+
20
+ `marin-shellbox` provides three Harbor import paths:
21
+
22
+ - `shellbox.agent:BashAgent`: an **external** agent. It runs in the Harbor process and calls an OpenAI-compatible endpoint from that process. Its `Bash` tool uses the selected environment's persistent shell.
23
+ - `shellbox.backends.qemu.environment:QemuEnvironment`: one persistent QEMU system guest per Harbor trial. It uses KVM when the process can access `/dev/kvm` and QEMU can initialize it, then falls back to software emulation (TCG). Running a prebuilt bundle needs no Docker daemon, user namespace, or guest network.
24
+ - `shellbox.backends.shellsim.environment:ShellSimEnvironment`: one in-memory [ShellSim](https://pypi.org/project/shellsim/) instance per trial. It uses ShellSim's built-in commands and ignores the task's Docker image or Dockerfile.
25
+
26
+ Install the backend dependencies you need:
27
+
28
+ ```sh
29
+ uv pip install 'marin-shellbox[shellsim]'
30
+ uv pip install 'marin-shellbox[qemu]'
31
+ uv pip install --prerelease allow 'marin-shellbox[daytona]'
32
+ uv pip install --prerelease allow 'marin-shellbox[iris]'
33
+ uv pip install 'marin-shellbox[gvisor]'
34
+ ```
35
+
36
+ The base wheel contains the Harbor adapter, machine API, and guest source. Harbor is the host application: install this wheel into an environment that already has the [Marin Harbor fork](../../config/external/harbor/pyproject.toml), whose `harbor==0.8.1` distribution is not published on PyPI. The machine API can be used without Harbor. The `shellsim` extra requires ShellSim 0.1.17 or newer within the 0.1 series. The `qemu` extra requires [quicksand-qemu](https://pypi.org/project/quicksand-qemu/) 0.5.12 or newer within the 0.5 series; it bundles QEMU and its shared libraries in a platform wheel. A QEMU guest bundle still needs a Linux amd64 kernel, static BusyBox, and `bios-microvm.bin`; OCI staging also needs Skopeo, `umoci`, `mkfs.ext4`, and `cpio`. These inputs are explicit until we have a portable, licensed guest-runtime wheel. The tested quicksand-qemu Linux wheel requires glibc 2.38 or newer; use a compatible host QEMU on older clusters.
37
+
38
+ ## Machine API
39
+
40
+ The package provides a Harbor-independent machine interface. QEMU and Docker factories accept a registry reference, a local Dockerfile, or a `PreparedImage`. QEMU also accepts a prebuilt guest bundle; Docker accepts a local image. `ShellSimMachineFactory` accepts only `ShellSimBuiltins()`. Daytona and Iris accept registry image references. Local gVisor accepts the same images as Docker. Each `create` returns a fresh machine with a persistent writable filesystem. `run` returns bytes, exit status, and output truncation flags. `upload`, `download`, and `close` complete the common interface.
41
+
42
+ Shared contracts and OCI image preparation live at the package root. Backend machines live under `shellbox.backends.{qemu,shellsim,docker,gvisor,daytona,iris}`. QEMU and ShellSim have Harbor environment adapters. The three new backends expose the machine contract; a Harbor environment adapter and persistent Bash support remain separate work.
43
+
44
+ | Backend | Image source | Network policy | Host requirement |
45
+ | --- | --- | --- | --- |
46
+ | Local gVisor | Docker image, registry image, Dockerfile, prepared OCI image | allow or deny | Docker daemon with `runsc` registered; Skopeo for image preparation |
47
+ | Daytona | Registry image reference | allow or deny | Daytona credentials and service access |
48
+ | Iris | Registry image reference | allow only | Iris controller and workers with gVisor profile support |
49
+
50
+ The `gvisor` extra adds no Python dependency: a wheel cannot register a Docker runtime on the host. The `daytona` extra pins the SDK used by the Harbor fork. Its OpenTelemetry dependencies include prereleases, so installing it needs `--prerelease allow`. The `iris` extra installs `marin-iris`; its current PyPI releases and related Marin dependencies also need `--prerelease allow`. A local checkout can supply Iris as a workspace dependency instead. Iris uses its existing `CONTAINER_PROFILE_GVISOR` job profile and `ExecInContainer` RPC. It does not launch a nested `runsc` process or actor. The Iris job network is not configurable per job, so `NetworkPolicy.DENY` fails at creation. Iris file transfer requires the task image's `/bin/sh`, `base64`, `tar`, `head`, `tail`, and `wc` utilities. Daytona uses the sandbox filesystem API for file transfer and requires `/bin/sh`, `tar`, `head`, and `wc` for commands and directory transfer. Daytona sandboxes and Iris jobs have a default six-hour lifetime to limit leaks when the harness exits without closing them.
51
+
52
+ ```python
53
+ from shellbox.backends.daytona.machine import DaytonaMachineFactory
54
+ from shellbox.image import RegistryImage
55
+ from shellbox.machine import Command, MachineSpec
56
+
57
+ machine = await DaytonaMachineFactory().create(MachineSpec(source=RegistryImage("ubuntu:24.04")))
58
+ try:
59
+ result = await machine.run(Command(("/bin/sh", "-c", "printf hello")))
60
+ finally:
61
+ await machine.close()
62
+ ```
63
+
64
+ Use `GvisorMachineFactory()` in place of `DockerMachineFactory()` for a local Docker daemon with `runsc` registered. Use `IrisMachineFactory(cluster="marin")` with `MachineSpec(..., network=NetworkPolicy.ALLOW)` for an Iris job. These backends provide one-shot commands and shared files. The current Harbor `BashAgent` needs a persistent `ShellSession`; it cannot use these backends directly until a session adapter is implemented.
65
+
66
+ ```python
67
+ from shellbox.machine import Command, MachineSpec, QemuBundle
68
+ from shellbox.backends.qemu.machine import QemuMachineFactory
69
+
70
+ machine = await QemuMachineFactory().create(MachineSpec(source=QemuBundle(bundle_path)))
71
+ try:
72
+ result = await machine.run(Command(("/bin/sh", "-c", "echo hello")))
73
+ finally:
74
+ await machine.close()
75
+ ```
76
+
77
+ For Docker, use `DockerMachineFactory()` and `DockerImage("image:tag")` in the same `MachineSpec`. This form requires a locally available image. Backends reject unsupported source types; QEMU also rejects guest networking.
78
+
79
+ To prepare a registry image, use a standard image reference without `https://`:
80
+
81
+ ```python
82
+ from pathlib import Path
83
+ from shellbox.backends.qemu.image import QemuAssets
84
+ from shellbox.image import RegistryImage
85
+ from shellbox.machine import MachineSpec
86
+ from shellbox.backends.qemu.machine import QemuMachineFactory
87
+
88
+ spec = MachineSpec(source=RegistryImage("ghcr.io/astral-sh/uv:alpine3.21"))
89
+ machine = await QemuMachineFactory(
90
+ assets=QemuAssets(
91
+ qemu=Path("/opt/qemu/qemu-system-x86_64"),
92
+ kernel=Path("/opt/qemu/vmlinuz"),
93
+ busybox=Path("/opt/qemu/static-busybox"),
94
+ firmware=Path("/opt/qemu/firmware"),
95
+ libraries=Path("/opt/qemu/lib"),
96
+ umoci=Path("/usr/bin/umoci"),
97
+ disk_size_mb=512,
98
+ runtime_id="pinned-runtime-v1",
99
+ ),
100
+ image_cache=Path("/var/cache/marin-shellbox/oci"),
101
+ bundle_cache=Path("/var/cache/marin-shellbox/bundles"),
102
+ skopeo=Path("/usr/bin/skopeo"),
103
+ ).create(spec)
104
+ ```
105
+
106
+ The factory uses a process-local `ImageCache` to call `prepare_image` once per source. Preparation selects `linux/amd64`, copies it to a local OCI layout, records its selected manifest digest, and caches the layout by that digest. It accepts Docker Hub references such as `docker.io/library/ubuntu:24.04` and GHCR references. A registry reference is resolved once per Python process; a later process resolves it again. Use `@sha256:...` to fix the source version across processes. For a private registry, pass a Skopeo `authfile` path to the factory. The default Skopeo policy accepts unsigned images over verified TLS; pass `policy` to require a signature policy. Credentials are used only during preparation.
107
+
108
+ `DockerfileSource(context=..., dockerfile=...)` is the other source type. It requires a local Docker builder, builds for `linux/amd64`, and copies the result into the OCI cache. The cache fingerprints the Dockerfile and every path in its build context on each call. An unchanged context builds once per Python process; a changed context triggers a new build. A base image referenced by a mutable tag can change without a context edit, so pin `FROM` by digest for repeatable builds. `DockerMachineFactory` loads a prepared layout under a digest-derived local tag when needed. `QemuMachineFactory` needs `QemuAssets` and a `bundle_cache` to stage the same prepared layout into a guest bundle on first use. `tests/manual/check_prepared_image.py` checks both backends against one public GHCR image. The QEMU bundle cache key includes the prepared image digest, disk size, and caller-supplied `runtime_id`; change `runtime_id` when QEMU, kernel, BusyBox, firmware, or libraries change.
109
+
110
+ `run` remains noninteractive for setup and verification. It starts a new process for each call, while files persist until `close`. Its serial protocol buffers command output in the guest and streams it to the host in bounded chunks; a timeout destroys the machine. `QemuMachine.open_shell()` opens a separate PTY-backed Bash session for agent commands. `tests/manual/check_machine.py` checks one-shot command results, persistent files, and file transfer. `tests/manual/check_shell.py` checks shell state, input, interruption, jobs, bounded output, and reset.
111
+
112
+ The wheel contains Python integration, guest init code, and the source for a small PTY helper. Staging compiles that helper with `cc -static` and `-lutil`; the runtime host does not need a compiler when it uses a prebuilt bundle. The runtime host needs QEMU, its libraries, firmware, a Linux kernel, and a **static** BusyBox staged into a bundle. A minimal guest can be staged with:
113
+
114
+ ```sh
115
+ shellbox-stage \
116
+ --qemu /path/to/qemu-system-x86_64 \
117
+ --kernel /path/to/vmlinuz \
118
+ --busybox /path/to/static-busybox \
119
+ --firmware /path/to/qemu-firmware \
120
+ --libraries /path/to/qemu-libraries \
121
+ --output /opt/marin-shellbox/guest
122
+ ```
123
+
124
+ The `firmware` directory must contain `bios-microvm.bin` from SeaBIOS. The `libraries` directory must contain every shared library required by the QEMU executable on the runtime host. The bundle can be prepared on a build machine and copied to the runtime host.
125
+ Re-stage bundles built with an earlier prototype when updating this package; the guest and host serial protocols must match.
126
+
127
+ With the `qemu` extra, `quicksand_qemu.get_bin_dir()` gives the QEMU executable at `bin/qemu-system-x86_64` and its libraries at `bin/lib`. Pass those paths as `--qemu` and `--libraries` when staging. The current quicksand-qemu wheel does not supply `bios-microvm.bin`; pass a firmware directory that does. Its QEMU modules are loaded from the staged `lib/qemu` directory.
128
+
129
+ ## ShellSim backend
130
+
131
+ ShellSim requires no QEMU assets, Docker daemon, image pull, or build step. The `shellsim` extra accepts ShellSim releases from 0.1.17 to before 0.2. Select it in a Harbor job:
132
+
133
+ ```yaml
134
+ environment:
135
+ import_path: shellbox.backends.shellsim.environment:ShellSimEnvironment
136
+ agents:
137
+ - import_path: shellbox.agent:BashAgent
138
+ model_name: openai/local-model
139
+ kwargs:
140
+ base_url: http://127.0.0.1:8000/v1
141
+ ```
142
+
143
+ The task's `docker_image` and `environment/Dockerfile` are ignored. Files already in `environment/` are copied to the simulated workdir, which defaults to `/workspace`; Dockerfile instructions are never applied. ShellSim's trusted import accepts regular files and rejects symlinks and special files. ShellSim provides its own command set and in-memory filesystem. Use this backend only for tasks whose setup, agent commands, and verifier use [supported ShellSim facilities](https://pypi.org/project/shellsim/). Unsupported commands fail visibly. For direct machine use, pass `MachineSpec(source=ShellSimBuiltins())` to `ShellSimMachineFactory.create()`.
144
+
145
+ `BashAgent` offers only `Bash` with ShellSim. Each action completes before returning; interactive input and interruption are unavailable. Cwd, exports, functions, and virtual files persist across Bash actions. ShellSim currently also carries shell functions and exports into Harbor setup and verifier actions on the same instance. This differs from QEMU's separate verifier shell and can affect rewards. [ShellSim issue #89](https://github.com/rjpower/shellsim/issues/89) tracks an isolated shell action that shares the virtual filesystem. Treat ShellSim verifier results as exploratory until that isolation is available. The backend has no host filesystem or network access from simulated commands; the trusted harness copies task files into the virtual filesystem.
146
+
147
+ The default ShellSim limits per trial are 10 billion CPU units, 256 MiB modeled memory, 256 MiB virtual disk, and 128 MiB cumulative output. `cpu_limit`, `memory_mb`, `disk_limit_bytes`, and `output_limit_bytes` configure them. An action that exhausts a ShellSim resource terminates the trial. These are ShellSim fuel and working-set limits, not host CPU or RSS limits. Tasks that request internet are rejected unless `network_policy: deny` explicitly runs them offline.
148
+
149
+ Run the local Harbor smoke without QEMU:
150
+
151
+ ```sh
152
+ uv run --no-project --python config/external/harbor/.venv/bin/python \
153
+ lib/shellbox/tests/harbor_smoke.py shellsim /tmp/shellsim-smoke-jobs
154
+ ```
155
+
156
+ ## OCI image ingestion
157
+
158
+ On a build machine, copy a prebuilt image into an OCI layout, then stage it. For example:
159
+
160
+ ```sh
161
+ skopeo copy docker://docker.io/library/ubuntu:24.04 oci:/tmp/ubuntu-oci:ubuntu
162
+ shellbox-stage \
163
+ --qemu /path/to/qemu-system-x86_64 \
164
+ --kernel /path/to/vmlinuz \
165
+ --busybox /path/to/static-busybox \
166
+ --firmware /path/to/qemu-firmware \
167
+ --libraries /path/to/qemu-libraries \
168
+ --oci-layout /tmp/ubuntu-oci \
169
+ --oci-tag ubuntu \
170
+ --umoci /path/to/umoci \
171
+ --disk-size-mb 512 \
172
+ --output /opt/marin-shellbox/ubuntu
173
+ ```
174
+
175
+ Staging uses `umoci unpack --rootless` to apply OCI layers and whiteouts, then builds an ext4 guest disk. It restores file UID/GID values from the OCI layers, which rootless unpacking cannot retain on the host. Each Harbor trial gets its own writable disk copy. The bundle records the unpacked manifest digest, image environment, and working directory. The guest uses GNU Bash when the image supplies `/bin/bash`; otherwise it uses `/bin/sh`. OCI images must be Linux amd64, provide `/bin/sh`, and specify root as their default user. Entrypoint and CMD are not run because Harbor drives commands directly.
176
+
177
+ Harbor can prepare a task's `environment/Dockerfile` or `docker_image` automatically when configured as shown below. For prebuilt bundles, pass `--task-dockerfile` when staging a Dockerfile image or `--image-reference` when staging a registry image. Harbor checks the recorded Dockerfile SHA-256 or image reference before starting QEMU. These checks catch mismatched task inputs; they do not prove which Dockerfile built an OCI image. Compose tasks remain unsupported.
178
+
179
+ This staging step requires `umoci`, `mkfs.ext4` built with libarchive support, and `cpio` on the build machine. `mkfs.ext4 -d` reads a tarball so the guest filesystem retains OCI file ownership. A runtime host using only prebuilt bundles needs none of these tools. For repeatable builds, pin the source image by digest before copying it to the OCI layout. See the [OCI image layout](https://specs.opencontainers.org/image-spec/image-layout/) and [umoci unpack](https://umo.ci/quick-start/workflow/) documentation for the source format and layer unpacking.
180
+
181
+ Use the import paths in a Harbor job config:
182
+
183
+ ```yaml
184
+ environment:
185
+ import_path: shellbox.backends.qemu.environment:QemuEnvironment
186
+ kwargs:
187
+ guest_bundle: /opt/marin-shellbox/bash-image
188
+ agents:
189
+ - import_path: shellbox.agent:BashAgent
190
+ model_name: openai/local-model
191
+ kwargs:
192
+ base_url: http://127.0.0.1:8000/v1
193
+ ```
194
+
195
+ To use the image named by a task's `docker_image` or build its `environment/Dockerfile`, omit `guest_bundle` and configure image preparation:
196
+
197
+ ```yaml
198
+ environment:
199
+ import_path: shellbox.backends.qemu.environment:QemuEnvironment
200
+ kwargs:
201
+ image_cache: /var/cache/marin-shellbox
202
+ skopeo: /usr/bin/skopeo
203
+ qemu_assets:
204
+ qemu: /opt/qemu/qemu-system-x86_64
205
+ kernel: /opt/qemu/vmlinuz
206
+ busybox: /opt/qemu/static-busybox
207
+ firmware: /opt/qemu/firmware
208
+ libraries: /opt/qemu/lib
209
+ umoci: /usr/bin/umoci
210
+ disk_size_mb: 512
211
+ runtime_id: pinned-runtime-v1
212
+ agents:
213
+ - import_path: shellbox.agent:BashAgent
214
+ model_name: openai/local-model
215
+ kwargs:
216
+ base_url: http://127.0.0.1:8000/v1
217
+ ```
218
+
219
+ The task must specify exactly one source: `docker_image` or `environment/Dockerfile`. The first trial in a Harbor process prepares the image. Later trials with the same source reuse it; concurrent trials wait for the same preparation. A new Harbor process resolves registry references and builds Dockerfiles again, while the OCI and QEMU bundle caches on disk reuse matching digests. For fully offline trials, prepare the image ahead of time and use a prebuilt guest bundle. Image preparation requires Skopeo, `umoci`, `mkfs.ext4`, and `cpio` on the host. Dockerfile preparation also requires a working Docker builder. `registry_authfile` and `signature_policy` are optional environment kwargs for private registries and signed images; private registry authentication has not been tested end to end.
220
+
221
+ The 0.1 QEMU extra supplies the emulator but does not supply the guest kernel, BusyBox, firmware, or OCI staging tools, so task-image preparation needs the explicit paths above. A prebuilt `guest_bundle` needs no staging tools on the trial host. Skopeo is available through Linux distribution packages; its upstream project does not publish a portable standalone release binary and recommends distribution packages or its container image. We therefore leave Skopeo outside the wheel for now.
222
+
223
+ `BashAgent` runs in Harbor even though Harbor's config `mode` stays at its default `container`: that field selects a factory path and does not move a custom `BaseAgent` into the guest. The endpoint above is contacted from Harbor, not from QEMU. `BashAgent` requires an image with executable `/bin/bash` and a newly staged PTY-enabled bundle; a minimal BusyBox bundle still supports direct one-shot machine commands but cannot run this agent.
224
+
225
+ The guest has no network. By default, the environment rejects tasks that declare internet access. Set `environment.kwargs.network_policy: deny` to explicitly run such a task offline; commands that actually require internet will still fail. The TaskTrove fixture below uses this override because its task config leaves `allow_internet` at Harbor's default.
226
+ The guest defaults to 512 MiB RAM; set `environment.kwargs.guest_memory_mb` to change that limit.
227
+
228
+ Acceleration defaults to `auto`. This selects KVM when `/dev/kvm` is readable and writable by the Harbor process; QEMU falls back to TCG if KVM initialization fails. Set `environment.kwargs.acceleration: kvm` to require KVM or `tcg` to force software emulation. The environment logs the accelerator QEMU actually selected after startup. A host may have `/dev/kvm` but deny access to the current user; `auto` uses TCG in that case. The Python package cannot grant device access.
229
+
230
+ The local smoke test uses a deterministic OpenAI-compatible server and a Harbor task. Supply a PTY-enabled OCI bundle with `/bin/bash`:
231
+
232
+ ```sh
233
+ uv run --no-project --python config/external/harbor/.venv/bin/python \
234
+ lib/shellbox/tests/harbor_smoke.py /opt/marin-shellbox/bash-image /tmp/qemu-smoke-jobs
235
+ ```
236
+
237
+ For a task with `docker_image` or `environment/Dockerfile`, use `task-image` and supply the QEMU asset paths in a JSON file with the same keys as `qemu_assets` above:
238
+
239
+ ```sh
240
+ uv run --no-project --python config/external/harbor/.venv/bin/python \
241
+ lib/shellbox/tests/harbor_smoke.py task-image /tmp/qemu-image-smoke-jobs \
242
+ --task-path /path/to/task \
243
+ --image-cache /var/cache/marin-shellbox \
244
+ --skopeo /usr/bin/skopeo \
245
+ --qemu-assets-json /path/to/qemu-assets.json
246
+ ```
247
+
248
+ The TaskTrove `nl2bash` fixture can be built on a Docker-capable build machine, copied to an OCI layout, and staged with `--task-dockerfile` pointing to its extracted `environment/Dockerfile`. Its unchanged Harbor task passes with:
249
+
250
+ ```sh
251
+ uv run --no-project --python config/external/harbor/.venv/bin/python \
252
+ lib/shellbox/tests/harbor_smoke.py /opt/marin-shellbox/nl2bash /tmp/nl2bash-jobs \
253
+ --task-path /path/to/extracted/nl2bash \
254
+ --command-file lib/shellbox/tests/manual/tasktrove_nl2bash_command.sh \
255
+ --network-policy deny
256
+ ```
257
+
258
+ This example uses a deterministic fake model to issue one Bash tool call. It exercises Harbor's external agent, task setup upload, QEMU command execution, and the real TaskTrove verifier; it does not evaluate model quality.
259
+
260
+ `tests/harbor_smoke.py --commands-json /path/to/commands.json` sends successive Bash calls from a JSON list. For example, the first call can run `cd /tmp; export ANSWER='hello from qemu'`, and the second can use `$ANSWER` and `$PWD`. `tests/manual/check_shell.py /opt/marin-shellbox/bash-image` checks interactive behavior without Harbor.
261
+
262
+ `benchmarks/tasktrove/sample.py` extracts a pinned 100-task TaskTrove Clean sample into a local directory. It needs `fsspec` and `pyarrow`:
263
+
264
+ ```sh
265
+ uv run --no-project python lib/shellbox/benchmarks/tasktrove/sample.py /tmp/marin-shellbox-benchmark
266
+ ```
267
+
268
+ Build one Docker image per `dockerfile_id` in `sample.json`, copy each image to an OCI layout, and stage it with its `environment/Dockerfile`. Name the images `tasktrove-clean-qemu:<dockerfile_id>` and the bundles `/tmp/marin-shellbox-benchmark/bundles/<dockerfile_id>`. Then run:
269
+
270
+ ```sh
271
+ uv run --no-project --python config/external/harbor/.venv/bin/python \
272
+ lib/shellbox/benchmarks/tasktrove/compare.py /tmp/marin-shellbox-benchmark --concurrency 2
273
+ ```
274
+
275
+ The comparison runs the same empty and available oracle checks in offline Docker containers and fresh QEMU guests. It records one JSON result per check and fails on a verdict mismatch.
276
+
277
+ ## Current support boundary
278
+
279
+ The guest has persistent files for a trial. A minimal bundle uses BusyBox `ash` for one-shot commands; an OCI bundle uses its image shell. Harbor setup and verifier commands still run as root in separate shell processes. The agent's `Bash` calls use one interactive `/bin/bash` per trial, so cwd, exports, functions, and jobs persist across calls. The verifier does not inherit that shell state. The guest has no network interface.
280
+
281
+ The QEMU image must contain the task tools and verifier dependencies needed without guest internet. `Bash` runs through a guest PTY on a separate virtio-serial channel. One `Bash` tool returns JSON with `output`, `status`, `exit_code`, and `truncated`. Pass `command` to start a command; omit it to read a running command, pass `input` to send text, or set `signal` to `interrupt` to send Ctrl-C. Use at most one of `command`, `input`, and `signal` per call. `Bash` waits at most 30 seconds per call; a longer command remains active for later reads or interruption. PTY stdout and stderr are combined. A tool result retains at most 128 KiB and marks excess output as truncated; excess output cannot be retrieved later. Agents can redirect large output to a file and inspect a smaller excerpt. `exit` or shell failure resets Bash while files remain. The one-shot serial protocol still buffers setup and verifier output in guest files. The bundle copies a full ext4 disk for each trial; this can be costly for large images. Extended attributes, Linux capabilities, device nodes outside `/dev`, and volume semantics have not been checked against an OCI runtime.
282
+
283
+ The guest is isolated by QEMU's emulated machine boundary, but this prototype has not been security audited. It is intended for nonhostile agents on an already controlled compute node.