Files
harbour-hermes/docs/PROTOCOL.md
T

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 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:

{
  "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 (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:

{"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=<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_DIR dedicata, password temporanea) e non ha toccato né il server live né i dati utente.