build: make embedded pi images reproducible
This commit is contained in:
+14
-5
@@ -1,17 +1,26 @@
|
||||
# build artefacts & deps
|
||||
# Build artefacts, local configuration, and runtime data never enter an image context.
|
||||
**/node_modules
|
||||
**/.venv
|
||||
**/__pycache__
|
||||
**/.pytest_cache
|
||||
**/dist
|
||||
**/*.pyc
|
||||
harness/.env
|
||||
harness/workspaces/psd.yaml
|
||||
deploy/thothii.env
|
||||
.git
|
||||
.worktrees
|
||||
.gitignore
|
||||
**/.env
|
||||
**/.env.*
|
||||
!.env.example
|
||||
!deploy/env/*.env.example
|
||||
deploy/secrets/
|
||||
harness/workspaces/psd.yaml
|
||||
**/*.log
|
||||
**/.DS_Store
|
||||
tht-workspace-psd
|
||||
coverage/
|
||||
.coverage
|
||||
.artifacts/
|
||||
data/
|
||||
sessions/
|
||||
workspace-registry/
|
||||
# docs/site (mkdocs build) — non necessari nelle immagini
|
||||
docs/superpowers/plans
|
||||
|
||||
+26
-7
@@ -2,6 +2,15 @@
|
||||
# thothii-core: Fastify (Node 22) + harness Python 3.12 (tht CLI) + runtime Pi.
|
||||
# Singolo container, entrypoint logico "server" (default).
|
||||
ARG PI_VERSION=0.80.3
|
||||
ARG IMAGE_VERSION=local
|
||||
|
||||
# ---- Stage 0: locked Pi runtime ----
|
||||
FROM node:22-bookworm AS pi-runtime-build
|
||||
ARG PI_VERSION
|
||||
WORKDIR /opt/pi-runtime
|
||||
COPY docker/pi-runtime/package.json docker/pi-runtime/package-lock.json ./
|
||||
RUN npm ci --omit=dev \
|
||||
&& test "$(./node_modules/.bin/pi --version)" = "$PI_VERSION"
|
||||
|
||||
# ---- Stage 1: backend TypeScript -> dist ----
|
||||
FROM node:22-bookworm AS backend-build
|
||||
@@ -14,6 +23,11 @@ RUN npm run build
|
||||
# ---- Stage 2: runtime (Python 3.12 nativo + Node 22 copiato, stesso glibc bookworm) ----
|
||||
FROM python:3.12-slim-bookworm AS runtime
|
||||
ARG PI_VERSION
|
||||
ARG IMAGE_VERSION
|
||||
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 apt-get update && apt-get install -y --no-install-recommends \
|
||||
@@ -29,10 +43,10 @@ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
|
||||
# Utente non-root
|
||||
RUN useradd --create-home --uid 10001 --shell /bin/bash thoth
|
||||
RUN mkdir -p /home/thoth/.pi/agent && chown -R thoth:thoth /home/thoth/.pi
|
||||
# Docker copies this owned directory into a newly-created named volume, allowing the non-root
|
||||
# runtime user to create the registry checkout, immutable snapshots, state, and locks.
|
||||
RUN mkdir -p /data/workspace-registry && chown -R thoth:thoth /data/workspace-registry
|
||||
# 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 \
|
||||
&& chown -R thoth:thoth /home/thoth/.pi /data
|
||||
|
||||
COPY harness/ /app/harness/
|
||||
# Pi scrive lock/settings in .pi: ownership thoth per sopravvivere al rebuild
|
||||
@@ -60,10 +74,14 @@ COPY --from=backend-build /src/backend/dist /app/backend/dist
|
||||
COPY --from=backend-build /src/backend/node_modules /app/backend/node_modules
|
||||
COPY backend/package*.json /app/backend/
|
||||
|
||||
# Runtime Pi (pacchetto npm puro JS, dipendenze prebuilt). Installato come root, eseguibile da thoth.
|
||||
RUN npm install -g @earendil-works/pi-coding-agent@${PI_VERSION}
|
||||
# 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
|
||||
RUN ln -s /opt/pi-runtime/node_modules/.bin/pi /usr/local/bin/pi \
|
||||
&& test "$(pi --version)" = "$PI_VERSION"
|
||||
|
||||
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 \
|
||||
@@ -72,8 +90,9 @@ ENV PATH="/opt/venv/bin:/usr/local/bin:$PATH" \
|
||||
|
||||
COPY scripts/verify-line-endings.sh /usr/local/bin/verify-line-endings
|
||||
COPY docker/core-entrypoint.sh docker/session-migrate.sh docker/ensure-pi-trust.mjs /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/session-migrate.sh
|
||||
&& chmod +x /app/docker/core-entrypoint.sh /app/docker/session-migrate.sh /app/docker/smoke/core-smoke.sh
|
||||
|
||||
WORKDIR /app/backend
|
||||
USER thoth
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# thothii-frontend: build Vite (React) + nginx-unprivileged (porta 8080).
|
||||
ARG IMAGE_VERSION=local
|
||||
FROM node:22-bookworm AS build
|
||||
WORKDIR /src
|
||||
COPY frontend/package*.json ./
|
||||
@@ -9,6 +10,10 @@ ENV VITE_BASE=/ VITE_BACKEND_URL=/api
|
||||
RUN npm run build
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime
|
||||
ARG IMAGE_VERSION
|
||||
LABEL org.opencontainers.image.title="thothii-frontend" \
|
||||
org.opencontainers.image.version="${IMAGE_VERSION}" \
|
||||
org.opencontainers.image.description="ThothII standalone frontend"
|
||||
COPY --from=build /src/dist /usr/share/nginx/html
|
||||
COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template
|
||||
COPY --chmod=755 docker/frontend-entrypoint.sh /usr/local/bin/frontend-entrypoint
|
||||
|
||||
Generated
+3
@@ -9,6 +9,9 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-coding-agent": "0.80.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@earendil-works/pi-coding-agent": {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Locked Pi runtime dependency for the ThothII core image",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-coding-agent": "0.80.3"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
test "$(id -u)" != "0"
|
||||
test "$(id -u)" = "10001"
|
||||
test -n "${PI_VERSION:-}"
|
||||
|
||||
node_version="$(node --version)"
|
||||
python_version="$(python --version 2>&1)"
|
||||
@@ -15,7 +16,10 @@ case "$python_version" in
|
||||
esac
|
||||
|
||||
tht --help >/dev/null
|
||||
pi --version >/dev/null
|
||||
test "$(pi --version)" = "$PI_VERSION"
|
||||
test ! -e /var/run/docker.sock
|
||||
touch /data/.core-smoke-writable
|
||||
rm /data/.core-smoke-writable
|
||||
|
||||
/app/docker/core-entrypoint.sh server &
|
||||
server_pid=$!
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
$repositoryRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $repositoryRoot
|
||||
|
||||
& docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Output "Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d"
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
status=$?
|
||||
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
printf '%s\n' 'Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d'
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
@@ -1,112 +1,52 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
project="thothii-task5-$(date +%s)-$$"
|
||||
expected_pi_version=$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)
|
||||
trap 'docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
bundle="$tmp/thothii.secrets"
|
||||
cat >"$bundle" <<'EOF'
|
||||
# disposable deployment-contract bundle
|
||||
THT_MODEL_API_KEY=test-model
|
||||
THT_VECTOR_BOOTSTRAP_PASSWORD=contract-bootstrap
|
||||
THT_VECTOR_MIGRATOR_PASSWORD=contract-migrator
|
||||
THT_VECTOR_READER_PASSWORD=contract-reader
|
||||
THT_VECTOR_WRITER_PASSWORD=contract-writer
|
||||
EOF
|
||||
chmod 0600 "$bundle"
|
||||
export THT_SECRETS_FILE="$bundle"
|
||||
test -n "$expected_pi_version"
|
||||
printf '{}\n' >"$tmp/pi-auth.json"
|
||||
chmod 0600 "$tmp/pi-auth.json"
|
||||
|
||||
docker compose config >"$tmp/base.yaml"
|
||||
grep -q '^ core:' "$tmp/base.yaml"
|
||||
grep -q '^ frontend:' "$tmp/base.yaml"
|
||||
grep -q 'host_ip: 127.0.0.1' "$tmp/base.yaml"
|
||||
grep -q 'AUTH_MODE: none' "$tmp/base.yaml"
|
||||
grep -q 'THOTH_PUBLIC_EXPOSURE: "false"' "$tmp/base.yaml"
|
||||
grep -q 'THT_SECRETS_FILE: /run/secrets/thothii.secrets' "$tmp/base.yaml"
|
||||
grep -q 'target: /home/thoth/.pi/agent/models.json' "$tmp/base.yaml"
|
||||
grep -q 'source: .*/deploy/pi/models.json' "$tmp/base.yaml"
|
||||
grep -q 'target: /home/thoth/.pi/agent/settings.json' "$tmp/base.yaml"
|
||||
if grep -q 'THT_[A-Z0-9_]*_SECRET_FILE:' "$tmp/base.yaml"; then
|
||||
echo "base Compose must not require legacy secret-file variables" >&2
|
||||
export PI_AUTH_FILE="$tmp/pi-auth.json"
|
||||
export THT_WORKSPACE_GIT_REMOTE="https://git.example.invalid/thothii/workspaces.git"
|
||||
# Let Docker assign loopback ports so this isolated contract test never collides with an operator stack.
|
||||
export THOTH_CORE_HTTP_PORT=0
|
||||
export THOTH_HTTP_PORT=0
|
||||
|
||||
docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
|
||||
core_label=$(docker image inspect --format '{{ index .Config.Labels "io.thothii.pi.version" }}' thothii-core:local)
|
||||
test "$core_label" = "$expected_pi_version"
|
||||
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' thothii-core:local)" = "thothii-core"
|
||||
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' thothii-frontend:local)" = "thothii-frontend"
|
||||
|
||||
docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml up --detach --wait --wait-timeout 90
|
||||
|
||||
docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml exec -T core sh -ceu '
|
||||
test "$(id -u)" = 10001
|
||||
test "$(pi --version)" = "$PI_VERSION"
|
||||
command -v pi >/dev/null
|
||||
test ! -e /var/run/docker.sock
|
||||
touch /data/.task5-writable
|
||||
rm /data/.task5-writable
|
||||
if find /app /home /data -xdev \( -iname "*chirone*" -o -iname "*omics*portal*" \) -print -quit | grep -q .; then
|
||||
echo "portal or Chirone path found in core image" >&2
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
|
||||
if docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml config | grep -Eqi 'docker\.sock|/var/run/docker|docker[-_]?daemon'; then
|
||||
echo "Compose must not mount a Docker socket or daemon" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose -f compose.yaml -f deploy/compose.local-vector.yaml \
|
||||
--profile local-vector config >"$tmp/local-vector.yaml"
|
||||
grep -q 'target: thothii.secrets' "$tmp/local-vector.yaml"
|
||||
if grep -Eq 'vector_(bootstrap|migrator|reader|writer)_password|THT_[A-Z0-9_]+_SECRET_FILE' "$tmp/local-vector.yaml"; then
|
||||
echo "rendered local-vector config contains legacy per-secret references" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'contract-' "$tmp/local-vector.yaml"; then
|
||||
echo "rendered local-vector config leaked a bundle secret value" >&2
|
||||
exit 1
|
||||
fi
|
||||
frontend_address=$(docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml port frontend 8080 | head -n 1)
|
||||
curl --fail --silent --show-error "http://$frontend_address/" >/dev/null
|
||||
curl --fail --silent --show-error "http://$frontend_address/api/health" >/dev/null
|
||||
|
||||
docker compose -f compose.yaml -f deploy/compose.local.yaml \
|
||||
config >"$tmp/local.yaml"
|
||||
if grep -q 'env_file:' "$tmp/local.yaml"; then
|
||||
echo "local Compose must use the root .env interpolation file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=test-model' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/thothii.secrets"
|
||||
THT_SECRETS_FILE="$tmp/thothii.secrets" \
|
||||
THT_DB_NAME=test THT_DWH_REST_URL=https://dwh.example.test \
|
||||
THT_VEC_REST_URL=https://vector.example.test THT_OLLAMA_URL=https://embed.example.test \
|
||||
docker compose -f compose.yaml -f deploy/compose.production.yaml \
|
||||
config >"$tmp/production.yaml"
|
||||
grep -q 'AUTH_MODE: upstream' "$tmp/production.yaml"
|
||||
grep -q 'THOTH_PUBLIC_EXPOSURE: "true"' "$tmp/production.yaml"
|
||||
grep -q 'THT_SECRETS_FILE: /run/secrets/thothii.secrets' "$tmp/production.yaml"
|
||||
grep -q 'target: thothii.secrets' "$tmp/production.yaml"
|
||||
if grep -q 'test-model' "$tmp/production.yaml"; then
|
||||
echo "rendered production config leaked the model API key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if PI_PROVIDER_API_KEY='must-not-leak' ./docker/core-entrypoint.sh doctor 2>"$tmp/legacy-model.err"; then
|
||||
echo "legacy generic model credential was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'PI_PROVIDER_API_KEY is unsupported' "$tmp/legacy-model.err"
|
||||
if grep -q 'must-not-leak' "$tmp/legacy-model.err"; then
|
||||
echo "legacy model credential leaked through entrypoint diagnostics" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'THT_VECTOR_READER_PASSWORD=one\nTHT_VECTOR_READER_PASSWORD=two\n' >"$tmp/invalid-bundle"
|
||||
chmod 0600 "$tmp/invalid-bundle"
|
||||
if THT_SECRETS_FILE="$tmp/invalid-bundle" ./docker/core-entrypoint.sh doctor \
|
||||
>"$tmp/invalid-bundle.out" 2>"$tmp/invalid-bundle.err"; then
|
||||
echo "entrypoint accepted an invalid secret bundle" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'THT_SECRETS_FILE points to an invalid secret bundle' "$tmp/invalid-bundle.err"
|
||||
if grep -q 'THT_VECTOR_READER_PASSWORD' "$tmp/invalid-bundle.err"; then
|
||||
echo "invalid bundle diagnostics leaked key material" >&2
|
||||
exit 1
|
||||
fi
|
||||
before_tmp=$(find "${TMPDIR:-/tmp}" -maxdepth 1 -type d -name 'thothii-secrets.*' -print | sort)
|
||||
THT_SECRETS_FILE="$bundle" ./docker/core-entrypoint.sh doctor >/dev/null 2>&1 || true
|
||||
after_tmp=$(find "${TMPDIR:-/tmp}" -maxdepth 1 -type d -name 'thothii-secrets.*' -print | sort)
|
||||
test "$before_tmp" = "$after_tmp"
|
||||
if grep -Eq 'THT_VECTOR_(BOOTSTRAP|MIGRATOR|READER|WRITER)_PASSWORD_FILE|target: vector_(bootstrap|migrator|reader|writer)_password|dwh_api_key|model_api_key|THT_[A-Z0-9_]+_SECRET_FILE' "$tmp/production.yaml"; then
|
||||
echo "production external config contains local direct vector secrets" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if awk '/^FROM / && $2 !~ /@sha256:/ { found=1 } END { exit !found }' \
|
||||
docker/core.Dockerfile docker/frontend.Dockerfile; then
|
||||
echo "every Dockerfile base must include an immutable digest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -qx 'deploy/\*' .dockerignore
|
||||
grep -qx '!deploy/vector/' .dockerignore
|
||||
grep -qx 'deploy/vector/\*' .dockerignore
|
||||
grep -qx '!deploy/vector/secret-policy.sh' .dockerignore
|
||||
|
||||
echo "container deployment security contract passed."
|
||||
echo "Task 5 container deployment contract passed."
|
||||
|
||||
@@ -16,6 +16,12 @@ docker buildx build --platform "$platform" --load \
|
||||
docker buildx build --platform "$platform" --load \
|
||||
-f docker/frontend.Dockerfile -t "$frontend_image" .
|
||||
|
||||
expected_pi_version=$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)
|
||||
test -n "$expected_pi_version"
|
||||
test "$(docker image inspect --format '{{ index .Config.Labels "io.thothii.pi.version" }}' "$core_image")" = "$expected_pi_version"
|
||||
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' "$core_image")" = "thothii-core"
|
||||
test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.title" }}' "$frontend_image")" = "thothii-frontend"
|
||||
|
||||
docker run --rm --platform "$platform" --entrypoint /app/docker/smoke/core-smoke.sh \
|
||||
"$core_image"
|
||||
./scripts/test-vector-migration-image.sh "$core_image" "$platform"
|
||||
|
||||
Reference in New Issue
Block a user