# Polyglot Explore container for the potion-polyglot toolkit (Potion). # # One image hosts every member repo (worker switches with `run-app `). Runtime union # across the estate: Node (dominant — 26 members, spanning the Node 14 lambdas to the Node 20 # API), Python (17 — ML pipelines, Flask services, data ETL), Terraform (5), PHP (1). # # This image only decides what run-app can BOOT. What makes a member gradable is its # harbor-tasks/raccoon-shared/Dockerfile., and a member with no inherited test suite is # still gradable via the rubric — so a member absent from this image is not "not worth grading". # Postgres is baked as cheap insurance (no member's verifier requires it). # # NOT baked (deliberately): # - (nothing yet — see the MongoDB note below) # # MongoDB IS required, and IS installable here. No member's *verifier* needs it (potion-app is # jsdom, potion-api's usable suites are sinon-mocked), but `run-app` on potion-app and potion-api # both do, and those are the two apps a worker is most likely to boot. An earlier note in this # file claimed MongoDB ships no arm64 debian-bookworm package and skipped it. That is true only of # MongoDB's *Debian* repo; the **Ubuntu jammy arm64** packages install cleanly on bookworm — # verified 2026-07-31 on this platform: mongodb-org-server 8.0.28 installs, mongod starts, and a # write round-trips. Bake it from that repo rather than demoting the estate's flagship app to # read-only. # - GPU/CUDA — the potion-ai* members load weights from a now-defunct bucket (never in git), # so they are read-and-edit here regardless. # - PHP/MySQL — potion-wp-site's first-party code (its custom theme) IS graded, through its # own hand-authored harbor image with php-cli + composer; it just doesn't boot in Explore. # # Runtimes: # - Node 14 / 16 / 18 / 20 via nvm (run-app's node selector switches per member) # - Python 3.10 via uv (agent str_replace_editor needs >=3.10; also the Python members) # - PostgreSQL baked in FROM debian:bookworm ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential git curl ca-certificates gnupg procps sudo xz-utils \ libssl-dev zlib1g-dev \ postgresql postgresql-client \ && rm -rf /var/lib/apt/lists/* # --- Node via nvm: 14 / 16 / 18 / 20 (prebuilt). Default 20 symlinked to /usr/local/bin so the # toolkit's own `node -e` (run-app/welcome read toolkit.json) always works; run-app switches PATH # per member. yarn into each version. v20.* glob (Docker RUN uses dash; nvm.sh is bash-only). --- ENV NVM_DIR=/usr/local/nvm RUN mkdir -p "$NVM_DIR" \ && curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \ && bash -c '. "$NVM_DIR/nvm.sh" \ && for v in 14 16 18 20; do nvm install "$v" && nvm use "$v" && npm install -g yarn; done \ && nvm alias default 20' \ && for b in node npm npx yarn; do ln -sf "$NVM_DIR"/versions/node/v20.*/bin/"$b" /usr/local/bin/"$b"; done # --- MongoDB 8.0 (the product DB: potion-app + potion-api both need it to BOOT) --- # From MongoDB's **Ubuntu jammy** arm64 repo, not the Debian one. MongoDB publishes no arm64 # packages for debian/bookworm (verified: no apt candidate), which is why an earlier revision of # this image skipped Mongo and left the estate's flagship app unbootable. The jammy arm64 build # installs and runs fine here — verified on this platform: mongodb-org-server 8.0.28 installs, # mongod starts, a write round-trips. `mongodb-mongosh` ships the shell so a worker can inspect # the DB. Data lives in /data/db, created here so mongod can start as root in the sandbox. RUN curl -fsSL https://pgp.mongodb.com/server-8.0.asc \ | gpg --dearmor -o /usr/share/keyrings/mongodb-8.gpg \ && echo "deb [ signed-by=/usr/share/keyrings/mongodb-8.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" \ > /etc/apt/sources.list.d/mongodb-org-8.0.list \ && apt-get update \ && apt-get install -y --no-install-recommends mongodb-org-server mongodb-mongosh \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /data/db \ && mongod --version | head -1 # --- Python via uv --- # 3.10 stays the default `python3`: it is what this estate's Python members run under. # 3.11 is installed alongside it because harness setup reads the registry with `tomllib` # (3.11+), and post-create runs under `set -e` — an image with only 3.10 fails container # creation. setup-harnesses.sh tries python3, then python3.13/3.12/3.11, so exposing the # newer one under its versioned name is enough and leaves the members' default untouched. RUN curl -fsSL https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \ && uv python install 3.10 \ && ln -sf "$(uv python find 3.10)" /usr/local/bin/python3 \ && uv python install 3.11 \ && ln -sf "$(uv python find 3.11)" /usr/local/bin/python3.11 \ && python3 --version \ && python3.11 -c "import tomllib; print('tomllib ok on', __import__('sys').version.split()[0])" # --- PostgreSQL trust auth (OVERWRITE pg_hba; Debian default `local … peer` is first-match) --- RUN PG_VERSION=$(ls /etc/postgresql) \ && printf 'local all all trust\nhost all all 127.0.0.1/32 trust\nhost all all ::1/128 trust\nhost all all 0.0.0.0/0 trust\n' > "/etc/postgresql/${PG_VERSION}/main/pg_hba.conf" \ && echo "listen_addresses='*'" >> "/etc/postgresql/${PG_VERSION}/main/postgresql.conf" # Startup: start postgres AND mongod. printf, NOT a heredoc (colima's legacy builder writes an # empty file from a Dockerfile heredoc → ENTRYPOINT "exec format error"). No single quotes in the # body. mongod is backgrounded with --fork and waited on the same way pg is, so a member's # setupCmd/startCmd never races an unready DB; its log goes to /var/log/mongod.log for triage. RUN printf '#!/bin/bash\nset -e\nPG_VERSION=$(ls /etc/postgresql)\nsudo pg_ctlcluster ${PG_VERSION} main start\nuntil pg_isready -h localhost -p 5432 -U postgres >/dev/null 2>&1; do sleep 0.5; done\nmkdir -p /data/db\nmongod --dbpath /data/db --bind_ip 127.0.0.1 --fork --logpath /var/log/mongod.log >/dev/null 2>&1 || echo "warning: mongod failed to start, see /var/log/mongod.log"\nuntil mongosh --quiet --eval "db.runCommand({ping:1})" >/dev/null 2>&1; do sleep 0.5; done\nexec "$@"\n' > /usr/local/bin/start-services.sh \ && chmod +x /usr/local/bin/start-services.sh USER root # --- Playwright + Chromium, for driving the app in a real browser ------------- # Self-contained under /opt — the member's own runtime is untouched. ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright RUN apt-get update -qq \ && apt-get install -y -qq --no-install-recommends \ xz-utils \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxrandr2 \ libasound2 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libatspi2.0-0 \ libcups2 \ libdbus-1-3 \ libgbm1 \ libnspr4 \ libnss3 \ libxkbcommon0 \ libpango-1.0-0 \ libcairo2 \ libxshmfence1 \ libx11-xcb1 \ libxcb-dri3-0 \ libdrm2 \ && rm -rf /var/lib/apt/lists/* RUN set -eux; \ arch="$(dpkg --print-architecture)"; \ case "$arch" in amd64) nodearch=x64;; arm64) nodearch=arm64;; *) echo "unsupported arch: $arch" >&2; exit 1;; esac; \ curl -fsSL "https://nodejs.org/dist/v20.19.5/node-v20.19.5-linux-${nodearch}.tar.xz" -o /tmp/pw-node.tar.xz; \ mkdir -p /opt/pw-node; \ tar -xJf /tmp/pw-node.tar.xz -C /opt/pw-node --strip-components=1; \ rm /tmp/pw-node.tar.xz; \ export npm_config_prefix=/opt/pw-node PATH="/opt/pw-node/bin:$PATH"; \ /opt/pw-node/bin/npm install -g playwright@1.56.0; \ test -d /opt/pw-node/lib/node_modules/playwright; \ /opt/pw-node/bin/node /opt/pw-node/lib/node_modules/playwright/cli.js install chromium # `pw ` runs Node with `require("playwright")` resolvable (CommonJS). RUN printf '#!/bin/sh\nNODE_PATH=/opt/pw-node/lib/node_modules exec /opt/pw-node/bin/node "$@"\n' > /usr/local/bin/pw \ && chmod +x /usr/local/bin/pw # Fail the build if Chromium cannot start. RUN printf 'const{chromium}=require("playwright");(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.setContent("

ok

");if(await p.textContent("#t")!=="ok")throw new Error("bad render");await b.close();console.log("chromium OK");})()\n' > /tmp/pw-check.js \ && pw /tmp/pw-check.js \ && rm -f /tmp/pw-check.js ENV IS_SANDBOX=1 RUN mkdir -p /root/.claude && echo '{"permissions":{"deny":["WebFetch","WebSearch"]}}' > /root/.claude/settings.json WORKDIR /workspace # Resolver for the DNS jail (.devcontainer/dns-jail-container.sh, applied by # post-start.sh); if this does not land, Explore just runs unjailed. RUN (command -v apk >/dev/null 2>&1 && apk add --no-cache dnsmasq bind-tools) \ || (apt-get update && apt-get install -y --no-install-recommends dnsmasq-base dnsutils \ && rm -rf /var/lib/apt/lists/*) \ || true ENTRYPOINT ["/usr/local/bin/start-services.sh"] CMD ["sleep", "infinity"]