The reference implementation of the sandbox contract: exec and whole-file access over HTTP, scoped to one workspace directory.
A sandbox is a live, isolated workspace you drive from the outside — create one, run commands in it, read and write its files, tear it down. The orchestrator creates, routes, and reaps sandboxes; it does not sit in the middle of your commands. Exec and files are served by the sandbox image, at the sandbox's own URL. That is what this server is.
Any image answering these three routes on the pool's port is a valid sandbox image. This one is the default.
curl -X POST http://localhost:3000/execute \
-H "Content-Type: application/json" \
-d '{"command": "python -c \"print(2+2)\"", "timeoutSeconds": 30}'{"exitCode": 0, "stdout": "4\n", "stderr": "", "durationMillis": 41, "truncated": false}command runs through $SANDBOX_SHELL -c with the workspace as its working
directory, in its own process group so a timeout takes the whole process tree
with it.
- Serialized. One sandbox is one seat — a second
/executeagainst a busy sandbox is409, because concurrent execs would race on a shared filesystem. timeoutSecondsdefaults to 60.0means run until it exits. A command killed by the timeout reportsexitCode: 124, thetimeout(1)convention.- Output is capped at 1 MiB per stream, past which
truncatedistrue. Beyond that, write to a file and read it back through/files.
curl -X PUT http://localhost:3000/files/main.py --data-binary @main.py # 204
curl http://localhost:3000/files/out.json # the bytes
curl http://localhost:3000/files/ # JSON listing
curl -X DELETE http://localhost:3000/files/out.json # 204Whole-file read, write, and remove, relative to the workspace. PUT creates
parent directories and truncates an existing file. GET on a directory lists it
as {"entries": [{"name", "size", "isDir"}]}. DELETE removes a directory
tree. Absolute paths and .. are 400; a missing path is 404.
/files is a convenience, not a security boundary — it is scoped to the
workspace, but /execute runs with the container's full filesystem reach
either way. The boundary is the container and its
isolation tier.
For anything bulkier than a handful of files, use the orchestrator's
artifacts at create time — they are materialized into the workspace before the
sandbox reports ready.
{"status": "ok"} once the server is listening. This is the pool's probe
subject.
| Variable | Default | |
|---|---|---|
SANDBOX_WORKSPACE |
/workspace |
working directory for exec, root for /files |
SANDBOX_PORT |
3000 |
listen port |
SANDBOX_SHELL |
/bin/sh |
shell /execute runs commands through |
docker run -p 3000:3000 ghcr.io/open-runtimes/sandbox:latestThe image is Alpine plus the server — busybox is the shell /execute runs
commands through, and it runs as an unprivileged user with /workspace as its
working directory. Base images are pinned by digest.
Locally, either build it or take a binary from a release (linux and macOS, amd64 and arm64):
SANDBOX_WORKSPACE=$(mktemp -d) go run .
docker build -t sandbox .The reference image carries a shell and nothing else. A language sandbox is this binary dropped into a runtime image:
FROM ghcr.io/open-runtimes/sandbox:latest AS contract
FROM python:3.12-slim
COPY --from=contract /usr/local/bin/sandbox /usr/local/bin/sandbox
RUN useradd -m sandbox && mkdir /workspace && chown sandbox /workspace
ENV SANDBOX_WORKSPACE=/workspace SANDBOX_PORT=3000
USER sandbox
WORKDIR /workspace
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/sandbox"]Nothing about the contract is language-specific — the server only ever shells
out, so whatever is on PATH in your image is what /execute can run.
go test -race ./...
go vet ./...
golangci-lint runCI lints, tests, builds the image, and smoke-tests the contract inside it.
Pushing a v* tag publishes a multi-arch image to
ghcr.io/open-runtimes/sandbox and attaches the binaries to a GitHub release.