Files
ThothII/docs/superpowers/plans/2026-07-12-local-docker-deploy-implementation.md
T

44 KiB
Raw Blame History

ThothII — Local Docker Deployment (Profile A, co-located) — Implementation Plan

Data: 2026-07-12 Scope: Deploy professionale su Docker locale dei due container applicativi (thothii-core + thothii-frontend) sul server che ospita già Supabase (DWH) e pgvector. Profilo A — server co-locato dell'architettura approvata (specs/2026-07-11-portable-deployment-architecture-design.md §8.1). Stato: piano dettagliato, pronto all'esecuzione task-by-task.


0. Decisioni consolidate (verificate con l'utente)

Decisione Scelta Razionale / evidenza
DWH transport direct (Postgres diretto a :5438, schema datawarehouse) Server co-locato, niente PostgREST/CA. Codice già supporta transport: direct.
Vector transport direct (Postgres diretto a :5438, schema vectors) — entrambi in locale open_searcher() (harness/tht/cli/vector_cmd.py:64-77) usa già DirectSearcher quando vector_rest è assente. Nessuna modifica al codice: basta non configurare vector_rest.

Istanza DB unica (verificato): :5437 è il pooler Supavisor (richiede user tenant, rifiuta postgres bare); :5438 è l'accesso diretto alla stessa istanza Postgres (postgres/postgres) che contiene entrambi gli schema datawarehouse (163 tabelle fact/dim/bridge) e vectors (evidence, memory, schema_records + ext. vector). → ThothII usa 5438 per DWH e vector, una sola migrazione ruoli. | Credenziali DB | Ruoli dedicati least-privilege (thoth_dwh_reader, thoth_vector_rw) | Read-only by construction sul DWH. SQL di migrazione in Fase 1. | | Persistenza | Root unica /home/chirone/thothii-data/ (bind mount → /data) | Trasparente per backup/ispezione, fuori dal repo git. | | Evidence | /home/chirone/chirone/etl/docs/evidence (36 MD curati) montato RO | Corpus esistente nell'ETL. | | Rete verso Supabase/pgvector/Ollama | host.docker.internal (host-gateway) | Disaccoppiato dai container Supabase; funziona a prescindere dal daemon. | | Modello (GLM 5.2 via zai) | API pubblica HTTPS, nessun VPN nel container | Su server co-locato DWH/vector sono locali; il modello è internet pubblico. |

Verifiche chiave già eseguite sul codice attuale:

  • Nessun file Docker/Compose esiste (greenfield).
  • pi = pacchetto npm puro JS @earendil-works/pi-coding-agent@0.80.2 (niente native addon da compilare) → containerizzabile con npm install -g.
  • Credenziali modello in ~/.pi/agent/{auth.json (zai/deepseek keys), settings.json, models.json, trust.json}.
  • Gap codice #1: backend/src/server.ts:5 ascolta su 127.0.0.1 hardcoded → va reso configurabile (Fase 0).
  • Gap codice #2: PiProcessManager (backend/src/pi/pi-process-manager.ts:26) prepende harnessDir/.venv/bin al PATH del child Pi e usa cwd=harnessDir → nel container serve symlink /app/harness/.venv → /opt/venv.
  • psd.yaml è symlink rotto (path macOS) → si crea un nuovo workspace local.yaml (Fase 2); il backend seleziona il workspace via settings.json {workspace:"local"} + workspaces/<name>.yaml (tht-runner.ts:46-51).
  • Host: :5438 accesso diretto Postgres (DWH datawarehouse + vector vectors, stessa istanza; :5437 è il pooler Supavisor non usato da ThothII), :11434 Ollama — tutti in ascolto su 0.0.0.0. Docker 29.1.1 + Compose v2.40.3.

1. Architettura del deploy

                     ┌─────────────────────────────┐
                     │   Host (server co-locato)    │
                     │  :5438 Postgres (DWH + vector) │
                     │  :11434 Ollama (embeddings)    │
                     └──────────────┬───────────────┘
                                    │ host.docker.internal (host-gateway)
   ┌────────────────────────────────┴────────────────────────────────┐
   │  Docker network "thothii-net"                                    │
   │                                                                  │
   │  ┌──────────────────────┐         ┌─────────────────────────┐   │
   │  │ thothii-frontend     │  /api   │ thothii-core            │   │
   │  │ nginx-unprivileged   │────────▶│ Fastify :8787           │   │
   │  │ React build + proxy  │         │  └─ pi --mode rpc       │   │
   │  │ :8080 (esposto)      │         │      └─ tht (harness)   │   │
   │  └──────────────────────┘         └────────┬────────────────┘   │
   └────────────────────────────────────────────┼────────────────────┘
                                                │ bind mounts
              /home/chirone/thothii-data  ───────┘
                ├─ sessions/  artifacts/  indexes/  corpus/
                ├─ settings/settings.json
                ├─ pi-config/  (→ /home/thoth/.pi : credenziali modello)
                └─ evidence/   (RO, da /home/chirone/chirone/etl/docs/evidence)

Immagini prodotte (esattamente 2, come da architettura):

  1. thothii-core:local — backend Fastify (Node) + harness Python (tht) + runtime Pi. Un solo container, entrypoint server.
  2. thothii-frontend:local — build statica React servita da nginx-unprivileged con reverse proxy /api → core:8787.

Infrastruttura (Supabase, pgvector, Ollama) resta esterna e condivisa — non si istanzia un terzo container ThothII.


2. Fase 0 — Prerequisiti di codice (backend host bind)

Obiettivo: il backend deve poter ascoltare su 0.0.0.0 nel container, restando 127.0.0.1 di default per il dev locale.

Files:

  • Modify: backend/src/config.ts
  • Modify: backend/src/server.ts
  • Test: backend/test/config.test.ts

Step 0.1 (TDD — RED): aggiungi test

expect(loadConfig({ HOST: "0.0.0.0" })).toMatchObject({ host: "0.0.0.0" });
expect(loadConfig({})).toMatchObject({ host: "127.0.0.1" }); // default dev-safe

cd backend && npx vitest run test/config.test.ts → FAIL.

Step 0.2 (GREEN):

  • config.ts: aggiungi host: env.HOST ?? "127.0.0.1" all'AppConfig interface + nel return di loadConfig.
  • server.ts:5: sostituisci host: "127.0.0.1" con host: config.host.

Step 0.3 (verify): cd backend && npx vitest run && npx tsc --noEmit -p . → PASS.

Commit: fix(backend): make listen host configurable via HOST env


3. Fase 1 — Ruoli DB least-privilege (migrazione SQL one-shot)

Obiettivo: creare thoth_dwh_reader (read-only sul DWH) e thoth_vector_rw (read+write su vectors). Eseguito una tantum contro i Postgres esistenti.

File:

  • Create: deploy/sql/10-dwh-roles.sql (ruolo read-only su datawarehouse)
  • Create: deploy/sql/20-vector-roles.sql (ruolo read+write su vectors) — entrambi eseguiti sulla stessa istanza :5438

deploy/sql/10-dwh-roles.sql (DWH, schema datawarehouse, read-only):

-- Ruolo read-only per ThothII sul DWH. Sostituire :PWD con un secret forte.
DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'thoth_dwh_reader') THEN
    CREATE ROLE thoth_dwh_reader LOGIN PASSWORD :'PWD';
  END IF;
