From 0c9b44e61f0e4783a105679c20f70d8cc43603ad Mon Sep 17 00:00:00 2001 From: User Date: Sun, 12 Jul 2026 16:27:44 +0200 Subject: [PATCH] docs(deploy): local docker + portal embed implementation plan --- ...7-12-local-docker-deploy-implementation.md | 979 ++++++++++++++++++ 1 file changed, 979 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-12-local-docker-deploy-implementation.md diff --git a/docs/superpowers/plans/2026-07-12-local-docker-deploy-implementation.md b/docs/superpowers/plans/2026-07-12-local-docker-deploy-implementation.md new file mode 100644 index 00000000..f331a82b --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-local-docker-deploy-implementation.md @@ -0,0 +1,979 @@ +# 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/.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 +```typescript +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): +```sql +-- 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): +```sql +-- 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`): +```bash +PGPASSWORD=postgres psql -h localhost -p 5438 -U postgres -d postgres \ + -v PWD="" -f deploy/sql/10-dwh-roles.sql +PGPASSWORD=postgres psql -h localhost -p 5438 -U postgres -d postgres \ + -v 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`:** +```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`):** +```bash +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`:** +```sh +# === 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: +```bash +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`: +```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`:** +```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`:** +```sh +#!/usr/bin/env bash +# Entrypoints logici: server (default) | doctor | tht | 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:** +```bash +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`:** +```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`:** +```nginx +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:** +```bash +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`:** +```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:** +```bash +docker compose config --quiet +``` + +--- + +## 9. Fase 7 — Bootstrap, migrazioni e smoke test + +**One-shot (sulla macchina host):** + +```bash +# 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//`. + +**Script di smoke automatizzato:** +- Create: `scripts/docker-smoke.sh` +```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 `
` (`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: +```yaml +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`:** +```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:** +```sh +cd frontend +VITE_BASE=/datamart-builder/assets/ VITE_BACKEND_URL=/datamart-builder/api npm run build +# → dist/manifest.json + dist/index-.js + dist/index-.css (asset alla root) +``` +Il `frontend.Dockerfile` (Parte A) deve accettare questi arg come `ARG` e passarli al build: +```dockerfile +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 `/`: +```nginx +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`: +```python +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 ') + 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`:** +```html +{% extends "vertical_base.html" %} +{% load i18n vite %} + +{% block title %}Datamart Builder{% endblock %} + +{% block content %} +
+

Datamart Builder

+ {# Mount point dell'SPA ThothII. L'app eredita i colori GSD dal portale (già conforme). #} +
+
+{% endblock %} + +{% block extra_javascript %} +{{ "{% vite_assets %}" }} {# emette