Skip to content

Repository files navigation

code-execution-sandbox

An MCP server that gives an LLM one tool, execute_code: run a short bash or python program in an isolated, network-disabled Docker container and get back its stdout, stderr, and exit code. It's a standalone Go service — a sibling project to Miranda, not a package inside it — meant to be wired into Miranda (or any other MCP client) as one more tool source.

MCP client (Miranda, Claude, ...) <--Streamable HTTP--> code-execution-sandbox <--docker run--> ephemeral sandbox container

The server itself runs host-native — a single static Go binary, no Docker for the process itself — and talks to the local Docker daemon to spawn a fresh, hardened container per call. See CLAUDE.md for the full security model and why this isn't "just run it in Docker too."

Building

Requires Go 1.25+ (the module pins go 1.25.0; with GOTOOLCHAIN=auto — the default — go build fetches a matching toolchain automatically) and a working Docker install (docker CLI + daemon, docker compose).

go build -o code-execution-sandbox ./cmd/code-execution-sandbox
# or
make build

Cross-compiling for another host (e.g. the deploy target) is just:

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o code-execution-sandbox-linux-amd64 ./cmd/code-execution-sandbox

Building the sandbox runtime image

Every execute_code call runs against a prebuilt image (docker/sandbox.Dockerfile), never a base image pulled at request time:

docker compose build sandbox
# or
make docker-build

Running

cp config/config.yaml.dist config/config.yaml   # optional — every field has a default
cp .env.example .env
# fill in SANDBOX_MCP_TOKEN in .env

make docker-build   # build the sandbox runtime image, once
make run            # build + run the server

The server listens on :8788 by default: GET /healthz (unauthenticated; also checks the local Docker daemon is reachable) and POST /mcp (the MCP endpoint, requires Authorization: Bearer <SANDBOX_MCP_TOKEN>).

Testing

make test         # go test ./... -race — unit tests everywhere;
                   # internal/sandbox's and test/integration's
                   # Docker-backed tests skip automatically if the
                   # sandbox image hasn't been built yet
make docker-test   # docker-build + test — the full suite, nothing skipped
make lint          # golangci-lint run ./...
make fmt           # gofmt + goimports
make check         # fmt + lint + test — run this before committing

make lint/make check need golangci-lint and goimports on PATHmake tools installs both.

Deploying

./scripts/deploy.sh

Cross-compiles for linux/amd64, ships the binary plus the Docker build context over SSH, rebuilds the sandbox runtime image on the server, and restarts the systemd --user service that runs it. See .claude/skills/deploy/SKILL.md for the full breakdown.

Configuration

Copy config/config.yaml.dist to config/config.yaml and edit it — every field has a built-in default (see internal/config.Default()), so you only need to override what differs:

http_addr: ":8788"
auth_token_env: "SANDBOX_MCP_TOKEN"
logging:
  level: "info"
sandbox:
  image: "code-execution-sandbox-runtime:latest"
  max_concurrent: 4
  default_timeout_seconds: 10
  max_timeout_seconds: 60
  max_code_bytes: 65536
  max_output_bytes: 1048576
  memory_limit: "1g"
  cpu_limit: "1"
  pids_limit: 128

The auth token itself is never put in config.yamlauth_token_env names an environment variable to read it from at startup. For local development, copy .env.example to .env and fill it in (loaded via internal/envfile; a variable already set in the real environment always wins). The server refuses to start if that variable is unset or empty.

Debug logging

Logs go to stdout as log/slog text output (same as Miranda), which lands in the journal under systemd --user — see .claude/skills/deploy. Set logging.level: "debug" to log every incoming execute_code call (language, timeout, and the full submitted code, not just its size) plus its result (exit code, duration, timed-out flag). This is deliberately more verbose than Miranda's usual debug-log convention of summarized fields — seeing exactly what a client ran is the point — so leave it at "info" unless you're actively diagnosing something.

Wiring into Miranda

Add an entry to Miranda's config/mcp.yaml:

mcp:
  servers:
    - name: code_exec
      url: "http://192.168.1.50:8788/mcp"
      token_env: "SANDBOX_MCP_TOKEN"
      enabled: true

and set SANDBOX_MCP_TOKEN in Miranda's own .env to the same value configured here. Miranda will then expose the tool as code_exec_execute_code (see Miranda's internal/mcp.Manager tool-name prefixing).

Security model

See CLAUDE.md for the full write-up. Summary: every call runs in a fresh, --rm'd container with --network=none, a read-only root filesystem (only a small noexec tmpfs at /tmp), --cap-drop=ALL, --security-opt no-new-privileges, a non-root user, and hard CPU/memory/pids/time limits. Code is piped over stdin, never placed in argv or a shell string, so there's no injection surface. Nothing persists between calls, and no host filesystem or Docker socket is ever exposed to the sandboxed process.

Project layout

cmd/code-execution-sandbox/  entrypoint: config, wiring, HTTP listen, graceful shutdown
internal/config/             Default()+YAML config
internal/envfile/            .env loader
internal/sandbox/            Executor interface + Docker-backed Runner (the security-critical part)
internal/mcpserver/          the execute_code MCP tool
internal/httpserver/         bearer-token auth + /healthz
docker/sandbox.Dockerfile    hardened runtime image (python3 + bash, curl/ffmpeg/faster-whisper, non-root)
docker-compose.yml           builds the runtime image (never `up`, see CLAUDE.md)
test/integration/            real HTTP + real MCP wire protocol, end to end

About

An MCP server that gives an LLM one tool, miranda_sandbox_code_execution: run a short bash or python program in an isolated, network-disabled Docker container and get back its stdout, stderr, and exit code.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages