WebSocket API
Usejarvis uses a WebSocket connection for the live dashboard experience.
Default endpoint:
ws://localhost:3142/wsIf you serve the dashboard over HTTPS behind a proxy, the browser will use wss://.../ws.
Authentication
Section titled “Authentication”Access is JWT-only. There is no shared dashboard token, and by default nothing is open: unauthenticated requests to /api/* and /ws get a 401 (JSON {"error":"Unauthorized"} for API and WebSocket routes, an HTML error page for page loads). The only bypass is auth.insecure_open_access: true, intended for pre-enrollment setup.
Two tokens exist:
- Enrollment token (long-lived, ES256 JWT): minted by
jarvis enroll "<device-name>". It is accepted on exactly two endpoints:GET /sidecar/connect(the sidecar WebSocket upgrade) andPOST /sidecar/token, both via anAuthorization: Bearerheader. It is deliberately rejected everywhere else, so a leaked data-plane credential is bounded by the access-token TTL. - Access token (short-lived): minted by
POST /sidecar/token, which returns{ "access_token": "...", "expires_in": 600 }. It lives 10 minutes. This is the credential for the dashboard,/api/*, and/ws.
The access token is carried as a token cookie. A valid ?token=<access token> on any page request triggers a 302 redirect to the same path with the parameter stripped and sets token=...; Path=/; SameSite=Lax; HttpOnly (plus Secure when the request is HTTPS, including via x-forwarded-proto).
Custom client flow:
jarvis enroll "my-client"on the brain machine to get an enrollment JWTPOST /sidecar/tokenwithAuthorization: Bearer <enrollment JWT>to mint an access token- Call the API or open
/wswith thetokencookie set; re-mint before the 10 minute expiry
Note for /ws: the upgrade checks the token cookie, not an Authorization header, and also enforces an Origin check (the origin must match the daemon’s origin or host), returning 403 on mismatch.
GET /health is public and returns { status, version, uptime, clients, timestamp }.
Message Shape
Section titled “Message Shape”The daemon’s WebSocket messages use this envelope:
{ "type": "chat", "payload": {}, "id": "optional-id", "priority": "normal", "timestamp": 1710000000000}Important note:
- The field is
payload, notdata
Supported Message Types
Section titled “Supported Message Types”The shipped daemon defines these top-level message types:
chat, command, status, stream, error, notification, tts_start, tts_text, tts_end, voice_start, voice_end, voice_text, voice_confirmation_expired, interview_start, interview_user_message, interview_assistant, interview_done, interview_error, thinking_start, thinking_end, realtime_status, realtime_transcript, task_event, workflow_event, goal_event, site_event, settings_applied
Client → Server Messages
Section titled “Client → Server Messages”The server accepts exactly these types; anything else returns { "type": "error", "payload": { "message": "Unknown message type: <t>" } }:
chat: send a user messagecommand: system-style requestsstatus: request daemon statusvoice_start: begin a voice turn ({ requestId, currentRoom? }); binary WAV audio frames followvoice_end: end the audio stream and trigger transcriptionvoice_text: browser-side STT result ({ requestId?, text, currentRoom? }); skips daemon transcription and cancels any in-flight audio sessioninterview_start,interview_user_message: the profile interview flow
Server → Client Messages
Section titled “Server → Client Messages”stream: token streaming during a response (sub-agent output usespayload.source: "sub-agent")notification: typed events discriminated bypayload.source:
payload.source | Meaning |
|---|---|
task_update | Task state changed |
assistant_message | Proactive assistant message |
content_update | Content pipeline update |
approval_request | Governed action awaiting approval ({ request, shortId, impact, intent }, priority: "urgent" when urgent) |
emergency_state | Emergency stop engaged or cleared |
sidecar_event | Sidecar connected/disconnected or status change |
awareness_event | Awareness observation or suggestion |
settings_applied: emitted after DB-backed settings hot-apply, payload{ sections, ok, errors? }(section names only, never values)tts_start/tts_text/tts_end: sentence-level TTS lifecycle; binary MP3 frames are interleaved.tts_start.payload.containsWaketells the client to suppress the wake listener during playbacktask_event: task lifecycle events for consumers tracking long-running work
Binary Frames
Section titled “Binary Frames”Voice audio (client → server, WAV) and TTS audio (server → client, MP3 per sentence) travel as binary WebSocket frames alongside the JSON stream.
Sidecar Protocol
Section titled “Sidecar Protocol”The sidecar WebSocket (/sidecar/connect) speaks a separate message family (RPC request, rpc_result, rpc_progress, sidecar_event) with binary-reference semantics for payloads over 256 KB and a two-timeout RPC model. It also supports ?channel=audio for a dedicated realtime-audio pipe. If you are integrating a device rather than a dashboard client, use that protocol, documented in the product repo under docs/sidecar/SIDECAR_PROTOCOL.md, not this one.
Reverse Proxy Requirements
Section titled “Reverse Proxy Requirements”- Forward WebSocket upgrades for
/wsand/sidecar/connect - Do not gate the public routes:
/health,/sidecar/connect,/api/sidecars/.well-known/jwks.json,/api/webhooks/*, andOPTIONSrequests - Preserve cookies and
x-forwarded-proto - Set
daemon.brain_domainto the external origin so minted tokens point sidecars at the right endpoints - The server closes idle connections after 30 seconds; keep WebSocket ping/pong flowing