# syntax=docker/dockerfile:1.7 # thothii-core: Fastify (Node 24.16) + harness Python 3.12 (tht CLI) + runtime Pi. # Singolo container, entrypoint logico "server" (default). ARG PI_VERSION=0.80.3 ARG IMAGE_VERSION=local # ---- Pinned Node source for the runtime binary and npm ---- FROM node:24.16.0-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7 AS node-runtime # ---- Pinned native storage helper used by the authenticated backend ---- FROM golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651 AS tht-auth-storage-build WORKDIR /src/tools/tht COPY tools/tht/go.mod tools/tht/go.sum ./ RUN go mod download COPY tools/tht ./ RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/tht-auth-storage ./cmd/tht # ---- Stage 0: locked Pi runtime ---- FROM node:24.16.0-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7 AS pi-runtime-build ARG PI_VERSION ARG PI_RUNTIME_PACKAGE_VERSION ARG PI_PACKAGE_NAME=@earendil-works/pi-coding-agent WORKDIR /opt/pi-runtime COPY docker/pi-runtime/package.json docker/pi-runtime/package-lock.json ./ RUN if [ -n "$PI_RUNTIME_PACKAGE_VERSION" ]; then \ node -e 'const fs=require("node:fs"); const [name,version]=process.argv.slice(1); const manifest=JSON.parse(fs.readFileSync("package.json","utf8")); manifest.dependencies[name]=version; fs.writeFileSync("package.json",JSON.stringify(manifest,null,2)+"\\n");' "$PI_PACKAGE_NAME" "$PI_RUNTIME_PACKAGE_VERSION"; \ npm install --package-lock-only --ignore-scripts --omit=dev "$PI_PACKAGE_NAME@$PI_RUNTIME_PACKAGE_VERSION"; \ fi \ && npm ci --omit=dev \ && test "$(./node_modules/.bin/pi --version)" = "$PI_VERSION" # ---- Stage 1: backend TypeScript -> dist ---- FROM node:24.16.0-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7 AS backend-build WORKDIR /src/backend COPY backend/package*.json ./ RUN npm ci COPY backend/ ./ RUN npm run build # ---- Stage 2: runtime (Python 3.12 nativo + Node 24.16 copiato, stesso glibc bookworm) ---- FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS runtime ARG PI_VERSION ARG IMAGE_VERSION ARG INSTALL_SENSITIVITY_NER=false LABEL org.opencontainers.image.title="thothii-core" \ org.opencontainers.image.version="${IMAGE_VERSION}" \ org.opencontainers.image.description="ThothII core with its embedded Pi runtime" \ io.thothii.pi.version="${PI_VERSION}" # Runtime tools RUN set -eux; \ runtime_packages="curl ca-certificates ripgrep fd-find tini git openssh-client libseccomp2"; \ if ! command -v flock >/dev/null 2>&1; then \ runtime_packages="$runtime_packages util-linux"; \ fi; \ apt-get update; \ apt-get install -y --no-install-recommends $runtime_packages; \ rm -rf /var/lib/apt/lists/*; \ command -v flock >/dev/null 2>&1; \ ln -s /usr/bin/fdfind /usr/local/bin/fd # Node 24.16 + npm copiati dall'immagine ufficiale (stesso Debian bookworm → binario compatibile) COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ && ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx # Utente non-root RUN useradd --create-home --uid 10001 --shell /bin/bash thoth # Docker copies these owned directories into newly-created named volumes, allowing the non-root # runtime user to create application settings, sessions, registry snapshots, state, and locks. RUN mkdir -p /home/thoth/.pi/agent /data/settings /data/sessions /data/workspace-registry /data/workspace-secrets \ /data/auth/sessions /data/auth/oidc \ && chown -R thoth:thoth /home/thoth/.pi /data \ && chmod 0700 /data/auth /data/auth/sessions /data/auth/oidc COPY harness/ /app/harness/ # Pi scrive lock/settings in .pi: ownership thoth per sopravvivere al rebuild RUN chown -R thoth:thoth /app/harness/.pi # Harness: venv nativo (python 3.12) + tht installato (non-editable). # tht carica workflow.yaml module-relative (Path(__file__).parent.parent); con il package # in site-packages quel path non contiene workflow.yaml -> lo copiamo dopo l'install. # (pip install -e non funziona: pip non riconosce /app/harness come editable req.) # psycopg2-binary è wheel → niente gcc/libpq-dev. RUN python -m venv /opt/venv \ && /opt/venv/bin/pip install --no-cache-dir --upgrade pip \ && (cd /app/harness && /opt/venv/bin/pip install --no-cache-dir .) \ && cp /app/harness/workflow.yaml /opt/venv/lib/python3.12/site-packages/workflow.yaml # Il workspace locale predefinito converge sul file canonico. Il CLI onora anche THT_CONFIG, # quindi cambiare CWD non cambia l'identita' dello workspace. RUN mkdir -p /app/harness/config \ && cp --remove-destination /app/harness/workspaces/local.yaml /app/harness/config/tht.yaml # PiProcessManager (backend) prepende harnessDir/.venv/bin al PATH del child Pi → symlink al venv reale RUN ln -s /opt/venv /app/harness/.venv # The optional NER dependency layer is independent of backend source and build artifacts so it can # be reused when only TypeScript or worker code changes. COPY backend/python/sensitivity-ner-requirements.txt /app/backend/python/sensitivity-ner-requirements.txt RUN if [ "$INSTALL_SENSITIVITY_NER" = "true" ]; then \ python -m venv /opt/sensitivity-ner; \ /opt/sensitivity-ner/bin/pip install --no-cache-dir --upgrade pip; \ /opt/sensitivity-ner/bin/pip install --no-cache-dir -r /app/backend/python/sensitivity-ner-requirements.txt; \ elif [ "$INSTALL_SENSITIVITY_NER" != "false" ]; then \ echo "INSTALL_SENSITIVITY_NER must be true or false" >&2; exit 2; \ fi # Backend: dist + node_modules (stesso Node major 24 + glibc bookworm → compatibili) COPY --from=backend-build /src/backend/dist /app/backend/dist COPY --from=backend-build /src/backend/node_modules /app/backend/node_modules COPY backend/scripts/ssh-askpass.mjs /app/backend/scripts/ssh-askpass.mjs COPY backend/python /app/backend/python COPY backend/package*.json /app/backend/ RUN chmod 0755 /app/backend/scripts/ssh-askpass.mjs # Runtime Pi is installed only from the committed lockfile. The image exposes its immutable # executable directly, so no host Pi installation or writable global npm directory is needed. COPY --from=pi-runtime-build /opt/pi-runtime/node_modules /opt/pi-runtime/node_modules COPY --from=tht-auth-storage-build /out/tht-auth-storage /usr/local/bin/tht-auth-storage RUN ln -s /opt/pi-runtime/node_modules/.bin/pi /usr/local/bin/pi \ && test "$(pi --version)" = "$PI_VERSION" \ && test -x /usr/local/bin/tht-auth-storage ENV PATH="/opt/venv/bin:/usr/local/bin:$PATH" \ PI_VERSION="${PI_VERSION}" \ HOST=0.0.0.0 PORT=8787 \ THT_HARNESS_DIR=/app/harness \ THT_BIN=/opt/venv/bin/tht \ THT_AUTH_STORAGE_BIN=/usr/local/bin/tht-auth-storage \ PI_BIN=pi \ HOME=/home/thoth COPY scripts/verify-line-endings.sh /usr/local/bin/verify-line-endings COPY docker/core-entrypoint.sh docker/workspace-maintenance-entrypoint.sh docker/session-migrate.sh docker/catalog-migrate.sh docker/ensure-pi-trust.mjs docker/embedding-model-init.sh /app/docker/ COPY docker/smoke/core-smoke.sh /app/docker/smoke/core-smoke.sh RUN /usr/local/bin/verify-line-endings /app/docker \ && chmod +x /app/docker/core-entrypoint.sh /app/docker/workspace-maintenance-entrypoint.sh /app/docker/session-migrate.sh /app/docker/embedding-model-init.sh /app/docker/smoke/core-smoke.sh WORKDIR /app/backend USER thoth EXPOSE 8787 HEALTHCHECK --interval=15s --timeout=3s --retries=5 --start-period=30s \ CMD curl -fsS http://127.0.0.1:8787/health || exit 1 ENTRYPOINT ["/usr/bin/tini","--","/app/docker/core-entrypoint.sh"] CMD ["server"]