#!/usr/bin/env bash # mc-ai-docker.sh — run an mc-ai autoplay batch inside a freeze-proof Docker # container. Mirrors scripts/godot-docker.sh, applied to the simulator side. # # Why: bare `systemd-run --user` could not bound the host blast radius — a # runaway autoplay batch wedged apricot while concurrent mc-claude sessions # were running. Docker's cgroup caps (--cpus / --memory / --memory-swap / # --pids-limit) provide a hard ceiling the host cannot exceed regardless of # how many seeds the batch spawns. # # Usage: # scripts/mc-ai-docker.sh godot --version # scripts/mc-ai-docker.sh bash tools/autoplay-batch.sh 1 50 /work/.local/out # # Environment: # MC_AI_IMAGE_TAG image tag to run (default: mc-ai:latest). apricot-run.sh # launcher.sh sets this to mc-ai: so the image # matches the worktree SHA being driven. # MC_AI_CPUS cgroup cpu cap (default: 4) # MC_AI_MEMORY cgroup memory cap (default: 6g) — swap is pinned equal # so the container cannot escape via swap. # MC_AI_PIDS cgroup pid cap (default: 256) # MC_AI_WORKTREE host worktree to bind-mount at /work (default: repo root # containing this script). apricot launcher sets it to # the per-run scratch worktree path. # MC_AI_OUTPUT_DIR optional host output dir; if set, bind-mounted at # /work/.local/out and made writable to uid 1000. # MC_AI_CONTAINER_NAME optional `docker run --name` value (lets the caller # trap and docker-kill on exit). # MC_AI_EXTRA_ENV space-separated VAR=value pairs forwarded to the # container via `-e`. Use this to pass through # AUTOPLAY_GODOT_BIN, AI_USE_MCTS, PARALLEL, etc. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" DOCKERFILE="$REPO_ROOT/tools/docker/Dockerfile.mc-ai" IMAGE="${MC_AI_IMAGE_TAG:-mc-ai:latest}" WORKTREE="${MC_AI_WORKTREE:-$REPO_ROOT}" CPUS="${MC_AI_CPUS:-4}" MEMORY="${MC_AI_MEMORY:-6g}" PIDS_LIMIT="${MC_AI_PIDS:-256}" ensure_docker_running() { if ! docker info >/dev/null 2>&1; then echo "ERROR: Docker daemon not reachable. Start dockerd before invoking mc-ai-docker.sh." >&2 exit 2 fi } ensure_image() { if docker image inspect "$IMAGE" >/dev/null 2>&1; then return 0 fi # `latest` is the only tag we auto-build — SHA-pinned tags must be built # explicitly by the caller (apricot-run.sh launcher does this). Failing # loudly here avoids silently building stale `latest` when the caller # forgot to pre-build the SHA tag. if [[ "$IMAGE" != "mc-ai:latest" ]]; then echo "ERROR: image $IMAGE not found and is not the auto-buildable mc-ai:latest tag." >&2 echo " Build it first with:" >&2 echo " docker build --tag $IMAGE --file $DOCKERFILE $WORKTREE" >&2 exit 2 fi echo "[mc-ai-docker] image $IMAGE not found — building from $WORKTREE..." >&2 DOCKER_BUILDKIT=1 docker build \ --tag "$IMAGE" \ --file "$DOCKERFILE" \ "$WORKTREE" } ensure_output_dir() { if [[ -z "${MC_AI_OUTPUT_DIR:-}" ]]; then return 0 fi mkdir -p "$MC_AI_OUTPUT_DIR" } main() { if [[ $# -eq 0 ]]; then echo "usage: mc-ai-docker.sh [args...]" >&2 exit 64 fi ensure_docker_running ensure_image ensure_output_dir local tty_flag="" if [[ -t 1 ]]; then tty_flag="-t" fi local extra_env_args=() if [[ -n "${MC_AI_EXTRA_ENV:-}" ]]; then # Word-split intentionally — each token is VAR=value. # shellcheck disable=SC2206 local _envs=( ${MC_AI_EXTRA_ENV} ) for kv in "${_envs[@]}"; do extra_env_args+=( -e "$kv" ) done fi local output_mount_args=() if [[ -n "${MC_AI_OUTPUT_DIR:-}" ]]; then output_mount_args=( --volume "$MC_AI_OUTPUT_DIR:/work/.local/out" ) fi local name_args=() if [[ -n "${MC_AI_CONTAINER_NAME:-}" ]]; then name_args=( --name "$MC_AI_CONTAINER_NAME" ) fi # --init reaps zombies (Godot is signal-noisy under headless timeouts). # --cap-drop=ALL + --security-opt=no-new-privileges is the no-privileged # posture the brief asked for. docker run \ --rm \ -i $tty_flag \ --init \ --cap-drop=ALL \ --security-opt no-new-privileges \ --cpus "$CPUS" \ --memory "$MEMORY" \ --memory-swap "$MEMORY" \ --pids-limit "$PIDS_LIMIT" \ "${name_args[@]}" \ --volume "$WORKTREE:/work" \ "${output_mount_args[@]}" \ "${extra_env_args[@]}" \ --workdir /work \ "$IMAGE" \ "$@" } main "$@"