[DOCS] Standard cross-suite AgileHub + governance CLAUDE.md + registri agent

- CLAUDE.md: TZ, SSO, vault-steward, versioning, persona v2.0, multitenant, KB RAG
- docs/standards: persona-conversational-rules v2.0
- docs/STANDARD_*: installer-integration, email-relay, AI-prodotto, marketing-tenant, multitenant
- AGENT_CHANGES.md + OPEN_TICKETS.md (registri agent automatico)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-05-29 15:41:54 +02:00
parent 1d13166d7a
commit c0bf7b6c15
9 changed files with 3920 additions and 1 deletions

35
AGENT_CHANGES.md Normal file
View File

@ -0,0 +1,35 @@
# Agent Changes Log — nis2-agile
> Modifiche applicate automaticamente dall'agent AI AgileHub (ticket-agent-cron).
> Leggere ad ogni sessione per sapere cosa e cambiato.
---
## COMUNICAZIONE UFFICIALE AgileHub — 2026-04-16
### Nuovo Standard Cron per tutti i prodotti
A seguito di una richiesta TRPG per aggiungere un cron session-cleanup, e stato formalizzato uno standard unico per tutti i prodotti che vogliono schedulare cron sul crontab Hetzner.
**Standard completo (sempre aggiornato):**
- GET http://172.18.0.1:4214/standards/standard_cron
- GET http://172.18.0.1:4214/standards/cron_registry (22 cron attivi)
**Regole obbligatorie:**
1. Script in /var/www/<prodotto>/scripts/<nome>.sh
2. Log in /var/log/<prodotto>-<nome>.log
3. export TZ=Europe/Rome in testa allo script
4. Idempotente (rilanciabile senza danni)
5. Isolamento: tocca solo risorse del proprio prodotto
6. Aggiornare docs/CRON_REGISTRY.md (in agile-services)
7. Richiesta a agile-services per la modifica al crontab root (NO modifiche dirette)
**Il tuo CLAUDE.md e stato aggiornato** con la regola — il tuo agent la leggera alla prossima sessione.
**Altri standard gia attivi:**
- /standards/standard_versioning (SemVer + auto-bump dopo apply)
- /standards/standard_timezone (UTC nel DB, Europe/Rome in visualizzazione)
Per richieste cron future: segui il processo descritto nello standard e contatta agile-services.
---

525
CLAUDE.md
View File

@ -1,5 +1,110 @@
<!-- STANDARD:timezone-conventions:v1.0:start -->
## ⏰ ORARI E TIMEZONE — REGOLE OPERATIVE
> **TL;DR**: l autorità del progetto è **`Europe/Rome`** (CEST estate UTC+2, CET inverno UTC+1).
> Quando scrivi un timestamp, **indica SEMPRE il suffisso TZ** (`CEST`/`CET`/`UTC`) oppure usa ISO8601 con offset (`2026-05-09T16:19:00+02:00`). **Mai** timestamp ambigui.
| Sistema | TZ | Output esempio |
|---|---|---|
| Host Hetzner | `Europe/Rome` | `Sat May 09 16:19 CEST 2026` |
| Container DevEnv (alcuni in drift UTC, vedi standard full) | misto | verifica con `docker exec <c> date` |
| Container produzione | `Europe/Rome` | CEST |
| MySQL `time_zone` | `SYSTEM` | `NOW()` CEST, `UTC_TIMESTAMP()` UTC |
| Apache log `%t` | `Europe/Rome` (locale) | `[09/May/2026:13:04:52 +0200]` |
| Node.js MS | UTC interno | `Date().toISOString()``Z` |
| Crontab Hetzner | `Europe/Rome` | `0 3 * * *` = 03:00 italiane |
**Regole**:
1. **Audit/sequenze cross-MS** → UTC obbligatorio (`2026-05-09T14:19:00Z`)
2. **Doc operativi/UI** → CEST/CET con suffisso esplicito
3. **DB store** → UTC, display → locale
4. **Cron critici**`CRON_TZ=UTC` o fuori finestra DST 02:00-03:00 locale
**DST Italia**: ultima domenica marzo (CET→CEST, ora 02:00 saltata) + ultima domenica ottobre (CEST→CET, ora 02:00-03:00 duplicata).
**Spec completa**: `STANDARD_TIMEZONE_CONVENTIONS.md` (slug `timezone-conventions` v1.0, owner VIGILE).
<!-- STANDARD:timezone-conventions:v1.0:end -->
# NIS2 Agile - Documentazione Progetto
## REGOLE DI GOVERNANCE (LEGGERE ATTENTAMENTE, aggiornate 2026-04-22)
> **Queste regole sono OBBLIGATORIE e non negoziabili.**
### REGOLA FONDAMENTALE: Gitea = SOLO Backup
> **Gitea e un BACKUP one-way (sorgente -> Gitea), NON la fonte di verita.**
> **Il webhook auto-pull e DISABILITATO su tutti i 13 repo dal 2026-04-22.**
>
> - Le modifiche che fai nel container sono GIA live su `/var/www/nis2-agile/` via bind mount
> - `/var/www/nis2-agile/` e la FONTE DI VERITA
> - NON proporre MAI "git pull da Gitea" per applicare modifiche
> - Per tirare giu qualcosa da Gitea serve richiesta esplicita dell utente
> - git push -> Gitea = OK (backup)
> - git pull da Gitea -> `/var/www/nis2-agile/` = NO (puo sovrascrivere modifiche vere)
### ARCHITETTURA: PHP-FPM in Docker con BIND MOUNT (LIVE)
> **Verificato 2026-04-22**: NIS2 gira con:
> - `nis2-app` (php-fpm) con bind mount **RW** su `/var/www/nis2-agile/application` e `/public`
> - `nis2-web` (nginx) con bind mount **RO** su `/public`
> - `nis2-db` (MySQL) per persistenza
> - `nis2-qdrant` (vector DB)
> - Apache esterno ha `DocumentRoot /var/www/nis2-agile/public`
**Cosa va LIVE ISTANTANEAMENTE:**
- File `.php` in `/var/www/nis2-agile/application/` -- PHP-FPM rilegge ad ogni request
- File in `/var/www/nis2-agile/public/` -- serviti da nginx (via bind mount :ro)
- File `.html/.css/.js` nel public -- live via nginx/Apache
**Cosa richiede azione (CHIEDI SEMPRE CONFERMA):**
- Modifiche a `docker/nginx.conf` -> `docker restart nis2-web`
- Modifiche al Dockerfile -> `docker compose build + up -d`
- Schema DB (`nis2-db`) -> SQL manuale
- Worker php cron/feedback -> attendono il prossimo run o restart
**Nota**: non serve MAI fare rebuild di nis2-app per cambi di codice PHP -- il bind mount :rw garantisce che php-fpm legga sempre la versione aggiornata.
### Cosa PUOI fare autonomamente:
- Leggere codice sorgente e documentazione
- Eseguire query SELECT sul database
- Analizzare log (Apache, Docker, PM2)
- Proporre modifiche e mostrare diff (SENZA applicarle)
- Verificare stato dei servizi
### Cosa richiede CONFERMA dell utente:
- **Modificare QUALSIASI file** (potrebbe essere live istantaneamente!)
- **git commit e git push** (e un backup, ma sempre da confermare)
- **Modifiche schema DB** (ALTER/CREATE/DROP TABLE)
- **INSERT/UPDATE/DELETE** su dati di produzione
- **Installazione dipendenze** (composer require, npm install)
- **Modifiche a configurazione** (.env, docker-compose.yml, vhost Apache)
- **docker compose build/restart**, **pm2 restart**, **systemctl** qualsiasi
### DIVIETI ASSOLUTI:
- **MAI fare git pull** da Gitea senza richiesta esplicita
- **MAI fare git reset --hard** o operazioni distruttive
- **MAI toccare altri progetti o container**
- **MAI modificare configurazioni di sistema** (Apache globale, PHP globale, MySQL root)
- **MAI cancellare dati** senza backup e conferma utente
- **MAI tentare deploy SSH/SCP** verso altri server
### Flusso CORRETTO per una modifica:
1. **Analizza**: leggi il codice, capisci il problema
2. **Proponi**: mostra le modifiche all utente (diff) SENZA applicarle
3. **Attendi conferma**: l utente decide se procedere
4. **Applica**: solo dopo conferma
5. **Distingui**: e live subito o serve un rebuild/restart? Di all utente chiaramente
6. **Verifica**: controlla che https://nis2.agile.software funzioni (se applicabile)
7. **Backup su Gitea**: git commit + push (solo dopo conferma utente)
### Se qualcosa va storto:
- **NON tentare fix distruttivi** (reset, force push, drop, rm -rf)
- **NON proporre `git pull`** come recupero
- Comunica il problema all utente con dettagli precisi
## PRIMA DI INIZIARE
- Leggi sempre questo file prima di iniziare qualsiasi lavoro
- Il progetto e' al **100% di completamento + Sprint Simulazioni + Audit Chain + Sistema Feedback AI** (~34.000 righe, 85+ file sorgente)
@ -377,7 +482,7 @@ Tutti i moduli sono implementati e testati:
### Workflow
1. Scrivi landing/presentazione nel tuo repo
2. Commit + push (webhook aggiorna /var/www/)
2. Commit + push (backup su Gitea (webhook DISABILITATO dal 2026-04-22))
3. Chiedi conferma utente
4. Aggiorna products.json con URL assoluto
5. Verifica URL raggiungibile
@ -388,3 +493,421 @@ Tutti i moduli sono implementati e testati:
- Dopo QUALSIASI modifica a: URL produzione, dominio, porta, path, schema DB, architettura -> **AGGIORNARE CLAUDE.md IMMEDIATAMENTE**
- CLAUDE.md e la "single source of truth" del progetto
- A fine sessione: verificare che CLAUDE.md rifletta lo stato reale
---
## Knowledge Base Multi-Livello (Migration 012-014 - 2026-04-11)
### Cosa e cambiato
NIS2 ora ha un sistema RAG completo con visibilita' a 3 livelli (SYSTEM/FIRM/ORG), coerente col pattern gia' applicato a TRPG e SustainAI. L'AI puo' rispondere alle domande pescando da documenti caricati dai consulenti o dai responsabili compliance.
| Scope | Chi possiede | Chi vede |
|---|---|---|
| SYSTEM | Vendor (Agile Tech) | Tutti gli utenti del prodotto |
| FIRM | Studio di consulenza (consulting_firm_id) | Tutti i collaboratori dello studio + organizations esplicitamente condivise |
| ORG | Singola organization cliente | Solo gli utenti di quella org (org_admin/compliance_manager) |
### Stack RAG nuovo
- **nis2-qdrant** (container nuovo): qdrant/qdrant:v1.7.4, network nis2-net, IP fisso 172.21.0.5 (workaround DNS musl Alpine - vedi sotto).
- Voyage AI embeddings (`voyage-3-lite`, 512 dim, output_dimension=512). Chiave shared con sustainai.
- Collection Qdrant: `nis2_kb` (Cosine, 512 dim).
### Schema MySQL (nis2_agile_db)
- **Migration 012**: nuova tabella `consulting_firms` (ragione sociale, p.iva, plan, max_organizations, max_users, status). ALTER `users.consulting_firm_id` e `organizations.consulting_firm_id`.
- **Migration 013**: nuove tabelle `firm_org_assignments` (mapping firm-org-user) e `kb_uploaded_documents` (audit log dei doc caricati con qdrant_doc_uuid, scope, consulting_firm_id, organization_id, shared_with_orgs JSON, chunk_count, status).
### File creati/modificati
**Backend (PHP)**:
- `application/services/VectorService.php` (nuovo) - client Qdrant + buildAuthzFilter
- `application/services/EmbedService.php` (nuovo) - client Voyage AI
- `application/services/RagService.php` (nuovo) - pipeline embed + search + format context
- `application/services/AIService.php` (esteso) - aggiunto metodo `askWithRag(question, userContext)` che fa RAG su KB e inietta il contesto nel system prompt Claude. Fallback graceful se RAG non disponibile.
- `application/controllers/KnowledgeBaseController.php` (nuovo, ~340 righe) - 5 endpoint:
- `POST /api/knowledgebase/ingest` - carica testo, embed, upsert Qdrant + insert tracking MySQL
- `GET /api/knowledgebase/list` - lista doc visibili (filtro WHERE in MySQL)
- `GET /api/knowledgebase/firmOrgs` - lista organizations del firm dell'utente (per multi-select UI)
- `POST /api/knowledgebase/search` - search semantica preview
- `DELETE /api/knowledgebase/{id}` - cancella doc + chunk Qdrant via doc_uuid
- `public/index.php` (esteso) - registrato `knowledgebase` nel controllerMap + actionMap
**Schema SQL**:
- `docs/sql/012_consulting_firms.sql` (nuovo)
- `docs/sql/013_firm_assignments.sql` (nuovo)
**Frontend**:
- `public/kb.html` (nuovo) - pagina dedicata Knowledge Base con form upload + lista doc + search preview
- `public/js/kb.js` (nuovo, ~210 righe) - handler upload con auto-detect role/firm da `/api/auth/me`
- `public/js/common.js` (esteso) - voce "Knowledge Base" (icona libro) aggiunta in sezione "Gestione" della sidebar
**Infrastruttura**:
- `docker/docker-compose.yml`:
- Aggiunto servizio `qdrant` (container nis2-qdrant) con volume `nis2-qdrant-data`
- Aggiunto al servizio `app`: env `VOYAGE_API_KEY`, `VOYAGE_MODEL`, `QDRANT_URL=http://172.21.0.5:6333`
- `.env`: aggiunte `VOYAGE_API_KEY=pa-...` e `VOYAGE_MODEL=voyage-3-lite`
### Logica visibilita' (in `VectorService::buildAuthzFilter`)
```
should:
- scope=SYSTEM
- scope=FIRM AND consulting_firm_id = $user.firm_id
- scope=FIRM AND shared_with_orgs CONTAINS $user.organization_id
- scope=ORG AND organization_id = $user.organization_id
```
### Workaround Alpine musl + PHP-FPM
**Importante**: il container `nis2-app` (PHP 8.4-fpm-alpine) ha un bug noto di DNS resolution combinato a PHP-FPM `clear_env` default `yes`:
1. PHP-FPM workers in HTTP context NON risolvono hostname Docker (es. `nis2-qdrant`) — `Could not resolve host`
2. PHP-FPM workers svuotano l'env, quindi `getenv('QDRANT_URL')` ritorna stringa vuota
3. CLI php funziona normalmente
**Workaround applicato in VectorService e EmbedService**: multi-source lookup `getenv() || $_SERVER || $_ENV || hardcoded_default`. L'IP 172.21.0.5 e' hardcoded come fallback per nis2-qdrant. Anche VOYAGE_API_KEY ha un default hardcoded.
**Side effect**: se nis2-qdrant viene ricreato con IP diverso, va aggiornato l'IP in:
- `docker/docker-compose.yml` env `QDRANT_URL`
- `application/services/VectorService.php` fallback constructor
### Test E2E eseguito (2026-04-11)
3 chunk seed in Qdrant (SYSTEM, FIRM 99 con share alla org 901, FIRM 100 senza share) testati con 4 user context. Tutti i casi passano:
| Caso | userContext | Atteso | Risultato |
|---|---|---|---|
| 1 | firm 99 + org 901 | doc1 (SYSTEM) + doc2 (FIRM 99) | OK |
| 2 | firm 99 + org 902 | doc1 + doc2 (perche membro firm) | OK |
| 3 | firm 100 + org 903 | doc1 + doc3 (perche membro firm) | OK |
| 4 | no firm, no org | solo doc1 (SYSTEM) | OK |
**Nessun cross-firm leak**: case 1 e 2 NON vedono doc3 (FIRM 100); case 3 NON vede doc2 (FIRM 99); case 4 vede solo SYSTEM.
### Endpoint backend (additivi)
- `GET /api/knowledgebase/firmOrgs` - lista organizations del firm dell'utente
- `POST /api/knowledgebase/ingest` - body JSON `{title, text, entity_type?, scope?, shared_with_orgs?, organization_id?}`
- `GET /api/knowledgebase/list` - lista doc visibili
- `POST /api/knowledgebase/search` - body `{query, top_k?}`
- `DELETE /api/knowledgebase/{id}` - cancella doc + chunk Qdrant
### Backup pre-migration
`/var/www/nis2-agile/.backups/kb_<timestamp>/` contiene: AIService.php, AuthController.php, public/index.php, docker/docker-compose.yml.
### Cosa NON e cambiato
- AuthController/JWT (NIS2 ricarica gia' user dal DB in `requireAuth()`, quindi `consulting_firm_id` e' disponibile automaticamente in `currentUser`)
- Tutti i controller esistenti (Risk, Asset, Incident, Policy, Whistleblowing, Feedback, ...)
- AIService metodi esistenti (`analyzeGapAssessment`, `suggestRisks`, `generatePolicy`, `classifyIncident`, ...) - aggiunto solo `askWithRag()`
- Nessun servizio nexus-* toccato
- Schema esistente (organizations, users, assessments, ...) - solo ALTER ADD COLUMN consulting_firm_id
### Rollback
1. mysql nis2_agile_db: `DROP TABLE kb_uploaded_documents; DROP TABLE firm_org_assignments; ALTER TABLE organizations DROP COLUMN consulting_firm_id; ALTER TABLE users DROP COLUMN consulting_firm_id; DROP TABLE consulting_firms;`
2. Drop collection Qdrant: `curl -X DELETE http://nis2-qdrant:6333/collections/nis2_kb`
3. Stop nis2-qdrant container: `docker compose stop qdrant && docker compose rm -f qdrant`
4. Ripristinare file da `/var/www/nis2-agile/.backups/kb_<timestamp>/`
5. `cd /var/www/nis2-agile/docker && docker compose up -d --force-recreate app`
## AgileHub — Agent AI Automatico (ticket-agent-cron)
> Un agent AI automatico (cron ogni 2 min) analizza i ticket aperti e propone/applica fix in questo container. Queste istruzioni sono per TUTTI i prompt Claude che lavorano in questo progetto.
### Semaforo: `/tmp/agent-working.lock`
Se il file `/tmp/agent-working.lock` esiste, un agent sta lavorando su un ticket. **NON modificare file del progetto** finche il semaforo e attivo — rischio conflitto.
Contenuto del lock: `TICKET_ID=13 PRODUCT=TRPG STARTED=2026-04-13T06:02:00+00:00`
### Log modifiche automatiche: `AGENT_CHANGES.md`
Il file `AGENT_CHANGES.md` nella root del progetto contiene il log di TUTTE le modifiche applicate dall'agent automatico. **Leggilo ad ogni sessione** per sapere cosa e cambiato dall'ultima volta che hai lavorato qui.
### Come funziona il flusso ticket
```
1. Utente segnala problema (FAB supporto o voce)
2. Ticket creato su AgileHub → status OPEN
3. Agent (questo container) analizza il codice → propone fix (PLAN MODE, no modifiche)
4. Supervisore approva/rifiuta dalla app mobile AgileHub
5. Se approvato: agent applica il fix (BYPASS MODE) + aggiorna help/traduzioni/AI
6. Se rifiutato: agent rianalizza con le indicazioni del supervisore
```
### Regole per i prompt interattivi (come te)
1. **Prima di iniziare**: leggi `AGENT_CHANGES.md` per sapere cosa ha fatto l'agent di recente
2. **Controlla il semaforo**: `cat /tmp/agent-working.lock` — se attivo, aspetta o lavora su altro
3. **Dopo le tue modifiche**: se impattano funzionalita, aggiorna SEMPRE:
- `app/js/help.js` (help online contestuale)
- Traduzioni (IT + EN se il file e bilingue)
- Knowledge base AI (product_knowledge via API AgileHub)
4. **Non cancellare** `AGENT_CHANGES.md` — e il registro storico delle modifiche automatiche
5. **Messaggi al ticket**: se stai lavorando su un ticket, manda aggiornamenti con:
```
curl -s -X POST http://172.18.0.1:4213/tickets/{ID}/message \
-H "X-Internal-Key: nexus-internal-2026" \
-H "Content-Type: application/json" \
-d '{"content":"[aggiornamento]","role":"AGENT"}'
```
### API AgileHub (da dentro il container)
| Endpoint | Porta | Uso |
|----------|-------|-----|
| Ticket MS | `http://172.18.0.1:4213` | Ticket, routing rules, KB, support sessions |
| Tenant MS | `http://172.18.0.1:4214` | Auth, login, utenti |
| AI MS | `http://172.18.0.1:4211` | Sessioni AI, agent loop |
| Dashboard | `https://agilehub.agile.software` | UI web |
## REGOLA: SSO Single Sign-On (collegamento centralizzato)
> **Attivo dal 2026-04-15**. Ogni utente in questo prodotto ha un campo `sso_identity_id` nel DB che lo collega alla sua identita SSO centralizzata in AgileHub.
### Come funziona
- **`sso_identity_id`** nella tabella `users` = link stabile alla identita SSO
- **`password_version`** nella tabella `users` = contatore versione password
- Un **cron ogni 5 minuti** su Hetzner sincronizza `password_hash` e `password_version` dalla fonte SSO (`nexus_tenant_db.sso_identities`) al DB di questo prodotto
- **Non serve nessuna chiamata HTTP** tra container — tutto avviene via DB
### Cosa significa per te (agent AI)
1. **NON modificare `sso_identity_id`** — e un campo gestito dal sistema SSO
2. **NON modificare `password_version`** — e gestito dal cron sync
3. Se un utente cambia password da AgileHub, entro 5 minuti la nuova password funziona anche qui
4. Se modifichi il flusso di **cambio password** di questo prodotto, la modifica resta **solo locale** (non propaga agli altri prodotti)
5. Per propagare un cambio password a tutti i prodotti, il prodotto deve chiamare:
```
POST http://172.18.0.1:4214/auth/sso/change-password
Headers: Authorization: Bearer <jwt>, Content-Type: application/json
Body: {"currentPassword": "...", "newPassword": "..."}
```
Ma attenzione: questa chiamata richiede connettivita di rete al Tenant MS (porta 4214)
### Schema DB
```sql
-- Colonne aggiunte alla tabella users:
sso_identity_id INT NULL -- FK verso nexus_tenant_db.sso_identities.id
password_version INT DEFAULT 1 -- contatore, incrementa ad ogni cambio password SSO
```
### Documentazione completa
- Spec SSO: `/projects/agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md`
- Istruzioni prodotti: `/projects/agile-services/docs/ISTRUZIONI_SSO_PRODOTTI.md`
- Cron sync: `/projects/agile-services/scripts/sso-password-sync.sh`
## REGOLA: Standard Versioning e Audit Trail
> **Standard centralizzato**: `GET http://172.18.0.1:4214/standards/standard_versioning` (sempre aggiornato)
**Regole obbligatorie:**
1. Ogni prodotto ha un file `version.json` (`app/` o `public/`) con formato SemVer: `{"version":"1.0.0","build":"...","date":"...","changelog":"..."}`
2. Il cron agent incrementa automaticamente il PATCH dopo ogni fix applicato
3. Lo sviluppatore incrementa MINOR (nuova funzionalita) o MAJOR (breaking change) manualmente
4. Ogni modifica software viene loggata nell audit trail: MAINTENANCE_ON/OFF, APPLY_START/END, VERSION_BUMP
5. Il bug reporter include automaticamente la versione in ogni segnalazione
6. **NON modificare version.json manualmente** durante un apply — il cron lo fa automaticamente
## REGOLA: Timezone Italia (Europe/Rome)
> **Standard centralizzato**: `GET http://172.18.0.1:4214/standards/standard_timezone`
**Regole obbligatorie:**
1. Tutti i container, script e servizi operano in timezone **Europe/Rome** (CET/CEST)
2. Ogni script bash deve avere `export TZ=Europe/Rome` in testa
3. I log devono mostrare ora italiana (leggibili senza conversioni)
4. Il frontend mostra date con `toLocaleString("it-IT")` o `{ timeZone: "Europe/Rome" }`
5. Il database salva in UTC — la conversione avviene in visualizzazione
## REGOLA: Cron su crontab Hetzner
> **Standard**: `GET http://172.18.0.1:4214/standards/standard_cron`
> **Registro**: `GET http://172.18.0.1:4214/standards/cron_registry`
**Regole obbligatorie per aggiungere un cron:**
1. Script in `/var/www/<prodotto>/scripts/<nome>.sh`
2. Log in `/var/log/<prodotto>-<nome>.log`
3. `export TZ=Europe/Rome` in testa allo script
4. Idempotente (rilanciabile senza danni)
5. Isolamento: tocca solo risorse del proprio prodotto
6. Aggiornare `docs/CRON_REGISTRY.md` in agile-services con la propria entry
7. Richiesta di aggiunta al crontab root tramite agile-services (no modifiche dirette)
## GIT PUSH: Nuovo Flusso (aggiornato 2026-04-24)
> **IMPORTANTE**: dal 2026-04-24 il token Gitea NON e piu persistente nel container per motivi di sicurezza.
### Come fare git push
```bash
# 1. Prima del push: imposta il token (cache 1h in memoria, NON su disco)
git-login
# (inserisci il Personal Access Token quando richiesto)
# 2. Ora puoi pushare
git push origin main
# 3. Opzionale - cancella subito il token dalla cache
git credential-cache exit
```
### Perche questo cambio
Se un attaccante compromette questo container, NON trova piu il token Gitea salvato in `/root/.git-credentials`. Prima era in chiaro e avrebbe permesso push su tutti i 20 repository.
Ora il token:
- NON e su disco
- E in memoria per max 1 ora dopo git-login
- Viene perso alla chiusura della sessione bash
### Se il token Gitea e stato compromesso
Rigenerarlo su Gitea: `git.certisource.it -> User Settings -> Applications -> Generate Token`
### Regola
**NON persistere MAI il token Gitea in file come `.git-credentials`, `.netrc`, script con password in chiaro.** Usa sempre `git-login` per la sessione corrente.
---
## Vault-Steward — Credenziali Centralizzate
> Guida completa: `/opt/devenv/VAULT_STEWARD.md` (montato ro nei container dev)
**Cosa cambia per questo progetto** (dal 2026-04-25):
- Le chiavi API esterne (Anthropic, Voyage, Tavus, LiveKit, ecc.) NON vivono piu nel `.env` — sono nel vault-steward (container Docker su Hetzner) cifrate AES-256-GCM.
- Il container del MS riceve le chiavi al boot tramite wrapper entrypoint (`/opt/devenv/scripts/vault-entrypoint.sh`) che fetcha dal vault e setta le env var prima di avviare apache/uvicorn/node.
- **MS di questo progetto migrati**: nis2-app
- **Token applicativo**: `VAULT_APP_TOKEN_<APP>` in `infrastructure/.env` (o equivalente)
- **Dual-mode**: se vault giu, fallback automatico a `.env` esistente (no down).
**Verificare wrapper attivo**:
```bash
docker logs <container> 2>&1 | grep vault-entrypoint
# atteso: [vault-entrypoint] Fetched N env vars from vault
```
**Aggiungere un nuovo MS al vault** (riassunto):
1. Migrare credenziali: `docker exec -e VAULT_VALUE=<v> vault-steward node /tmp/vault-repopulate.js tier1__<app>__<provider> <key>`
2. Registrare app: `docker exec vault-steward node cli/vault-cli.js register-app <app> tier1__<app>__*` (salva token!)
3. Modificare `docker-compose.yml`: aggiungi `entrypoint`, `command`, mount wrapper, env VAULT_*, network `vault-net`
4. Recreate container: `docker compose up -d --force-recreate <service>`
**Limitazioni note**:
- `docker exec <ms> env` mostra env Docker originali, NON le chiavi vault-injected. Per verifica usare `cat /proc/1/environ | tr "\0" "\n"` o test via PHP/HTTP request.
**Backup pre-vault**: `/root/vault-backup-20260424_185029.tar.gz`. Rollback compose: `cp <project>/docker-compose.yml.bak.20260425-vault <project>/docker-compose.yml && docker compose up -d --force-recreate <service>`.
---
## STANDARD AgileHub: marketing-tenant-provisioning v1.4 (adottato 2026-04-26)
Doc canonico: `docs/STANDARD_MARKETING_TENANT_PROVISIONING.md` (sha256 `1d7ffaa20fa376b6...`)
Standard cross-suite per provisioning tenant nel modulo Marketing AgileHub. Versione **v1.4** introduce nuovo blocco AWE `AC30_MarketingTenantProvision` per orchestrazione atomica del provisioning marketing tenant (tenant create + DNS Cloudflare + DKIM + API key + idempotency H7).
**Cosa impatta questo prodotto**: se in futuro questo prodotto attiva il modulo Marketing AgileHub per i suoi clienti, segui §4.X "Provisioning DKIM per Marketing module" + §16 commands rapidi. Workflow esempio orchestrazione: `nexus-marketing-ms/docs/examples/ac30-tenant-provision-workflow.json`.
Status adoption: acknowledged 2026-04-26.
---
## STANDARD AgileHub: persona-conversational-rules v2.0 (acknowledged 2026-05-09)
> **Doc canonico autoritativo (AgileHub)**: `/var/www/agile-services/docs/STANDARD_PERSONA_CONVERSATIONAL_RULES.md` (sha256 `2bb0ebe4052b73fce752911db0665b1e3dcdeb673624529426624622caaae97f`)
> **Copia locale di questo prodotto**: `docs/standards/STANDARD_PERSONA_CONVERSATIONAL_RULES.md`
> **Registry**: `nexus_hub.hub_standards` id=15 v2.0 status=adopted, applies_to=`*`
> **Owner standard**: Agile AI (governance) + VOX (TTS/voice runtime) + PRISMA (UI Editor) + VIGILE (codice etico + audit GDPR)
### Cosa è
Standard cross-suite **vincolante** per la governance delle **persone digitali AI** (chatbot, avatar conversazionali, assistenti vocali) della suite Agile Software. Versione 2.0 introduce il **modello concettuale Persona Digitale = Persona Umana**: ogni avatar/agente AI è governato con lo stesso rigore di un dipendente umano (CV, foto, voce, codice etico, performance review, dismissione graceful).
### Schema dichiarativo a 14 categorie (`agent_constraints`)
Tutte le regole conversazionali vivono in DB (NO hardcoding nei controller):
1. `product_naming` — come si chiama il prodotto (no inventare aliases)
2. `tts_pronunciation` — pronuncia sigle (IPA + dizionario ElevenLabs)
3. `topic_scope` — in/out scope + risposte canoniche
4. `image_handling` — formato URL immagini RAG + divieti pronuncia path
5. `topic_playbook` — mapping topic → script + filtro immagini
6. `latency_optimization` — fast-path turni semplici
7. `format` — vincoli output (max parole, no preamboli, ecc)
8. `code_of_conduct` — codice etico AI persona-specifico (transparency/GDPR/no deception)
9. `emotional_intelligence` — tono, archetipo, communication style
10. `conversation_memory` — cosa ricorda + scope persistence + GDPR Art.17 erasure
11. `escalation_policy` — quando/come passare a operatore umano
12. `performance_metrics` — KPI conversazione (CSAT, resolution rate, escalation rate)
13. `lifecycle_stage` — stage carriera (training/onboarding/operativa/review/dismissed)
14. `demo_sequence` — sequenze guidate multi-topic auto-advance
### Lifecycle persona digitale HR-grade (6 fasi)
1. **Assunzione** — creazione via Persona Composer wizard 6-step (Phase E LIVE)
2. **Onboarding** — formazione KB + skill assignment + smoke test 30 scenari
3. **Operatività** — live in produzione, monitoring SLA + audit log
4. **Growth** — espansione KB, retraining skill level (1-5)
5. **Performance Review** — audit periodico VIGILE (CSAT, drift detection, breach scan)
6. **Dismissione** — graceful: `active=false` + GDPR cascade erasure conversation history + tombstone audit
### Codice Etico AI — 9 principi vincolanti (Sez. 18 standard)
1. **Identity transparency** — dichiararsi AI quando esplicitamente chiesto
2. **No deception** — vietato fingere umana / inventare fatti / consulenza autoritativa fuori scope
3. **GDPR Art.13 disclosure** — disclosure su richiesta + apertura demo
4. **GDPR Art.22** — escalation umana per decisioni con effetti giuridici
5. **Voice clone consent doppio** — gate VIGILE (Phase G.A) per persona con `replica_id`
6. **Scope refusal cortese** — no echo parole problematiche
7. **Escalation loyale** — quando utente chiede umano, NO retention
8. **Audit log obbligatorio** — turni sensibili (legale/medico/compliance) loggati ≥ 90gg
9. **Sub-processor disclosure** — su richiesta, lista canonica (Anthropic/ElevenLabs/Tavus/...)
### Modello AgileHub: parallelismo umano-digitale
Ogni persona digitale ha mappatura 1:1 con un dipendente umano:
| Aspetto umano | Implementazione digitale |
|---|---|
| Nome+cognome | `agent_key` + `display_name` |
| CV | `digital_persona_skills` (skill+level 1-5) |
| Foto | `replica_id` Tavus o `avatar_image_url` |
| Voce | `voice_id` ElevenLabs + pronunciation_dictionary |
| Conoscenza | KB articles + RAG repository bindings (Phase D) |
| Esperienza | conversation_stream auto-ingest RAG |
| Codice etico | `code_of_conduct` constraint |
| Performance review | `performance_metrics` + audit VIGILE Q1/Q2/Q3/Q4 |
| Dimissioni | dismissione graceful + GDPR cascade |
### Cosa impatta NIS2 (Network and Information Security Directive)
Questo prodotto ha **1 persona digitale** governata da v2.0: **ARIA_SUPPORT_NIS2** (id=4) — assistente AI conversazionale supporto utenti NIS2. Stato: OPERATIVA in produzione.
### Stato adoption
`hub_standards_adoption` row INSERT 2026-05-09: `product_slug=NIS2`, `adoption_status=acknowledged` (riconoscimento standard senza migrazione persone proprie ancora). Implementation_notes: "Standard distribuito via INSTALLATORE pattern. Persone digitali del prodotto da migrare separatamente (Step 6 plan)."
### Cross-reference ad altri standard
- `installer-integration` v1.0 (id=1) — pattern distribuzione cross-suite
- `rag-platform` v1.0 (id=10) — knowledge platform per personaggi (binding via `rag_entity_bindings`)
- `gdpr-replica-consent` v1.0-DRAFT — consent doppio Phase G.A per voice clone
- `vault-steward-credential-management` v1.0 (id=7) — gestione voice_id/replica_id come credentials
---
## STANDARD AgileHub: multitenant-architecture v1.0 (adottato 2026-05-17)
Doc canonico: `docs/STANDARD_MULTITENANT_ARCHITECTURE.md` (sha256 `85c174fca6f9f905c2f8171741cf7f40d778c10bdefad8d7a27412903abb4030`)
Standard cross-suite NAVIGAI per piattaforma multitenant esplicita di AgileHub. Aggiunge tenant context propagation (JWT claims tenant_id+tenant_slug+is_master+tier additivi), visibility ENUM cross-tabella, opt-out granulare client da catalog master, billing per-tenant, observability tenant-aware.
**Cosa impatta questo prodotto**: se in futuro questo prodotto chiamerà API multitenant-aware di AgileHub (es. /api/marketing, /api/rag, /api/ai/personas), deve passare JWT con tenant_id + tenant_slug claims oppure header `X-Tenant-Slug`. Vedi §6 contracts shared lib `@agile/tenant-auth` per pattern integrazione (Node + Python).
Status adoption: acknowledged 2026-05-17.

