otari-sandbox-container

August 10, 2026 · View on GitHub

Code-execution sandbox container — a Python REPL behind a multi-session HTTP API. Pairs with mozilla-ai/gateway to provide code_execution tool support, but the contract is plain HTTP so any client that speaks it can use the container directly.

Built images are published to Docker Hub:

mzdotai/otari-sandbox-container:latest
mzdotai/otari-sandbox-container:<sha>

Each session has its own /var/sandbox/sessions/<id>/ tree with a private workspace and Python REPL subprocess. State (variables, imports) persists across /exec calls within a session and is wiped on DELETE /sessions/{id}.

The wire shapes returned by POST /exec match Anthropic's code_execution_20250825 content blocks (code_execution_tool_result, bash_code_execution_tool_result, text_editor_code_execution_tool_result) so consumers that already parse Anthropic shapes work without translation.

Layout

sandbox/
  models.py           # Pydantic shapes (request, result blocks)
  runner.py           # Long-lived Python REPL with sentinel protocol
  exec_server.py      # FastAPI app: /sessions, /exec, /files, /health
  text_editor.py      # view/create/str_replace/insert/undo_edit handlers
tests/
Dockerfile            # python:3.12-slim base, pinned package set
Makefile              # build, test, run

Local development

make install   # uv sync
make test      # run pytest
make build     # build the Docker image (otari-sandbox-container:dev)
make run       # docker run -p 8080:8080

Pulling the published image

docker pull mzdotai/otari-sandbox-container:latest
docker run --rm -p 8080:8080 mzdotai/otari-sandbox-container:latest

API

POST   /sessions                       -> create session
GET    /sessions/{id}                  -> session metadata
POST   /sessions/{id}/exec             -> run code in session
DELETE /sessions/{id}                  -> destroy session
GET    /sessions/{id}/files            -> download a file from the workspace
GET    /sessions/{id}/files/list       -> list workspace files
GET    /health                         -> 200 if server alive

A streaming exec endpoint (SSE stdout/stderr deltas) is a planned follow-up; the current /exec is request/response only.

The full request/response schemas live in sandbox/models.py.

Observability

Telemetry export (traces, metrics, logs) is off by default — this image is pulled and run by arbitrary third parties, so it should never phone home unless an operator explicitly opts in. Structured JSON logging is available independently of telemetry export.

Env varDefaultDescription
TELEMETRY_ENABLEDfalseEnables OTLP export of traces, metrics, and logs.
TELEMETRY_SERVICE_NAMEotari-sandbox-containerOTel service.name resource attribute.
TELEMETRY_OTLP_ENDPOINThttp://localhost:4318Base OTLP/HTTP endpoint; /v1/traces, /v1/metrics, /v1/logs are appended per signal.
TELEMETRY_OTLP_HEADERS(unset)Raw OTLP exporter headers, e.g. an auth header for a hosted collector. Leave unset for collectors that don't require auth.
ENVIRONMENTproductionReported as the deployment.environment resource attribute and in JSON log lines.
LOG_FORMATtexttext or json.
LOG_LEVELINFODEBUG, INFO, WARNING, ERROR, or CRITICAL.

Enabling telemetry requires an OTLP/HTTP-compatible collector reachable at TELEMETRY_OTLP_ENDPOINT. Deploying an actual collector (as a sidecar, a cluster-level agent, or a hosted endpoint) is the responsibility of whatever deployment runs this image — this repo only emits telemetry, it doesn't provision anywhere to send it.

Security notes

  • The container runs as a non-root user (uid 1000).
  • One container hosts many isolated sessions; sessions can't see each other's workspaces.
  • /var/sandbox/sessions is intended to be mounted as tmpfs in production so session state stays ephemeral.
  • Don't expose the HTTP port to untrusted networks. There's no authentication on the API — the assumption is that the gateway (or another trusted component) is the sole client, reached over a private Docker network or similar. The gateway's docker-compose.yml binds the sandbox port to 127.0.0.1 for this reason.

License

Apache 2.0. See LICENSE.