JBot WebSocket Protocol

Vollständige Protokollspezifikation — Verbindungsaufbau, Nachrichtenformate, Event-System, Medien-Upload & Node.js-Client

1. Verbindungsaufbau & Authentifizierung

Der JBot WebSocket-Server ist ein asynchroner Server (basierend auf der Python-Bibliothek websockets), der sowohl TCP-basierte als auch Unix-Domain-Socket-Verbindungen akzeptiert. Clients erhalten nach erfolgreichem Handshake eine ready-Nachricht mit einer zugewiesenen chat_id und client_id.

Verbindungs-URLs

TCP (ws:// / wss://)

ws://<host>:<port><path>?client_id=<id>&token=<token>
wss://<host>:<port><path>?client_id=<id>&token=<token>
  • host: Standardmäßig 127.0.0.1 (konfigurierbar)
  • port: Standardmäßig 8765
  • path: Standardmäßig / (konfigurierbar)
  • wss://: Verfügbar wenn ssl_certfile und ssl_keyfile konfiguriert sind

Unix Domain Socket

unix:<socket_path><path>?client_id=<id>&token=<token>
  • Nur verfügbar wenn unix_socket_path gesetzt ist
  • Socket-Pfad muss absolut sein, wird mit chmod 0o600 gesichert

Query-Parameter

ParameterErforderlichBeschreibung
client_idNeinEindeutige Client-Identifikation (max. 128 Zeichen). Wird für allow_from-Autorisierung und als sender_id verwendet. Falls nicht angegeben, wird eine zufällige ID wie anon-<12-hex-chars> generiert.
tokenAbhängigAuthentifizierungs-Token

Authentifizierung

1. Statischer Token
Der Client sendet den Token als Query-Parameter: ?token=mein-geheimer-token. Der Vergleich erfolgt mittels hmac.compare_digest (timing-safe).

2. Ausgestellte Kurzzeit-Tokens
Wenn token_issue_path konfiguriert ist, kann der Client vor dem WebSocket-Handshake ein kurzlebiges Token per HTTP abrufen:

GET <path> (z. B. /issue-token)
Authorization: Bearer <token_issue_secret>
{
  "token": "<short-lived-token>",
  "expires_in": 300
}
  • token_ttl_s: Gültigkeitsdauer in Sekunden (30–86.400, Standard: 300)
  • Tokens sind single-use – nach Verwendung ungültig

3. Kein Token
Wenn websocket_requires_token: false gesetzt ist, sind Verbindungen auch ohne Token erlaubt.

4. Wildcard-Host-Schutz
Wenn host auf 0.0.0.0 oder :: gesetzt ist, muss entweder token oder token_issue_secret gesetzt sein – andernfalls lehnt die Konfigurationsvalidierung ab.

allow_from – Client-ID-Autorisierung

{
  "websocket": {
    "allow_from": ["*"]
  }
}
  • ["*"]: Alle Clients erlaubt (Standard)
  • ["client-a", "client-b"]: Nur Clients mit diesen IDs

Die ready-Nachricht

Nach erfolgreichem WebSocket-Upgrade sendet der Server sofort:

{
  "event": "ready",
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "client_id": "mein-client-1"
}
FeldTypBeschreibung
eventstringImmer "ready"
chat_idstringUUIDv4 – Default-Chat-Session dieser Verbindung
client_idstringDie effektive Client-ID (entweder der übergebene Wert oder generiert)

Konfigurationsreferenz

FeldTypStandardBeschreibung
enabledboolfalseWebSocket-Server aktivieren
hoststring"127.0.0.1"Bind-Adresse
portint8765TCP-Port
unix_socket_pathstring""Alternativ: Unix-Socket-Pfad (absolut)
pathstring"/"WebSocket-Upgrade-Pfad (muss mit / beginnen)
tokenstring""Statischer Auth-Token
token_issue_pathstring""HTTP-Pfad zum Ausstellen von Kurzzeit-Tokens
token_issue_secretstring""Secret für Token-Ausstellungs-Endpoint
token_ttl_sint300Gültigkeitsdauer ausgestellter Tokens (30–86.400 s)
websocket_requires_tokenbooltrueToken für Verbindungsaufbau erforderlich
allow_fromlist[string]["*"]Erlaubte Client-IDs
streamingbooltrueStreaming aktiviert
max_message_bytesint37_748_736Maximale Frame-Größe (≈36 MB)
ping_interval_sfloat20.0Ping-Intervall (5–300 s)
ping_timeout_sfloat20.0Ping-Timeout (5–300 s)
ssl_certfilestring""TLS-Zertifikat (für WSS)
ssl_keyfilestring""TLS-Private-Key (für WSS)

Ping/Pong

Der Server sendet automatisch WebSocket-Ping-Frames im konfigurierten Intervall. Clients müssen mit Pong antworten. Bei Timeout wird die Verbindung getrennt.

Verbindungsabbau

Bei Verbindungsabbau wird der Client aus allen Chat-Subscriptions entfernt. Eine neue Verbindung kann sich mit attach wieder in bestehende Sessions einklinken.

2. Client → Server Nachrichten

Der JBot WebSocket-Server akzeptiert zwei Nachrichtenformate:

  1. JSON-Envelopes (empfohlen): JSON-Objekte mit einem type-Feld
  2. Legacy Plain-Text (einfach): Rohe Textstrings oder {"content": "..."}-Objekte

Legacy Plain-Text

Einfache Textnachrichten ohne type-Feld werden als Chat-Nachricht an die Default-Session gesendet.

Hallo JBot, was kannst du?

Oder als JSON-Objekt (erkannt an content/text/message-Key ohne type):

{"content": "Hallo JBot"}

JSON-Envelopes

Alle modernen Nachrichten verwenden dieses Format:

{
  "type": "<typ>",
  "chat_id": "<uuid>",
  "content": "<nachricht>",
  ...
}

message – Chat-Nachricht senden

{
  "type": "message",
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "content": "Erkläre mir JBot",
  "media": [
    { "data_url": "data:image/png;base64,...", "name": "screenshot.png" }
  ],
  "webui": true,
  "turn_id": "optional-turn-id",
  "cli_apps": "optional-cli-apps",
  "mcp_presets": "optional-mcp-presets",
  "image_generation": { "enabled": true, "aspect_ratio": "16:9" }
}
FeldTypErforderlichBeschreibung
typestringJaImmer "message"
chat_idstringJaUUIDv4 (Pattern: ^[A-Za-z0-9_:-]{1,64}$)
contentstringNein*Nachrichtentext (*optional bei Medien-Anhang)
mediaarrayNeinListe von Medien-Objekten
webuiboolNeintrue wenn vom JBot WebUI gesendet
cli_appsstringNeinCLI-App-Mentions
mcp_presetsstringNeinMCP-Preset-Mentions
image_generationobjectNein{"enabled": true, "aspect_ratio": "16:9"}

Event-Ablauf: goal_status: runningdelta / reasoning_deltamessageturn_endgoal_status: idle

new_chat – Neue Session erstellen

{"type": "new_chat"}

attach – Bestehender Session beitreten

{"type": "attach", "chat_id": "550e8400-e29b-41d4-a716-446655440000"}

fork_chat – Chat abzweigen

{"type": "fork_chat", "chat_id": "<quell-uuid>"}

set_workspace_scope – Workspace ändern

{
  "type": "set_workspace_scope",
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "path": "/home/user/projects/meinprojekt"
}

transcribe_audio – Audio-Transkription

{"type": "transcribe_audio", ...}

Error-Antworten

{"event": "error", "detail": "invalid chat_id", "reason": "malformed"}
detailBedeutung
"invalid chat_id"Chat-ID ungültig
"missing content"Weder Text noch Medien
"image_rejected"Medien-Validierung fehlgeschlagen
"unknown type: ..."Unbekannter Envelope-Typ
"workspace_scope_rejected"Workspace-Berechtigung verweigert

3. Server → Client Events

Alle vom Server gesendeten Nachrichten sind JSON-Objekte mit einem event-Feld.

EventBeschreibung
readyVerbindung etabliert
attachedChat / Attach bestätigt
messageAgent-Antwort (final)
deltaStreaming-Text-Delta
stream_endStreaming-Segment beendet
reasoning_deltaReasoning-Stream-Delta
reasoning_endReasoning-Stream beendet
tool_hint / progressTool-Ausführung
file_editDateiänderung
turn_endTurn abgeschlossen
goal_statePersistenter Goal-Status
goal_statusrunning / idle
session_updatedSession-Metadaten geändert
runtime_model_updatedModell gewechselt
errorFehler

ready / attached

{"event": "ready", "chat_id": "...", "client_id": "..."}
{"event": "attached", "chat_id": "..."}

Streaming: delta / stream_end

{"event": "delta", "chat_id": "...", "text": "JBot", "stream_id": "..."}
{"event": "stream_end", "chat_id": "...", "text": "JBot ist...", "stream_id": "..."}

Reasoning: reasoning_delta / reasoning_end

{"event": "reasoning_delta", "chat_id": "...", "text": "Ich analysiere..."}
{"event": "reasoning_end", "chat_id": "..."}

Lifecycle: goal_status / turn_end / goal_state

{"event": "goal_status", "status": "running", "started_at": 1712000000.123}
{"event": "turn_end", "chat_id": "...", "latency_ms": 1234, "goal_state": {...}}
{"event": "goal_status", "status": "idle"}

Fan-Out-Verhalten

  • Events mit chat_id nur an abonnierte Clients
  • runtime_model_updated und globale session_updated an alle verbundenen Clients
  • goal_state / goal_status ohne Subscriber: nur Log

4. Nachrichtenfluss & Lebenszyklus

1. Verbindungsaufbau

Client Server | | |── WS Upgrade ──────────────────────────────>| | ws://host:8765/?client_id=myapp&token=... | | | |<──────────────────── { event: "ready", | | chat_id: "<uuid>", | | client_id: "myapp" } |

2. Einfache Chat-Nachricht (Legacy)

Client Server | | |── "Hallo JBot" ───────────────────────────>| |<── { event: "goal_status", status:"running"}| |<── { event: "delta", text: "Hallo" } | |<── { event: "delta", text: "!" } | |<── { event: "stream_end", text: "Hallo!..."} | |<── { event: "message", kind: "answer" } | |<── { event: "turn_end", ... } | |<── { event: "session_updated", ... } | |<── { event: "goal_status", status:"idle" } |

3. Multi-Chat mit JSON-Envelopes

Client Server |── { "type": "new_chat" } ─────────────────>| |<── { event: "attached", chat_id:"<neu>" } | |<── { event: "session_updated", ... } | |<── { event: "goal_state", ... } | | | |── { "type": "message", chat_id:"<neu>", | | "content": "Hi", "media":[...] } ──────>| |<── { event: "goal_status", running } | |<── { event: "delta", text: "..." } | |<── { event: "message", kind: "answer" } | |<── { event: "turn_end", ... } | |<── { event: "goal_status", idle } |

4. Reasoning-Modelle

Client Server |── { "type": "message", ... } ─────────────>| |<── goal_status: running | |<── reasoning_delta: "Ich analysiere..." | |<── reasoning_delta: " die Anfrage..." | |<── reasoning_end | |<── delta: "Basierend auf..." | | [... normales Streaming ...] | |<── turn_end / goal_status: idle |

reasoning_delta / reasoning_end erscheinen vor dem normalen delta-Stream. Clients sollten Reasoning-Text visuell abgrenzen (z. B. zusammenklappbarer "Thinking"-Block).

5. Tool-Ausführung mit Fortschritt

Client Server |── { "type": "message", content: "Erstelle README" } ──>| |<── goal_status: running | |<── message, kind: "tool_hint" | |<── message, kind: "progress", tool_events:[...]| |<── file_edit: { edits: [{path:"README.md",...}]}| |<── delta: "Ich habe..." | |<── message, kind: "answer", tool_events:[...] | |<── turn_end / goal_status: idle |

6. Reconnect & Session-Wiederherstellung

Client Server │── Verbindung getrennt ────────────────────│ │ [... Zeit vergeht ...] | |── WS Upgrade (neu) ───────────────────────>| |<── ready: { chat_id: "<neue-uuid>" } | |── attach: { chat_id: "<alte-uuid>" } ────>| |<── attached: { chat_id: "<alte-uuid>" } | |<── goal_state / goal_status (running, falls aktiv)|

7. Multi-Client Fan-Out

Client A Server Client B |── attach(X) ──────────>|<── attach(X) ───| |── message(X) ─────────>|── delta ───────>| |<── delta ──────────────|── delta ───────>| |<── message ────────────|── message ─────>| |<── turn_end ───────────|── turn_end ────>|

Zustandsautomat (Client-Sicht)

┌──────────┐ │ Verbinden │ └─────┬────┘ │ ready ┌─────▼────┐ ┌────────>│ Bereit │<────────┐ │ └─────┬────┘ │ │ new_chat │ attach │ session_updated │ │ │ goal_state │ ┌──────────▼──────────┐ │ │ │ Session abonniert │───┘ │ └──────────┬──────────┘ │ │ message │ ┌──────────▼──────────┐ │ │ Turn läuft │ │ │ goal_status:running│ │ │ delta / reasoning │ │ │ tool_hint/progress │ │ │ file_edit │ │ └──────────┬──────────┘ │ │ turn_end │ ┌──────────▼──────────┐ │ │ Session abonniert │ │ │ goal_status: idle │ │ └─────────────────────┘ │ └──── reconnect ──>│

Globale Broadcast-Events

  • runtime_model_updated – bei Modell-Wechsel
  • session_updated – bei globalen Session-Änderungen

5. Medien-Upload-Spezifikation

Bild- und Video-Uploads direkt im WebSocket-Frame als Base64-kodierte Data-URLs im media-Array des message-Envelopes.

{
  "type": "message",
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "content": "Analysiere dieses Bild",
  "media": [
    { "data_url": "data:image/png;base64,...", "name": "screenshot.png" }
  ]
}
FeldTypErforderlichBeschreibung
data_urlstringJaBase64-Data-URL mit MIME-Type-Präfix
namestringNeinOptionaler Dateiname

Unterstützte MIME-Typen & Limits

Bilder

MIME-TypeErweiterungMax. GrößeMax. Anzahl
image/png.png8 MB4
image/jpeg.jpg/.jpeg8 MB4
image/webp.webp8 MB4
image/gif.gif8 MB4

Ausgeschlossen: image/svg+xml (Sicherheitsgründe: XSS-Risiko).

Videos

MIME-TypeErweiterungMax. GrößeMax. Anzahl
video/mp4.mp420 MB1
video/webm.webm20 MB1
video/quicktime.mov20 MB1

Pro-Nachricht-Limits

LimitWert
Max. WebSocket-Frame37.748.736 Bytes (≈36 MB)
Max. Bilder pro Nachricht4
Max. Video pro Nachricht1
Pro Bild8.388.608 Bytes (8 MB)
Pro Video20.971.520 Bytes (20 MB)

Fehlerbehandlung

reasonBedeutung
malformedUngültiges Medien-Objekt
decodeBase64-Dekodierung fehlgeschlagen
mimeMIME-Type nicht erlaubt
sizeDateigröße überschritten
too_many_imagesMehr als 4 Bilder
too_many_videosMehr als 1 Video

Server-seitige Verarbeitung

  1. MIME-Type aus Data-URL extrahieren
  2. Validierung gegen Allowlist
  3. Größenprüfung (vor Dekodieren anhand Base64-Länge)
  4. Speicherung unter <media_dir>/websocket/<uuid>_<name>
  5. Pfade als media an Agenten weitergeben
  6. In Antwort: media und/oder media_urls mit signierten HTTP-URLs

Base64-Overhead

original_bytes ≈ base64_chars × 3 / 4

Eine 8 MB große Datei erzeugt ≈10.9 MB Base64 (×1.37 Overhead).

6. Node.js-Referenzimplementierung

Vollständiger JBot WebSocket-Client in Node.js unter Verwendung der ws-Bibliothek. Abhängigkeit:

npm install ws

Alternativ kann die in Node.js 22+ eingebaute WebSocket-API verwendet werden.

Basis-Client: Verbinden und Nachricht senden

// client-basic.js
import WebSocket from 'ws';

const HOST = '127.0.0.1', PORT = 8765, TOKEN = 'mein-token';
const url = `ws://${HOST}:${PORT}/?client_id=nodejs-app&token=${TOKEN}`;
const ws = new WebSocket(url);

ws.on('open', () => console.log('✅ Verbunden mit JBot'));
ws.on('message', (data) => {
  console.log(`📩 [${JSON.parse(data).event}]`, data.toString());
});
ws.on('close', (c, r) => console.log(`🔌 Geschlossen: ${c} ${r}`));
ws.on('error', (e) => console.error('❌', e.message));

setTimeout(() => ws.send('Hallo JBot, was kannst du?'), 5000);

JBotClient – Vollständiger Client

Der vollständige Source-Code inklusive TypeScript-Typdefinitionen ist als Markdown verfügbar: websocket/06-nodejs-client.md

Kern-API

MethodeBeschreibung
connect()Verbindet, resolved mit {chatId, clientId}
sendMessage(text, chatId?)JSON-Envelope senden
sendMessageWithMedia(text, media, chatId?)Mit Medien-Anhang
sendText(text)Legacy Plain-Text
createChat()Neue Session
attachChat(chatId)Session beitreten
setWorkspace(chatId, path)Workspace ändern
close()Trennen

Event-Handler (überschreibbar via Konstruktor)

onReady(chatId, clientId)
onMessage(msg)
onDelta(msg)
onStreamEnd(msg)
onReasoning(type, msg)       // type: "delta" | "end"
onToolHint(msg)
onProgress(msg)
onFileEdit(msg)
onTurnEnd(msg)
onGoalState(msg)
onGoalStatus(msg)
onSessionUpdated(msg)
onRuntimeModelUpdated(msg)
onError(msg)
onClose(code, reason)

Implementierungshinweise

  1. JSON-Envelopes bevorzugen – Legacy Plain-Text nur für Demos
  2. Auf ready warten – Keine Nachrichten vor ready senden
  3. Streaming akkumulieren – Deltas sammeln, stream_end enthält finalen Text
  4. Reasoning abgrenzen – Vor dem normalen Antwort-Stream
  5. Turn-Lebenszyklus – Beginnt mit goal_status: running, endet mit idle
  6. Fehler abfangenerror-Events immer behandeln
  7. Reconnectchat_id speichern, nach Reconnect mit attach subscriben