44 KiB
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, rifiutapostgresbare);:5438è l'accesso diretto alla stessa istanza Postgres (postgres/postgres) che contiene entrambi gli schemadatawarehouse(163 tabelle fact/dim/bridge) evectors(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 connpm 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:5ascolta su127.0.0.1hardcoded → va reso configurabile (Fase 0). - Gap codice #2:
PiProcessManager(backend/src/pi/pi-process-manager.ts:26) prependeharnessDir/.venv/binal PATH del child Pi e usacwd=harnessDir→ nel container serve symlink/app/harness/.venv → /opt/venv. psd.yamlè symlink rotto (path macOS) → si crea un nuovo workspacelocal.yaml(Fase 2); il backend seleziona il workspace viasettings.json {workspace:"local"}+workspaces/<name>.yaml(tht-runner.ts:46-51).- Host:
:5438accesso diretto Postgres (DWHdatawarehouse+ vectorvectors, stessa istanza;:5437è il pooler Supavisor non usato da ThothII),:11434Ollama — tutti in ascolto su0.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):
thothii-core:local— backend Fastify (Node) + harness Python (tht) + runtime Pi. Un solo container, entrypointserver.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: aggiungihost: env.HOST ?? "127.0.0.1"all'AppConfiginterface + nel return diloadConfig.server.ts:5: sostituiscihost: "127.0.0.1"conhost: 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 sudatawarehouse) - Create:
deploy/sql/20-vector-roles.sql(ruolo read+write suvectors) — 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 intht/config.py(LshConfig); l'esempio usan_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(aggiungideploy/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/apitutte le chiamate (REST + SSE) diventano same-origin e passano dal proxy. Confermare chefrontend/src/api/client.tsconcatenaVITE_BACKEND_URLsenza forzarehttp://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:
corenon espone porte sull'host: è raggiungibile solo viafrontend(nginx) sulla retethothii-net. Per diagnosi dirette, aggiungere temporaneamenteports: ["8787:8787"].host.docker.internal:host-gatewayrisolve 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):
- Apri
http://localhost:8080, crea una nuova domanda. - 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).
docker compose logs -f coreper correlation id backend→pi→tht.- 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.shdocker/frontend.Dockerfile,docker/nginx.confcompose.yaml.dockerignoredeploy/thothii.env.example,deploy/thothii.env(gitignored)deploy/sql/10-dwh-roles.sql,deploy/sql/20-vector-roles.sqlharness/workspaces/local.yaml(workspace co-locato direct)scripts/docker-smoke.shbackend/test/config.test.ts(se non esiste, estensione)
Modificati:
backend/src/config.ts(campohost)backend/src/server.ts(usaconfig.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:
tardi/home/chirone/thothii-data/(sessions/settings/pi-config). Per il vector:pg_dump --schema=vectorssu:5438. - Rotazione secret ruoli DB:
ALTER ROLE ... PASSWORD, aggiornaredeploy/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_PROCESSESlimita i child Pi concurrenti.
13. Item aperti / future (NON bloccanti per questo deploy)
- Adapter
pgvector_directformale (Plan 3local-pgvector-profile): il path direct funziona già via config, ma incapsularlo nel contrattoVectorStoremigliorerebbe testabilità e parità REST/direct. Branch enhancement opzionale. - 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. - TLS termination: per esposizione non-localhost, aggiungere un reverse proxy (Caddy/Traefik) davanti a
:8080con certificati. Out of scope per il Docker locale. - Container pgvector locale (Profile B): se in futuro si vuole disaccoppiare dal Supabase esistente, il
local-vectorprofile (Plan 3 Task 3) aggiungepgvector/pgvector:pg16+ migrazioni + volume. - Path portabili (
resolve_workspace_paths): il container-packaging Plan 1 rende i path relativi risolti sottoTHT_DATA_ROOT. Qui si aggira con path assoluti interni (/data/...), invarianti rispetto all'host — accettabile e più semplice. Confermare seRISOLTO: è un'unica istanza Postgres;:5437e:5438sono la stessa istanza:5438accesso diretto (usato da ThothII per DWH+vector),:5437pooler 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) → templatekokoro/datamart_builder.html(oggi vuoto). - Link sidebar "Datamart Builder" in
templates/partials/left-sidebar.html:104-111, gated da capabilitydatamart_builder.access(gruppo Authentikomics-datamart-builder). - Portale Docker:
web(Gunicorn :8000) +nginx(nginx:alpine, :80) su reteomics_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 sunginx/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 %}
DatamartBuilderViewresta invariato: CapabilityRequiredMixin +datamart_builder.accessprotegge 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)
- Password ruoli DB — genera e inserisci in
deploy/thothii.env:THT_DB_PASSWORD(thoth_dwh_reader),THT_VEC_PASSWORD(thoth_vector_rw). Esegui i SQLdeploy/sql/10-dwh-roles.sql+20-vector-roles.sql(Fase 1) conpsqlsull'host. Conferma porte SupabaseRISOLTO (verificato): istanza Postgres unica;:5438= accesso diretto condatawarehouse+vectors;:5437= pooler Supavisor (ignorato). Esegui entrambi i SQL10-dwh-roles.sql+20-vector-roles.sqlsu 5438 (psql ... -p 5438 -U postgres).- Credenziali modello Pi — copia da
~/.pi/agent/a/home/chirone/thothii-data/pi-config/agent/i fileauth.json,settings.json,models.json,trust.json(Fase 3).chmod -R go-rwx. - settings.json — incolla in
/home/chirone/thothii-data/settings/settings.jsonla stringa provider/model copiata dal tuo~/.pi/agent/settings.json(workspace:"local", thinking a piacere). - 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)
- Fase 0 → commit (backend
HOST). omics_portalup (crea la rete), poiThothIIdocker compose build && up -d.docker compose run --rm core doctor --json→ dwh+vector+embeddings ok.- Restart
nginx+webdel portale dopo aver editatonginx.conf+ aggiunto template tag.
Verifica finale
- Apri
https://aritmolab.policlinicosandonato.it/datamart-buildercome utente conomics-datamart-builder: vedi il portale (sidebar+chrome) con dentro ThothII funzionante (nuova domanda, F1 gate, F4 schema dal DWH, retrieval evidence). - Conferma: nessun redirect visibile, SSE funziona (transcript live), il backend
:8787NON è raggiungibile dall'esterno.
Manutenzione
- Aggiornamento immagini ThothII:
git pull && docker compose build && up -d+ restartwebportale (refresh cache manifest). - Rotazione password DB:
ALTER ROLE+ aggiornadeploy/thothii.env+up -d core. - Backup:
/home/chirone/thothii-data/(sessions/settings/pi-config) +pg_dump --schema=vectorssu:5438.