πŸ—

EmiBrain β€” Architettura e Stato Deploy

Reference tecnica completa del sistema AI interno Emisfera. Docker, WebSSH, Git, Brain Protocol β€” tutto quello che serve per continuare lo sviluppo.

Ultimo aggiornamento: 7 aprile 2026

8Container
5Brain attivi
608Righe server.js
5Bare repos
Architettura Docker core

Overview

EmiBrain gira su un server Hetzner CAX21 (ARM64, 8GB RAM, 80GB SSD, Debian 12). Tutto in Docker CE 29.3 + Compose 5.1. Ogni utente ha il suo container workspace isolato.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Server Hetzner (178.104.48.34 / Tailscale 100.70.197.28) β”‚
β”‚                                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ emibrain-    β”‚  β”‚ emibrain-    β”‚  β”‚ emibrain-    β”‚   β”‚
β”‚  β”‚ nginx        β”‚  β”‚ php          β”‚  β”‚ admin        β”‚   β”‚
β”‚  β”‚ :80 β†’ proxy  β”‚  β”‚ FPM :9000    β”‚  β”‚ :3100 webssh β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ :8080 api    β”‚   β”‚
β”‚         β”‚                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚         β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚         β”‚  β”‚         Workspace Containers             β”‚   β”‚
β”‚         β”‚  β”‚  ws-giobi  ws-puddu  ws-bodini          β”‚   β”‚
β”‚         β”‚  β”‚  ws-ricci  ws-franzini  ws-stagista     β”‚   β”‚
β”‚         β”‚  β”‚  (Claude Code + tmux + brain)            β”‚   β”‚
β”‚         β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚         β”‚                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  /var/emibrain/                                      β”‚ β”‚
β”‚  β”‚  β”œβ”€β”€ brains/{slug}/     ← volume mount (rw)          β”‚ β”‚
β”‚  β”‚  β”œβ”€β”€ repos/{slug}.git   ← bare repos (backup)        β”‚ β”‚
β”‚  β”‚  β”œβ”€β”€ shared/            ← volume mount (ro)           β”‚ β”‚
β”‚  β”‚  β”œβ”€β”€ database.sqlite    ← Laravel DB                  β”‚ β”‚
β”‚  β”‚  └── docker/            ← build context               β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Container

ContainerRuoloRAMPorte
emibrain-nginxReverse proxy, static files, Cloudflare SSLβ€”80
emibrain-phpPHP 8.4 FPM per Laravelβ€”9000
emibrain-adminWebSSH server (Node.js), Docker socket, brain APIβ€”3100, 8080
emibrain-ws-*Workspace utente (Claude Code + tmux)512M-1Gβ€”

Volume Mount

HostContainerMode
/var/emibrain/brains/{slug}/home/brain/workspacerw
/var/emibrain/shared/home/brain/sharedro
/var/run/docker.sock/var/run/docker.sock (solo admin)rw
Key: I brain vivono sull'host, non nei container. Il container e sacrificabile, il brain no.

Dockerfile.workspace

L'immagine workspace include:

  • Debian bookworm-slim + Python3, Node.js, git, tmux, curl, sqlite3
  • Claude Code installato globalmente via npm
  • SSH server (per accesso diretto via ssh brain@container)
  • Brain template baked in /opt/emibrain/brain-template/
  • Entrypoint che fa auto-bootstrap al primo avvio: crea boot/, wiki/, diary/, git init, primo commit
Decisione: Il brain template e nell'immagine, non in uno script esterno. Creare un nuovo utente = aggiungere un servizio al docker-compose e fare up -d.
Server WebSSH (608 righe) core

Architettura

Un server Node.js nel container emibrain-admin che fa da ponte tra il browser e i container workspace. Gestisce sessioni tmux via docker exec, serve file, e fornisce le API per il brain explorer.

Browser (xterm.js)
    β”‚ WebSocket
    β–Ό
Nginx (:80)
    β”‚ proxy_pass
    β–Ό
emibrain-admin:3100 (server.js)
    β”‚ docker exec -it
    β–Ό
emibrain-ws-{slug} β†’ tmux -L webssh attach-session

Endpoint HTTP

EndpointCosa fa
GET /sessionsLista sessioni tmux + auto-title da Claude jsonl
POST /sessions/newCrea sessione tmux + lancia Claude Code
POST /sessions/killKilla sessione tmux
GET /brain/projectsLista progetti da wiki/projects/
GET /brain/diaryLista diary entries per mese
GET /brain/todosLista TODO aperti
GET /files/treeDirectory listing
GET /files/readLeggi file (text)
GET /files/rawServe file binario (download)
GET /files/searchCerca file per nome

WebSocket

Il terminale nel browser usa xterm.js che si connette via WebSocket al server admin. Il server fa docker exec -it nel container workspace e attacca la sessione tmux. Il resize del terminale viene propagato via messaggio JSON {type: "resize", cols, rows}.

Auth: JWT

Ogni richiesta porta un JWT firmato con WEBSSH_SECRET. Il JWT contiene:

  • user_id β€” ID utente Laravel
  • workspace_uid β€” UUID del brain
  • brain_slug β€” slug leggibile (usato per path filesystem)
  • provider β€” docker, claude, gemini

Durata: 12 ore. Il frontend fa auto-refresh via endpoint Laravel /webssh/{brain}/refresh-token.

Nota: Il server accetta sia WEBSSH_JWT_SECRET che WEBSSH_SECRET come env var (fallback per compatibilita con il docker-compose attuale).

Auto-title sessioni

Quando l'endpoint /sessions viene chiamato, il server legge i file .claude/projects/-home-brain-workspace/*.jsonl nel container via docker exec python3, estrae il primo messaggio user come titolo. Mapping per ordine cronologico (sessione tmux piu recente = jsonl piu recente).

Il frontend fa polling ogni 30 secondi per aggiornare i titoli nella sidebar.

Tmux lockdown

La config tmux (webssh/tmux-webssh.conf) disabilita tutti i binding pericolosi:

  • No split pane (unbind ", %)
  • No nuove finestre (unbind c, n, p)
  • No detach (unbind d, D)
  • No right-click menu (unbind MouseDown3Pane)
  • Mouse ON per scroll (scrollback tmux con rotella)
Trade-off: Con mouse on, per selezionare testo nel browser serve Shift+click.
Nginx Proxy infra

Routing

PathTargetNote
/webssh/wsadmin:3100WebSocket upgrade
/webssh/sessionsadmin:3100/sessionsAPI sessioni
/webssh/files/*admin:3100/files/*File explorer
/webssh/brain/*admin:3100/brain/*Brain explorer
/websshd/*Stesse regoleDocker provider (alias)
/websshg/*Stesse regoleGemini provider (alias)
/*.phpphp:9000Laravel via FastCGI

Public files

Ogni brain ha la cartella public/ servita via public.emibrain.it:

URLPath host
public.emibrain.it/bodini//var/emibrain/brains/bodini/public/
public.emibrain.it/giobi//var/emibrain/brains/giobi/public/
public.emibrain.it/puddu//var/emibrain/brains/puddu/public/
Git: Bare Repos + Cron Sync backup

Architettura

Container workspace          Host (emibrain)              (futuro)
/home/brain/workspace/       /var/emibrain/repos/         GitHub
    git commit          ──→  {slug}.git (bare repo)  ──→  deploy key
                             cron ogni 5 min              (non ancora)

I container fanno solo git commit. Il push al bare repo avviene dall'host via cron ogni 5 minuti (/var/emibrain/scripts/brain-git-sync.sh). I container non vedono la directory /var/emibrain/repos/ β€” il push e esterno.

Stato bare repos

BrainBranchUltimo commit
giobimainCheckpoint: brain - onboarding
puddumainInstall brain skill
bodinimasterBackup pre-forkbomb test
riccimasterBrain initialized from template
franzinimasterBrain initialized from template
Validato: Il 7 aprile Bodini ha cancellato tutto il suo brain. Ripristinato in 30 secondi da git clone /var/emibrain/repos/bodini.git.

Cron script

/var/emibrain/scripts/brain-git-sync.sh β€” gira come root ogni 5 minuti. Per ogni brain:

  • Compara HEAD del brain con il bare repo
  • Se diversi, fa sudo -u emibrain git push local {branch}
  • Log in /var/log/emibrain/git-sync.log
Auth: Magic Link + JWT auth

Flusso login

  • Utente inserisce email su emibrain.it/login
  • Laravel genera magic link (token SHA-256, scade 1h)
  • Email inviata via [email protected] (Purelymail SMTP)
  • Click sul link β†’ sessione Laravel attiva
  • Dashboard mostra i brain dell'utente
  • Click su brain β†’ genera JWT β†’ apre WebSSH/Dchat

Whitelist

Solo email @emisfera.it possono registrarsi. [email protected] ha accesso admin a tutti i brain.

Brain Template + Onboarding nuovo

Template nel Docker image

L'immagine workspace contiene /opt/emibrain/brain-template/ con:

FileScopo
boot/brain.mdBrain Protocol v5 β€” come funziona il brain
boot/soul.mdPersonalita dell'AI (template generico)
boot/user.mdTemplate vuoto per info utente
boot/local.yamlConfig con placeholder __SLUG__ e __ROLE__
domain.mdSymlink a /home/brain/shared/domain.md
.env.exampleTemplate variabili d'ambiente
.gitignoreIgnora .env, storage/, node_modules/
CLAUDE.mdIstruzioni per Claude Code

Auto-bootstrap (entrypoint)

Al primo avvio del container, workspace-entrypoint.sh:

  • Crea directory structure: boot/, wiki/, diary/, todo/, inbox/, public/, storage/
  • Copia template da /opt/emibrain/brain-template/
  • Interpola local.yaml con BRAIN_USER e BRAIN_ROLE (env vars dal compose)
  • Symlink domain.md β†’ shared
  • git init + primo commit automatico
Per aggiungere un utente: Aggiungi il servizio al docker-compose.yml, crea il brain dir, docker compose up -d ws-nuovo. L'entrypoint fa il resto.

local.yaml

Ogni brain ha un boot/local.yaml con:

  • platform: container
  • realm: emibrain
  • role: user|admin|root
  • public_url: https://public.emibrain.it/{slug}
  • git.push: cron β€” il brain sa che non deve pushare

domain.md (shared)

File unico in /var/emibrain/shared/domain.md, montato read-only in tutti i container. Descrive:

  • Cos'e EmiBrain e Emisfera
  • Ruoli (admin/user)
  • Che sei in un container Docker
  • Come funziona public/ e git
  • Cosa NON fare (sudo, apt-get, docker, file fuori workspace)
Stato Attuale β€” Cosa Funziona status

Funzionante

FeatureStatoNote
Container workspaceOK6 container, tutti up
WebSSH terminaleOKSessioni tmux, auto-attach
Brain explorer (sidebar dx)OKProgetti, diary, TODO
File explorerOKTree, read, download
Session auto-titlesOKDa Claude jsonl, polling 30s
Git bare reposOKCron 5 min, validato con restore
Tmux lockdownOKNo split, no window, no detach
Tmux mouse scrollOKShift+click per selezione
Magic link authOKWhitelist @emisfera.it
Brain template + bootstrapOKEntrypoint auto-crea brain
domain.md sharedOKUn file per tutti via symlink
local.yaml con public_urlOKInterpolato da entrypoint

Da fare

FeatureStatoNote
GitHub syncTODOBare repos pronti, manca deploy key
Skill /public domain-awareTODODeve leggere public_url da local.yaml
shared/ knowledge baseTODOPer ora solo domain.md, futuro: API
Sidebar resize handleFAILTentato, non funzionava. Workaround: input numerico
Rename sessioni manualeTODOFrontend c'e, endpoint server mancante
Docker image rebuildTODODockerfile pronto, non ancora buildato
Copia/incolla nativoWORKAROUNDShift+click funziona
Decisioni Architetturali design

D1: Brain su host, non in container

I dati del brain vivono su /var/emibrain/brains/{slug}/ e vengono montati nei container. Se il container muore, il brain sopravvive. Backup con strumenti standard (git, rsync, tar).

D2: WebSSH via docker exec, non SSH

Il server admin fa docker exec nei container workspace per attaccare le sessioni tmux. Non usa SSH tra container β€” meno overhead, meno config, meno superficie d'attacco.

D3: Git push esterno al container

I container non hanno credenziali git. Fanno solo commit. Un cron sull'host pusha ai bare repos ogni 5 minuti. Zero auth dentro i container, zero rischio di leak credenziali.

D4: Server webssh ibrido (312 β†’ 608 righe)

Il server originale (312 righe, nell'immagine Docker del 18 marzo) gestiva solo sessioni tmux via docker exec. E stato esteso a 608 righe aggiungendo gli endpoint brain explorer e file explorer che leggono direttamente dal filesystem host (montato in /var/emibrain). Non usa il server ABChat da 1263 righe perche quello gestisce tmux sull'host, non via docker exec.

Attenzione: Il server.js vive nel container layer (copiato via docker cp). Al prossimo docker compose rm + up torna all'immagine originale (312 righe). Il Dockerfile aggiornato non e ancora stato buildato.

D5: Un branch per EmiBrain

EmiBrain usa il branch emibrain/docker-sessions del repo giobi/abchat. ABChat e Generations restano su main. Le modifiche specifiche EmiBrain (Docker mode, slug paths, tmux lockdown) non impattano gli altri realm.

Repository e File Chiave reference

Repo: giobi/abchat

Branch emibrain/docker-sessions

PathCosa
docker/Dockerfile.workspaceImmagine workspace con brain template
docker/Dockerfile.adminImmagine admin (webssh + Docker socket)
docker/docker-compose.ymlStack completo
docker/workspace-entrypoint.shBootstrap brain al primo avvio
docker/brain-template/Template brain baked nell'immagine
docker/webssh-server.jsServer admin (608 righe)
webssh/server.jsServer ABChat originale (1263 righe, NON usato su EmiBrain)
webssh/tmux-webssh.confConfig tmux locked-down
website/Laravel app (ABChat)

Sul server EmiBrain

PathCosa
/var/emibrain/brains/Brain data (per slug)
/var/emibrain/repos/Bare git repos (backup)
/var/emibrain/shared/File condivisi (domain.md)
/var/emibrain/docker/Build context Docker
/var/emibrain/database.sqliteDB Laravel
/var/emibrain/scripts/brain-git-sync.shCron sync bare repos
/home/web/emibrain.it/repo/Clone giobi/abchat (branch emibrain)
/home/web/emibrain.it/website/Symlink a repo/website/