Skip to content

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ChattyDuck - Duckburg Smart City

TL;DR: Assistente al cittadino che risponde esclusivamente sui contenuti certificati dell'ente, esposti tramite Model Context Protocol (MCP). Il contenuto non sta nel prompt del modello, e non sta nemmeno in due posti: nasce nel CMS dell'ente e arriva al modello attraverso un corpus con vocabolario, identificatori, versioni e impronte.

Prototipo dimostrativo sul Comune di Paperopoli (fittizio). I modelli AI (Gemini e Claude) rispondono solo sulla base di quei contenuti, citando la sezione da cui prendono ogni informazione.

Il corpus non è legato a questo CMS: parla il vocabolario del modello Comuni, che prescrive a ogni ente italiano quali attributi deve avere una scheda servizio. Per attaccare un altro gestore di contenuti si scrive un adattatore, anche in un altro linguaggio, e il resto non cambia.

Architettura

Progetto Ruolo
ChattyDuck.Corpus Servizio del corpus (porta 5200): custodisce i contenuti certificati di uno o più enti e li espone. Non conosce alcun CMS. Schema pubblicato su /schema/corpus-1.0.json.
Duckburg.Ingestione Adattatore fra il CMS di Paperopoli e il corpus (porta 5250). È il progetto che si riscrive per ogni CMS. Temporizzato, con innesco manuale su POST /esegui.
ChattyDuck.McpServer Server MCP dell'ente (porta 5000). Legge il corpus e lo espone ai modelli con gli strumenti cerca, scheda, elenca. Trasporto Streamable HTTP su /mcp.
Duckburg.Portal Portale del Comune (porta 5100), Razor Pages in stile Designers Italia. Assistente come widget su tutte le pagine e a pagina intera su /assistente. Il "cittadino informato".
Duckburg.ServiziOnline Portale dei servizi online (porta 5300), stesso layout e assistente del Portal. Il "cittadino attivo" (PNRR Missione 1): Area personale accessibile solo con SPID/CIE tramite Duckburg.Identity.
Duckburg.Identity Sistema di accesso del Comune: Relying Party OpenID Connect Federation 1.0 (profilo SPID/CIE), fork di SPID-CIE-OIDC in stile Paperopoli. Entity id http://identity.paperopoli.test:8001; dopo il login rimanda al portale chiamante con un token firmato (SSO).
Duckburg.Valutazione Modulo di valutazione (porta 5400): wrapper web del validatore ufficiale del modello Comuni.
Duckburg.DockerLaunch Helper di avvio: esegue docker compose up -d --build per la federazione SPID/CIE prima degli altri progetti. Se Docker non è disponibile, avvisa e prosegue.
ChattyDuck.Quack Razor Class Library dell'assistente: UI chat, endpoint POST /chat, GET /chat/usage, GET /debug/tools, orchestrazione dei modelli.
ChattyDuck.Models Implementazioni intercambiabili di IModelService (Gemini, Claude), tracking dei consumi.
Duckburg.Portal.Cms Modello dati del CMS: entità, DbContext e opzioni. Ha due consumatori, il portale che serve le pagine e l'adattatore che le legge.
ChattyDuck.Mcp Client MCP verso il server MCP, usato dal ponte Gemini.

Principio architetturale: il system prompt definisce solo il comportamento del modello; i contenuti risiedono unicamente nel corpus. Il corpus non è la proiezione di un CMS: parla il vocabolario del modello Comuni, che prescrive a ogni ente italiano quali attributi deve avere una scheda servizio. A monte, ogni CMS ha il proprio adattatore; a valle, nessuno sa più da dove i contenuti vengano. Vedi Dal CMS al corpus.

CMS del cliente  ◀──legge── Ingestione ──scrive──▶ Corpus ◀──legge── MCP Server ◀──── ChattyDuck
                            (una per CMS)                                          ◀──── Anthropic
                                                                                   ◀──── client MCP di terzi

Ogni freccia indica chi chiama chi. L'ingestione è l'unica che si sveglia da sola, su un proprio temporizzatore. Il server MCP ha tre clienti e due non sono nostri: per questo dev'essere pubblico.

I due modelli si collegano al corpus in modo diverso:

  • Gemini: non supporta MCP nativamente: il portale fa da bridge, traducendo i tool MCP in functionDeclarations ed eseguendo le chiamate come functionResponse.
  • Claude: supporta MCP nativamente tramite il connettore della Messages API (parametro mcp_servers, header beta mcp-client-2025-11-20): si collega direttamente all'endpoint pubblico del server, senza bridge.

Quick Start

# 1. Configurazione: copia i template e inserisci le API key
Copy-Item Duckburg.Portal�ppsettings.template.json      Duckburg.Portal�ppsettings.json
Copy-Item ChattyDuck.Corpus�ppsettings.template.json    ChattyDuck.Corpus�ppsettings.json
Copy-Item Duckburg.Ingestione�ppsettings.template.json  Duckburg.Ingestione�ppsettings.json
Copy-Item ChattyDuck.McpServer�ppsettings.template.json ChattyDuck.McpServer�ppsettings.json
# la stessa chiave in Corpus:Enti[0].ChiaveIngestione e Ingestione:ChiaveCorpus

# 2. Portale e CMS (porta 5100): al primo avvio crea il database e i contenuti
dotnet run --project Duckburg.Portal

# 3. Corpus (porta 5200) e adattatore (porta 5250)
dotnet run --project ChattyDuck.Corpus
dotnet run --project Duckburg.Ingestione

# 4. Server MCP (porta 5000)
dotnet run --project ChattyDuck.McpServer

L'ordine non conta. I servizi partono comunque e si allineano da soli: finché il corpus non è pronto, il server MCP risponde 503 con stato allineamento e i suoi strumenti dicono al modello di avvisare l'utente invece di rispondere sul vuoto. Un elenco vuoto il modello lo leggerebbe come "questa informazione non esiste".

L'adattatore rilegge il CMS ogni 15 minuti. Per non aspettare dopo una modifica:

curl -X POST http://localhost:5250/esegui          # rilegge il CMS
curl -X POST http://localhost:5000/corpus/reload   # riallinea il server MCP

In Visual Studio i profili di avvio multiplo sono in DuckburgSmartCity.slnLaunch:

  • Assistente: corpus, adattatore, server MCP e portale. È quello che serve per l'assistente.
  • Tutto (Docker + servizi): aggiunge la federazione SPID/CIE in Docker, identità, servizi online e valutazione.

Verifica senza API key:

  • GET http://localhost:5000/health: stato del corpus, contenuti e sezioni indicizzati
  • GET http://localhost:5250/health: esito dell'ultima ingestione
  • GET http://localhost:5100/debug/tools: strumenti MCP visibili al ponte Gemini
  • GET http://localhost:5100/chat/usage: stato dei consumi per modello

Configurazione

I file appsettings*.json reali sono esclusi dal versioning: nel repository ci sono solo i template. In alternativa: variabili d'ambiente o dotnet user-secrets.

Portal

Chiave Variabile d'ambiente Note
Gemini:ApiKey Gemini__ApiKey Google AI Studio, free tier
Gemini:Model - default gemini-2.5-flash
Anthropic:ApiKey Anthropic__ApiKey Anthropic Console, a consumo
Anthropic:Model - es. claude-haiku-4-5
Anthropic:McpEndpoint Anthropic__McpEndpoint URL pubblico del server MCP: deve essere raggiungibile dai server Anthropic (localhost non funziona)
Registry:McpEndpoint - endpoint del bridge Gemini (default http://localhost:5000/mcp)
Cms:Database:Provider - Sqlite (default), SqlServer, PostgreSql, MySql, Oracle; vedi CMS del portale
Cms:Admin:Password Cms__Admin__Password password dell'area di amministrazione (l'utente è in Cms:Admin:Username)

Corpus e Ingestione

Chiave Progetto Note
Corpus:Database:Provider Corpus Sqlite (default), SqlServer, PostgreSql, MySql
Corpus:Enti[].Id Corpus ente servito; ognuno ha la propria chiave di scrittura
Corpus:Enti[].ChiaveIngestione Corpus l'adattatore di quell'ente non può toccare il corpus di un altro
Corpus:ChiaveLettura Corpus vuota significa lettura aperta: il corpus contiene solo ciò che l'ente pubblica già
Ingestione:UrlCorpus Ingestione dove pubblicare l'istantanea
Ingestione:ChiaveCorpus Ingestione deve corrispondere a ChiaveIngestione dell'ente
Ingestione:IntervalloMinuti Ingestione ogni quanto rileggere il CMS (default 15, 0 per disattivare)
Ingestione:Cms Ingestione provider e stringa di connessione del CMS di partenza