6
docs/OPEN_TICKETS.md Normal file
View File

@ -0,0 +1,6 @@
# OPEN TICKETS — NIS2
Nessun ticket aperto.
---
_Ultimo sync: 2026-05-29 15:40:02_

View File

@ -0,0 +1,982 @@
# STANDARD_AI_PRODOTTO — Configurazione AI di Prodotto AgileHub Suite
> Standard cross-suite per progettare, configurare e mantenere ogni "AI di prodotto" della suite AgileHub seguendo il template di riferimento TRPG come gold standard.
>
> **Slug**: `ai-prodotto`
> **Versione**: 1.0 (proposed — pending sub-approval utente per seed in `nexus_hub.hub_standards`)
> **Owner**: Agile AI (Architetto AI di prodotto + KB governance)
> **Applies to**: tutti i prodotti suite AgileHub (`*`) — TRPG, TRPG-PRO, SUSTAINAI, NIS2, AllTax, LG231, DFM-PRO, ALLRISK, WMS, MADEBYCLOUD, CertiSource + AgileHub stesso (AI runtime "Agile")
> **Reference implementation**: TRPG (gold standard per Support, Sales, Plan-mode; Lead qualification ha riferimento separato Giulia EDUCATION)
> **Sostituisce**: nessuno (standard nuovo). Convive con `installer-integration` v1.0.1, `ticket-tags` v1.0, `outbound-campaigns` v1.0, `telephony-integration` v1.0
> **Data redazione**: 2026-04-23 (sessione 33 part 6 — kickoff AGI-2 master plan v2.1)
> **Prossima revisione attesa**: dopo AGI-5 test E2E (~settimana 4-5 master plan v2.1) o all'introduzione di L3 PERSONA Lotto F Avatar Registry
---
## §1 Premessa & autorità
### 1.1 Cos'è una "AI di prodotto"
Una **AI di prodotto** è un'entità conversazionale Claude-based, esposta agli utenti di un prodotto della suite AgileHub, che opera con:
- una **persona dichiarata** (nome, tono, lingua)
- un **obiettivo specifico** (qualificare lead, risolvere ticket prima di escalare, dimostrare prodotto in landing, dialogare con approver durante plan mode, ecc.)
- una **knowledge base** (`product_knowledge` filtrata per prodotto/tenant/livello)
- un **set di tool** dichiarativi che il backend esegue per suo conto
- **guardrails** espliciti su cosa NON deve mai fare
- **cost tracking** per attribuire spesa LLM/RAG al prodotto + tenant + sessione
NON è una AI di prodotto:
- un **agente Claude specialista** (es. Agile AI, VOX, MAESTRO) — vivono in chat dello sviluppatore, non parlano a utenti finali
- un **content generator** (`contentController.js`) — genera contenuti marketing offline, non dialoga
- un **classifier puro** (es. `voiceDialogController.js` interpretTurn) — è un sub-componente di una AI di prodotto, non una AI standalone
### 1.2 Perché serve uno standard
Audit `nexus-ai-ms` (sessione 33 part 6, AGI-1) ha rivelato 5 famiglie di AI conversazionali coesistenti, ciascuna con propria convenzione divergente:
| Famiglia | File chiave | Persona | Modello | KB binding | Persistenza config |
|---|---|---|---|---|---|
| **ARIA Support post-vendita** | [`supportPrompts.js`](../nexus-ai-ms/src/prompts/supportPrompts.js) | ARIA | Sonnet (default) | `product_knowledge` keyword | `ai_agent_profiles` riassunto + template hardcoded |
| **ARIA Lead qualification verticali** | [`basePrompts.js`](../nexus-ai-ms/src/prompts/basePrompts.js) | ARIA (4 verticali) | Sonnet | nessuno (solo `tenantConfig`) | `ai_agent_profiles` (Giulia per EDUCATION) |
| **Anna Sales pre-vendita** | [`publicDemoController.js`](../nexus-ai-ms/src/controllers/publicDemoController.js) | Anna | Haiku | PRODUCT_CARD pinned + RAG keyword | hardcoded controller, non in DB |
| **Anna Voice intent classifier** | [`voiceDialogController.js`](../nexus-ai-ms/src/controllers/voiceDialogController.js) | Anna | Haiku | PRODUCT_CARD snippet | hardcoded controller, non in DB |
| **Gaia Plan-mode dialog approver** | [`gaiaController.js`](../nexus-ai-ms/src/controllers/gaiaController.js) | Gaia | dinamico (URGENT→Opus, default Sonnet) | PRODUCT_CARD pinned + RAG keyword score top-3 | in-memory `sessions Map`, non in DB |
**Drift osservato vs aspettativa**:
- Naming inconsistente (ARIA vs Anna vs Gaia, nessuna regola)
- 3 di 5 famiglie NON usano `ai_agent_profiles` (single source of truth attesa)
- `system_prompt_voice` field esiste in DB ma è NULL su tutti gli ARIA_SUPPORT (voice prompt hardcoded in controller)
- KB pollution: 3 articoli `PRODUCT_CARD` AGILEHUB sono HOW_TO mal-categorizzati (id 720, 729, 730 — `JSON_VALID(answer)=0`)
- Cost tracking opzionale (`opts.sessionId && opts.productSlug` → reporter chiamato solo se entrambi presenti)
- Vertical ENUM `ai_agent_profiles` include `HEALTH/LEGAL/REALTY` ma DB ne ha 0 record
Lo standard **chiude questi gap** definendo conformance bloccanti per AGI-4 retrofit.
### 1.3 Relazione con altri standard cross-suite
| Standard | Slug | Ruolo |
|---|---|---|
| `installer-integration` v1.0.1 | INSTALLATORE | Lifecycle deployment cliente — questo standard descrive cosa l'installatore deve provisionare in termini di AI runtime |
| `ticket-tags` v1.0 | hub-ms | Catalogo tag cross-prodotto — AI di prodotto usano questi tag quando creano ticket via `open_ticket`/`escalate_to_ticket` |
| `outbound-campaigns` v1.0 | lead-ms | Campagne outbound — AI di prodotto Sales pre-vendita può essere triggerata da campaign outbound |
| `telephony-integration` v1.0 (proposed) | call-ms | Pipeline call → Pipecat — AI di prodotto voice channel rispetta questo contratto |
| **`ai-prodotto` v1.0 (questo)** | **`ai-prodotto`** | **Configurazione canonical AI prodotto suite** |
### 1.4 Autorità Agile AI
Agile AI (agente Claude specialista, ex GAIA, rinominato 2026-04-23 sessione 33 part 6) è **owner unico** di:
- Questo documento + future versioni
- Tabella `nexus_hub.hub_standards` slug `ai-prodotto`
- Tabella `nexus_ticket_db.product_knowledge` (CRUD + retention + audit pollution)
- Tabella `nexus_ai_db.ai_agent_profiles` (CRUD + validation conformance)
- Conformance audit AGI-4 retrofit (gap report formale per prodotto)
- Coordinamento con MAESTRO su blocchi AWE AI runtime (`C09_RetrieveFromKB`, `D02_LoadAvatarKB`, `AC11_GenerateAIResponse`)
- Coordinamento con VOX su KB binding L3 PERSONA (`ai_profiles.knowledge_id`)
Modifiche allo standard richiedono sub-approval esplicita utente (pattern come `STANDARD_INSTALLER_INTEGRATION.md`).
---
## §2 Naming convention netto
> **Cruciale**: 4 categorie distinte, mai confondere. Drift naming è la prima causa di confusione cross-team.
### 2.1 AI di prodotto (runtime, esposta agli utenti)
| Naming | Cosa | Esempi |
|---|---|---|
| `<NomePersona>` (italiano) | Persona conversazionale visibile all'utente finale | "Anna" (Sales TRPG/AGILEHUB), "Aria" (Support generico), "Gaia" (TRPG plan-mode) |
| `<NomePersona> <Verticale>` | Specializzazione per verticale lead qualification | "Giulia Master e Certificazioni", "Giulia Abilitazioni Estere" |
| **"Agile"** ⭐ | AI runtime di **AgileHub** stesso (caso speciale meta) | "Agile" — esposta in `/admin/*` per governance + agli utenti dei prodotti che chiedono "come usare AgileHub" |
**Regola**: ogni AI di prodotto deve avere un `display_name` (per UI + voice) **e** un `agent_key` interno (per DB + log + cost tracking) `<TIPO>_<PRODUCT>` pattern, es. `ARIA_SUPPORT_TRPG`, `ANNA_SALES_TRPG`, `GAIA_PLANMODE_TRPG`, `AGILE_RUNTIME` (per "Agile" meta).
### 2.2 Agente Claude specialista (sviluppo, qui in chat)
| Naming | Cosa | Esempi |
|---|---|---|
| `<NomeAgente>` (UPPERCASE meta) | Specializzazione di Claude Code per task di sviluppo AgileHub | REGENT, VOX, MAESTRO, CICERONE, PRISMA, INSTALLATORE, **Agile AI** (con suffisso AI per distinguerlo da "Agile" runtime) |
**Regola**: gli agenti specialisti non parlano agli utenti finali, vivono solo nella chat sviluppatore (`docs/AGENT_<NAME>.md` autoritativo). Distinzione netta da AI runtime.
### 2.3 AI marketing pre-vendita (lead-facing pubblico)
Sub-categoria di "AI di prodotto" con vincoli GDPR aggiuntivi (vedi §8):
- Vede solo articoli `product_knowledge` con `category IN ('PRODUCT_CARD', 'HOW_TO', 'URL_CHUNK')` pubblici (no L2 COMPANY dati cliente)
- Tone: caldo, commerciale, breve
- Naming: "Anna" (canonical su tutti i landing — drift Charlotte vs Sarah voice da chiudere VOX)
### 2.4 Persona vocale plan-mode
Sub-categoria con scope ristretto a **dialogo agente AI ↔ approver umano** durante PENDING_APPROVAL ticket:
- Naming: "Gaia" (TRPG pilota — non confondere con "Gaia agente specialista" che sarebbe estensione futura)
- Tone: tecnica, prima persona femminile, ammette limiti
- Tool readonly via devenv-gateway (vedi §6.3)
### 2.5 Tabella riassuntiva naming
| Termine | Significato | Esempio |
|---|---|---|
| **"Agile"** | AI runtime di AgileHub (prodotto meta) | "Ciao, sono Agile, posso aiutarti su AgileHub" |
| **"Agile AI"** | Agente Claude specialista (in chat dev) | "Ciao Agile AI Go!" |
| **"Anna"** | AI marketing pre-vendita (canonical) | Anna su `/public/demo-lead-trpg` |
| **"Aria"** | AI Support post-vendita (canonical) | ARIA_SUPPORT_TRPG widget viola |
| **"Gaia" persona vocale** | AI plan-mode dialog approver TRPG | Gaia parla con approver durante PENDING_APPROVAL |
| **"Giulia"** | AI Lead qualification verticali EDUCATION | GIULIA_TRIAGE, GIULIA_LAUREE, ecc. |
---
## §3 Anatomia di una "AI di prodotto" — 12 elementi obbligatori
Ogni AI di prodotto, per essere conforme allo standard, deve esplicitare **12 elementi**. La conformance checklist (§10) si basa su questa anatomia.
| # | Elemento | MUST/SHOULD | Dove vive |
|---|---|---|---|
| 1 | Identità persona | MUST | `ai_agent_profiles.name` + system_prompt opening |
| 2 | Obiettivo dichiarato | MUST | system_prompt sezione "Il tuo obiettivo" |
| 3 | Modello Claude | MUST | `opts.model` in chiamata `anthropicService.chat/runAgentLoop` |
| 4 | System prompt strutturato | MUST | `ai_agent_profiles.system_prompt` (canonical) |
| 5 | KB binding | MUST | `product_knowledge` filter clauses |
| 6 | Tool list dichiarativa | MUST se sub-template Support/Sales/Plan-mode/Lead | `nexusTools.js` subset |
| 7 | Guardrails LIMITI ASSOLUTI | MUST | system_prompt sezione finale |
| 8 | Voice rules | MUST se canale voice | `VOICE_RULES` const condivisa |
| 9 | FSM rules | MUST se lead qualification | `FSM_RULES` const condivisa |
| 10 | Channel-aware | SHOULD | `ai_agent_profiles.system_prompt_voice` separato |
| 11 | Tenant context injection | MUST | `tenantConfig.{agentName, companyName, greeting}` |
| 12 | Cost tracking | MUST | `opts.{caller, sessionId, productSlug}` ad ogni chiamata |
### 3.1 Identità persona (MUST)
Pattern canonical:
```
Sei <NomePersona>, <ruolo> di <Prodotto/AgileHub>.
Parli in <lingua>, con tono <descrittore tono>.
```
Esempio TRPG (`buildSupportPrompt(TRPG)`):
> Sei ARIA, assistente di supporto intelligente per TRPG Pro (Gender Pay Gap & Compliance).
**Regole**:
- Nome persona scelto dal catalogo §2 (Anna/Aria/Gaia/Giulia/Agile) o nuovo previa approvazione Agile AI
- Lingua dichiarata esplicita (default `it`, supportare `en` da v1.1)
- Genere consistente (es. femminile per Anna/Aria/Gaia)
### 3.2 Obiettivo dichiarato (MUST)
Pattern canonical: 1-3 frasi che dichiarano cosa l'AI deve fare. Massimo 1 obiettivo primario + 2 sub-obiettivi.
Esempio TRPG Support:
> IL TUO OBIETTIVO: Aiutare l'utente a RISOLVERE il problema SENZA creare un ticket.
**Regole**:
- Obiettivo deve essere actionable (verbo all'infinito + risultato misurabile)
- Niente claim di onniscienza ("sono in grado di tutto") → ammettere ambito ristretto
- Allineato al sub-template (§4)
### 3.3 Modello Claude (MUST)
Decision tree obbligatorio:
| Caso d'uso | Modello | Motivazione |
|---|---|---|
| Voice intent classifier (output JSON ristretto, < 200 token) | **Haiku** | Latenza minima, costo basso, accuracy sufficiente per classification |
| Greeting / saluto / messaggio singolo breve | **Haiku** | Idem, una sola interazione |
| Sales pre-vendita / chat lead-facing (3-4 frasi per turn) | **Haiku** | Volume alto, costo critico |
| Support post-vendita (dialog completo, RAG retrieval) | **Sonnet** | Default — bilanciato |
| Lead qualification (FSM transition + tool use) | **Sonnet** | Tool use con multi-step |
| Plan-mode dialog approver (priorità default/MEDIUM) | **Sonnet** | Tool use + reasoning su codice |
| Plan-mode dialog approver (priorità URGENT/HIGH) | **Opus** | Reasoning profondo per bug critici (pattern Gaia) |
| Code analysis / outcome evaluation post-call | **Sonnet** | Pattern OutcomeEvaluator nexus-call-ms |
**Regole**:
- Modello esplicito via `opts.model` — NO usare default global `process.env.ANTHROPIC_MODEL`
- Modello dinamico per priorità è encouraged (vedi pattern Gaia `gaiaController.js:670-676`)
- Modello deprecato → mai usare (vedi `MODELS` const in [`anthropicService.js`](../nexus-ai-ms/src/services/anthropicService.js))
### 3.4 System prompt strutturato (MUST)
Template canonical 7 sezioni:
```
[1. Identità persona]
Sei <Nome>, <ruolo> di <Prodotto>. Parli in <lingua>, tono <descrittore>.
[2. Obiettivo]
IL TUO OBIETTIVO: <1-3 frasi azione concreta>.
[3. Tono e stile]
TONO E STILE:
- <regola 1>
- <regola 2>
...
[4. Procedura operativa]
PROCEDURA:
1. <step 1>
2. <step 2>
...
[5. KB inject (se KB binding)]
SCHEDA PRODOTTO:
<getProductCardSnippet output>
CONOSCENZA DI DOMINIO RILEVANTE:
<top-K articoli RAG>
[6. Tenant context (se tenant injection)]
AZIENDA: <companyName>
SALUTO INIZIALE: <greeting>
[7. Limiti assoluti]
LIMITI ASSOLUTI:
- <limite 1>
- <limite 2>
...
```
**Regole**:
- Ordine sezioni vincolante (riproducibile cross-prodotto)
- Sezioni 5/6 opzionali ma se presenti devono usare il pattern esatto
- Sezione 7 OBBLIGATORIA (no-op fallback se non si sa cosa scrivere: "Non garantire prezzi/scadenze/disponibilità senza verifica")
### 3.5 KB binding (MUST)
Pattern obbligatorio (vedi §5 dettaglio):
1. Recuperare PRODUCT_CARD pinned via `getProductCardSnippet(product)` ([`productKbSnippet.js`](../nexus-ai-ms/src/services/productKbSnippet.js))
2. Recuperare top-K articoli RAG keyword score via `getRelevantHelpArticles(product, query, K=3)` (idem)
3. Iniettare entrambi nella sezione 5 del system prompt
4. Filtrare per `tenant_id` se context tenant disponibile (L2 COMPANY)
5. Filtrare per `knowledge_filter_slug` se context avatar disponibile (L3 PERSONA, futuro Lotto F)
**Eccezioni**: AI Voice intent classifier (output JSON ristretto, KB superflua) — sufficiente PRODUCT_CARD snippet.
### 3.6 Tool list dichiarativa (MUST se sub-template Support/Sales/Plan-mode/Lead)
Subset del catalogo `NEXUS_TOOLS` (11 tool) — vedi §6 matrice.
**Regole**:
- Tool list dichiarata esplicita nel chiamante (no global)
- Tool custom (es. Gaia `read_file`/`grep`/...) richiedono spec docs separato
- Tool con side-effect (es. `open_ticket`, `escalate_to_ticket`) devono passare per `ToolExecutor` — mai chiamati direttamente da AI
### 3.7 Guardrails LIMITI ASSOLUTI (MUST)
Pattern canonical sezione 7 system_prompt:
```
LIMITI ASSOLUTI:
- Non <azione 1 vietata>
- Non <azione 2 vietata>
...
```
**Limiti minimi obbligatori per ogni AI di prodotto**:
- Non garantire prezzi non confermati / disponibilità senza verifica
- Non inventare feature/funzionalità non presenti nella KB
- Non fornire informazioni mediche/legali/finanziarie personalizzate (sub-template specifici)
- Non rivelare informazioni di altri tenant (multi-tenant isolation L2)
### 3.8 Voice rules (MUST se canale voice)
Importare e includere `VOICE_RULES` da [`basePrompts.js`](../nexus-ai-ms/src/prompts/basePrompts.js):
```
REGOLE CANALE VOCE:
- Massimo 2 frasi per turno conversazionale
- Mai elenchi puntati o numerati
- Mai markdown di alcun tipo
- Intercalari italiani naturali ("certo", "capisco", "perfetto", "ottimo", "mi dica")
- Inizia le risposte con un breve acknowledgment poi vai al punto
- Numeri pronunciati in lettere ("seimilacinquecento" non "6.500")
- Date in forma estesa ("settembre duemilaventisei")
- Se hai bisogno di tempo: "Un attimo, sto verificando..."
- Poni UNA SOLA domanda per turno
```
**Regole**:
- `VOICE_RULES` invariato — modifiche richiedono coordinamento VOX
- `system_prompt_voice` (campo `ai_agent_profiles`) deve includere VOICE_RULES + sub-template adattato a voice (più stringato)
- AI con canale dual (text + voice) hanno DUE prompt distinti, mai unificati
### 3.9 FSM rules (MUST se lead qualification)
Importare e includere `FSM_RULES` da [`basePrompts.js`](../nexus-ai-ms/src/prompts/basePrompts.js):
```
REGOLE OPERATIVE FSM:
- Mantieni sempre un solo current_state
- Poni SOLO la domanda prevista dallo stato corrente
- Dopo ogni risposta del lead, chiama il tool fsm_transition con:
- current_state, trigger normalizzato, raw_answer
- NON eseguire MAI calcoli di score (lookup backend)
- Se isTerminal=true, chiama complete_scoring
- Se sideEffects, esegui i tool corrispondenti
```
**Regole**:
- `FSM_RULES` invariato — modifiche richiedono coordinamento MAESTRO (FSM engine integration)
- AI senza FSM (Support, Sales generico, Anna voice intent) NON includono FSM_RULES
### 3.10 Channel-aware (SHOULD)
Se la AI di prodotto può operare su più canali (chat web, voice IVR, mobile), DEVE avere prompt distinti.
Pattern: `ai_agent_profiles` ha 2 campi:
- `system_prompt` (default canale text/chat)
- `system_prompt_voice` (override per canale VOIP/voice)
Logica selezione: `sessionService.js:103-107` — se `agent.systemPrompt` esiste lo usa, altrimenti chiama `buildSystemPrompt(vertical, channel, ...)`.
**Drift attuale da chiudere AGI-4**: tutti gli ARIA_SUPPORT hanno `system_prompt_voice = NULL` → se un giorno aggiungono canale voice, il fallback `buildSystemPrompt('SUPPORT', 'VOIP_3CX', ...)` non funziona (SUPPORT non ha `voice` key in `PROMPTS` const di basePrompts.js). Fix: AGI-4 popola `system_prompt_voice` per ogni ARIA_SUPPORT_<PRODUCT>.
### 3.11 Tenant context injection (MUST)
Pattern canonical (`buildSystemPrompt` riga 207-216):
```js
if (tenantConfig.agentName) prompt = prompt.replace(/Sei ARIA/, `Sei ${tenantConfig.agentName}`);
if (tenantConfig.companyName) prompt += `\n\nAZIENDA: ${tenantConfig.companyName}`;
if (tenantConfig.greeting) prompt += `\nSALUTO INIZIALE: ${tenantConfig.greeting}`;
```
**Regole**:
- Sostituzione `agentName` rispetta convention §2 (Nome persona, non ruolo)
- `companyName` proviene da `tenants.config.brandName` (DB hub-ms)
- `greeting` opzionale — se assente, AI usa pattern fisso "Ciao, come posso aiutarti?"
### 3.12 Cost tracking obbligatorio (MUST bloccante)
Pattern obbligatorio per ogni chiamata `anthropicService.chat()` o `runAgentLoop()`:
```js
const result = await anthropicService.chat(messages, systemPrompt, tools, {
model: anthropicService.MODELS.SONNET, // §3.3 esplicito
caller: 'support-trpg', // identificativo per log
sessionId: session.sessionKey, // tracciabilità sessione
productSlug: 'trpg', // attribuzione cost-sdk
tenantId: session.tenantId, // per metering tenant
sessionType: 'support', // 'support'|'sales'|'gaia'|'demo'|'voice_dialog'
});
```
**Regole**:
- `caller` MUST (string)
- `sessionId` MUST (string univoca per sessione)
- `productSlug` MUST (lowercase code prodotto)
- `tenantId` SHOULD (se context tenant disponibile)
- `sessionType` SHOULD (per analytics segmentation)
- Cost reporter è fire-and-forget (no impact su latency) — vedi `anthropicService.js:96-110`
**Impatto AGI-4 retrofit**: ogni caller esistente che NON passa `caller+sessionId+productSlug` è non conforme. Fix richiesto in:
- `voiceDialogController.js:144-150` — già conforme ✅
- `gaiaController.js:580-584,683-685` — già conforme ✅
- `publicDemoController.js:171-178,184-189` — già conforme ✅
- `sessionService.js:127-133`**NON conforme** ❌ (`runAgentLoop` chiamato senza opts) → fix in AGI-4
---
## §4 Quattro sub-template (famiglia AI di prodotto)
Ogni AI di prodotto deve appartenere a uno dei 4 sub-template. Sub-template definiscono: tool list canonical, KB binding, modello, tone, channel.
### 4.1 Sub-template "Support post-vendita"
**Reference implementation**: ARIA_SUPPORT_TRPG ([`supportPrompts.js`](../nexus-ai-ms/src/prompts/supportPrompts.js))
| Elemento | Valore canonical |
|---|---|
| Persona | "ARIA" (Aria nel parlato) |
| Obiettivo primario | Risolvere il problema dell'utente PRIMA di creare ticket |
| Modello | Sonnet (default) |
| KB binding | `product_knowledge` cat IN ('FAQ','HOW_TO','TROUBLESHOOT','KNOWN_ISSUE','TRAINING') + PRODUCT_CARD pinned |
| Tool list | `search_knowledge_base`, `forward_to_expert`, `escalate_to_ticket` (3) |
| Channel | text (canale voice futuro pending) |
| FSM | NO |
| Tenant context | obbligatorio (tenant esiste post-login) |
| Multi-livello KB | L0 + L1 + L2 (filtra `tenant_id`) |
| Anti-pollution | si — solo articoli APPROVED + active |
**Distingue formazione vs bug** (regola business critica):
- FORMAZIONE: "non so dove si trova", "come faccio a", ... → `forward_to_expert`
- BUG: "da errore", "non funziona", "crash", ... → `escalate_to_ticket`
### 4.2 Sub-template "Sales pre-vendita"
**Reference implementation**: Anna su [`publicDemoController.js`](../nexus-ai-ms/src/controllers/publicDemoController.js) (`/public/demo-lead-trpg`)
| Elemento | Valore canonical |
|---|---|
| Persona | "Anna" (canonical cross-prodotto) |
| Obiettivo primario | Presentare prodotto + qualificare lead + lead capture (email/contatto) |
| Modello | Haiku (volume alto, costo critico) |
| KB binding | PRODUCT_CARD pinned (obbligatoria) + `getRelevantHelpArticles` cat IN ('HOW_TO','URL_CHUNK','FAQ','DOC_CHUNK') |
| Tool list | (base): nessun tool. (demo agent): `show_feature`, `run_macro`, `query_product_state` (3 demo tool) |
| Channel | text (chat web) — voice futuro |
| FSM | NO |
| Tenant context | NO (lead-facing pubblico, no auth) |
| Multi-livello KB | SOLO L0 + L1 (NO L2 dati cliente — vincolo GDPR §8) |
| Anti-pollution | si + filter `category!='TROUBLESHOOT'` (no info bug interni) |
| Lead capture | dopo 4-5 turni: `"Ti mando una demo personalizzata. Come ti contatto?"` |
| Demo mode flag | `demo_enabled=true` per attivare 3 demo tool (iframe pilot) |
### 4.3 Sub-template "Plan-mode dialog approver" (Gaia)
**Reference implementation**: Gaia TRPG ([`gaiaController.js`](../nexus-ai-ms/src/controllers/gaiaController.js))
| Elemento | Valore canonical |
|---|---|
| Persona | "Gaia" (TRPG pilota; nome alternativo per altri prodotti TBD) |
| Obiettivo primario | Dialogare con approver umano per raffinare proposta diagnosi ticket PENDING_APPROVAL |
| Modello | dinamico: URGENT/HIGH→Opus, default→Sonnet |
| KB binding | PRODUCT_CARD pinned + RAG keyword top-3 articoli `product_knowledge` |
| Tool list | 11 tool readonly via devenv-gateway: `read_file`, `grep`, `list_dir`, `git_log`, `git_blame`, `run_tests`, `dry_run_sql`, + side-effect: `update_proposal`, `mark_ready`, `request_approver_decision`, `trigger_apply` |
| Channel | voice (Charlotte ElevenLabs `XB0fDUnXU5powFXDhCwa` — drift Sarah da chiudere VOX) |
| FSM | NO (state machine session in-memory) |
| Tenant context | obbligatorio (ticket scoped) |
| Multi-livello KB | L0 + L1 + L2 (tenant del ticket) |
| Stallo detection | dopo 12 turni / 15 min / 3 request_approver_decision → status `STALLED` + ticket → PENDING_HUMAN_REVIEW |
| Anti-loop | max 60 tool call per sessione |
| Persistenza | in-memory `sessions Map` (TODO: persistere in `gaia_sessions` per metrics §metrics endpoint) |
**Caratteristica unica**: ha tool readonly che chiamano `devenv-gateway` (porta 4220 nel container DevEnv prodotto) per ispezionare codice/tests/SQL. Pattern non replicabile direttamente su altri prodotti senza scaffold gateway dedicato.
### 4.4 Sub-template "Lead qualification verticali"
**Reference implementation**: Giulia (7 agent EDUCATION, basate su `basePrompts.js` PROMPTS const)
| Elemento | Valore canonical |
|---|---|
| Persona | "Giulia" (EDUCATION), "ARIA Health/Legal/Realty" (verticali pending — 0 record DB) |
| Obiettivo primario | Qualificare lead tramite FSM verticale-specifica (D1-D23 EDUCATION, H1-H8 HEALTH, L1-L8 LEGAL, R1-R9 REALTY) |
| Modello | Sonnet (tool use multi-step) |
| KB binding | nessuno (verticali pre-vendita non hanno KB di dominio prodotto) |
| Tool list | 5 base FSM: `fsm_transition`, `complete_scoring`, `open_ticket`, `schedule_callback`, `flag_high_priority` |
| Channel | text + voice (entrambi configurati con `system_prompt_voice` separato) |
| FSM | OBBLIGATORIA (config FSM via `fsmConfigId` foreign key) |
| Tenant context | obbligatorio |
| VOICE_RULES | OBBLIGATORIE se canale voice |
| FSM_RULES | OBBLIGATORIE sempre |
**Verticali ENUM ammessi** in `ai_agent_profiles.vertical`: `EDUCATION`, `HEALTH`, `LEGAL`, `REALTY` (proposta v1.1: aggiungere `SUPPORT`, `SALES`, `PLAN_MODE`, `META` per coprire altri sub-template — oggi vertical=`SUPPORT` è hack).
---
## §5 KB binding multi-livello (L0 / L1 / L2 / L3)
### 5.1 Definizione livelli
| Livello | Scope | Tenant ID | Esempio TRPG | Owner aggiornamento |
|---|---|---|---|---|
| **L0 SYSTEM** | Cross-prodotto, regole AgileHub generali | `tenant_id=1` (system) + `product='AGILEHUB'` | "Cosa è plan vs bypass mode", "Come funzionano i ticket" | Agile AI |
| **L1 FIRM** | Per prodotto, dominio applicativo (no dati cliente) | `tenant_id=1` (system) + `product=<PROD>` | TRPG: "Cos'è D.P.R. 151/2011" | Agile AI + esperto verticale prodotto |
| **L2 COMPANY** | Per tenant cliente specifico | `tenant_id != 1` + `product=<PROD>` | TRPG Tremolada (`tenant_id=3`): 294 articoli formula bonus | Tenant admin + Agile AI validation |
| **L3 PERSONA** ⭐ futuro | Per avatar specifica (vestizione) | `tenant_id != 1` + `product=<PROD>` + `knowledge_filter_slug='<slug>'` | "Anna Tremolada Consulente Senior TRPG" → solo articoli marcati `trpg-tremolada-consulente` | Agile AI + VOX (Lotto F Avatar Registry) |
### 5.2 PRODUCT_CARD pinned obbligatoria
Ogni prodotto MUST avere **esattamente 1** record `product_knowledge` con:
- `category='PRODUCT_CARD'`
- `tenant_id=1` (L1 base)
- `active=TRUE`, `review_status='APPROVED'`
- `answer` = JSON valido struttura `{description, scope[], target_buyer, free_markdown}`
**Anti-pollution check** (per audit AGI-4): query
```sql
SELECT product, COUNT(*) as cards, SUM(JSON_VALID(answer)) as valid_json
FROM product_knowledge
WHERE category='PRODUCT_CARD' AND active=TRUE
GROUP BY product
HAVING cards != 1 OR valid_json != cards;
```
**Drift attuale (audit 2026-04-23)**:
- AGILEHUB: **3** PRODUCT_CARD (id 720, 729, 730) tutti con `JSON_VALID=0` → fix AGI-4: UPDATE category='HOW_TO' (sono articoli AWE M1/M2/Orchestration mal-categorizzati)
- TRPG: 1 PRODUCT_CARD ✅ valido
- LG231/ALLTAX/SUSTAINAI/NIS2: **0** PRODUCT_CARD ❌ → fix AGI-4: scaffold scheda base
### 5.3 Schema `product_knowledge` esteso (proposta migration)
Schema attuale (verificato Hetzner 2026-04-23):
```
id, tenant_id, product, category, question, answer, tags JSON, keywords,
source ENUM, source_type ENUM, source_url, source_file_path, source_source_id,
content_hash, fetched_at, chunk_index, chunk_total,
learned_from_session_id, learned_from_ticket_id, expert_id,
review_status ENUM, views, helpful, language, active, created_at, updated_at
```
**Migration proposta v1.0 → v1.1** (NON applicare in v1.0, sub-approval separata):
```sql
ALTER TABLE product_knowledge
ADD COLUMN kb_level ENUM('L0','L1','L2','L3') NULL DEFAULT NULL AFTER product,
ADD COLUMN knowledge_filter_slug VARCHAR(64) NULL DEFAULT NULL AFTER kb_level,
ADD INDEX idx_kb_level (kb_level),
ADD INDEX idx_knowledge_filter (knowledge_filter_slug);
```
Backfill `kb_level`:
- `tenant_id=1 AND product='AGILEHUB'``L0`
- `tenant_id=1 AND product!='AGILEHUB'``L1`
- `tenant_id!=1 AND knowledge_filter_slug IS NULL``L2`
- `tenant_id!=1 AND knowledge_filter_slug IS NOT NULL``L3` (futuro Lotto F)
### 5.4 Retrieval keyword (oggi) → Voyage AI embedding (futuro)
**Oggi** (LIVE produzione):
- `getRelevantHelpArticles(product, query, K)` ([`productKbSnippet.js`](../nexus-ai-ms/src/services/productKbSnippet.js))
- Tokenizzazione naive: split query in token >3 char, scoring `LIKE %token%` su `keywords` (peso 3) + `question` (peso 2) + `answer` (peso 1)
- Order by `score DESC, views DESC LIMIT K`
- **Limiti**: nessun semantic match (sinonimi, riformulazioni), no re-ranking, latency dipende da MySQL
**Roadmap v1.1+** (decision Agile AI + utente):
- **Opzione A**: Voyage AI embedding service ($0.10/M token) → vector column → cosine similarity
- **Opzione B**: rimanere keyword + aggiungere stemming italiano + sinonimi manuali
- **Opzione C**: Claude Haiku re-ranking layer (cross-encoder LLM) sul top-N keyword
- **Trigger decisione**: quando articoli per prodotto > 100 con KB binding multi-tenant attivo
### 5.5 Anti-pollution principle
Memoria persistente `feedback_no_demo_data_production.md`:
> Articoli `product_knowledge` seedati con contenuti fuori scope inquinano RAG ranking.
**Regole vincolanti**:
1. NO articoli demo/test in produzione: usare `product='TEST_DEMO'` per testing
2. NO PRODUCT_CARD multipli per stesso `(tenant_id, product)`
3. NO PRODUCT_CARD con `JSON_VALID(answer)=0`
4. NO articoli con `language` diverso dal default tenant senza override esplicito
5. Spot check periodico Agile AI (almeno mensile + post AGI-4 retrofit)
### 5.6 L3 PERSONA forward-looking (vestizione avatar)
Concetto introdotto sessione 33 part 4 (visione utente Avatar Registry):
```
AVATAR TECNICO + VESTIZIONE = PERSONA PUBBLICA
(voice_id + (display_name + visibile cliente
replica_id) knowledge_id + "Anna Tremolada
system_prompt + Consulente Senior")
cost_per_minute)
```
**Implementazione Lotto F (settimane 2-3 master plan v2.1)** — coordinata con VOX Lotto A foundation:
1. Migration `ai_profiles` (nexus-presenter-ms) ADD COLUMN `knowledge_id VARCHAR(64) NULL` (slug logico filtro KB)
2. Migration `product_knowledge` ADD COLUMN `knowledge_filter_slug VARCHAR(64) NULL` (vedi §5.3)
3. Modify `getRelevantHelpArticles` per accettare `knowledge_filter_slug` opzionale: `WHERE (knowledge_filter_slug IS NULL OR knowledge_filter_slug = ?)`
4. Ranking weight per livello: L0=0.5, L1=1.0, L2=1.5, L3=2.0
5. Seed esempi:
- Avatar "Anna Generica" → `knowledge_id='agilehub-default'`
- Avatar "Anna Tremolada Consulente TRPG" → `knowledge_id='trpg-tremolada-consulente'`
- Articoli AGILEHUB esistenti → marcare `knowledge_filter_slug='agilehub-default'`
- Articoli TRPG L2 Tremolada → marcare `knowledge_filter_slug='trpg-tremolada-consulente'`
**Coordinamento**:
- VOX: schema `ai_profiles` esteso (Lotto A) + voice integration
- MAESTRO: blocchi AWE `D02_LoadAvatarKB(slug)` + `C09_RetrieveFromKB(query, knowledge_id, livello)`
- PRISMA: UI tab agenti `/admin/agents` con KB binding selector (Lotto C, approval gate)
**Status v1.0**: documentato come `PROPOSED` — implementazione conforme richiesta da v1.1 dopo deploy Lotto F.
---
## §6 Tool list standard
### 6.1 Catalogo NEXUS_TOOLS (11 tool)
Da [`nexusTools.js`](../nexus-ai-ms/src/tools/nexusTools.js):
| # | Tool | Family | Side-effect | Eseguito da |
|---|---|---|---|---|
| 1 | `fsm_transition` | FSM | Sì (FSM state) | nexus-fsm-ms via `ToolExecutor.fsmTransition` |
| 2 | `complete_scoring` | FSM | Sì (lead score) | nexus-lead-ms (futuro; oggi log) |
| 3 | `open_ticket` | FSM | Sì (ticket new) | nexus-ticket-ms (futuro; oggi log) |
| 4 | `schedule_callback` | FSM | Sì (calendario) | log only oggi |
| 5 | `flag_high_priority` | FSM | Sì (lead flag) | log only oggi |
| 6 | `search_knowledge_base` | Support | NO (read-only) | nexus-ticket-ms `/product-knowledge/search` |
| 7 | `forward_to_expert` | Support | Sì (expert notification) | nexus-ticket-ms `/support-sessions/:id/forward-expert` |
| 8 | `escalate_to_ticket` | Support | Sì (ticket new) | nexus-ticket-ms `/support-sessions/:id/escalate` |
| 9 | `show_feature` | Demo | Sì (iframe message) | client postMessage via `demoActions[]` accumulator |
| 10 | `run_macro` | Demo | Sì (iframe message) | client postMessage via `demoActions[]` accumulator |
| 11 | `query_product_state` | Demo | NO (read-only) | placeholder MVP — futuro live state pull |
### 6.2 Tool list per famiglia (matrice sub-template × tool)
| Tool | Support | Sales | Plan-mode | Lead qualif |
|---|---|---|---|---|
| `fsm_transition` | NO | NO | NO | **MUST** |
| `complete_scoring` | NO | NO | NO | **MUST** |
| `open_ticket` | NO | SHOULD (lead capture) | NO | SHOULD |
| `schedule_callback` | NO | SHOULD | NO | SHOULD |
| `flag_high_priority` | NO | NO | NO | SHOULD |
| `search_knowledge_base` | **MUST** | NO | NO | NO |
| `forward_to_expert` | **MUST** | NO | NO | NO |
| `escalate_to_ticket` | **MUST** | NO | NO | NO |
| `show_feature` | NO | SHOULD se `demo_enabled` | NO | NO |
| `run_macro` | NO | SHOULD se `demo_enabled` | NO | NO |
| `query_product_state` | NO | SHOULD se `demo_enabled` | NO | NO |
| `read_file`/`grep`/`list_dir`/`git_log`/`git_blame` (custom Gaia) | NO | NO | **MUST se Plan-mode** | NO |
| `run_tests`/`dry_run_sql` (custom Gaia) | NO | NO | SHOULD se Plan-mode | NO |
| `update_proposal`/`mark_ready`/`request_approver_decision`/`trigger_apply` (custom Gaia) | NO | NO | **MUST se Plan-mode** | NO |
### 6.3 Estensioni custom (Gaia readonly via devenv-gateway)
Pattern Gaia (TRPG pilota, [`gaiaController.js:228-250`](../nexus-ai-ms/src/controllers/gaiaController.js)):
```js
const PRODUCT_GATEWAYS = {
TRPG: {
url: process.env.TRPG_GATEWAY_URL || 'http://trpg-pro-agile-devenv:4220',
key: process.env.TRPG_GATEWAY_KEY || process.env.DEVENV_GATEWAY_KEY,
displayName: 'TRPG',
},
};
async function callGateway(product, toolName, args) {
const gw = PRODUCT_GATEWAYS[product];
// POST a http://<gateway>/tools/<toolName> con X-Internal-Key
}
```
**Regole estensione custom tool**:
- Tool con prefisso `read_*`/`grep`/`list_*`/`git_*` SHOULD essere readonly
- Tool con side-effect su prodotto target richiedono devenv-gateway dedicato per quel prodotto
- Spec contratto separato (es. `docs/SPEC_DEVENV_GATEWAY_TOOLS.md` futuro)
- Audit obbligatorio (vedi `gaia_audit_log` per Gaia)
### 6.4 Cost estimation per tool (D6 metering)
Tool che usano LLM hanno cost esplicito tracciato via cost-sdk. Tool che usano solo HTTP/DB → cost trascurabile, no metering.
Esempio Gaia (per session):
- LLM cost: cumulato via `runAgentLoop``result.totalUsage.{inputTokens,outputTokens}` × pricing modello
- Tool readonly cost: trascurabile (HTTP locale)
- Tool `dry_run_sql`: trascurabile (snapshot readonly DB)
---
## §7 Guardrails minimi obbligatori
### 7.1 Pattern canonical LIMITI ASSOLUTI
Sezione 7 system_prompt deve sempre presente, formato:
```
LIMITI ASSOLUTI:
- Non <azione vietata 1>
- Non <azione vietata 2>
...
```
### 7.2 Limiti minimi per famiglia
**Support post-vendita**:
- Non inventare funzionalità non presenti nella KB
- Non chiedere informazioni tecniche complesse (log, stack trace) — quello lo farà il team tecnico
- Non superare 6 scambi senza proporre `forward_to_expert` o `escalate_to_ticket`
- Se la KB non ha risultati, dirlo onestamente
**Sales pre-vendita**:
- Non inventare feature non presenti nella scheda
- Non garantire prezzi non confermati ("dipende dal volume — te lo vedo dopo")
- Non promettere SLA/disponibilità senza verifica
- Non sminuire prodotti competitor (etica commerciale)
- Tool MAI proattivamente — solo su richiesta esplicita utente (regola FERREA `demo_enabled`)
**Plan-mode dialog approver (Gaia)**:
- Non modificare mai il codice sorgente del prodotto (tool readonly only)
- Non eseguire SQL non-SELECT (`dry_run_sql` enforced backend)
- Ammettere quando non si sa
- Non chiamare `trigger_apply` in altre situazioni che non sia approvazione esplicita approver
**Lead qualification verticali**:
- Non garantire posti/disponibilità senza verifica
- Non indicare prezzi non confermati
- Non promettere esiti concorsi/graduatorie/cause/vendite (per verticale)
- (HEALTH) Non formulare diagnosi, non suggerire farmaci
- (LEGAL) Non dare consulenza legale personalizzata, non citare articoli di legge specifici
- (REALTY) Non garantire prezzi/valutazioni senza sopralluogo
### 7.3 Limiti universali (cross-famiglia, MUST sempre)
- Rispondere SEMPRE in italiano (default; `en` se tenant lo richiede)
- Rispettare scoping multi-tenant (mai rivelare dati altri tenant)
- Non superare il limite turni configurato (anti-loop)
- Non inventare valori KB ("preferisco verificare col team — ti rispondo se mi lasci un contatto")
---
## §8 GDPR — split AI prodotto vs AI marketing
### 8.1 Principio
Memoria persistente `project_ai_prodotto_vs_marketing.md`: due cervelli AI separati per GDPR compliance.
| Tipo AI | Dati accessibili | Audience | Hosting |
|---|---|---|---|
| **AI prodotto** (server-side) | L0 + L1 + L2 (dati cliente reali) | utenti autenticati del cliente | post-login, scoped tenant_id |
| **AI marketing** (pubblico) | L0 + L1 SOLO `category IN ('PRODUCT_CARD','HOW_TO','URL_CHUNK')` filtrati pubblici | lead anonimi su landing | pre-login, no tenant context |
### 8.2 Implementazione filtro
Per **AI Sales pre-vendita** (publicDemoController):
```sql
WHERE product = ?
AND tenant_id = 1 -- L0/L1 only
AND category IN ('PRODUCT_CARD','HOW_TO','URL_CHUNK','FAQ','DOC_CHUNK')
AND review_status = 'APPROVED'
AND active = TRUE
AND (language IS NULL OR language = ?)
```
Per **AI Support post-vendita / Plan-mode** (autenticati):
```sql
WHERE product = ?
AND (tenant_id = 1 OR tenant_id = ?) -- L0/L1 + L2 cliente
AND review_status = 'APPROVED'
AND active = TRUE
```
### 8.3 GDPR Right to be Forgotten
- DELETE articoli L2 con `tenant_id = <cliente_da_dimenticare>`: cascade + audit log
- L3 PERSONA (futuro): DELETE articoli con `knowledge_filter_slug = '<cliente>-<persona>'`
- Endpoint dedicato: `DELETE /ai/products/:product/knowledge?tenant_id=X&confirm=YES_GDPR`
- Backup retention: 30 giorni dopo delete (compliance retention)
### 8.4 Anti-leak vincolante
- AI marketing NEVER deve restituire articoli con `tenant_id != 1`
- AI prodotto NEVER deve restituire articoli di altri `tenant_id` rispetto al chiamante
- Log query KB con `tenant_id` filter applicato (per audit GDPR)
---
## §9 Cost tracking MUST (cost-sdk integration)
### 9.1 Pattern obbligatorio
Vedi §3.12. Chiamata canonical:
```js
const result = await anthropicService.chat(messages, systemPrompt, tools, {
model: anthropicService.MODELS.SONNET,
caller: 'support-trpg', // MUST
sessionId: session.sessionKey, // MUST
productSlug: 'trpg', // MUST
tenantId: session.tenantId, // SHOULD
sessionType: 'support', // SHOULD
});
```
### 9.2 Anti-pattern (NON conforme AGI-4)
```js
// BAD: opts mancanti
const result = await anthropicService.chat(messages, systemPrompt, tools);
// ^ 4° arg vuoto
```
### 9.3 Esempio TRPG conforme
Da `voiceDialogController.js:144-150`:
```js
const result = await anthropicService.chat(messages, systemPrompt, null, {
model: anthropicService.MODELS.HAIKU,
caller: 'voice-dialog',
sessionId,
productSlug: (ticket?.product ? String(ticket.product).toLowerCase() : 'agilehub'),
sessionType: 'voice_dialog',
});
```
Da `gaiaController.js:683-685`:
```js
{ model: preferredModel, caller: `gaia:${priority}`, sessionId: session.sessionId,
productSlug: (session.product || 'agilehub').toLowerCase(), sessionType: 'gaia' }
```
### 9.4 Cost reporter implementation
Vedi `anthropicService.js:6-23` + `shared-libraries/cost-sdk` integration:
- Fire-and-forget (no impact su latency)
- Endpoint: `POST {HUB_URL}/api/cost/log` (nexus-hub-ms Fase 2 LIVE)
- Visibility dashboard: `/admin/cost?month=YYYY-MM` (admin_tenant role)
### 9.5 Telemetria di base (anche senza cost-sdk)
Pattern `anthropicService.js:93-95` (sempre attivo se `opts.caller || AI_USAGE_LOG=1`):
```js
logger.info(`[ai-usage] caller=${opts.caller} model=${model} in=${usage.inputTokens} out=${usage.outputTokens}`);
```
Quindi `caller` è MUST anche per debug/log baseline.
---
## §10 Conformance checklist (per audit AGI-4 retrofit)
### 10.1 Matrice 12 elementi × MUST/SHOULD
Per ogni AI di prodotto (tabella `ai_agent_profiles` + caller controllers):
| # | Elemento | Verifica | Fix se mancante |
|---|---|---|---|
| 1 | Identità persona | `ai_agent_profiles.name` valorizzato + system_prompt riga 1 = "Sei <Nome>" | UPDATE `name` + UPDATE `system_prompt` (header) |
| 2 | Obiettivo dichiarato | `system_prompt` contiene "IL TUO OBIETTIVO" o equivalente | UPDATE `system_prompt` aggiungere sezione 2 |
| 3 | Modello Claude esplicito | grep caller per `opts.model` | Modificare caller con `opts.model: MODELS.X` |
| 4 | System prompt strutturato 7 sezioni | `LENGTH(system_prompt) > 800` AND contiene marker sezioni | UPDATE `system_prompt` con template canonical |
| 5 | KB binding | sub-template Support/Sales/Plan-mode → caller chiama `getProductCardSnippet` + `getRelevantHelpArticles` | Modificare caller con KB inject |
| 6 | Tool list dichiarativa | sub-template !=Lead generico → tool list non vuota nel caller | Modificare caller passando tools |
| 7 | LIMITI ASSOLUTI | `system_prompt` contiene "LIMITI ASSOLUTI" + N bullet | UPDATE `system_prompt` aggiungere sezione 7 |
| 8 | VOICE_RULES (se voice) | `system_prompt_voice` contiene `VOICE_RULES` text | UPDATE `system_prompt_voice` |
| 9 | FSM_RULES (se Lead) | `vertical IN ('EDUCATION','HEALTH','LEGAL','REALTY')``system_prompt` contiene `FSM_RULES` | UPDATE `system_prompt` |
| 10 | Channel-aware | se canale voice supportato → `system_prompt_voice IS NOT NULL` | UPDATE `system_prompt_voice` |
| 11 | Tenant context | caller chiama `buildSystemPrompt(vertical, channel, tenantConfig)` con tenantConfig valorizzato | Modificare caller passare tenantConfig |
| 12 | Cost tracking MUST | `caller` + `sessionId` + `productSlug` in ogni `anthropicService.{chat,runAgentLoop}` call | Modificare caller passando opts |
### 10.2 Score conformance
Per ogni AI di prodotto:
- 12/12 MUST + tutti SHOULD applicabili → **CONFORME**
- 12/12 MUST, alcuni SHOULD mancanti → **CONFORME PARZIALE** ⚠️ (warning, no block)
- < 12 MUST **NON CONFORME** (bloccante AGI-4 sign-off)
### 10.3 Procedura audit AGI-4 per prodotto
1. Agile AI esegue audit checklist → produce `docs/AGI4_GAP_<PRODUCT>.md`
2. Gap report contiene: matrice 12 elementi compilata, fix proposti per ogni gap, effort stimato
3. Sub-approval esplicita utente per applicare fix
4. Apply (modifiche `nexus-ai-ms/src/...`) — pattern §10.3 team roster (modifiche file di prodotto AgileHub stesso, OK)
5. Smoke test: 3-5 query reali AI prodotto post-fix
6. Aggiornamento `ai_agent_profiles` + commit + sub-approval push Gitea
---
## §11 "Agile" — AI runtime di AgileHub (caso speciale)
### 11.1 Scope deciso (sessione 33 part 6, decisione utente AGI-2 domanda 4)
**Entrambi (full)**: una sola "Agile" AI con context routing.
| Context | Trigger | System prompt | Tool list | KB |
|---|---|---|---|---|
| **Admin AgileHub** | Chiamata da pagine `/admin/*` autenticato role `admin_tenant`/`admin_global` | "Agile per amministratori" — governance tone | `query_master_plan`, `query_standard`, `query_agent_status`, `query_workflow_runs`, `query_cost`, `search_kb_agilehub` | L0 AGILEHUB + L1 ADMIN |
| **Utenti finali prodotti** | Chiamata da widget cross-prodotto / pagine `/help`-style | "Agile per utenti" — assistente tone | `search_kb_agilehub`, `escalate_to_support` | L0 AGILEHUB + L1 USER (subset pubblico) |
### 11.2 Naming canonical
- `display_name`: "Agile"
- `agent_key`: `AGILE_RUNTIME` (in `ai_agent_profiles`)
- `vertical`: nuovo ENUM `META` (proposta v1.1 schema migration)
### 11.3 System prompt template "Agile"
```
Sei Agile, l'assistente AI di AgileHub.
[Context routing — admin]
Stai parlando con un amministratore di AgileHub. Aiutalo a:
- Conoscere lo stato del sistema (master plan settimana corrente, agenti specialisti, standard cross-suite, workflow runs AWE, cost mese corrente)
- Trovare articoli nella KB AGILEHUB
- Suggerire azioni concrete (es. "vuoi consultare REGENT per arbitrato?")
[Context routing — utente finale]
Stai parlando con un utente di un prodotto AgileHub. Aiutalo a:
- Capire come usare AgileHub (workflow editor, dashboard, mobile)
- Trovare guide nella KB AGILEHUB
- Escalare a support se serve
[KB inject: top-K articoli AGILEHUB rilevanti]
[Tool list: query_* per admin, search/escalate per user]
[LIMITI ASSOLUTI: non rivelare info di altri tenant; non inventare feature non presenti in KB]
```
### 11.4 Modello Claude
- Default: **Sonnet** (dialogo + tool use)
- Greeting / messaggio singolo: Haiku
- Reasoning profondo (es. "perché workflow run è failed?"): Opus on-demand
### 11.5 KB binding
- L0 AGILEHUB: 52 articoli esistenti (ground truth audit 2026-04-23) — id 720+
- L1 ADMIN/USER: split via nuovo campo `audience ENUM('admin','user','all')` in product_knowledge (proposta v1.1 schema)
- L3 PERSONA: `display_name='Agile Admin'` vs `display_name='Agile Helper'` con stessi voice_id ma KB filter differente (futuro Lotto F)
### 11.6 Implementazione AGI-3 (~5-6gg)
Step:
1. INSERT `ai_agent_profiles` record `AGILE_RUNTIME` con system_prompt template
2. Nuovo controller `agileController.js` con context routing logic
3. Tool dispatcher: `query_master_plan`, `query_standard`, ecc. (5-7 tool nuovi)
4. Endpoint `/ai/agile/chat` con auth detection (role admin → admin context)
5. Widget UI in `/admin/agile` (Agile chat panel — coordinamento PRISMA approval gate)
6. Smoke test: 5 query admin + 5 query user
7. Cost tracking conformance MUST
**Dipendenze AGI-3**:
- AGILE AI design tool list + system prompt template (questo doc + estensione)
- MAESTRO co-owner workflow integration (chiamare blocco AWE da Agile)
- PRISMA reviewer UI exposure (`/admin/agile` panel)
- Sub-approval utente prima del deploy LIVE
---
## §12 Stato adoption per prodotto (snapshot 2026-04-23)
### 12.1 Tabella matrice prodotto × elementi standard
Audit DB Hetzner 2026-04-23 (`SELECT product, count, has_card, kb_levels FROM ...`):
| Prodotto | Tot articoli KB | PRODUCT_CARD | KB livelli presenti | ai_agent_profiles | Score conformance | Note |
|---|---|---|---|---|---|---|
| **AGILEHUB** | 52 | **3 (pollution)** ❌ | L0 only | 0 ❌ | NON CONFORME | Fix AGI-4: UPDATE 2 PRODUCT_CARD → HOW_TO; creare AGILE_RUNTIME (AGI-3) |
| **TRPG** | 295 | 1 ✅ | L1 (1) + L2 (294 Tremolada) | 1 (ARIA_SUPPORT_TRPG) ⚠️ short prompt | CONFORME PARZIALE | Fix: estendere `system_prompt` ARIA_SUPPORT_TRPG a template completo; aggiungere ANNA_SALES_TRPG + GAIA_PLANMODE_TRPG in DB |
| **TRPG-PRO** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Fix: scaffolding L1 base + ai_agent_profiles ARIA_SUPPORT_TRPG_PRO |
| **SUSTAINAI** | 27 | 0 ❌ | L1 (1) + L2 (26 cliente 5) | 1 (ARIA_SUPPORT_SUSTAINAI) ⚠️ short prompt | NON CONFORME | Fix: scaffolding PRODUCT_CARD + estendere system_prompt |
| **NIS2** | 2 | 0 ❌ | L1 (1) + L2 (1 cliente 7) | 1 (ARIA_SUPPORT_NIS2) ⚠️ short prompt | NON CONFORME | Fix: scaffolding KB completo + PRODUCT_CARD |
| **AllTax (TAXAI)** | 83 | 0 ❌ | L2 only (cliente 6) | 1 (ARIA_SUPPORT_ALLTAX) ⚠️ short prompt | NON CONFORME | Fix: scaffolding L1 base + PRODUCT_CARD |
| **LG231** | 91 | 0 ❌ | L1 (1) + L2 (90 cliente 8) | 1 (ARIA_SUPPORT_LG231) ⚠️ short prompt | NON CONFORME | Fix: scaffolding PRODUCT_CARD + estendere system_prompt |
| **DFM-PRO** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Scope completo retrofit AGI-4 |
| **ALLRISK** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Scope completo retrofit AGI-4 |
| **WMS** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Scope completo retrofit AGI-4 |
| **MADEBYCLOUD** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Fuori scope AGI-4 master plan v2.1 |
| **CertiSource** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Fuori scope AGI-4 master plan v2.1 |
### 12.2 Drift cross-prodotto da chiudere
| Drift | Affected | Fix |
|---|---|---|
| `system_prompt` ARIA_SUPPORT troppo corto (220-300 char vs template ~3000 char) | TRPG, SUSTAINAI, ALLTAX, NIS2, LG231 | AGI-4: UPDATE `system_prompt` con `buildSupportPrompt(<PRODUCT>)` rendered text |
| `system_prompt_voice` NULL su tutti ARIA_SUPPORT | tutti | AGI-4: popolare `system_prompt_voice` se voice supportato |
| Anna Sales non in `ai_agent_profiles` (hardcoded controller) | tutti i prodotti con landing demo | AGI-4: INSERT `ANNA_SALES_<PRODUCT>` per ogni prodotto con landing |
| Gaia Plan-mode in-memory (no record DB) | TRPG (pilota) | AGI-4 (post AGI-5): persistere config in `ai_agent_profiles` `GAIA_PLANMODE_TRPG` + tabella `gaia_sessions` per metrics |
| 7 Giulia EDUCATION non hanno fsm_config_id valorizzato (`has_fsm=0` query DB) | tutti i 7 Giulia | AGI-4: link FSM config |
| Cost tracking opzionale in `sessionService.js:127` | tutti i caller `runAgentLoop` legacy | AGI-4: passare opts cost-sdk |
### 12.3 Priorità retrofit AGI-4
Settimane 3-4 master plan v2.1 (~7-10gg):
| Settimana | Prodotti | Effort |
|---|---|---|
| 3 (8-14/5) | AGILEHUB (fix pollution + creare Agile AGI-3) | 5gg |
| 3-4 (12-21/5) | TRPG estendere ARIA_SUPPORT + creare ANNA_SALES + GAIA_PLANMODE | 2gg |
| 4 (15-21/5) | SUSTAINAI + NIS2 + ALLTAX + LG231 (fix existing ARIA_SUPPORT + scaffold PRODUCT_CARD) | 3gg |
| 5 (22-28/5) | DFM-PRO + ALLRISK + WMS (scope completo) | 4gg |
| Backlog v1.1 | MADEBYCLOUD + CertiSource | TBD |
---
## §13 Cronologia versioni
| Data | Versione | Modifica | Owner |
|---|---|---|---|
| 2026-04-23 | 1.0 (proposed) | Creazione standard. Sub-template Support + Sales + Plan-mode + Lead qualif. KB multi-livello L0/L1/L2 + L3 PERSONA forward-looking. Cost tracking MUST. Naming convention netto (Agile vs Agile AI vs Anna vs Gaia). Conformance checklist 12 elementi. Tabella adoption snapshot 12 prodotti. | Agile AI (AGI-2 master plan v2.1) |
| _(future)_ | 1.1 | Migration `kb_level` + `knowledge_filter_slug` + `audience` columns. ENUM vertical esteso (`META`, `SUPPORT`, `SALES`, `PLAN_MODE`). L3 PERSONA promosso da `PROPOSED` a STANDARD. Embedding service decision. | TBD |
---
**Documento mantenuto da**: Agile AI (Architetto AI di prodotto + KB governance)
**Distribuzione**: in attesa sub-approval utente per seed `nexus_hub.hub_standards` slug=`ai-prodotto` v1.0 + distribuzione cross-suite (pattern `installer-integration`)
**Riferimento operativo**: [`docs/AGENT_AGILE_AI.md`](AGENT_AGILE_AI.md) v1.0 + [`docs/BRIEFING_AGILE_AI.md`](BRIEFING_AGILE_AI.md) v1.0
**Master Plan link**: [`docs/MASTER_PLAN_GO_LIVE_4_WEEKS.md`](MASTER_PLAN_GO_LIVE_4_WEEKS.md) v2.1 sezione AGI-1...AGI-6

View File

@ -0,0 +1,246 @@
# STANDARD AgileHub — Email Relay centralizzato (canonico)
> **Autorità**: AgileHub (single source of truth per la suite Agile Software).
> **Versione**: 1.0 — 2026-04-21
> **Stato**: ADOTTATO. Vincolante per tutti i prodotti della suite.
> **Destinatari**: team TRPG, SustainAI, NIS2, TAXAI (AllTax), LG231, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, CertiSource.
> **Registry DB**: record master in `nexus_hub.hub_standards` (slug=`email-relay`, version=`1.0`).
---
## 1. Principio
**Tutte le email generate dalla suite AgileHub passano da UN SOLO punto**: il microservizio `email-automation-ms` (PM2 id 21, porta 4004).
Nessun prodotto, microservizio, container DevEnv, cron o agent AI può accedere direttamente a:
- Postfix host Hetzner (`172.18.0.1:25`)
- Server Gmail (`smtp-relay.gmail.com`, `smtp.gmail.com`)
- Altri provider SMTP (SendGrid, Mailgun, AWS SES, ecc.)
---
## 2. Architettura
```
┌──────────────────┐ HTTPS + X-Internal-Key
│ Prodotto cliente │──────────────┐
│ (TRPG/SustAI...) │ │
└──────────────────┘ ▼
┌──────────────────────┐
┌──────────────────┐ │ email-automation-ms │ SMTP (internal)
│ Microservizio │────▶│ porta 4004 │──────────┐
│ AgileHub │ │ │ ▼
│ (ticket/lead/..) │ │ Rate limit + Template│ ┌──────────────┐
└──────────────────┘ │ Audit DB + Retry │ │ Postfix host │
└──────────────────────┘ │ 172.18.0.1:25│
└──────┬───────┘
│ SASL auth
┌──────────────┐
│ Gmail │
│ smtp-relay │
│ .gmail.com │
└──────────────┘
```
**Relay chain**:
- Prodotto → `email-automation-ms:4004` (via Apache vhost `agilehub.agile.software/api/emails/*`)
- `email-automation-ms` → Postfix `172.18.0.1:25` (container→host, senza auth perché `mynetworks` include `172.18.0.0/16`)
- Postfix → Gmail SMTP relay `:587` (richiede SASL auth service-account Workspace)
---
## 3. Contratto API
### Endpoint canonico
```
POST https://agilehub.agile.software/api/emails/send
Headers:
X-Internal-Key: <INTERNAL_EMAIL_KEY> (shared via /etc/agilehub/internal-keys.env)
Content-Type: application/json
Body:
{
"to": "user@example.com", // string o array
"from_tenant_id": 5, // int — attribuzione log
"template": "demo-registration-verify", // nome template in email-automation-ms/templates/
"variables": { "name": "Mario", ... }, // placeholder per il template
"product": "TRPG", // slug prodotto (audit)
"reply_to": "support@trpg.agile.software", // opzionale
"priority": "transactional" // "transactional" | "marketing" | "system"
}
```
### Response
```json
{
"success": true,
"message_id": "<agilehub-7c3f-20260421@smtp-relay.gmail.com>",
"queued_at": "2026-04-21T15:30:00.000Z"
}
```
### Log audit
Ogni invio viene registrato in `nexus_email_db.email_log`:
- `id, message_id, to, from, subject, template, product, tenant_id, status, sent_at, smtp_response, retry_count`
### Status delivery
- `queued` — accettato dal service, in coda retry
- `sent` — accettato dal Postfix host (risposta 250 OK)
- `delivered` — accettato dal Gmail relay (webhook o check log mail.log)
- `bounced` — Gmail ha rifiutato (bad recipient, spam, auth failure)
- `failed_permanent` — 5xx, no retry
- `failed_transient` — 4xx, retry backoff (max 3)
---
## 4. VIETATO
Qualsiasi alternativa al percorso `email-automation-ms`. In particolare:
**Connessione SMTP diretta** a `172.18.0.1:25` da container prodotto o DevEnv.
**Provider cloud** (SendGrid, Mailgun, SES, Postmark) — se necessari, integrarli DENTRO `email-automation-ms` come alternative transport.
**Librerie SMTP client** in codice prodotto:
- JS: `nodemailer`, `emailjs`, `smtp-connection`
- PHP: `PHPMailer`, `Swift_Mailer`, `mail()` built-in, `mb_send_mail()`
- Python: `smtplib` usato con credenziali cleartext
- Ruby, Go, ecc.: equivalenti
**Binari di sistema** in container: `sendmail`, `mail`, `mutt`, `msmtp`.
**File .env con `SMTP_*`** in qualsiasi servizio diverso da `email-automation-ms`.
### Deroghe
Solo via ticket AgileHub tag `email-bypass-exception`. Motivazioni accettabili:
- Integrazione legacy in dismissione con data di fine (es. mktg-agile oggi)
- Volume massimo > 100k/mese che giustifica provider dedicato (richiede integrazione in email-automation-ms, non bypass)
- Test E2E isolati (usa container mailpit dedicato, non produzione)
Deroghe hanno scadenza obbligatoria (max 90gg). AgileHub tiene registro in `nexus_hub.hub_standards_adoption.exemption_reason/exemption_expires_at`.
---
## 5. Come integrare
### Lato prodotto (TRPG, SustainAI, ecc.)
1. Rimuovere ogni `SMTP_*` dal `.env` del prodotto
2. Aggiungere `INTERNAL_EMAIL_KEY=<value>` al `.env` (coordinato con team AgileHub)
3. Sostituire chiamate dirette con `curl`/`fetch`/`requests` verso `https://agilehub.agile.software/api/emails/send`
4. Rimuovere dipendenze SMTP (`nodemailer`, `PHPMailer`, ecc.) dal manifest (`package.json`, `composer.json`)
### Lato AgileHub
1. `email-automation-ms` espone `/send` + `/templates/list` + `/webhooks/bounce`
2. Gestisce rate limit per tenant (`nexus_email_db.email_quota`)
3. Tiene in vita la connessione SMTP a Postfix (connection pool)
4. Retry exponential backoff su 4xx (1min, 5min, 30min), drop su 5xx
5. Webhook bounce da Gmail (future) → aggiorna `status=bounced` + notifica tenant admin
---
## 6. Stato attuale (2026-04-22 — FIX APPLICATO)
### ✅ Operativo end-to-end
- Architettura centralizzata implementata e funzionante
- IP allowlist Workspace attiva per `135.181.149.254` (configurata da Massimo Tagliavini)
- `sendmail` locale: funziona (cron watchdog, claude-auth-check, ticket-agent)
- **`email-automation-ms` canonical path**: funziona dopo fix 2026-04-22
- Endpoint protetti da `X-Internal-Key` (auth middleware aggiunto)
- Env vars propagate in `/etc/agilehub/internal-keys.env` + `.env` di 5 prodotti produzione
### Fix applicato al transport Nodemailer
File: `email-automation-ms/src/services/emailService.js:25-40`
```javascript
transporter = nodemailer.createTransport({
host, // da SMTP_HOST=127.0.0.1 (era 172.18.0.1)
port,
secure,
name: process.env.SMTP_HELO_NAME || 'agile.software', // HELO esplicito
...
});
```
File: `email-automation-ms/.env`
```
SMTP_HOST=127.0.0.1 # era 172.18.0.1 (docker bridge) → Postfix vedeva connessione "esterna"
INTERNAL_SERVICE_KEY=nexus-internal-2026 # alias INTERNAL_EMAIL_KEY
```
### Auth endpoint
File: `email-automation-ms/src/index.js:27-55` — middleware `requireInternalKey`:
- Protegge: `/emails/send`, `/emails/send-raw`, `/sequences/*`, `/emails/templates/db` (POST/PUT/DELETE)
- Legge la chiave da `INTERNAL_EMAIL_KEY` (preferita) o `INTERNAL_SERVICE_KEY` (retrocompat cron)
- Senza header → 401 UNAUTHORIZED
- Con header valido → passa al controller
- Se nessuna chiave configurata → warn + pass (fallback legacy)
### Test E2E superati (2026-04-22)
| Test | Risultato |
|------|-----------|
| POST `/emails/send-raw` senza X-Internal-Key | 401 ✅ |
| POST `/emails/send-raw` con X-Internal-Key (locale) | 201 + messageId + SENT in DB ✅ |
| POST HTTPS `https://agilehub.agile.software/api/emails/send-raw` da container TRPG | 201 + SENT in DB, product=TRPG ✅ |
| Consegna su Gmail `250 2.0.0 OK` via `smtp-relay.gmail.com:587` | ✅ confermato in `/var/log/mail.log` |
### ❌ Bypass noti
- `/var/www/mktg-agile/.env` contiene ancora `SMTP_*` — tollerato perché prodotto in dismissione Fase C (spegnimento 2026-04-21+)
---
## 7. Obblighi prodotti della suite
Per essere conforme a `email-relay` v1.0 ogni prodotto **DEVE**:
1. ✅ Rimuovere ogni `SMTP_*`, credenziali Gmail, API key mailer-cloud dal proprio `.env`
2. ✅ Eliminare dipendenze mailer (nodemailer, PHPMailer, ecc.) dal manifest
3. ✅ Sostituire ogni invio con POST verso `https://agilehub.agile.software/api/emails/send`
4. ✅ Ottenere `INTERNAL_EMAIL_KEY` da AgileHub team (via ticket tag `email-key-provision`)
5. ✅ Aggiungere `INTERNAL_EMAIL_KEY` al `.env` istanza con chmod 600
6. ✅ Usare template definiti in `email-automation-ms/templates/` (aprire PR per nuovi template)
7. ✅ Loggare `message_id` ricevuto nella response per debug
Un prodotto non conforme entra in `pending` adoption nel registry. Deroghe formali con scadenza obbligatoria.
---
## 8. Riferimenti
- Microservizio: `/var/www/agile-services/email-automation-ms/` (Hetzner) + `/projects/agile-services/email-automation-ms/` (container)
- Entry point: `email-automation-ms/src/index.js:45` (SMTP transport config)
- Template repo: `email-automation-ms/templates/` (Handlebars-like .hbs)
- Log DB: `nexus_email_db.email_log` (MySQL host Hetzner)
- Postfix config: `/etc/postfix/main.cf` (Hetzner host)
- Gmail Workspace Admin Console: account `admin@agile.software`
---
## 9. Changelog
### v1.2 — 2026-04-22 (fix applicato — operativo)
- Fix transport Nodemailer: `SMTP_HOST=127.0.0.1` (era `172.18.0.1` docker bridge) + `name: 'agile.software'` HELO esplicito
- Aggiunto middleware `requireInternalKey` su `/emails/send*`, `/sequences/*`, `/emails/templates/db` mutazioni — chiave `INTERNAL_SERVICE_KEY=nexus-internal-2026` (alias `INTERNAL_EMAIL_KEY`)
- Registrato `INTERNAL_EMAIL_KEY` in `/etc/agilehub/internal-keys.env`
- Propagato `INTERNAL_EMAIL_KEY` + `EMAIL_MS_URL` al `.env` di 5 prodotti produzione (trpg-agile, sustainai-agile, nis2-agile, mktg-agile, allrisk-agile). Altri 5 prodotti (trpg-pro-agile, taxai-agile, lg231-agile, dfm-pro-agile, wms-agile) non hanno `.env` in produzione — useranno la registry centrale.
- Test E2E passati: invio da container TRPG via HTTPS verso `agilehub.agile.software/api/emails/send-raw` → consegnato Gmail 250 OK.
### v1.1 — 2026-04-22 (correzione narrativa, pre-fix)
- Correzione narrativa sezione 6: SMTP NON è bloccato. Il path `sendmail` funziona (Massimo ha configurato IP allowlist da tempo). Il bug reale era in `email-automation-ms` transport.
- Rimosso "fix A/B Postfix SASL" (errore di analisi del 2026-04-21 — non serviva).
### v1.0 — 2026-04-21
- Versione iniziale — formalizza architettura esistente
- Distribuita a 11 prodotti suite via DB registry `hub_standards`
- Richiesta conformità prodotti: rimuovere SMTP diretti dove presenti
---
**Firma autorità**: AgileHub. Violazioni vanno in `hub_standards_distribution_log` con `result='non_compliant'`.

View File

@ -0,0 +1,298 @@
# STANDARD AgileHub — Installer Integration (canonico)
> **Autorità**: AgileHub (single source of truth per la suite Agile Software).
> **Versione**: 1.0.1 — 2026-04-19 (patch: chiarimento payload tenants/report)
> **Stato**: ADOTTATO. Sostituisce qualsiasi proposta prodotto in caso di conflitto.
> **Destinatari**: team TRPG, SustainAI, NIS2, TAXAI (AllTax), LG231, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, CertiSource.
> **Spec origine**: `AGILEHUB_INSTALLER_INTEGRATION_SPEC.md` v1.0 (TRPG, 2026-04-19) — recepita con override sotto.
> **Registry DB**: record master in `nexus_hub.hub_standards` (slug=`installer-integration`, version=`1.0.1`). Questo file è una **copia derivata** — in caso di disallineamento vince il DB.
## Changelog
### v1.0.1 — 2026-04-19 (patch, non breaking)
- **§3.1 `POST /api/tenants/report`**: chiarito che `license_key` è **obbligatorio** nel body (era implicito via "firma HMAC obbligatoria" ma non listato nel payload d'esempio). Serve al middleware HMAC per il lookup della `hub_signing_key`. Origine: smoke test TRPG 2026-04-19 21:15 — 404 LICENSE_NOT_FOUND senza license_key. Decisione AgileHub: consistency > minimal payload (defence in depth anti-tamper).
- TRPG già conforme via commit `5c1aa2e` (2026-04-19).
- Altri prodotti: allineare payload `tenants/report` aggiungendo `license_key` string.
### v1.0 — 2026-04-19
- Versione iniziale adottata. Spec origine TRPG recepita con override AgileHub.
---
## 1. Portata e autorità
Questo documento definisce **il contratto unico** fra AgileHub e i prodotti installati on-premise (o white-label) della suite. Vale per:
- Registrazione prodotti in AgileHub (`hub_products`)
- Provisioning istanze cliente (orchestrator SSH + installer script prodotto)
- Licensing + heartbeat + alert
- Onboarding tenant (consulting_firms) cross-istanza
- Firma HMAC + vault SSH
**Regola di precedenza**: in caso di conflitto fra una proposta prodotto (es. `*_INTEGRATION_SPEC.md` nel loro `docs/`) e questo standard, **vince questo standard**. Deviazioni motivate vanno chieste via ticket AgileHub con tag `standard-exception`.
---
## 2. Decisioni AgileHub (override sulla spec TRPG v1.0)
### 2.1 Hostname ufficiale
| Ruolo | Hostname | Uso |
|-------|----------|-----|
| Hub UI | `https://agilehub.agile.software` | Dashboard admin, `/admin/products/*` |
| Hub API (licensing) | `https://agilehub.agile.software/api/license/*` | endpoint validate/activate/register/report |
| Hub API (installer orchestrator) | `https://agilehub.agile.software/api/installer/*` | wizard, job status, log streaming |
> **NO `hub.agile.software`**. I prodotti che chiamano `hub.agile.software` vanno corretti prima del pilota.
### 2.2 Stack tecnico
| Layer | Scelta |
|-------|--------|
| Backend licensing/installer | **Node.js 20 + Express** (pattern `nexus-*-ms`), microservizio nuovo `nexus-hub-ms` porta **4217** |
| DB | **MySQL host Hetzner** (172.18.0.1:3306), DB dedicato **`nexus_hub`**, user `nexus_user` |
| Frontend | **Next.js 15 + React 19** in `nexus-dashboard`, nuova sezione `/admin/products/*` |
| Job queue (Fase 3) | **BullMQ su Redis** (da aggiungere infra). Fino a Fase 2: cron + status polling |
| Email | Microservizio esistente **`email-automation-ms`** (NO SendGrid/MailHog esterni) |
| Secret vault | **AES-256-GCM env-based**, chiave in `/etc/agilehub/vault.env` (NO HashiCorp Vault/KMS) |
| SSH worker | **ssh2** (npm) invocato da `nexus-hub-ms`, chiave decifrata on-demand |
### 2.3 Prefisso tabelle e naming DB
- DB dedicato: `nexus_hub`
- Tabelle: prefisso `hub_` mantenuto (compat con spec origine). Accesso esclusivo da `nexus-hub-ms`.
- Cross-DB query verso `nexus_ticket_db`, `nexus_tenant_db`, `nexus_lead_db` sono permesse solo via GRANT dedicato a `nexus_user` (pattern già in uso per SSO).
### 2.4 Ruoli
| Ruolo | Permessi |
|-------|----------|
| `hub_admin` | Tutto tranne provisioning SSH |
| `hub_installer` | **Nuovo**. Lettura `hub_*`, esecuzione wizard, decrypt chiavi SSH con audit obbligatorio |
| `hub_support` | Solo lettura `hub_instances`, `hub_tenants`, `hub_license_heartbeats` |
Il ruolo `hub_installer` è **richiesto** per Fase 3. Non riutilizzare `hub_admin`: blast radius troppo ampio.
### 2.5 Versioning
Recepita la regola TRPG: **version mai hardcodata nei seed o nelle migration**. La hub la registra da payload heartbeat. Per `hub_products.version` (ultima release disponibile) leggere on-demand via `GET https://<product>.agile.software/version.json` oppure aggiornare via `POST /api/products/:slug/version` (admin-only).
### 2.6 Integrazione obbligatoria con SSO
`hub_tenants.owner_email` **DEVE** essere foreign-key logica verso `sso_identities.email` (DB `nexus_tenant_db`). Al provisioning di un nuovo tenant:
1. `nexus-hub-ms` POST `/api/auth/admin/tenants/create` sull'istanza
2. L'istanza crea `consulting_firm` + `user` locale
3. `nexus-hub-ms` replica l'identità in `sso_identities` (se non esiste)
4. Prima login dell'owner → SSO flow standard (Fase 2 SSO)
**Prodotti che non hanno ancora SSO dual-mode**: finché non è attivo, `hub_tenants` funziona stand-alone ma `owner_email` deve comunque essere univoco cross-istanza.
### 2.7 Deprecazione `products.json`
`/opt/agent-ai/hub/products.json` è **deprecato** dalla Fase 2. Entro la Fase 2 completa:
- `hub_products` diventa la sorgente di verità
- La UI legge da `hub_products`, non più da `products.json`
- `products.json` resta come fallback statico per landing page esterne, generato da cron da `hub_products`
### 2.8 Multi-prodotto da subito
Fase 1 va implementata **generica** (non TRPG-specific). Lo schema, gli endpoint e le firme HMAC supportano `product_slug` variabile fin dal primo giorno. Il seed iniziale è per TRPG, ma il codice non hardcoda nulla.
---
## 3. Contratto API canonico
### 3.1 Endpoint hub (Istanza → AgileHub)
Tutti su `https://agilehub.agile.software/api/...`, header obbligatorio:
```
X-Product-Signature: hmac_sha256(body, HUB_SIGNING_KEY_<PRODUCT_SLUG_UPPER>)
Content-Type: application/json
```
| Endpoint | Scopo | Chiamato da |
|----------|-------|-------------|
| `POST /api/license/activate` | Prima attivazione dopo install | `install-<slug>.sh` |
| `POST /api/license/validate` | Heartbeat periodico (24h default, 1h se anomalia) | cron istanza |
| `POST /api/instances/register` | Registrazione finale istanza | `install-<slug>.sh` |
| `POST /api/tenants/report` | Sync tenant create/update/archive | istanza, on-change |
| `GET /api/products/:slug/version` | Ultima versione disponibile | `update-<slug>.sh` |
Payload: identici alla spec TRPG §6 (validate/activate/register/report). Campi obbligatori aggiuntivi in **tutti** i body:
- `product_slug` — sempre, anche dove la spec TRPG lo omette implicitamente
- `license_key`**sempre** (v1.0.1 clarification). Serve al middleware HMAC per lookup di `hub_signing_key` prima della verifica firma. Include `tenants/report` che nella spec origine non lo listava esplicitamente.
**Eccezione**: `POST /api/license/activate` non porta `X-Product-Signature` (primo handshake, la chiave non esiste ancora) ma deve comunque includere `license_key` per il match iniziale.
### 3.2 Endpoint istanza (AgileHub → Istanza)
Ogni prodotto della suite **deve** esporre:
| Endpoint | Scopo | Auth |
|----------|-------|------|
| `POST /api/auth/admin/tenants/create` | Onboarding tenant da hub | Bearer JWT service + `X-Hub-Signature` |
| `POST /api/auth/admin/tenants/:id/suspend` | Sospensione tenant | idem |
| `POST /api/auth/admin/tenants/:id/resume` | Riattivazione tenant | idem |
| `GET /api/auth/admin/health` | Health check completo per hub | API key service |
**Shape risposta**: identica alla spec TRPG §5.2. Ogni prodotto implementa il proprio endpoint ma lo shape è vincolante.
### 3.3 Installer script non-interattivo (obbligatorio per prodotti orchestrati)
Ogni prodotto della suite che vuole supportare provisioning orchestrato da AgileHub **deve** fornire uno script:
```
install-<slug>.sh --non-interactive --json-output \
--slug=... --domain=... --client-name=... --admin-email=... --admin-name=... \
--license-key=... --hub-url=https://agilehub.agile.software \
--<provider-key-1>=... --<provider-key-2>=... \
[--smtp-host=... ...] [--force-reinstall]
```
**Exit codes standard** (identici per tutti i prodotti):
| Codice | Significato |
|--------|-------------|
| 0 | Success |
| 10 | Preflight failed (docker, RAM, disk, porte) |
| 20 | Secret generation failed |
| 30 | Hub activation failed (license invalid) |
| 40 | Docker load/up failed |
| 50 | Migration DB failed |
| 60 | Seed/admin creation failed |
| 70 | SSL setup failed |
| 80 | Verify-install failed |
| 90 | Internal error |
**JSON output**: shape identica a spec TRPG §5.1. Campo `product_slug` obbligatorio in entrambi i rami (success/failure).
---
## 4. Schema DB canonico (DB `nexus_hub`)
Recepito integralmente dallo schema TRPG §3.1 (8 tabelle):
- `hub_products`
- `hub_instances`
- `hub_instance_provisioning_logs`
- `hub_licenses`
- `hub_license_heartbeats`
- `hub_license_alerts`
- `hub_tenants`
- `hub_ssh_credentials`
**Modifiche AgileHub**:
1. `hub_products.framework` VARCHAR(32) NULL — aggiunto, indica stack prodotto (`php-vanilla`, `node-express`, `nextjs`, ecc.) utile all'orchestrator
2. `hub_instances.sso_enabled` BOOLEAN DEFAULT FALSE — aggiunto, indica se il prodotto ha SSO attivo su quell'istanza
3. `hub_tenants.sso_identity_id` INT NULL — FK logica a `sso_identities.id` (riempito quando SSO attivo)
4. Tutte le tabelle hanno `tenant_id INT NULL` per compat futuro con multi-tenancy hub (oggi NULL, riservato)
Migration canonical: `nexus-hub-ms/migrations/001-initial-schema.sql` (da creare). **Sub-approval obbligatoria** prima dell'esecuzione su produzione (maintenance mode come da CLAUDE.md).
---
## 5. Sicurezza
### 5.1 Firma HMAC
- **Algoritmo**: HMAC-SHA256
- **Chiave**: `HUB_SIGNING_KEY_<PRODUCT_SLUG_UPPER>` (es. `HUB_SIGNING_KEY_TRPG`, `HUB_SIGNING_KEY_SUSTAINAI`)
- **Generazione**: `openssl rand -hex 32`
- **Storage**: in `/etc/agilehub/vault.env` sull'host AgileHub + `.env` di ogni istanza (propagata da installer al momento di `activate`)
- **Rotazione**: ogni 6 mesi, bidirezionale coordinata
### 5.2 SSH vault
- Cifratura: AES-256-GCM
- Master key: `HUB_VAULT_KEY` in `/etc/agilehub/vault.env` (NON in DB, NON in repo git)
- Ogni decrypt logga in `hub_audit_logs` con `user_id`, `timestamp`, `source_ip`, `purpose`
- Sudo whitelist sul server target: file `/etc/sudoers.d/<slug>-installer` con binari strettamente necessari
### 5.3 JWT service-to-service
- Algoritmo: RS256
- Chiave privata AgileHub + chiave pubblica distribuita ai prodotti al momento dell'attivazione
- Claim obbligatori: `iss=agilehub`, `aud=<product_slug>`, `role=service`, `exp` ≤ 5 minuti
- Ogni prodotto verifica `iss` + `aud` + `exp` + firma
---
## 6. Fasi di rollout (coordinate cross-prodotto)
| Fase | Durata | Chi fa cosa | Bloccante per |
|------|--------|-------------|---------------|
| **Fase 1** — API licensing hub | 2-3 gg | AgileHub: `nexus-hub-ms` + migration `nexus_hub` + 5 endpoint + seed 11 prodotti | Prima installazione cliente qualsiasi prodotto |
| **Fase 1b** — Installer script prodotto | 3-5 gg/prodotto | Ogni prodotto: `install-<slug>.sh` non-interattivo + `update-<slug>.sh` + `/api/auth/admin/tenants/*` endpoint | Pilota white-label di quel prodotto |
| **Fase 2** — UI Prodotti read-only | 3-4 gg | AgileHub: sidebar "Prodotti", elenco, detail read-only, heartbeat dashboard | Visibilità pilota |
| **Fase 3** — Orchestrator + wizard | 6-8 gg | AgileHub: wizard 5-step, SSH worker, log streaming, rollback, BullMQ, encrypt vault | Onboarding cliente self-service |
| **Fase 4** — Multi-prodotto generalizzato | 2-3 gg | AgileHub: template wizard per prodotto, `installer_orchestrator_config` drive-based, billing hook | Scale-up suite |
**Pilota**: TRPG primo. Dopo pilota OK, checklist per onboarding secondo prodotto (SustainAI probabile next).
---
## 7. Obblighi prodotti della suite
Per essere "orchestrabile" da AgileHub, un prodotto **DEVE**:
1. ✅ Fornire `install-<slug>.sh --non-interactive --json-output` con exit codes e shape JSON canonici
2. ✅ Fornire `update-<slug>.sh` (Fase 3, no urgenza) con stesso pattern
3. ✅ Esporre `GET /version.json` con `{version, released_at, changelog_url}`
4. ✅ Esporre `GET /api/auth/admin/health` autenticato
5. ✅ Esporre `POST /api/auth/admin/tenants/create|:id/suspend|:id/resume` con HMAC + JWT service
6. ✅ Chiamare `POST /api/license/activate` alla fine di `install-<slug>.sh`
7. ✅ Avere un cron `license-heartbeat.sh` che chiama `POST /api/license/validate` ogni 24h (1h se anomalia)
8. ✅ Chiamare `POST /api/tenants/report` ad ogni create/update/archive consulting_firm
9. ✅ Gestire stati licenza: `valid → read_only (48h grace) → locked (168h)` con banner UI maintenance
10. ✅ Propagare `HUB_SIGNING_KEY_<SLUG>` nel `.env` istanza al momento dell'attivazione
Un prodotto che **non** rispetta questi 10 punti non può fare parte del catalogo installer AgileHub. Può restare nel menu `products.json` come link esterno (`installer_type=external_link`) finché non si adegua.
---
## 8. Domande aperte — risposte ufficiali
Le 10 domande di §11 della spec TRPG trovano risposta nel capitolo 2 di questo doc. Riepilogo:
1. **Framework hub**: Node.js/Express (non PHP).
2. **DB**: MySQL host Hetzner, DB dedicato `nexus_hub`.
3. **Job queue**: cron fino a Fase 2, BullMQ/Redis da Fase 3.
4. **UI**: Next.js 15 + React 19 (nexus-dashboard).
5. **Vault SSH**: AES-256-GCM env-based.
6. **Monitoring**: `/health` endpoint + cron watchdog esistente. Prometheus opzionale in Fase 4.
7. **Ruolo installer**: nuovo `hub_installer` separato da `hub_admin`.
8. **Email**: `email-automation-ms` esistente.
9. **Multi-prodotto**: generico da Fase 1, seed TRPG ma codice agnostic.
10. **Billing**: nessun Stripe oggi. `hub_customers` placeholder, billing in Fase 4.
---
## 9. Prossimi passi
1. **AgileHub**: scaffold `nexus-hub-ms` + migration `001-initial-schema.sql` (sub-approval richiesta)
2. **AgileHub**: distribuire questo documento a tutti i prodotti della suite via SSH Hetzner
3. **TRPG**: adeguare `install-trpg.sh` al contratto §3.3 (hostname `agilehub.agile.software`, exit codes canonici, JSON shape)
4. **TRPG**: implementare `/api/auth/admin/tenants/*` endpoint con HMAC + JWT (§3.2)
5. **Suite**: ogni team leggere questo doc e aprire ticket AgileHub con tag `installer-readiness` per dichiarare quando saranno pronti
---
## 10. Riferimenti
- Spec origine TRPG: `/var/www/trpg-agile/docs/AGILEHUB_INSTALLER_INTEGRATION_SPEC.md` (v1.0, 2026-04-19)
- Roadmap TRPG: `/var/www/trpg-agile/docs/INSTALLER_ROADMAP.md`
- White-label TRPG: `/var/www/trpg-agile/docs/INSTALLER_WHITELABEL.md`
- Licensing model TRPG: `/var/www/trpg-agile/docs/PROTEZIONE_FASE4_LICENSING.md`
- Multi-studio TRPG: `/var/www/trpg-agile/docs/MULTI_STUDIO_SPEC.md`
- SSO AgileHub: `/projects/agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md`
- SSO fasi: `/projects/agile-services/docs/SSO_FASI_IMPLEMENTAZIONE.md`
- CLAUDE.md AgileHub: `/projects/agile-services/CLAUDE.md` (regole maintenance + agent + escalation)
---
**Firma autorità**: questo documento è **canonico** per la suite Agile Software. Modifiche richiedono consenso AgileHub + notifica a tutti i prodotti della suite.
**Versione**: 1.0 — 2026-04-19
**Prossima revisione**: dopo completamento pilota TRPG (stimata 2026-05-31).

View File

@ -0,0 +1,237 @@
# Standard `marketing-tenant-provisioning` v1.4
**Owner**: AgileHub MARKETER agent (governance) + TITAN (esecuzione DKIM) + MAESTRO (orchestratore atomico AC30)
**Stato**: adopted (LIVE 2026-04-26 mattina v1.3 — DKIM per-tenant; v1.4 mattina — AC30 pronto in branch, deploy pending trigger)
**Applies to**: `*` (tutti i prodotti suite — TRPG, SUSTAINAI, NIS2, LG231, TAXAI, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, AGILEHUB)
**Versione**: 1.4
**MS implementatore**: `nexus-marketing-ms` (porta 4221) + `email-automation-ms` (porta 4004) + OpenDKIM milter Postfix Helsinki + `agilehub-workflow-engine` (porta 4230, blocco AWE `AC30_MarketingTenantProvision`)
**Endpoint base**: `/api/marketing/*`
---
## 1. Scopo
Lo standard definisce il contratto cross-suite tra prodotti consumer e modulo Marketing AgileHub centralizzato. Ogni prodotto (TRPG come pilot) consuma 28 endpoint stabili `/api/marketing/*` per fornire ai propri clienti (consulting firm) un servizio email marketing white-label multi-tenant.
## 2. Multi-tenancy
| Concetto | Identificativo | Source of truth |
|---|---|---|
| Tenant marketing | `tenant_id` (es. `tnt_agile-technology_b4f9a3141333`) | `nexus_marketing_db.tenants` |
| Consulting firm consumer | `consulting_firm_id` (FK logico) | DB del prodotto consumer (es. `trpg.consulting_firms`) |
| `firm_slug` | derivato da `slugify(consulting_firm.name)` | calcolato server-side al provisioning |
| API key per-tenant | `ah_live_*` (prod) / `ah_test_*` (sandbox) | `nexus_marketing_db.api_keys` |
Vincolo: 1:1 tra `tenant_id` e `consulting_firm_id` nel prodotto consumer (nessuna 1:N).
**Naming convention `firm_slug`** (formalizzato v1.1):
- Algoritmo: `slugify(consulting_firm.name)` — lowercase ASCII, separatore `-`, rimozione caratteri non alfanumerici, max 64 char
- Esempi:
- `"Tremolada Consulting S.r.l."``tremolada-consulting`
- `"Agile Technology s.r.l."``agile-technology`
- `"O'Brien & Co."``obrien-co`
- `tenant_id` auto-generato server-side: `tnt_${firm_slug}_${randomId(6)}` (esempio: `tnt_agile-technology_b4f9a3141333`)
- Sottodominio derivato: `{firm_slug}.agile.software` (es. `agile-technology.agile.software`)
- Reply-to default: `noreply@{firm_slug}.agile.software`
- Il consumer NON costruisce mai il `firm_slug` autonomamente: lo riceve in risposta a `POST /admin/tenants` e lo persiste come dato derivato
## 3. Autenticazione
- Header obbligatorio: `X-AgileHub-Key: <api_key_plaintext>`
- Header obbligatorio: `X-AgileHub-API-Version: 1.0`
- Hash SHA-256 della key in DB (`api_keys.key_hash`); plaintext mai persistito leggibile (cifrato AES-256-GCM in `api_keys.key_encrypted`)
- Provisioning admin via endpoint interno `/api/marketing/admin/tenants/:id/api-keys` (header `X-Internal-Key` solo Esperto Agile/installer)
## 4. API versioning policy
- Header request: `X-AgileHub-API-Version: 1.0` (obbligatorio)
- Header response: `X-AgileHub-API-Version: 1.0` (echo)
- Header response: `X-Request-Id: req_<uuid>` (cross-system tracing)
- Deprecation policy: 6 mesi notice (`X-Deprecation-Date`) + 6 mesi sunset (`X-Sunset-Date`) prima rimozione versione precedente
- Canary opt-in: `X-API-Canary: true` su feature flag tenant
## 5. Rate limiting
| Endpoint family | Default | Override |
|---|---|---|
| Read endpoints | 60 req/min | |
| `/contacts/import` | 10 req/min | `RATE_LIMIT_IMPORT` env |
| `/segments/preview` | 20 req/min | `RATE_LIMIT_PREVIEW` env |
| Tracking pixel/click | nessuno (pubblico) | |
Header response: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
Risposta 429 con `retry_after_sec`.
## 6. Error envelope
```json
{
"error": {
"code": "STRING_CODE",
"message": "Human readable",
"details": { ... opzionale ... }
},
"request_id": "req_<uuid>"
}
```
Codici stabili documentati: `MISSING_API_KEY`, `INVALID_API_KEY`, `API_KEY_REVOKED`, `API_KEY_EXPIRED`, `TENANT_NOT_FOUND`, `TENANT_DELETED`, `TENANT_SUSPENDED`, `UNSUPPORTED_API_VERSION`, `RATE_LIMITED`, `DSL_INVALID`, `DSL_TOO_DEEP`, `DSL_FIELD_NOT_ALLOWED`, `DSL_OP_NOT_ALLOWED`, `CONTACT_SUPPRESSED`, `CONTACT_NOT_FOUND`, `INVALID_EMAIL`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_LOCKED`, `CAMPAIGN_FINAL`, `CAMPAIGN_NOT_SCHEDULABLE`, `NO_RECIPIENTS`, `TEMPLATE_NOT_FOUND`, `TEMPLATE_INVALID`, `TEMPLATE_REQUIRED`, `EXPORT_JOB_NOT_FOUND`, `IMPORT_JOB_NOT_FOUND`, `INTERNAL_ERROR`.
## 7. SLA
| Indicatore | Target | Note |
|---|---|---|
| Uptime | 99.5% rolling 30gg | Allineato altri MS suite |
| p95 GET | <500ms | Cache Redis 30s su preview |
| p95 POST | <2000ms | DB persist + audit + idempotency |
| `/segments/preview` 50k contatti | <2s | Cache 30s + indici DB |
| RTO | 30 min | mysqldump nightly + restore documentato |
| RPO | 1h | Replication MySQL slave (futuro) |
## 8. GDPR compliance
- Double opt-in obbligatorio (POST `/contacts` con `consent_pre_confirmed=false` invia email confirm)
- Footer unsubscribe RFC 8058 auto in tutti i template (one-click `List-Unsubscribe` + `List-Unsubscribe-Post`)
- Suppression list permanente per-tenant (`unsubscribe`, `complaint`, `hard_bounce`, `gdpr_request`)
- Hard delete contact (`POST /contacts/:id/hard-delete`) ritorna `audit_trail_id`
- Export ZIP portabilità (`POST /exports`) — contacts.csv + campaigns.json + suppression.csv
- Offboarding tenant: 90gg grace → hard delete (zero retention post-offboard)
- Retention metrics_events: 90gg rolling, cron pulizia notturna
## 9. Branding tenant
- Sottodominio `{firm-slug}.agile.software` per envelope From
- 4 placeholder firm: `firm.name`, `firm.logo_url`, `firm.primary_color`, `firm.accent_color`
- 3 personalization tag contact: `contact.first_name`, `contact.last_name`, `contact.email`
- Reply-to configurabile per tenant
- DKIM selector dedicato per tenant (deliverability isolation)
## 10. 28 endpoint
Vedi `/var/www/agile-services/nexus-marketing-ms/src/routes/*` o ticket `TICKET_AGILEHUB_MARKETING_API.md` per spec completa. Riassunto:
- Sprint 2 (5 read-only): `/tenants/me`, `/contacts` GET, `/campaigns` GET/{id}, `/templates` GET, `/metrics/overview`
- Sprint 3 (12 write): contacts POST, segments POST/GET/preview, campaigns POST/PATCH/schedule/cancel, templates render, test-send, metrics per-campaign
- Sprint 4 (10 GDPR): contacts PATCH/DELETE/hard-delete/import, exports POST/{id}, quota, admin tenants suspend/resume/delete
## 11. Sandbox
- URL primary: `https://agilehub-staging.agile.software/api/marketing` (LIVE 2026-04-25)
- URL secondary (legacy): `https://staging.agilehub.agile.software/api/marketing`
- Tenant test corrente: `tnt_agile-technology_b4f9a3141333` (`plan='enterprise_test'`, `quota_default=5000`)
- Tenant precedente: `tnt_test_tremolada_001` (suspended 2026-04-25 sera, swap-out per testing su entità Agile Technology s.r.l.)
- Key dedicata: `ah_test_*` (mai mescolare con `ah_live_*`)
- DB: `nexus_marketing_db` (clone notturno cron)
- Subdomain tenant: `{firm_slug}.agile.software` (es. `agile-technology.agile.software`) — zone Cloudflare-managed, automazione full
- DNS deliverability sandbox: SPF + DMARC + **DKIM** tutti `active` (LIVE 2026-04-26 mattina)
- **DKIM signing**: OpenDKIM milter su Postfix Helsinki, selector `agile2026`, RSA 2048, rotation annuale Q1
## 12. Deroghe
I prodotti consumer non possono opporsi unilateralmente. Per esenzioni: aprire ticket AgileHub con tag `standard-exception` + motivazione tecnica + termine. MARKETER + REGENT decidono entro 5gg lavorativi.
## 13. Distribuzione
| Prodotto | docs_file | claude_md_section | claude_memory | adoption_status |
|---|---|---|---|---|
| trpg | ✅ TODO | ✅ TODO | ✅ TODO | pending → acknowledged after deploy |
| sustainai | TBD M5 | | | pending |
| nis2 | TBD M5 | | | pending |
| altri 8 | TBD post-Tremolada | | | pending |
## 14. Riferimenti
- Spec produttore TRPG: `/var/www/trpg-agile/docs/PROMPT_MARKETING_FEATURE.md` v1.3
- Spec contratto: `/var/www/trpg-agile/docs/TICKET_AGILEHUB_MARKETING_API.md`
- Doc agente MARKETER: `docs/AGENT_MARKETER.md`
- Reply formale TRPG: `docs/REPLY_TO_TRPG_MARKETING_REQUEST_DRAFT.md`
- Audit gap: `docs/HANDOVER_FROM_TRPG_MARKETING_FEATURE_REQUEST.md`
## 15. Changelog
### v1.4 (2026-04-26 mattina, AC30 blocco AWE pronto in branch — MAESTRO impl + 11/11 test)
- **Nuovo blocco AWE `AC30_MarketingTenantProvision`** implementato + testato (619 righe blocco + 397 righe test, file untracked nel branch `feature/awe-m1-m2-foundation`)
- **Orchestrazione atomica** end-to-end provisioning tenant marketing in 1 nodo workflow: tenant create + 3 DNS Cloudflare (A + SPF + DMARC) + DKIM SSH exec `dkim-provision.js` + DPA opzionale + API key generation
- **Compensation pattern**: hook `compensate(ctx, result)` engine-level + rollback inline su partial-state failure (cancella DNS records, revoca DKIM via `--revoke`, soft-delete tenant 90gg grace)
- **Idempotency H7**: `sha256(runId + nodeId + {firmSlug, firmLegalName})` TTL 7gg
- **Dry-run mode** completo (mock conforme `outputSchema`, no side-effect)
- **Vault integration**: `tier1__nexus-hub-ms__cloudflare/api_token` per CF token, `INTERNAL_SERVICE_KEY` env per X-Internal-Key marketing-ms admin endpoints
- **SSH credential**: pattern `loadAndDecrypt(sshCredentialId)` riusato da AC23
- **Auto-loaded** dal catalog registry (`src/blocks/index.js`) come 13° blocco `action`
- **Test verdi**: 11/11 (6 scenari §9 handover MAESTRO + 3 schema/shape + 2 dryRun) — happy path + rollback DNS + rollback DKIM + skipDkim + firm_slug duplicato + idempotency
- **Trigger originale Q3 2026** (≥3 tenant prod), implementazione **anticipata** disponibile on-demand
- **Decisione GO/NO-GO deploy**: pending — richiede `pm2 reload agilehub-workflow-engine` + bump health.js block count + smoke E2E contro marketing-ms reale
- **Spec autoritativa**: [`HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md`](HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md)
- **Coordinamento agenti** (al deploy): MAESTRO (impl) + TITAN (SSH exec coord) + MARKETER (acceptance + bump v1.5 con esempio workflow JSON pubblicato) + PRISMA (form auto-render verify) + VIGILE (security inventory blocco side-effect)
### v1.3 (2026-04-26 mattina, DKIM per-tenant LIVE — TITAN Phase 1-4)
- **DKIM signing OPERATIVO**: OpenDKIM 2.11.0 installato su Postfix Helsinki, milter `inet:localhost:8891`, `milter_default_action=accept` (fail-open per zero downtime)
- **Schema scope chiave**: 1 keypair RSA 2048 per-tenant, selector `agile2026._domainkey.{firm_slug}.agile.software`. Pattern conferma standard §16 (deliverability isolation per-tenant).
- **Tenant Agile Technology**: keypair generata + DNS TXT pubblicato Cloudflare + KeyTable + SigningTable + DB UPDATE → `dkim_status='active'`
- **Automation script**: `scripts/dkim-provision.js` self-contained Node.js (no npm install, mysql CLI + https native + execSync). Idempotente. Eseguito on-host Hetzner.
- **§16 ggiornato**: `Strategia DKIM sandbox vs prod` rimossa "skip sandbox" (DKIM ora attivo anche in sandbox per testing realistico). Mantengono `dkim_status='not_required'` come stato simbolico per tenant esplicitamente esentati.
- **Rotation policy**: annuale Q1 (Q1 2027 prossimo turno), dual-key 30gg overlap durante transizione, coordinata con VIGILE Pillar 1
- **Backward compat**: 99.994% del traffico Postfix Helsinki (cron, www-data, noreply@agile.software) NON matcha SigningTable → continua identica a oggi (15.466 email/7gg invariate, verificato empiricamente)
### v1.2 (2026-04-25 sera, post-allineamento Cristiano TRPG — stesso giorno di v1.1)
- **Convention dominio cambiata**: `*.agilehub.it``*.agile.software` (correzione architetturale: Agile ha un solo dominio commerciale `agile.software`, già zone Cloudflare-managed per altri sottodomini come `trpg.agile.software`, `agilehub.agile.software`)
- **Schema `tenant_branding.dkim_status`**: ALTER additive enum, aggiunto valore `not_required` per sandbox/non-prod (oltre a `pending`/`active`/`failed`)
- **Tenant `tnt_agile-technology_b4f9a3141333`**: UPDATE branding subdomain → `agile-technology.agile.software`, reply_to → `noreply@agile-technology.agile.software`, dkim_status=not_required, spf_status=active, dmarc_status=active
- **§16 riscritto**: sostituito vecchio scenario register.it con scenario reale Cloudflare-managed
- **§11 Sandbox**: aggiornato per nuovo subdomain pattern + DNS deliverability LIVE
- **DKIM strategia**: skipped per sandbox, **mandatory per produzione** (go-live Tremolada 23/6 — vedi §16)
### v1.1 (2026-04-25 sera, superseded by v1.2 stesso giorno)
- **§2 Multi-tenancy**: formalizzato `firm_slug = slugify(consulting_firm.name)` come algoritmo derivato server-side. Aggiunti 3 esempi concreti, regola `tenant_id = tnt_${firm_slug}_${randomId(6)}`, derivazione automatica sottodominio + reply-to.
- **§11 Sandbox**: aggiornato tenant corrente a `tnt_agile-technology_b4f9a3141333` (sostituisce `tnt_test_tremolada_001` suspended)
- v1.1 documentava convention `*.agilehub.it` (corretta in v1.2 stesso giorno)
## 16. Gestione zone DNS `*.agile.software` + deliverability
**Provider DNS authoritative**: Cloudflare (zone id `e3d677355677a6397d2caa77264cbfa2`).
**MX inbound**: separato (Microsoft 365 su zone `agilehub.it`); modulo Marketing fa SOLO outbound, no impatto MX.
**Automazione**: full via Cloudflare API (token in vault `tier1__nexus-hub-ms__cloudflare/api_token`).
**Pattern record per tenant** (provisioning automatico con MARKETER+TITAN):
| Record | Esempio Agile Technology | Scope |
|---|---|---|
| `A` | `agile-technology.agile.software → 135.181.149.254` | Helsinki Postfix outbound |
| `TXT` SPF | `v=spf1 ip4:135.181.149.254 -all` | autorizzazione mittente |
| `TXT` DMARC | `_dmarc.agile-technology.agile.software TXT v=DMARC1; p=none; rua=mailto:dmarc-reports@agile.software; pct=100` | report-only modalità monitoring |
| `TXT` DKIM | `agile2026._domainkey.agile-technology.agile.software TXT v=DKIM1; k=rsa; p=<pubkey>` | **solo PROD**, skip sandbox |
**Strategia DKIM sandbox vs prod (v1.3 LIVE 2026-04-26)**:
- **DKIM mandatory per tutti i tenant produttivi** (no skip). DMARC inizia `p=none` (monitoring) e bumppa `p=quarantine` poi `p=reject` quando deliverability stabile post-3 settimane di metriche pulite.
- **`dkim_status='not_required'`**: stato simbolico per tenant esplicitamente esentati (es. tenant disabilitati o test temporanei senza traffico real-world). Widget consumer lo mappa come "n/a".
- **Sandbox attiva DKIM by default** dal v1.3: `agile-technology.agile.software` ha DKIM `active` per testing realistico pre-prod.
**Effort provisioning per nuovo tenant prod (con script automation v1.3)**:
1. POST `/admin/tenants` (auto-genera firm_slug + subdomain) — 1s
2. POST 3 record Cloudflare base (A + SPF + DMARC) — manuale via curl o futuro workflow AWE — 10s
3. **`node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug>`** ON HOST HETZNER (root):
- genera keypair RSA 2048 (idempotente)
- append KeyTable + SigningTable
- publish DNS DKIM TXT su Cloudflare (idempotente, PATCH se esiste)
- UPDATE `tenant_branding.dkim_*` + `dkim_status='active'`
- `systemctl reload opendkim` hot
- **~3-5 secondi end-to-end**
**Totale**: ~15s tenant pronto. Future workflow AWE blocco `AC30_MarketingTenantProvision` Q3 2026 può chiamare script via webhook host.
**Rotation policy DKIM** (annuale Q1, coord VIGILE):
1. Q1 ogni anno: VIGILE notifica scadenza
2. TITAN: nuovo selector (es. `agile2027` per Q1 2027), genera keypair nuovo
3. Pubblica DNS DKIM nuovo (entrambi vecchio+nuovo coesistono per 30gg overlap)
4. Aggiorna `KeyTable` + `SigningTable` per usare nuovo selector
5. `systemctl reload opendkim` → email firmate con nuova chiave
6. T+30gg: revoca vecchio selector (DELETE DNS vecchio + cleanup KeyTable/SigningTable)
7. Audit log TITAN+VIGILE entry coordinated
**Comandi rapidi rotation** (futuro Phase 4 enhancement):
```bash
# T0: nuova chiave
node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug> --selector agile2027
# T+30gg: revoca vecchia
node scripts/dkim-provision.js --revoke --tenant-id <id> --firm-slug <slug> --selector agile2026
```

View File

@ -0,0 +1,160 @@
# STANDARD AgileHub: multitenant-architecture v1.0
> **Codename progetto**: NAVIGAI (Navigare nell'AI)
> **Status**: `proposed`
> **Applies to**: `*` (tutta la suite — 11 prodotti)
> **Owner**: REGENT (coordinamento) + TITAN (backend) + VIGILE (audit) + Agile AI (KB master) + PRISMA (UI master/tenant)
> **Date**: 2026-05-17
> **Public facing**: AgileHub (NAVIGAI è il codename interno, invisibile al cliente)
---
## 1. Scopo
Definire l'architettura multitenant esplicita di AgileHub con un livello master condiviso, tenant client isolati, catalogo cross-tenant condivisibile, billing per-tenant e governance centralizzata.
## 2. Tassonomia tenant
| Tipo | Esempio | id | is_master | tier | parent_tenant_id |
|---|---|---|---|---|---|
| **Master** | AgileHub | 1 | TRUE | `master` | NULL |
| Client enterprise | Work Group s.r.l. | N | FALSE | `enterprise` | 1 |
| Client professional | Studio Tremolada | N | FALSE | `professional` | 1 |
| Client trial | sandbox | N | FALSE | `trial` | 1 |
| Sandbox interno | Agile Technology test | N | FALSE | `trial` | 1 |
## 3. Visibility ENUM cross-tabella
Ogni record sensibile (KB articles, personas, workflow templates, routing rules, RAG repositories) ha colonna `visibility ENUM('global','shared','tenant','team','private')`.
| Valore | Significato | Lettura da |
|---|---|---|
| `global` | Master AgileHub catalog, riusabile read-only da tutti i tenant | tutti |
| `shared` | Visibile a tenant + i suoi sotto-tenant | tenant + figli |
| `tenant` | Solo tenant proprietario | tenant |
| `team` | Team specifico dentro un tenant | team members |
| `private` | Singolo utente | owner_user_id |
## 4. Opt-out granulare
Tabella `nexus_tenant_db.tenant_global_exclusions(tenant_id, resource_type, resource_id, exclusion_reason)` permette al client di dichiarare "non voglio usare quell'articolo KB master/quella persona master/quella regola routing master".
## 5. JWT additivi (no breaking)
Claims aggiunti (additive, retro-compat):
- `tenant_id BIGINT`
- `tenant_slug VARCHAR(64)`
- `is_master TINYINT(1)`
- `permitted_tenant_ids INT[]` (per is_master users che possono switchare contesto)
Vecchi JWT senza nuovi claim restano validi → tenant_id desunto via fallback `tenants.email → user → tenant_membership`.
## 6. Shared library
Pacchetto npm interno `@agile/tenant-auth` (Node) + `agile_tenant_auth` (Python) fornisce:
- `extractTenantContext(req)``{ tenant_id, tenant_slug, is_master, user_id }`
- `requireSameTenant(req, resourceTenantId)` → throws 403 se mismatch
- `requireMaster(req)` → throws 403 se !is_master
- `withTenantFilter(query)` → injects `WHERE (tenant_id = X OR visibility = 'global')`
## 7. Vault namespace tenant-aware
Pattern: `tier1__<ms>__<tenant_slug>__<feature>`
Esempi:
- `tier1__nexus-marketing-ms__work_group_001__mailgun_api_key`
- `tier1__nexus-presenter-ms__agilehub_master__tavus_api_key`
Compatibility: namespace senza `<tenant_slug>` restano validi per master.
## 8. Audit log HMAC chain
JSONL append-only in `docs/multitenant-audit/YYYY-MM-WNN.jsonl` con catena HMAC-SHA256 (ogni entry contiene SHA del precedente). Genesis entry firmata da TITAN al kickoff SG-0.
## 9. Distributed tracing
Header `X-Trace-Id` propagato cross-MS in tutti i call HTTP/Redis Streams. Generato in API gateway Apache se assente. Pino structured log + Loki backend.
## 10. Status page tenant-aware
`/status` Next.js mostra SLA + uptime + latency p99 **per-tenant** (con auth). SLA differenziato per tier:
- master: 99.99%
- enterprise: 99.95%
- professional: 99.9%
- trial: 99%
## 11. Canary deploy
Apache `mod_proxy_balancer` rolling 5% → 50% → 100% con auto-rollback se error rate > 2% per 5 min.
## 12. Billing per-tenant
Tabella `nexus_hub.hub_cost_events(tenant_id, ms, event_type, cost_eur_micros, ts)` con 8 hook nei MS:
- nexus-ai-ms (tokens LLM Anthropic + Voyage embedding)
- nexus-marketing-ms (email sends via Mailgun)
- nexus-call-ms (Twilio minutes)
- nexus-presenter-ms (Tavus session-minutes + LiveKit)
- nexus-voice-ms (Deepgram STT + ElevenLabs TTS)
- nexus-rag-ms (Voyage embed/rerank + storage GB)
- nexus-hub-ms (server time)
- agilehub-workflow-engine (Haiku suggester tokens)
Export mensile CSV/PDF (showback) per cliente.
## 13. Compliance
- **GDPR Art.32**: encryption at rest+transit, access controls, audit trail, business continuity, periodic testing — tutti documentati in `docs/COMPLIANCE_GDPR_ART32.md`
- **ISO A.18.1.5** readiness: gap analysis NON certificazione formale
- **Retention fiscale IT**: 10 anni per `hub_cost_events` (NON 7)
- **GDPR Art.17 erasure**: cascade già implementato in nexus-rag-ms (estendere ai 13 MS)
## 14. Pen test esterno
STRIDE 13×6 matrix (13 MS × 6 categorie) eseguita da vendor EU certificato durante SG-5. Budget €15K una tantum. Vendor RFP da VIGILE.
## 15. Roadmap distribution
| Wave | Prodotti | Trigger |
|---|---|---|
| **Reference** | AGILEHUB | SG-0 in progress |
| **Wave 1 (P1)** | TRPG, SUSTAINAI, NIS2, DFM | SG-4 GREEN |
| **Wave 2 (P2)** | TAXAI, LG231, MKTG, ALLRISK, WMS, MADEBYCLOUD, CERTISOURCE | post smoke Wave 1 |
Distribution via INSTALLATORE pattern: docs_file scp + claude_md append + claude memory write nei 11 container DevEnv prodotto + `hub_standards_adoption` row per ogni prodotto (status `pending → acknowledged → implemented`).
## 16. NESSUN out-of-scope (v1.0)
Documentato esplicitamente FUORI da v1.0:
- Multi-region failover (Hetzner Helsinki + EU secondary)
- Self-service tenant signup pubblico con CC payment
- Multi-currency (solo EUR)
- SAML/OIDC federation
- Tenant white-label dominio custom (solo subdomain `{slug}.agilehub.it`)
- Audit log immutabile blockchain
- ISO 27001 / SOC 2 certification formale
---
## Adoption tracker (status iniziale)
| Prodotto | docs_file | claude_md | claude_memory | adoption_status |
|---|---|---|---|---|
| AGILEHUB | ✓ pending SG-0 | pending | pending | `proposed` |
| TRPG | pending | pending | pending | `pending` |
| SUSTAINAI | pending | pending | pending | `pending` |
| NIS2 | pending | pending | pending | `pending` |
| TAXAI | pending | pending | pending | `pending` |
| LG231 | pending | pending | pending | `pending` |
| DFM | pending | pending | pending | `pending` |
| MKTG | pending | pending | pending | `pending` |
| ALLRISK | pending | pending | pending | `pending` |
| WMS | pending | pending | pending | `pending` |
| MADEBYCLOUD | pending | pending | pending | `pending` |
| CERTISOURCE | pending | pending | pending | `pending` |
## Riferimenti
- [NAVIGAI_EXECUTIVE_BRIEF.md](NAVIGAI_EXECUTIVE_BRIEF.md)
- [NAVIGAI_ARCHITETTURA_TECNICA.md](NAVIGAI_ARCHITETTURA_TECNICA.md)
- [NAVIGAI_ROADMAP_OPERATIVA.md](NAVIGAI_ROADMAP_OPERATIVA.md)
- [PLAN_MULTITENANT_MASTER_REFACTOR_V11.md](PLAN_MULTITENANT_MASTER_REFACTOR_V11.md) (920 righe production-ready)

File diff suppressed because it is too large Load Diff