END $$;
GRANT USAGE ON SCHEMA datawarehouse TO thoth_dwh_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA datawarehouse TO thoth_dwh_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA datawarehouse
  GRANT SELECT ON TABLES TO thoth_dwh_reader;

deploy/sql/20-vector-roles.sql (pgvector, schema vectors, read+write):

-- Ruolo read+write per ThothII (indexing + similarity search diretta).
-- La separazione reader/writer resta rilevante solo per il path REST (non usato qui).
DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'thoth_vector_rw') THEN
    CREATE ROLE thoth_vector_rw LOGIN PASSWORD :'PWD';
  END IF;
END $$;
CREATE SCHEMA IF NOT EXISTS vectors;
GRANT USAGE, CREATE ON SCHEMA vectors TO thoth_vector_rw;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA vectors TO thoth_vector_rw;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA vectors TO thoth_vector_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO thoth_vector_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors
  GRANT USAGE, SELECT ON SEQUENCES TO thoth_vector_rw;
-- L'estensione pgvector deve esistere; se assente:
-- CREATE EXTENSION IF NOT EXISTS vector;

Esecuzione (one-shot, con psql sull'host o da un container postgres:16):

PGPASSWORD=postgres psql -h localhost -p 5438 -U postgres -d postgres \
  -v PWD="<dwh_reader_pwd>" -f deploy/sql/10-dwh-roles.sql
PGPASSWORD=postgres psql -h localhost -p 5438 -U postgres -d postgres \
  -v PWD="<vector_rw_pwd>" -f deploy/sql/20-vector-roles.sql

Verifica: psql ... -c "\du thoth_*" mostra i ruoli; un login thoth_dwh_reader NON riesce a fare UPDATE/DELETE su datawarehouse.* (tentativo → permission denied).


4. Fase 2 — Workspace local.yaml (direct transport, path logici nel container)

Obiettivo: un workspace co-locato con DWH+vector entrambi direct, path assoluti interni al container sotto /data (invarianti rispetto all'host).

File:

  • Create: harness/workspaces/local.yaml

harness/workspaces/local.yaml:

# Workspace ThothII — Profilo A (server co-locato). DWH + vector BOTH direct, no REST.
# Segreti SOLO in env (compose env_file: deploy/thothii.env). Path interni al container (/data).
language: it

database:
  host: ${THT_DB_HOST}          # host.docker.internal
  port: ${THT_DB_PORT}          # 5438 (stessa istanza del vector)
  database: ${THT_DB_NAME}      # postgres
  schema: datawarehouse
  user: ${THT_DB_USER}          # thoth_dwh_reader
  password: ${THT_DB_PASSWORD}
  transport: direct             # <-- Postgres diretto, niente PostgREST

# Nessuna sezione `rest`: non usata con transport=direct.

paths:
  sessions: /data/sessions
  artifacts: /data/artifacts
  indexes: /data/indexes

examples:
  max_per_column: 10

lsh:
  signature_size: 64
  n_gram: 3
  threshold: 0.5
  max_values_per_column: 1000

eligibility:
  max_declared_len: 128
  max_avg_length: 40
  max_sampled_len: 200
  ignore_columns: [etl_last_update]

evidence:
  source_root: /data            # il corpus è montato a /data/evidence
  evidence_dir: evidence        # → /data/evidence

embeddings:
  base_url: ${THT_OLLAMA_URL}   # http://host.docker.internal:11434
  model: nomic-embed-text-v2-moe
  dim: 768
  batch_size: 32

# Vector: diretto (read+write). Assenza di vector_rest/vector_write_rest
# fa sì che open_searcher()/open_store() selezionino DirectSearcher/VectorStore.
vector_db:
  host: ${THT_VEC_HOST}         # host.docker.internal
  port: ${THT_VEC_PORT}         # 5438
  database: postgres
  schema: vectors
  user: ${THT_VEC_USER}         # thoth_vector_rw
  password: ${THT_VEC_PASSWORD}

# Nessuna sezione vector_rest / vector_write_rest: tutto diretto in locale.

vector:
  max_chunk_chars: 4000

search:
  rrf_k: 60
  top_schema_tables: 12
  schema_chunk_pool: 150

execution:
  allow: [cte_test, explain, preview, aggregate, export]
  max_preview_rows: 10
  max_export_rows: 100000
  statement_timeout_ms: 30000
  warn_execution_ms: 5000
  max_aggregate_cells: 20
  forbidden_functions: [set_config, dblink, dblink_exec, lo_import]

Verifica (post-build, dentro il container o via tht doctor):

docker compose run --rm core tht -c workspaces/local.yaml doctor --json
# Mi aspetto: dwh ok, vector_read ok, vector_write ok, embeddings ok (Ollama raggiungibile).

Nota: lsh.n_gram — verificare il nome esatto del campo in tht/config.py (LshConfig); l'esempio usa n_gram. Allineare prima di committare.


5. Fase 3 — Secrets e .env

Principio: i secret non entrano mai nelle immagini. Vengono iniettati a runtime via Compose env_file (DWH/vector/Ollama) e via bind mount (credenziali modello Pi). Tutto gitignored.

Files:

  • Create: deploy/thothii.env.example (committato, redatto)
  • Create: deploy/thothii.env (gitignored — popolato a mano)
  • Modify: .gitignore (aggiungi deploy/thothii.env, /home/chirone/thothii-data/ se nel repo)

deploy/thothii.env.example:

# === ThothII core — env di runtime (compose env_file) ===
# Copiare in deploy/thothii.env e completare. NON committare thothii.env.

# --- DWH (direct, ruolo read-only) ---
THT_DB_HOST=host.docker.internal
THT_DB_PORT=5438
THT_DB_NAME=postgres
THT_DB_USER=thoth_dwh_reader
THT_DB_PASSWORD=__CHANGE_ME__

# --- Vector (direct, ruolo read+write) ---
THT_VEC_HOST=host.docker.internal
THT_VEC_PORT=5438
THT_VEC_USER=thoth_vector_rw
THT_VEC_PASSWORD=__CHANGE_ME__

# --- Embeddings (Ollama sull'host) ---
THT_OLLAMA_URL=http://host.docker.internal:11434

# --- Backend ---
AUTH_MODE=none                 # none | mock | oidc
MAX_PI_PROCESSES=4

Credenziali modello Pi (bind mount, non env): popolare /home/chirone/thothii-data/pi-config/agent/ una tantum copiando dall'installazione di sviluppo funzionante e trimmando ai soli provider necessari:

mkdir -p /home/chirone/thothii-data/pi-config/agent
cp ~/.pi/agent/auth.json    /home/chirone/thothii-data/pi-config/agent/auth.json      # contiene zai (+deepseek)
cp ~/.pi/agent/settings.json /home/chirone/thothii-data/pi-config/agent/settings.json # defaultProvider/Model
cp ~/.pi/agent/models.json  /home/chirone/thothii-data/pi-config/agent/models.json
cp ~/.pi/agent/trust.json   /home/chirone/thothii-data/pi-config/agent/trust.json
chmod -R go-rwx /home/chirone/thothii-data/pi-config

Montato in compose come /home/thirone/thothii-data/pi-config → /home/thoth/.pi (rw: Pi scrive sessions/bin/cache).

backend/data/settings.json equivalente container → /home/chirone/thothii-data/settings/settings.json:

{
  "workspace": "local",
  "provider": "zai",
  "model": "glm-5.2",
  "thinking": "medium"
}

(provider/model devono combaciare con una voce di pi-config/agent/auth.json + settings.json; copiare i valori esatti dal ~/.pi/agent/settings.json di sviluppo.)


6. Fase 4 — Immagine thothii-core

Files:

  • Create: docker/core.Dockerfile
  • Create: docker/core-entrypoint.sh
  • Create: .dockerignore

.dockerignore (radice repo):

**/node_modules
**/.venv
**/__pycache__
**/.pytest_cache
**/dist
harness/.env
harness/workspaces/psd.yaml
deploy/thothii.env
.git
**/*.log
tht-workspace-psd

docker/core.Dockerfile:

# syntax=docker/dockerfile:1.7
# thothii-core: Fastify (Node 22) + harness Python (tht) + runtime Pi.
# Multi-stage: build backend TS, build harness venv, runtime unificato non-root.
ARG PI_VERSION=0.80.2

# ---- Stage 1: backend TypeScript -> dist ----
FROM node:22-bookworm AS backend-build
WORKDIR /src/backend
COPY backend/package*.json ./
RUN npm ci
COPY backend/ ./
RUN npm run build

# ---- Stage 2: harness venv (psycopg2 compilato) ----
FROM python:3.11-slim-bookworm AS harness-build
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /src/harness
COPY harness/pyproject.toml ./
COPY harness/tht ./tht
# installazione NON editabile: il package finisce nel venv, indipendente dal sorgente
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir .

# ---- Stage 3: runtime (Node + venv + Pi) ----
FROM node:22-bookworm AS runtime
ARG PI_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 ca-certificates curl ripgrep fd-find tini \
    && rm -rf /var/lib/apt/lists/* \
    && ln -s /usr/bin/fdfind /usr/local/bin/fd

# Utente non-root
RUN useradd --create-home --uid 10001 --shell /bin/bash thoth

# Runtime Pi (pacchetto npm puro JS, dipendenze prebuilt)
RUN npm install -g @earendil-works/pi-coding-agent@${PI_VERSION}

# Venv harness (con psycopg2 compilato)
COPY --from=harness-build /opt/venv /opt/venv

# Backend: dist + node_modules (runtime deps gia installati)
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/

# Harness source: estensione gate (.pi), skill, workspaces, migrazioni
COPY harness/ /app/harness/
# PiProcessManager assume harnessDir/.venv/bin/tht sul PATH del child: symlink al venv reale
RUN ln -s /opt/venv /app/harness/.venv

ENV PATH="/opt/venv/bin:/usr/local/bin:$PATH" \
    HOST=0.0.0.0 PORT=8787 \
    THT_HARNESS_DIR=/app/harness \
    THT_BIN=/opt/venv/bin/tht \
    PI_BIN=pi \
    HOME=/home/thoth

COPY docker/core-entrypoint.sh /app/docker/core-entrypoint.sh
RUN chmod +x /app/docker/core-entrypoint.sh

WORKDIR /app/backend
USER thoth
EXPOSE 8787
HEALTHCHECK --interval=15s --timeout=3s --retries=5 --start-period=25s \
  CMD curl -fsS http://127.0.0.1:8787/health || exit 1
ENTRYPOINT ["/usr/bin/tini","--","/app/docker/core-entrypoint.sh"]
CMD ["server"]

docker/core-entrypoint.sh:

#!/usr/bin/env bash
# Entrypoints logici: server (default) | doctor | tht <args...> | preprocess
set -euo pipefail
cmd="${1:-server}"
case "$cmd" in
  server)    exec node /app/backend/dist/server.js ;;
  doctor)    shift; exec tht -c /app/harness/workspaces/local.yaml doctor "$@" ;;
  tht)       shift; exec tht "$@" ;;
  preprocess) shift; exec tht evidence index "$@" ;;
  *)         exec "$@" ;;
esac

Verifica build:

docker build -f docker/core.Dockerfile -t thothii-core:local .
docker run --rm thothii-core:local doctor --help      # tht raggiungibile
docker run --rm --entrypoint pi thothii-core:local --version  # pi raggiungibile
docker run --rm thothii-core:local node -e 'console.log(process.version)'

7. Fase 5 — Immagine thothii-frontend

Obiettivo: build statica React + nginx-unprivileged con reverse proxy /api → core:8787 (con supporto SSE). La build usa VITE_BACKEND_URL=/api (same-origin via proxy) → zero modifiche al codice frontend.

Files:

  • Create: docker/frontend.Dockerfile
  • Create: docker/nginx.conf

docker/frontend.Dockerfile:

# syntax=docker/dockerfile:1.7
# thothii-frontend: build Vite + nginx-unprivileged (porta 8080).
FROM node:22-bookworm AS build
WORKDIR /src
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
# same-origin: nginx proxierà /api -> core:8787
ARG VITE_BACKEND_URL=/api
ENV VITE_BACKEND_URL=$VITE_BACKEND_URL
RUN npm run build && npm run tsc 2>/dev/null || true   # build -> dist/

FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime
COPY --from=build /src/dist /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080

docker/nginx.conf:

server {
    listen 8080;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Reverse proxy verso il backend (same Docker network)
    location /api/ {
        proxy_pass http://core:8787/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # SSE: niente buffering, timeout lunghi
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        chunked_transfer_encoding on;
    }
}

Verifica PATH frontend: il client usa VITE_BACKEND_URL; con /api tutte le chiamate (REST + SSE) diventano same-origin e passano dal proxy. Confermare che frontend/src/api/client.ts concatena VITE_BACKEND_URL senza forzare http://host:port (in caso contrario, aggiungere supporto a URL relativo — modifica minore).

Verifica build:

docker build -f docker/frontend.Dockerfile -t thothii-frontend:local .
docker run --rm -d -p 8090:8080 thothii-frontend:local
curl -fsS http://localhost:8090/ | head   # serve index.html

8. Fase 6 — compose.yaml

File:

  • Create: compose.yaml

compose.yaml:

name: thothii

services:
  core:
    build:
      context: .
      dockerfile: docker/core.Dockerfile
    image: thothii-core:local
    env_file: [deploy/thothii.env]
    environment:
      HOST: 0.0.0.0
      PORT: 8787
      THT_HARNESS_DIR: /app/harness
      THT_BIN: /opt/venv/bin/tht
      PI_BIN: pi
      AUTH_MODE: ${AUTH_MODE:-none}
      SETTINGS_FILE: /data/settings/settings.json
      MAX_PI_PROCESSES: ${MAX_PI_PROCESSES:-4}
      # I secret THT_DB_*/THT_VEC_*/THT_OLLAMA_URL arrivano da env_file
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - /home/chirone/thothii-data:/data
      - /home/chirone/thothii-data/pi-config:/home/thoth/.pi
      - /home/chirone/chirone/etl/docs/evidence:/data/evidence:ro
    restart: unless-stopped
    networks: [thothii-net]

  frontend:
    build:
      context: .
      dockerfile: docker/frontend.Dockerfile
      args:
        VITE_BACKEND_URL: /api
    image: thothii-frontend:local
    ports:
      - "8080:8080"
    depends_on:
      core:
        condition: service_healthy
    restart: unless-stopped
    networks: [thothii-net]

networks:
  thothii-net:
    driver: bridge

Note:

  • core non espone porte sull'host: è raggiungibile solo via frontend (nginx) sulla rete thothii-net. Per diagnosi dirette, aggiungere temporaneamente ports: ["8787:8787"].
  • host.docker.internal:host-gateway risolve i servizi sull'host (Supabase :5437, pgvector :5438, Ollama :11434).
  • Healthcheck del core (nel Dockerfile) gateda depends_on: condition: service_healthy.

Verifica:

docker compose config --quiet

9. Fase 7 — Bootstrap, migrazioni e smoke test

One-shot (sulla macchina host):

# 0) Struttura persistenza
mkdir -p /home/chirone/thothii-data/{sessions,artifacts,indexes,corpus,settings,pi-config/agent}

# 1) Secret env
cp deploy/thothii.env.example deploy/thothii.env
$EDITOR deploy/thothii.env            # inserire password ruoli (Fase 1)

# 2) Credenziali modello Pi (vedi Fase 3)
cp ~/.pi/agent/{auth.json,settings.json,models.json,trust.json} \
   /home/chirone/thothii-data/pi-config/agent/
chmod -R go-rwx /home/chirone/thothii-data/pi-config

# 3) settings.json del backend
cat > /home/chirone/thothii-data/settings/settings.json <<'JSON'
{ "workspace": "local", "provider": "zai", "model": "glm-5.2", "thinking": "medium" }
JSON

# 4) Build immagini
docker compose build

# 5) Diagnostica adapter (prima di flare-up di sessioni reali)
docker compose run --rm core doctor --json
#   atteso: dwh ok, vector_read ok, vector_write ok, embeddings ok

# 6) Up
docker compose up -d
docker compose ps
curl -fsS http://localhost:8080/api/health    # via proxy nginx -> core

Smoke test funzionale (L2):

  1. Apri http://localhost:8080, crea una nuova domanda.
  2. Verifica: il backend spawna Pi (RPC), il workflow F1 produce un gate, una risposta al gate avanza la fase, F4 usa lo schema dal DWH (direct), la retrieval evidence restituisce risultati (embeddings su Ollama).
  3. docker compose logs -f core per correlation id backend→pi→tht.
  4. Verifica che una sessione finalizzata scriva artefatti in /home/chirone/thothii-data/sessions/<id>/.

Script di smoke automatizzato:

  • Create: scripts/docker-smoke.sh
#!/usr/bin/env bash
set -euo pipefail
docker compose config --quiet
docker compose build
docker compose up -d --wait
curl -fsS http://localhost:8080/api/health
echo "OK: stack healthy"
docker compose down

10. Matrice di verifica

Check Comando Atteso
Compose valido docker compose config --quiet exit 0
Core health curl localhost:8080/api/health 200 ok
tht nel container docker compose run --rm core tht --help help
Pi nel container docker compose run --rm --entrypoint pi core --version 0.80.x
Doctor adapter docker compose run --rm core doctor --json dwh+vector+embeddings ok
DWH read-only login thoth_dwh_reader + UPDATE permission denied
Vector RW login thoth_vector_rw + SELECT/INSERT su vectors ok
Evidence montata docker compose exec core ls /data/evidence 36 MD / sottocartelle
SSE proxy crea sessione nel browser eventi SSE arrivano
Persistenza sessione finalizzata artefatti in thothii-data/sessions/
Test suite (dopo Fase 0) cd backend && npx vitest run && npx tsc --noEmit -p . PASS

11. Inventario file (creati / modificati)

Nuovi:

  • docker/core.Dockerfile, docker/core-entrypoint.sh
  • docker/frontend.Dockerfile, docker/nginx.conf
  • compose.yaml
  • .dockerignore
  • deploy/thothii.env.example, deploy/thothii.env (gitignored)
  • deploy/sql/10-dwh-roles.sql, deploy/sql/20-vector-roles.sql
  • harness/workspaces/local.yaml (workspace co-locato direct)
  • scripts/docker-smoke.sh
  • backend/test/config.test.ts (se non esiste, estensione)

Modificati:

  • backend/src/config.ts (campo host)
  • backend/src/server.ts (usa config.host)
  • .gitignore (deploy/thothii.env)

Operationally generati (host, non nel repo):

  • /home/chirone/thothii-data/{sessions,artifacts,indexes,corpus,settings,pi-config}

12. Operatività

  • Log: docker compose logs -f core frontend. Log strutturati Fastify su stdout.
  • Aggiornamento immagini: git pull && docker compose build && docker compose up -d. I dati persistono sul bind mount.
  • Backup: tar di /home/chirone/thothii-data/ (sessions/settings/pi-config). Per il vector: pg_dump --schema=vectors su :5438.
  • Rotazione secret ruoli DB: ALTER ROLE ... PASSWORD, aggiornare deploy/thothii.env, docker compose up -d core.
  • Scale: il core mantiene stato in-memory dei processi Pi (uno per sessione); non scale-out orizzontale senza session affinity. MAX_PI_PROCESSES limita i child Pi concurrenti.

13. Item aperti / future (NON bloccanti per questo deploy)

  1. Adapter pgvector_direct formale (Plan 3 local-pgvector-profile): il path direct funziona già via config, ma incapsularlo nel contratto VectorStore migliorerebbe testabilità e parità REST/direct. Branch enhancement opzionale.
  2. Runtime-config frontend (window.__THOTHII_CONFIG__): elimina la dipendenza da rebuild Vite per cambiare backend URL. Per questo singolo deploy same-origin non serve; utile per multi-ambiente.
  3. TLS termination: per esposizione non-localhost, aggiungere un reverse proxy (Caddy/Traefik) davanti a :8080 con certificati. Out of scope per il Docker locale.
  4. Container pgvector locale (Profile B): se in futuro si vuole disaccoppiare dal Supabase esistente, il local-vector profile (Plan 3 Task 3) aggiunge pgvector/pgvector:pg16 + migrazioni + volume.
  5. Path portabili (resolve_workspace_paths): il container-packaging Plan 1 rende i path relativi risolti sotto THT_DATA_ROOT. Qui si aggira con path assoluti interni (/data/...), invarianti rispetto all'host — accettabile e più semplice.
  6. Confermare se :5437 e :5438 sono la stessa istanza RISOLTO: è un'unica istanza Postgres; :5438 accesso diretto (usato da ThothII per DWH+vector), :5437 pooler Supavisor. Migrazioni ruoli in un solo run su 5438.

14. Sequenza di esecuzione consigliata

Fase 0 (codice backend)  ──▶ commit
   │
   ├─ Fase 1 (ruoli DB SQL)          [one-shot host]
   ├─ Fase 2 (local.yaml)
   ├─ Fase 3 (secrets env + pi-config + settings.json)
   ├─ Fase 4 (core.Dockerfile)
   ├─ Fase 5 (frontend.Dockerfile + nginx)
   ├─ Fase 6 (compose.yaml)
   └─ Fase 7 (bootstrap + doctor + smoke L2)

Fasi 2–6 sono indipendenti e possono essere sviluppate in parallelo; Fase 7 le valida end-to-end. Fase 0 è prerequisito (il backend non risponderebbe fuori dal container senza HOST=0.0.0.0).


Parte B — Integrazione portale omics_portal (modalità embedded, PRODUZIONE)

La Parte A produce i due container ThothII standalone. La modalità operativa reale è però l'embedding nel portale Django omics_portal: il frontend ThothII appare come contenuto della pagina /datamart-builder dentro il template del portale (sidebar + chrome + colori GSD), e il backend è invisibile dall'esterno (tutto same-origin, senza redirect).

Verifiche sul portale (già esistenti — si riutilizzano):

  • Rotta datamart-builder/ → DatamartBuilderView (kokoro/datamart_catalog_views.py:33) → template kokoro/datamart_builder.html (oggi vuoto).
  • Link sidebar "Datamart Builder" in templates/partials/left-sidebar.html:104-111, gated da capability datamart_builder.access (gruppo Authentik omics-datamart-builder).
  • Portale Docker: web (Gunicorn :8000) + nginx (nginx:alpine, :80) su rete omics_network; nginx serve /static/, /media/, proxy / → django e /airflow/ → upstream (precedente di sub-app sotto prefisso).
  • nginx/nginx.conf è il nginx locale da estendere (montato read-only nel container nginx del portale).
  • Il dominio pubblico aritmolab.policlinicosandonato.it è esposto da un proxy esterno che hittinga il nginx del portale: operare su nginx/nginx.conf è sufficiente.
  • Nessun router nel FE ThothII → l'SPA monta su <div id="root"> (frontend/src/main.tsx:7), embeddabile senza basename.

B1. Topologia embedded

aritmolab.policlinicosandonato.it  →  [proxy esterno]  →  portal nginx :80 (container "nginx")
                                                            │  rete omics_network
   ┌────────────────────────────────────────────────────────┴───────────────────────────┐
   │  portal nginx (omics_portal/nginx/nginx.conf)                                        │
   │    /datamart-builder/api/    → thothii-core:8787   (SSE, auth_request)  [Parte A]   │
   │    /datamart-builder/assets/ → thothii-frontend:8080 (Vite dist)       [Parte A]   │
   │    /datamart-builder/        → django (web:8000) — render chrome + mount React     │
   │    /static/ /media/ /        → django/static                                        │
   ├─────────────────────────────────────────────────────────────────────────────────────┤
   │  web (Django/Gunicorn :8000)        nginx (portal)                                   │
   │  ─ datamart_builder.html + vite_assets tag                                          │
   └─────────────────────────────────────────────────────────────────────────────────────┘
   ┌── ThothII (compose proprio, reta omics_network esterna) ────────────────────────────┐
   │  thothii-core:8787        thothii-frontend:8080 (serve Vite dist + manifest.json)   │
   └─────────────────────────────────────────────────────────────────────────────────────┘

Principi: il backend ThothII non espone porte sull'host (invisibile); il browser parla solo con aritmolab.../datamart-builder/* (same-origin); le API/SSE passano dal proxy nginx del portale senza redirect.

B2. Rete condivisa — bridge tra i due compose

ThothII core + frontend devono essere raggiungibili dal nginx del portale (su omics_network). Si dichiara omics_network come external nel compose ThothII e vi si attaccano entrambi i servizi.

Modifica compose.yaml (ThothII) — aggiungi rete external + profilo embedded:

services:
  core:
    # ... (come Parte A) ...
    networks: [omics_network]          # <-- join rete portale (era thothii-net)
  frontend:
    # ... (come Parte A) ...
    networks: [omics_network]
    # in embedded NON si espongono porte sull'host:
    # ports: rimosso (il portale proxya)
networks:
  omics_network:
    external: true                      # creata dal compose omics_portal

Ordine di deploy: prima omics_portal up (crea omics_network), poi ThothII up. Verifica: docker network inspect omics_network mostra thothii-core, thothii-frontend, omics_portal-nginx-*, omics_portal-web-*.

B3. Build frontend "embedded" (base path + manifest)

Il build Vite deve (a) servire asset sotto /datamart-builder/assets/, (b) emettere manifest.json per far risolvere i nomi hashati da Django, (c) puntare le API a /datamart-builder/api.

Modifica frontend/vite.config.ts:

export default defineConfig(({ mode }) => ({
  plugins: [react()],
  // base: prefisso per gli asset. Default "/" (standalone); "/datamart-builder/assets/" in embedded.
  base: process.env.VITE_BASE ?? "/",
  // assetsDir vuoto in embedded: gli asset finiscono alla root di dist/ così il proxy
  // /datamart-builder/assets/ → frontend-root mappa 1:1 (niente raddoppio /assets/assets/).
  build: { manifest: true, outDir: "dist", assetsDir: process.env.VITE_BASE ? "" : "assets" },
  resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
}));

Build embedded:

cd frontend
VITE_BASE=/datamart-builder/assets/ VITE_BACKEND_URL=/datamart-builder/api npm run build
# → dist/manifest.json + dist/index-<hash>.js + dist/index-<hash>.css (asset alla root)

Il frontend.Dockerfile (Parte A) deve accettare questi arg come ARG e passarli al build:

ARG VITE_BASE=/datamart-builder/assets/
ARG VITE_BACKEND_URL=/datamart-builder/api
ENV VITE_BASE=$VITE_BASE VITE_BACKEND_URL=$VITE_BACKEND_URL

Il frontend container serve dist/ (incluso manifest.json) su :8080 come da Parte A (nginx-unprivileged).

B4. nginx portale — rotte /datamart-builder

Modifica omics_portal/nginx/nginx.conf — aggiungi upstream + location prima del catch-all /:

upstream django     { server web:8000; }
upstream airflow    { server 172.18.0.1:8087; }
upstream thothii_core      { server thothii-core:8787; }       # <-- nuovo
upstream thothii_frontend  { server thothii-frontend:8080; }   # <-- nuovo

server {
    listen 80; server_name _;
    client_max_body_size 100M;

    # ... /static/, /media/, /airflow/ invariati ...

    # --- ThothII: API + SSE verso il backend (auth_request gate, vedi B6) ---
    location /datamart-builder/api/ {
        auth_request /_thothii_auth;
        proxy_pass http://thothii_core/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_redirect off;
        # SSE: niente buffering, timeout lunghi
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        chunked_transfer_encoding on;
    }

    # --- ThothII: asset statici Vite (hashati) dal frontend container ---
    location /datamart-builder/assets/ {
        proxy_pass http://thothii_frontend/;
        proxy_set_header Host $host;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # auth_request subrequest (B6)
    location = /_thothii_auth {
        internal;
        proxy_pass http://django/datamart-builder/api-auth;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }

    # catch-all Django (invariato)
    location / { proxy_pass http://django; /* ... */ }
}

Nota sul path-stripping: proxy_pass http://thothii_core/ (slash finale) strip /datamart-builder/api/ → il core riceve /health, /sessions, ... (matcha le rotte Fastify). Stessa meccanica del proxy /airflow/ già in produzione.

B5. Django — template + view + template tag Vite

B5.1 Template tag per risolvere gli asset Vite dal manifest. Crea kokoro/templatetags/vite.py:

import json, urllib.request
from django import template
from django.core.cache import cache

register = template.Library()
_MANIFEST_URL = "http://thothii-frontend:8080/manifest.json"  # stessa rete omics_network

def _load_manifest() -> dict:
    m = cache.get("thothii_vite_manifest")
    if m is None:
        with urllib.request.urlopen(_MANIFEST_URL, timeout=2) as r:
            m = json.loads(r.read())
        cache.set("thothii_vite_manifest", m, timeout=None)
    return m

@register.simple_tag
def vite_assets(entry: str = "src/main.tsx") -> str:
    """Ritorna HTML <script>/<link> per l'entry Vite (asset hashati, cacheinati)."""
    try:
        e = _load_manifest()[entry]
    except Exception:
        return "<!-- thothii manifest non disponibile -->"
    tags = [f'<link rel="stylesheet" href="/datamart-builder/assets/{c}">'
            for c in e.get("css", [])]
    tags.append(f'<script type="module" src="/datamart-builder/assets/{e["file"]}"></script>')
    return "".join(tags)

(L'URL base /datamart-builder/assets/ deve matchare il VITE_BASE del build — vedi B3.)

B5.2 Sostituisci templates/kokoro/datamart_builder.html:

{% extends "vertical_base.html" %}
{% load i18n vite %}

{% block title %}Datamart Builder{% endblock %}

{% block content %}
<div class="container-fluid">
  <div class="page-title-box"><h4 class="page-title">Datamart Builder</h4></div>
  {# Mount point dell'SPA ThothII. L'app eredita i colori GSD dal portale (già conforme). #}
  <div id="root" style="height: calc(100vh - 140px);"></div>
</div>
{% endblock %}

{% block extra_javascript %}
{{ "{% vite_assets %}" }}  {# emette <script>/<link> con hash corretti dal manifest #}
{% endblock %}

DatamartBuilderView resta invariato: CapabilityRequiredMixin + datamart_builder.access protegge già la pagina.

B5.3 Verifica FE su path non-root: confermare che nessun modulo usi import.meta.env.BASE_URL per costruire URL assoluti (grep: nessun match → OK). L'SPA non usa router, quindi niente basename.

B6. Auth layer — gate delle API same-origin (raccomandato)

La pagina è già protetta da CapabilityRequiredMixin, ma le chiamate API /datamart-builder/api/* passano dal proxy nginx e bypassano la vista Django. Per gaterle professionalmente:

B6.1 Vista di capability check — aggiungi a kokoro/datamart_catalog_views.py:

from django.http import HttpResponse, HttpResponseForbidden
from accounts.capabilities import has_capability, DATAMART_BUILDER_ACCESS

def datamart_builder_api_auth(request):
    """auth_request target: 200 se l'utente autenticato ha datamart_builder.access, altrimenti 403."""
    if request.user.is_authenticated and has_capability(request.user, DATAMART_BUILDER_ACCESS):
        return HttpResponse("ok")
    return HttpResponseForbidden()

B6.2 URL — in kokoro/urls.py:

path("datamart-builder/api-auth", datamart_builder_api_auth, name="datamart_builder_api_auth"),

B6.3 Core in AUTH_MODE=none (crede al proxy): il gate di sicurezza è l'auth_request nginx → Django. Il cookie di sessione del portale viaggia same-origin sulla subrequest → Django valida.

Verifica: utente senza gruppo omics-datamart-builder → GET /datamart-builder/api/sessions restituisce 403; con il gruppo → 200.

B7. Pipeline di build/deploy embedded

# 1) Portale up (crea omics_network)
cd /home/chirone/omics_portal && docker compose up -d

# 2) ThothII: build + up (core + frontend su omics_network)
cd /home/chirone/ThothII
VITE_BASE=/datamart-builder/assets/ docker compose build
docker compose up -d

# 3) Portale: ricarica nginx con le nuove rotte + Django con il template tag
cd /home/chirone/omics_portal
docker compose exec web python manage.py collectstatic --noinput   # se servisse
docker compose restart nginx web

# 4) Smoke sul path pubblico
curl -fsS https://aritmolab.policlinicosandonato.it/datamart-builder/   # 200 + <div id="root">
curl -fsS https://aritmolab.policlinicosandonato.it/datamart-builder/api/health  # 200 ok (se auth)

Dopo un rebuild del FE (nuovi hash): invalidare la cache Django del manifest (docker compose exec web python manage.py shell -c "from django.core.cache import cache; cache.delete('thothii_vite_manifest')") o restart web.

B8. Matrice di verifica embedded (estende §10)

Check Comando Atteso
Rete condivisa docker network inspect omics_network core+frontend+portal nginx/web
Pagina pubblica curl .../datamart-builder/ 200, contiene <div id="root">
Asset Vite curl .../datamart-builder/assets/index-*.js 200 JS
Manifest curl http://thothii-frontend:8080/manifest.json (dal web) JSON
API same-origin browser: sessione nuova domanda SSE + chiamate OK, no redirect
Auth API (negativo) utente senza gruppo → GET /datamart-builder/api/sessions 403
Auth API (positivo) utente con gruppo 200
Colori/chrome browser visivo sidebar+header portale attorno all'SPA

Parte C — Checklist FINALE di setup (cose da fare tu)

Promemoria richiesto: azioni manuali non automatizzabili che restano a carico tuo per completare il deploy.

One-shot (preparazione)

  1. Password ruoli DB — genera e inserisci in deploy/thothii.env: THT_DB_PASSWORD (thoth_dwh_reader), THT_VEC_PASSWORD (thoth_vector_rw). Esegui i SQL deploy/sql/10-dwh-roles.sql + 20-vector-roles.sql (Fase 1) con psql sull'host.
  2. Conferma porte Supabase RISOLTO (verificato): istanza Postgres unica; :5438 = accesso diretto con datawarehouse + vectors; :5437 = pooler Supavisor (ignorato). Esegui entrambi i SQL 10-dwh-roles.sql + 20-vector-roles.sql su 5438 (psql ... -p 5438 -U postgres).
  3. Credenziali modello Pi — copia da ~/.pi/agent/ a /home/chirone/thothii-data/pi-config/agent/ i file auth.json, settings.json, models.json, trust.json (Fase 3). chmod -R go-rwx.
  4. settings.json — incolla in /home/chirone/thothii-data/settings/settings.json la stringa provider/model copiata dal tuo ~/.pi/agent/settings.json (workspace:"local", thinking a piacere).
  5. Gruppo Authentik — assegna gli utenti che devono usare Datamart Builder al gruppo omics-datamart-builder (abilita la voce di sidebar + la capability).

Build & deploy (in ordine)

  1. Fase 0 → commit (backend HOST).
  2. omics_portal up (crea la rete), poi ThothII docker compose build && up -d.
  3. docker compose run --rm core doctor --json → dwh+vector+embeddings ok.
  4. Restart nginx + web del portale dopo aver editato nginx.conf + aggiunto template tag.

Verifica finale

  1. Apri https://aritmolab.policlinicosandonato.it/datamart-builder come utente con omics-datamart-builder: vedi il portale (sidebar+chrome) con dentro ThothII funzionante (nuova domanda, F1 gate, F4 schema dal DWH, retrieval evidence).
  2. Conferma: nessun redirect visibile, SSE funziona (transcript live), il backend :8787 NON è raggiungibile dall'esterno.

Manutenzione

  • Aggiornamento immagini ThothII: git pull && docker compose build && up -d + restart web portale (refresh cache manifest).
  • Rotazione password DB: ALTER ROLE + aggiorna deploy/thothii.env + up -d core.
  • Backup: /home/chirone/thothii-data/ (sessions/settings/pi-config) + pg_dump --schema=vectors su :5438.