# Protocollo `hermes-webui` ↔ client Documentazione del protocollo HTTP/SSE usato da harbour-hermes. **Validato dal vivo il 12/09/2026** contro un'istanza reale di `nesquena/hermes-webui` (v0.52.x, `server.py`, ThreadingHTTPServer). > Il protocollo non è documentato ufficialmente: è replicato dal frontend web > del progetto (`static/*.js` è la reference implementation) e pinnato alla > versione attuale. Un aggiornamento del WebUI può richiedere aggiornamenti > al client. Server di riferimento: in ascolto su `127.0.0.1:8787` dietro reverse proxy (URL configurabile nell'app). TLS obbligatorio in produzione. ## 1. Autenticazione Cookie di sessione **`hermes_session`** = `token.firma_hmac` (TTL 30 giorni di default; `HERMES_WEBUI_SESSION_TTL` per cambiarlo). Auth attiva solo se il server ha una password impostata. | Endpoint | Metodo | Body / Risposta | |---|---|---| | `/api/auth/login` | POST | `{"password": "..."}` → `200 {"ok": true}` + `Set-Cookie` | | `/api/auth/status` | GET | `{"auth_enabled": bool, "logged_in": bool, "password_auth_enabled": bool, ...}` | | `/api/auth/logout` | POST | invalida il cookie | - Errori: `401 {"error": "Authentication required"}`; rate limit sul login. - Se `auth_enabled` è `false`, tutte le API rispondono senza cookie. - Il client persiste SOLO il cookie (file `0600`); la password non è mai salvata. ## 2. Sessioni `GET /api/sessions?sidebar_source=webui&exclude_hidden=1` → lista per la sidebar (stessi parametri del frontend web: solo sessioni WebUI visibili; senza parametri arrivano anche le CLI/cron e le nascoste — lista più lunga). **Cattura reale del 12/09/2026** (chiavi verificate): ```json { "sessions": [ { "session_id": "20260907_123845_c7e52d", "title": "Diagnosi guasto YunoHost", "workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace", "model": "deepseek-v4-flash", "message_count": 163, "created_at": 1788707925.6, "updated_at": 1788778530.726691, "pinned": false, "archived": false, "project_id": null, "profile": "default", "source_tag": "webui", "raw_source": "webui", "session_source": "webui", "source_label": "WebUI", "parent_session_id": null } ], "sidebar_reference_sessions": [ "..." ], "cli_count": 4, "archived_count": 0, "archived_webui_count": 0, "archived_cli_count": 0, "include_archived": false, "all_profiles": false, "active_profile": "default" } ``` ⚠ Qt 5.6 (kit Sailfish 5.1): i campi di questi oggetti NON si risolvono nei delegate QML quando il modello è una QVariantList (`model.campo` = undefined → righe vuote pur con dati corretti dal server). Il client usa un QAbstractListModel C++ (`SessionsModel`) con ruoli `session_id`, `title`, `message_count`, `updated_at`. `GET /api/session?session_id=ID` → `{"session": { ... }}` con in più: - `messages`: array di `{"role": "user"|"assistant", "content": "...", "timestamp": 1789188869.6, ...}` (i messaggi assistente possono includere `reasoning`, `finish_reason`, `id`, `_turnDuration`, `_turnTps`, `_firstTokenMs`; i messaggi "solo tool" hanno `content` vuoto) - `tool_calls`, `input_tokens`, `output_tokens`, `estimated_cost`, `read_only`, `active_stream_id`, `enabled_toolsets`, ... `POST /api/session/new` con body `{"workspace": "/path"}` (opzionale; il workspace deve essere sotto la home dell'utente, nella lista salvata o sotto il default) → `{"session": {...}}` (sessione vuota). ## 3. Chat con streaming `POST /api/chat/start`: ```json { "session_id": "f517dae7a2da", "message": "Rispondi solo con la parola pong", "model": "deepseek-v4-flash", "model_provider": "deepseek", "workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace", "profile": "default" } ``` Risposta: ```json { "stream_id": "a1e0ec82169b4673a13bf9bd466afd3f", "session_id": "f517dae7a2da", "pending_started_at": 1789188858.196835, "turn_id": "20260912T045418Z-ba85fcc8cebe", "title": "Rispondi solo con la parola pong. Non usare strumenti.", "effective_model_provider": "deepseek" } ``` Errori noti: `404` sessione inesistente; `400` "Missing required field(s)"; conflitto se la sessione ha già uno stream attivo ("session already has an active stream"). `GET /api/chat/stream?stream_id=ID` → **SSE** `text/event-stream`, heartbeat ogni 5 s (`: heartbeat`), ogni evento ha `id: :`: | Evento | Payload | Uso nel client | |---|---|---| | `context_status` | `{session_id, prefill}` | ignorato | | `token` | `{"text": "..."}` | **delta di testo** della risposta | | `metering` | `{tps, ttft_ms, usage, ...}` | ignorato (v1) | | `done` | `{"session": {...con messages finali}, "usage": {...}}` | **risincronizza** il modello e chiude il turno | | `title_status` | `{session_id, status, title}` | ignorato | | `title` | `{session_id, "title": "..."}` | aggiorna il titolo | | `stream_end` | `{session_id}` | fine stream | Altri eventi possono esistere (attività tool, approval/clarify) e vanno **ignorati** senza errore (il parser li scarta). Ripresa: `GET /api/chat/stream/status?stream_id=ID` → `{"active": bool, "stream_id", "replay_available"}`. Se `GET /api/session` mostra `active_stream_id` non nullo, riagganciare lo stream con lo stesso URL. Annullamento: `GET /api/chat/cancel?stream_id=ID` → `{"ok": true, "cancelled": bool, "stream_id"}`. ## 4. Voce ### Trascrizione (voce → testo) `POST /api/transcribe` — `multipart/form-data`, campo **`file`** (OGG/Opus, WebM, M4A, WAV...; il server converte con ffmpeg se serve): ```json {"ok": true, "transcript": "Questa è una prova di trascrizione vocale..."} ``` - Provider STT del server (qui: `local` = faster-whisper). Latenza osservata: ~10 s per 6 s di audio (modello `base`, CPU). - ⚠️ **Lingua**: con `stt.local.language: ''` whisper può tradurre l'italiano in inglese; impostare `stt.local.language: it` in `~/.hermes/config.yaml` (richiede riavvio del WebUI). - `GET /api/transcribe/capability` → `{"ok": true, "available": true, "provider": "local"}`. ### Sintesi (testo → voce) `POST /api/tts` — JSON: ```json {"text": "Ciao", "voice": "it-IT-ElsaNeural", "engine": "edge", "rate": "+0%", "pitch": "+0Hz"} ``` → `200` `audio/mpeg` (mp3) oppure `4xx` `{"error": "..."}`. - Limiti server: **5000 caratteri**; rate limit **2 s per client**; **allowlist voci hardcoded** nella funzione `_handle_tts` di `api/routes.py`. Voci ammesse oggi: zh-CN (5), en-US (2), fr-CA (4), fr-FR (3), id-ID (1) + **it-IT (4: ElsaNeural, DiegoNeural, IsabellaNeural, GiuseppeMultilingualNeural)** — le italiane sono state aggiunte da noi (patch + test): il server va **riavviato** per attivarle. - Engine: `edge` (default, gratis), `elevenlabs`, `openai` (richiedono chiavi configurate sul server). `browser` è solo client-side, non usabile qui. ## 5. Profili (selezione dell'utente) Le sessioni del WebUI appartengono a un *profilo* (campo `profile` nel payload di `/api/sessions`). Il client sceglie di quale profilo vedere le sessioni con lo switch per-client: | Endpoint | Metodo | Note | |---|---|---| | `/api/profiles` | GET | `{"profiles": [{"name", "path", "is_default", "is_active", ...}], "active": "..."}` | | `/api/profile/switch` | POST | body `{"name": "carlo"}` → `200` + `Set-Cookie: hermes_profile=.` | Il cookie `hermes_profile` e' **firmato lato server** (HMAC legato al token di sessione, anti-forging): il client non puo' inventarlo, deve riceverlo dallo switch e conservarlo nel cookie jar. Da quel momento **tutte** le richieste dell'app sono profilate lato server (`server.py` applica il profilo per-request dal cookie). Lo switch usa `process_wide=False`: **il profilo attivo globale del server non viene toccato** (la vista del browser resta la sua). Per tornare al profilo attivo del server basta rimuovere il cookie. Validato contro il server reale (2026-09: switch → 200 + Set-Cookie firmato; profilo inesistente → 404 `Profile 'x' does not exist`). Nota prestazioni: dopo uno switch la **prima** `GET /api/sessions` per quel profilo puo' richiedere 10-16 s anche su uno state DB minuscolo (cache fredde lato server); le chiamate successive tornano a pochi ms. Il client usa quindi un timeout generoso (60 s) e mostra lo stato di caricamento, mai un'attesa infinita. ## 6. Note operative - Tutti gli errori API sono JSON `{"error": "..."}`. - Il server non espone CORS: il client nativo non serve preflight. - Endpoint utili non usati dalla v1: `/api/clarify/*` e `/api/approval/*` (richieste interattive dell'agente: vanno gestite per non lasciare un run in attesa), `/api/upload` (allegati), `/api/settings`, `/api/models`, `/api/sessions/events` (SSE globale di invalidazione lista). - L'istanza di prova usata per la validazione era isolata (porta 8899, `HERMES_WEBUI_STATE_DIR` dedicata, password temporanea) e non ha toccato né il server live né i dati utente.