8.8 KiB
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):
{
"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 includerereasoning,finish_reason,id,_turnDuration,_turnTps,_firstTokenMs; i messaggi "solo tool" hannocontentvuoto)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:
{
"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:
{
"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):
{"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 (modellobase, CPU). - ⚠️ Lingua: con
stt.local.language: ''whisper può tradurre l'italiano in inglese; impostarestt.local.language: itin~/.hermes/config.yaml(richiede riavvio del WebUI). GET /api/transcribe/capability→{"ok": true, "available": true, "provider": "local"}.
Sintesi (testo → voce)
POST /api/tts — 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_ttsdiapi/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=<name>.<firma> |
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_DIRdedicata, password temporanea) e non ha toccato né il server live né i dati utente.