Server MCP

Chiave Note
Corpus:Url servizio del corpus (default http://localhost:5200)
Corpus:Ente ente da servire (default comune-paperopoli)
Corpus:Chiave chiave di lettura, se il corpus la richiede
Corpus:RiallineamentoMinuti intervallo di riallineamento al corpus (default 5, 0 per disattivare)
Registry:AccessToken opzionale; se valorizzato richiede Authorization: Bearer <token> o X-Access-Token

Endpoint pubblico

Il percorso Claude e i client MCP esterni richiedono un endpoint raggiungibile da Internet:

  • Sviluppo: ngrok http 5000 → https://<sottodominio>.ngrok-free.dev/mcp (da riportare in Anthropic:McpEndpoint; cambia a ogni riavvio del tunnel).
  • Produzione: dominio dedicato dietro reverse proxy, ambienti separati.

Utilizzo

L'assistente è disponibile su http://localhost:5100 (widget) e su /assistente (pagina intera), con selettore del modello.

Client MCP esterni: qualunque client MCP può consumare il corpus. La voce "Configura il tuo chatbot" in /assistente mostra la configurazione:

{
  "mcpServers": {
    "comune-paperopoli": {
      "type": "http",
      "url": "https://<dominio-pubblico>/mcp"
    }
  }
}

Verifica funzionale: domande di controllo, valide su ogni client:

  1. "Quanto costa il servizio mensa?" → la scheda servizio:mensa-trasporto-scolastico, sezione Costi
  2. "Quali eventi ci sono?" → l'elenco degli eventi, con quelli passati segnati come non validi
  3. "Quando scade la prima rata della TARI?" → 30 aprile, citando la sezione da cui viene
  4. "Che giorno passa l'umido nel quartiere Vesuvio?" → "Questa informazione non è nelle fonti."
  5. La stessa domanda su client diversi produce la stessa risposta, ancorata al corpus

Gli id delle sezioni sono nella forma contenuto#chiave, per esempio servizio:tari#tempi-e-scadenze. Sono stabili: cambiarli spezzerebbe le citazioni già date, ed è il motivo per cui li compone il corpus e non l'adattatore.

Test diretto del tool cerca, senza modelli:

curl -s http://localhost:5000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"cerca","arguments":{"query":"prima rata TARI"}}}'

Accesso con SPID/CIE (servizi online)

Il flusso di login usa la federazione OIDC italiana emulata in locale con l'infrastruttura ufficiale AgID (italia/spid-cie-oidc-django in Docker):

Browser -> Duckburg.ServiziOnline (5300) -> /accedi
        -> Duckburg.Identity (identity.paperopoli.test:8001)  [RP federato]
        -> OP SPID (trust-anchor.paperopoli.test:8000) o OP CIE (cie-provider.paperopoli.test:8002)
        <- callback OIDC su Identity -> token SSO firmato -> /auth/callback su ServiziOnline
        -> sessione cookie -> /area-personale

Setup (una volta sola):

# 1. Hostname locali (PowerShell da amministratore)
.\scripts\add-hosts.ps1

# 2. Chiavi demo del RP (gitignored)
.\scripts\setup-secrets.ps1

# 3. Federazione locale: Trust Anchor + OP CIE + Duckburg.Identity
docker compose up --build

Poi, dal repo:

dotnet run --project Duckburg.ServiziOnline   # porta 5300

Su http://localhost:5300 la card "Area personale del cittadino" chiede il login (credenziali demo user / oidcuser o admin / oidcadmin).

Le pagine Django della federazione non sono del Comune di Paperopoli: rappresentano gli enti centrali dello Stato fittizio di Palmipedia, con brand e palette distinti (override in infra/*/templates/), sullo stesso modello reale SPID/CIE:

Servizio Ruolo reale (Italia) Equivalente Palmipedia Stile
trust-anchor.paperopoli.test (onboarding) AgID, autorità di federazione AIDP: Agenzia per l'Identità Digitale di Palmipedia navy/argento/oro, sigillo di Stato
trust-anchor.paperopoli.test (login SPID locale) Gestore SPID privato (es. Poste, Aruba) BeccoID S.p.A.: soggetto privato accreditato AIDP viola/giallo, fumetto "da startup"
cie-provider.paperopoli.test (login CIE) Istituto Poligrafico e Zecca dello Stato, per conto del Ministero dell'Interno IPZP: Istituto Poligrafico e Zecca di Palmipedia, per conto del Ministero dell'Interno di Palmipedia verde/oro, medaglione a conio

Il test end-to-end della federazione (login CIE, refresh, logout via curl) è in e2e_test.sh.

I dump del Trust Anchor registrano il RP con entity id http://identity.paperopoli.test:8001; le chiavi private demo vivono in secrets/ (solo il .sample.json è versionato).

Dettagli tecnici

Monitoraggio dei consumi: il pannello "Limiti di utilizzo" sotto la chat riporta consumo e quota residua:

  • Claude: valori reali dagli header anthropic-ratelimit-*, intercettati da un DelegatingHandler (AnthropicRateLimitHandler).
  • Gemini: token da usageMetadata delle risposte; quota residua stimata localmente (Google non la espone via API, verificabile in AI Studio).

Il tracker (ModelUsageTracker) è in memoria e si azzera al riavvio.

Risposte non ancorate: se un client risponde con normativa nazionale generica anziché con i dati del corpus, non inserire i dati nel prompt: rinforzare le regole di comportamento e mostrare i passaggi recuperati accanto alla risposta (la UI lo fa già con il riquadro "Fonti recuperate").

CMS del portale

Il portale (Duckburg.Portal) include un CMS completo: tutti i contenuti (servizi, novità, organi, uffici, luoghi, eventi, documenti, pagine, menu e impostazioni del sito) sono nel database e gestibili da un'area di amministrazione.

  • Area admin: http://localhost:5100/admin (credenziali in Cms:Admin, default admin / paperopoli). Dashboard, elenco e CRUD per ogni tipo di contenuto, allineati all'architettura dell'informazione del modello Comuni.
  • Contenuti di default: i contenuti seed in stile Paperopoli sono marcati IsDefault. Con Cms:ProtectDefaultContent: true non sono modificabili né eliminabili (in sola lettura nell'admin, guardia lato server). Impostando false diventano gestibili.
  • Seed: al primo avvio, se Cms:SeedOnStartup: true, lo schema viene creato e popolato. Idempotente: agisce solo su database vuoto.
  • Libreria media: upload di immagini e allegati da /admin/media; i file finiscono in Duckburg.Portal/wwwroot/media (escluso dal versioning).
  • Posta in arrivo: le interazioni raccolte dalle pagine pubbliche finiscono anch'esse nel database e si consultano dall'admin: prenotazioni appuntamento (/admin/appuntamenti), segnalazioni di disservizio (/admin/segnalazioni), valutazioni di chiarezza (/admin/valutazioni, alimentate da POST /api/valutazione).

Database plug-and-play

Il provider si cambia da appsettings.json senza toccare il codice:

"Cms": {
  "Database": {
    "Provider": "Sqlite",           // Sqlite | SqlServer | PostgreSql | MySql | Oracle
    "ConnectionString": "Data Source=App_Data/paperopoli-cms.db"
  }
}

Esempi di stringa di connessione per gli altri motori:

  • PostgreSql: Host=localhost;Database=paperopoli;Username=postgres;Password=...
  • SqlServer: Server=localhost;Database=paperopoli;Trusted_Connection=True;TrustServerCertificate=True
  • MySql: Server=localhost;Database=paperopoli;User=root;Password=...
  • Oracle: User Id=paperopoli;Password=...;Data Source=localhost:1521/XEPDB1

Lo schema è creato con EnsureCreated (provider-agnostico) e le liste sono serializzate in JSON su colonne testo, così il modello resta portabile fra i motori.

Dal CMS al corpus

I contenuti pubblicati nel CMS diventano il corpus su cui l'assistente risponde. Fra i due c'è un confine netto, ed è quello che permette di attaccare un CMS qualsiasi.

Il modello. Un contenuto porta tre cose distinte:

  • attributi: fatti tipizzati. Una scadenza è una data, una tariffa è una tabella di importi, "prenotabile" è un booleano. Quanto costa la mensa smette di essere una ricerca testuale sperata e diventa la lettura di un attributo.
  • sezioni: prosa citabile, ognuna con id, versione e impronta SHA-256. È l'unità che una risposta indica come fonte.
  • relazioni: il grafo. Da un ufficio si risale ai servizi che eroga, alle novità che lo riguardano, ai documenti che pubblica.

Più la validità temporale, che rende riconoscibile un contenuto scaduto invece di lasciarlo citare come attuale.

L'adattatore. Duckburg.Ingestione è l'unico progetto che conosce le tabelle di partenza. Legge il CMS a intervalli, traduce nel vocabolario del corpus e pubblica un'istantanea intera, non le differenze: un adattatore che gira ogni ora non sa cosa è cambiato, sa solo com'è adesso.

Per un altro CMS si scrive un altro adattatore, anche in un altro linguaggio: il contratto è HTTP più uno schema JSON pubblicato, non un assembly .NET.

cp Duckburg.Ingestione/appsettings.template.json Duckburg.Ingestione/appsettings.json
dotnet run --project ChattyDuck.Corpus      # porta 5200
dotnet run --project Duckburg.Ingestione    # porta 5250
curl -X POST http://localhost:5250/esegui   # senza aspettare il giro

La validazione sta nel corpus, non negli adattatori, perché il corpus è uno e gli adattatori sono tanti. Distingue errori bloccanti (identificatori duplicati, impronte incoerenti, tipi sconosciuti) da avvisi (relazioni pendenti, chiavi fuori vocabolario): rifiutare un'istantanea intera per un avviso lascerebbe un ente senza assistente per un dettaglio.

Chi scrive un adattatore può provarlo senza toccare il corpus vivo:

curl -X POST http://localhost:5200/api/enti/<ente>/istantanea/verifica   -H "X-Corpus-Key: <chiave>" --data-binary @istantanea.json

Il recupero è in due stadi. Prima si cerca la scheda, valutando titolo, tipo, fatti e prosa come un documento unico; poi, dentro le schede migliori, si scelgono le sezioni pertinenti e si restituiscono raggruppate. L'unità di significato è il contenuto, non il frammento: una sezione come "Costi: dipende dall'ISEE" non dice a quale servizio appartenga, e ordinare frammenti isolati fa vincere la lunghezza invece della pertinenza.

Il server MCP espone tre strumenti invece di uno:

Strumento Quando
cerca domande in linguaggio naturale; restituisce schede con fatti e sezioni
scheda una scheda intera per id, con collegamenti
elenca "quali eventi ci sono", "quali uffici esistono"; nessuna ricerca per parole
curl -s http://localhost:5000/health
# {"stato":"pronto","pronto":true,"messaggio":"Corpus allineato.",
#  "ente":"comune-paperopoli","contenuti":67,"sezioni":201, ...}

Attaccare un altro CMS

È il caso per cui il corpus esiste come servizio separato. Serve solo un adattatore: un programma qualsiasi, in qualsiasi linguaggio, che legga il gestore di contenuti di partenza e pubblichi un'istantanea. Non deve referenziare nulla di questo repository.

Il contratto è lo schema JSON servito dal corpus stesso:

curl -s http://localhost:5200/schema/corpus-1.0.json

Le motivazioni delle scelte stanno dentro le descrizioni dello schema, non solo la forma dei campi.

Gli endpoint che un adattatore usa:

Metodo Percorso A cosa serve
POST /api/enti/{ente}/istantanea/verifica prova un'istantanea senza pubblicarla
PUT /api/enti/{ente}/istantanea pubblica, sostituendo per intero
GET /api/enti/{ente}/istantanea rilegge l'ultima pubblicata, con ETag
GET /api/enti/{ente}/versioni la storia delle pubblicazioni

La chiave va nell'intestazione X-Corpus-Key ed è per ente: non permette di scrivere sul corpus di un altro.

Si comincia dalla verifica. L'endpoint di prova restituisce l'elenco puntuale dei problemi, con il percorso di ognuno, senza toccare il corpus vivo:

{
  "valida": false,
  "errori": [
    { "percorso": "contenuti[3].sezioni[1].hash",
      "messaggio": "Impronta dichiarata non corrispondente al testo." }
  ],
  "avvisi": [
    { "percorso": "contenuti[7].relazioni[0].verso",
      "messaggio": "Relazione verso un contenuto assente dall'istantanea." }
  ]
}

Errori bloccanti e avvisi sono cose diverse: i primi rendono il corpus non verificabile, i secondi ne riducono la qualità. Un'istantanea con soli avvisi viene pubblicata, e gli avvisi tornano nella risposta come lista di cose da migliorare.

Tre cose che un adattatore non deve fare.

Non deve calcolare le impronte: se le omette le calcola il corpus. Reimplementare SHA-256 in ogni adattatore è un modo di sbagliare che spezzerebbe la verificabilità di ogni risposta senza che nessuno se ne accorga.

Non deve comporre gli id delle sezioni: bastano id del contenuto e chiave della sezione.

Non deve inventare fatti. Un campo va negli attributi solo se nel CMS di partenza è già strutturato: una data che è una data, un booleano che è un booleano. Se nel CMS è prosa libera, resta una sezione. Estrarre fatti dalla prosa con espressioni regolari significa indovinare, e un corpus che si dichiara certificato non può contenere fatti indovinati. La ricchezza del corpus resta così limitata da quella della sorgente, e il modello lo rende visibile invece di nasconderlo.

Per una sorgente che non si controlla, dove nessuno espone un'API, il modello prevede già provenienza.metodo: "estrazione" con una confidenza dichiarata, in alternativa a "mappatura". È il gancio per un adattatore che ricavi i contenuti dalle pagine pubblicate: stesso corpus, stessa citabilità, con l'incertezza dichiarata invece che nascosta.

Conformità al modello Comuni (misura 1.4.1)

Il portale segue l'architettura dell'informazione e i criteri di conformità del pacchetto Cittadino Informato del modello Comuni (PNRR 1.4.1): menu e pagine di secondo livello del vocabolario ufficiale, schede servizio strutturate con indice e data-element per l'App di valutazione, Bootstrap Italia, tassonomia argomenti del modello, FAQ, segnalazione disservizio, prenotazione appuntamenti senza autenticazione, widget di valutazione della chiarezza su ogni pagina, licenza CC-BY 4.0 nelle note legali.

Modulo di valutazione (Duckburg.Valutazione, porta 5400)

Wrapper web del validatore ufficiale pa-website-validator-ng (Lighthouse + Puppeteer):

  1. bash scripts/setup-valutazione.sh: clona e compila il validatore in Duckburg.Valutazione/tool (richiede Node 18+, npm, git).
  2. dotnet run --project Duckburg.Valutazione: apre l'interfaccia su http://localhost:5400.
  3. Dal footer del portale: "Valutazione adesione al modello" → avvia la scansione e consulta i report.

La pagina "Deviazioni dichiarate" documenta i criteri che la demo non supera per scelta (font display in stile fumetto, C.SI.1.1) o per natura dell'ambiente (HTTPS, dominio istituzionale, dichiarazione AgID: un ente immaginario non può registrarli).

Note

Prototipo dimostrativo, non pronto per la produzione. Il deploy è documentato in docs/DEPLOY.md.

Cosa non c'è, dichiarato. La ricerca nel corpus è lessicale, non semantica: nessun indice vettoriale. Migliorerebbe il richiamo sulle parafrasi, ma non risolve nessuno dei problemi che il modello del corpus affronta, e un indice di embedding è opaco e non riproducibile fra versioni del modello. Per un progetto la cui tesi è la verificabilità delle risposte, è un costo da pagare dopo la struttura, non prima.

Il tracker dei consumi vive in memoria e si azzera al riavvio. Le credenziali della demo stanno nella configurazione. Il modello dati del CMS copre l'architettura dell'informazione del modello Comuni, non i flussi di back office di un ente reale.

Licenze: il codice originale è sotto Apache 2.0 (LICENSE.txt). I componenti di terze parti inclusi nel repository (configurazione Django della federazione in infra/, Bootstrap Italia, pulsanti SPID/CIE, font) restano sotto le rispettive licenze: l'elenco è in NOTICE-spid-cie-oidc.txt.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages