Files
harbour-hermes/docs/PROTOCOL.md
T
kaneda 479edb0f99 fix(sessions): lista vuota sul device — modello C++ con ruoli (v0.1.2)
Causa: su Qt 5.6 (kit Sailfish 5.1) una QVariantList di QVariantMap
usata come model non fa risolvere i campi nei delegate QML
(model.title/message_count/updated_at = undefined -> righe vuote), pur
con payload corretto dal server (verificato con cattura reale del 12/09).

- Nuovo src/sessionsmodel.{h,cpp}: QAbstractListModel con ruoli
  session_id/title/message_count/updated_at; api.sessions ora e' il
  modello, api.sessionsCount per lo stato "nessuna sessione"
- /api/sessions con sidebar_source=webui&exclude_hidden=1 (come la
  sidebar del browser: solo sessioni WebUI, niente CLI/cron/nascoste)
- SessionsPage: describe(message_count, updated_at) con parametri espliciti
- PROTOCOL.md: payload sessions verificato + nota del pitfall Qt 5.6
2026-09-12 08:47:28 +02:00

187 lines
7.3 KiB
Markdown

# 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
(`https://hermes.hackatoniclife.com`). 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: <stream_id>:<seq>`:
| 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. 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.