# pi-devbox — variant image # # FROMs a base- image produced by Dockerfile.base and adds only # the variant-specific tools — currently just the pi install. Kept as a # separate file (rather than collapsed into Dockerfile.base) so future # variants (e.g. studio, studio-tex) can FROM the variant or extend # this Dockerfile with additional build args without rebuilding the # base on every pi version bump. # # Pass `--build-arg BASE_IMAGE=:base-` to select the base. # CI computes the base hash from Dockerfile.base + rootfs/ + # entrypoint*.sh and feeds it in. # # IMPORTANT: the base image sets NPM_CONFIG_PREFIX to # /home/developer/.pi/npm-global so runtime `pi install npm:...` and # `npm install -g` by the developer user lands on the named volume. # At BUILD time we want the baked binaries on /usr so they survive the # volume mount. Each `npm install -g` below therefore prefixes the # command with `NPM_CONFIG_PREFIX=/usr`. ARG BASE_IMAGE FROM ${BASE_IMAGE} ARG TARGETARCH ARG USER_NAME=developer # ── pi coding-agent + companions ───────────────────────────────────── # pi-toolkit and pi-extensions are cloned into /opt/. entrypoint-user.sh # runs each repo's install.sh on container start so symlinks land under # ~/.pi/agent/ on the named volume. # # PI_VERSION should be passed explicitly by CI as a concrete version # (resolved from `npm view @earendil-works/pi-coding-agent version`). # The default `latest` is for local dev convenience only — it has a # known cache-hit footgun in registry-cached CI builds: the resulting # build-arg string is byte-identical across builds, the layer-hash is # identical, and the registry buildcache silently reuses the layer # from whatever pi version was current when the cache was first # populated. CI MUST pass a resolved concrete version. See pi-devbox # v0.75.5b 2026-05-23 for the discovery + canonical fix. ARG PI_VERSION=latest ARG PI_TOOLKIT_REF=main ARG PI_EXTENSIONS_REF=main # Repo URLs default to the canonical gitea origin but are overridable so a # relocated/forked build can clone from a mirror or a different host # without editing this Dockerfile — same pattern as PI_FORK_REPO / # PI_OBSMEM_REPO / PI_STUDIO_REPO below. ARG PI_TOOLKIT_REPO=https://gitea.jordbo.se/joakimp/pi-toolkit.git ARG PI_EXTENSIONS_REPO=https://gitea.jordbo.se/joakimp/pi-extensions.git # pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub # under elpapi42. CI resolves these to commit SHAs to defeat the same # cache-hit footgun that affects PI_VERSION. ARG PI_FORK_REPO=https://github.com/elpapi42/pi-fork.git ARG PI_FORK_REF=master ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git ARG PI_OBSMEM_REF=master RUN set -e && \ # git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name # OR a commit SHA as $ref. Uses `git fetch + checkout FETCH_HEAD` # which (a) works with both name and SHA forms uniformly, and (b) defeats # the registry-buildcache footgun when CI passes a resolved SHA. The # earlier helper `git_clone_retry` (using `git clone --branch`) only # worked with branch names — a SHA-resolved build-arg made `git clone # --branch <40-char-SHA>` fail with "Remote branch not found". Surfaced # in pi-devbox v1.0.0-rerun (run 374) 2026-06-10 and fixed by switching # all four clones to git_fetch_ref. Both Gitea and GitHub allow fetching # arbitrary commits by default (uploadpack.allowReachableSHA1InWant). git_fetch_ref() { \ url="$1"; ref="$2"; dest="$3"; \ rm -rf "$dest"; mkdir -p "$dest"; \ git -C "$dest" init -q && git -C "$dest" remote add origin "$url" && \ for i in 1 2 3 4 5; do \ if git -C "$dest" fetch --depth 1 origin "$ref" && git -C "$dest" checkout -q FETCH_HEAD; then return 0; fi; \ echo "git fetch $url@$ref failed (attempt $i/5), retrying in $((i*5))s..."; \ sleep $((i*5)); \ done; \ return 1; \ } && \ if [ "${PI_VERSION}" = "latest" ]; then \ NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \ else \ NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \ fi && \ pi --version && \ git_fetch_ref "${PI_TOOLKIT_REPO}" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \ git_fetch_ref "${PI_EXTENSIONS_REPO}" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \ git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \ git_fetch_ref "${PI_OBSMEM_REPO}" "${PI_OBSMEM_REF}" /opt/pi-observational-memory && \ (cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \ (cd /opt/pi-observational-memory && npm install --omit=dev --no-audit --no-fund) && \ echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \ echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" && \ echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \ echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)" # ── Optional: pi-studio (:latest-studio variant) ───────────────────── # pi-studio (omaclaren/pi-studio) is a pi-package + theme providing a # two-pane browser workspace: prompt/response editor, KaTeX/Mermaid live # preview, and tmux-backed literate REPLs. Off by default; the studio # variant sets INSTALL_STUDIO=true. # # Vendored to /opt/pi-studio and registered at container start by # entrypoint-user.sh via `pi install /opt/pi-studio` — the SAME pattern # as pi-fork / pi-observational-memory above. We deliberately do NOT run # `pi install ` at build time: that writes into ~/.pi/agent, # which is a named volume, so a build-time install collides with / is # shadowed by the volume on first run. Vendoring to /opt (an image layer) # + a runtime local-path install keeps it on the image and idempotent. # # No build step is needed: pi-studio ships its browser bundle prebuilt in # git (client/studio-client.js) and pi loads index.ts directly; its # package.json scripts are only test/typecheck. So we just fetch + install # the 3 prod deps (@earendil-works/pi-ai, @sinclair/typebox, ws). # # PI_STUDIO_REF is CI-resolved to a commit SHA to defeat the registry- # buildcache cache-hit footgun (see the PI_VERSION note above). ARG INSTALL_STUDIO=false ARG PI_STUDIO_REPO=https://github.com/omaclaren/pi-studio.git ARG PI_STUDIO_REF=main RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \ set -e; \ rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \ git -C /opt/pi-studio init -q && \ git -C /opt/pi-studio remote add origin "${PI_STUDIO_REPO}" && \ ok=0; for i in 1 2 3 4 5; do \ if git -C /opt/pi-studio fetch --depth 1 origin "${PI_STUDIO_REF}" && \ git -C /opt/pi-studio checkout -q FETCH_HEAD; then ok=1; break; fi; \ echo "git fetch pi-studio@${PI_STUDIO_REF} failed (attempt $i/5), retrying in $((i*5))s..."; \ sleep $((i*5)); \ done; \ [ "$ok" = "1" ] && \ (cd /opt/pi-studio && npm install --omit=dev --no-audit --no-fund) && \ echo "pi-studio at $(cd /opt/pi-studio && git rev-parse --short HEAD)"; \ fi # STUDIO_PORT: advisory default consumed by docker-compose port publishing # and the recommended `/studio --no-browser --port "$STUDIO_PORT"` launch. # Harmless in the non-studio variant. NOTE: pi-studio hard-binds the server # to 127.0.0.1 inside the container (index.ts: .listen(port,"127.0.0.1")), # so reaching it from a browser needs a loopback bridge or host networking — # see the "Using pi-studio" section in README.md. ENV STUDIO_PORT=8765 # ── Optional: Go toolchain ─────────────────────────────────────────── # Off by default; opt in for users who run Go tools inside the devbox. ARG INSTALL_GO=false ARG GO_VERSION=latest RUN if [ "${INSTALL_GO}" = "true" ]; then \ GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ V="${GO_VERSION}" && \ if [ "$V" = "latest" ]; then \ V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \ awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \ fi && \ [ -n "$V" ] && \ echo "Installing Go ${V}" && \ curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \ ln -s /usr/local/go/bin/go /usr/local/bin/go && \ ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \ fi # ── Build provenance: OCI labels + on-disk build manifest ──────────── # Records exactly which pi version and companion-repo commits were baked # into THIS image, so a published tag is self-describing and reproducible # after the fact (CI logs rotate; a released image must not depend on # them). Previously the resolved SHAs only ever reached the CI build log. # # These ARGs are declared LAST, immediately before the layer that uses # them, so a changing BUILD_DATE / RELEASE_TAG / SOURCE_REVISION never # invalidates the expensive pi-install / clone layers above. ARG RELEASE_TAG=dev ARG BUILD_DATE= ARG SOURCE_REVISION= # MEMPALACE_TOOLKIT_REF is consumed in Dockerfile.base; re-declared here # only so its intended ref lands in the label set alongside the others. ARG MEMPALACE_TOOLKIT_REF=main LABEL org.opencontainers.image.version="${RELEASE_TAG}" \ org.opencontainers.image.revision="${SOURCE_REVISION}" \ org.opencontainers.image.created="${BUILD_DATE}" \ se.jordbo.pi-devbox.pi-version="${PI_VERSION}" \ se.jordbo.pi-devbox.pi-toolkit-ref="${PI_TOOLKIT_REF}" \ se.jordbo.pi-devbox.pi-extensions-ref="${PI_EXTENSIONS_REF}" \ se.jordbo.pi-devbox.pi-fork-ref="${PI_FORK_REF}" \ se.jordbo.pi-devbox.pi-obsmem-ref="${PI_OBSMEM_REF}" \ se.jordbo.pi-devbox.mempalace-toolkit-ref="${MEMPALACE_TOOLKIT_REF}" \ se.jordbo.pi-devbox.pi-studio-ref="${PI_STUDIO_REF}" # The manifest is written from GROUND TRUTH — the actual checked-out HEAD # of each /opt clone and the live `pi --version` — not merely the intended # build-args. That way it also exposes a clone that silently resolved to # something other than the requested ref. pi-studio is present only in the # studio variant (JSON null otherwise). RUN set -e; \ mkdir -p /etc/pi-devbox; \ rev() { git -C "$1" rev-parse HEAD 2>/dev/null || echo "unknown"; }; \ PI_V="$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')"; \ STUDIO_REV='null'; \ if [ -d /opt/pi-studio/.git ]; then STUDIO_REV="\"$(rev /opt/pi-studio)\""; fi; \ { \ echo '{'; \ echo " \"release_tag\": \"${RELEASE_TAG}\","; \ echo " \"build_date\": \"${BUILD_DATE}\","; \ echo " \"source_revision\": \"${SOURCE_REVISION}\","; \ echo " \"pi_version\": \"${PI_V}\","; \ echo " \"components\": {"; \ echo " \"pi-toolkit\": \"$(rev /opt/pi-toolkit)\","; \ echo " \"pi-extensions\": \"$(rev /opt/pi-extensions)\","; \ echo " \"pi-fork\": \"$(rev /opt/pi-fork)\","; \ echo " \"pi-observational-memory\": \"$(rev /opt/pi-observational-memory)\","; \ echo " \"mempalace-toolkit\": \"$(rev /opt/mempalace-toolkit)\","; \ echo " \"pi-studio\": ${STUDIO_REV}"; \ echo " }"; \ echo '}'; \ } > /etc/pi-devbox/build-manifest.json; \ echo "── build manifest ──"; cat /etc/pi-devbox/build-manifest.json # WORKDIR / ENTRYPOINT / CMD inherited from base.