WebSocket Protocol
Amurg uses JSON-encoded messages over WebSocket for all real-time communication. There are two WebSocket connections: Runtime ↔ Hub (/ws/runtime) and Client ↔ Hub (/ws/client). All messages share a common envelope format.
Envelope Format
Every message is a JSON object with a type field that determines the payload structure:
{
"type": "message.type",
"id": "optional-message-uuid",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:00Z",
"payload": { ... }
} | Field | Type | Description |
|---|---|---|
type | string | Message type identifier (required) |
id | string | Message UUID for idempotency (optional) |
session_id | string | Target session (optional, not used in hello/ack/ping) |
ts | string (ISO 8601) | Timestamp of the message |
payload | object | Type-specific payload (optional) |
Runtime ↔ Hub Messages
These messages flow over the /ws/runtime connection between the runtime process and the hub.
| Type | Direction | Description |
|---|---|---|
runtime.hello | Runtime → Hub | Initial authentication and endpoint registration |
hello.ack | Hub → Runtime | Acknowledge runtime registration |
session.create | Hub → Runtime | Request runtime to create a new session |
session.created | Runtime → Hub | Confirm session creation (with optional native handle) |
session.close | Both | Close a session (either side may initiate) |
user.message | Hub → Runtime | Forward user input to the agent |
agent.output | Runtime → Hub | Stream agent output (stdout/stderr/system) |
turn.started | Runtime → Hub | Agent has begun responding to a user message |
turn.completed | Runtime → Hub | Agent turn is complete (with optional exit code) |
stop.request | Hub → Runtime | Request the runtime to stop/cancel a session |
stop.ack | Runtime → Hub | Acknowledge stop request |
permission.request | Runtime → Hub | Agent requests tool approval from the user |
permission.response | Hub → Runtime | User's approval or denial of tool permission |
file.upload | Hub → Runtime | User-uploaded file forwarded to the agent (base64) |
file.available | Runtime → Hub | Agent-produced file sent to the hub (base64) |
endpoint.config_update | Hub → Runtime | Apply security/limits config override to an endpoint |
endpoint.config_ack | Runtime → Hub | Acknowledge config update |
runtime.token_refresh | Hub → Runtime | Provide a refreshed authentication token |
ping / pong | Both | Connection liveness heartbeat |
Runtime ↔ Hub Message Details
runtime.hello
Runtime → Hub — Sent immediately after WebSocket connection. Authenticates the runtime and registers all available endpoints.
{
"type": "runtime.hello",
"ts": "2024-01-15T10:30:00Z",
"payload": {
"runtime_id": "prod-runtime-01",
"token": "pre-shared-runtime-token",
"org_id": "default",
"endpoints": [
{
"id": "claude-code-prod",
"profile": "claude-code",
"name": "Claude Code (prod)",
"tags": { "env": "production" },
"caps": {
"native_session_ids": false,
"turn_completion": true,
"resume_attach": false,
"exec_model": "interactive"
},
"security": {
"permission_mode": "strict",
"allowed_tools": ["Bash", "Read", "Write"]
}
}
]
}
} hello.ack
Hub → Runtime — Confirms or rejects the runtime registration.
{
"type": "hello.ack",
"ts": "2024-01-15T10:30:00Z",
"payload": { "ok": true }
} session.create
Hub → Runtime — Requests the runtime to create a new agent session on the specified endpoint.
{
"type": "session.create",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:01Z",
"payload": {
"session_id": "session-uuid",
"endpoint_id": "claude-code-prod",
"user_id": "user-uuid"
}
} session.created
Runtime → Hub — Confirms session creation. May include a native session handle for integrated profiles.
{
"type": "session.created",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:01Z",
"payload": {
"session_id": "session-uuid",
"ok": true,
"native_handle": ""
}
} user.message
Hub → Runtime — Forwards user input to the agent. The message_id is a client-generated UUID for idempotency.
{
"type": "user.message",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:05Z",
"payload": {
"session_id": "session-uuid",
"message_id": "msg-uuid",
"content": "List all files in the current directory"
}
} agent.output
Runtime → Hub — Streams agent output. Each chunk has a monotonic seq number. The channel field indicates the output source. Set final: true on the last chunk of a turn.
{
"type": "agent.output",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:06Z",
"payload": {
"session_id": "session-uuid",
"message_id": "out-uuid",
"seq": 1,
"channel": "stdout",
"content": "file1.txt file2.txt src/",
"final": false
}
} turn.started
Runtime → Hub — Signals that the agent has begun processing a user message.
{
"type": "turn.started",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:05Z",
"payload": {
"session_id": "session-uuid",
"in_response_to": "msg-uuid"
}
} turn.completed
Runtime → Hub — Signals that the agent has finished responding. For run-to-completion profiles, includes the process exit code.
{
"type": "turn.completed",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:08Z",
"payload": {
"session_id": "session-uuid",
"in_response_to": "msg-uuid",
"exit_code": 0
}
} stop.request / stop.ack
Hub → Runtime / Runtime → Hub — Hub requests the runtime to stop or cancel the current operation. The runtime acknowledges with success or failure.
// stop.request (Hub → Runtime)
{
"type": "stop.request",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:10Z",
"payload": { "session_id": "session-uuid" }
}
// stop.ack (Runtime → Hub)
{
"type": "stop.ack",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:10Z",
"payload": { "session_id": "session-uuid", "ok": true }
} session.close
Both — Either side can initiate a session close with an optional reason.
{
"type": "session.close",
"session_id": "session-uuid",
"ts": "2024-01-15T10:35:00Z",
"payload": {
"session_id": "session-uuid",
"reason": "user_requested"
}
} permission.request
Runtime → Hub — Sent when an agent needs user approval for a tool action. The hub tracks pending requests with a 60-second timeout. If no response arrives in time, the request is auto-denied.
{
"type": "permission.request",
"session_id": "session-uuid",
"ts": "2024-01-15T10:31:02Z",
"payload": {
"session_id": "session-uuid",
"request_id": "req-uuid",
"tool": "Bash",
"description": "Execute: rm -rf /tmp/build",
"resource": "/tmp/build"
}
} | Field | Type | Description |
|---|---|---|
request_id | string | Unique ID for correlating request/response |
tool | string | Name of the tool requesting permission (e.g. "Bash", "Write") |
description | string | Human-readable description of the action |
resource | string | Optional resource path or identifier |
permission.response
Hub → Runtime (via Client interaction) — Carries the user's approval or denial back to the runtime. The always_allow flag indicates the user checked "always allow this tool," which the runtime may use to skip future prompts for that tool.
{
"type": "permission.response",
"session_id": "session-uuid",
"ts": "2024-01-15T10:31:05Z",
"payload": {
"session_id": "session-uuid",
"request_id": "req-uuid",
"approved": true,
"always_allow": false
}
} file.upload
Hub → Runtime — Forwards a user-uploaded file to the runtime as base64-encoded data. The hub stores the file on disk and also forwards it via WebSocket.
{
"type": "file.upload",
"session_id": "session-uuid",
"ts": "2024-01-15T10:32:00Z",
"payload": {
"session_id": "session-uuid",
"metadata": {
"file_id": "file-uuid",
"name": "input.csv",
"mime_type": "text/csv",
"size": 1024
},
"data": "base64-encoded-content..."
}
} file.available
Runtime → Hub — Notifies the hub that the agent has produced a file. The hub stores the file and makes it available for download via the HTTP API.
{
"type": "file.available",
"session_id": "session-uuid",
"ts": "2024-01-15T10:33:00Z",
"payload": {
"session_id": "session-uuid",
"metadata": {
"file_id": "file-uuid",
"name": "output.png",
"mime_type": "image/png",
"size": 45678
},
"data": "base64-encoded-content..."
}
} ping / pong
Both — Heartbeat messages for connection liveness detection.
{ "type": "ping", "ts": "2024-01-15T10:30:00Z" }
{ "type": "pong", "ts": "2024-01-15T10:30:00Z" } Client ↔ Hub Messages
These messages flow over the /ws/client connection between the UI client and the hub.
| Type | Direction | Description |
|---|---|---|
client.subscribe | Client → Hub | Subscribe to live updates for a session (with replay from after_seq) |
client.unsubscribe | Client → Hub | Stop receiving live updates for a session |
user.message | Client → Hub | Send a user message (routed to runtime) |
agent.output | Hub → Client | Streamed agent output |
turn.started | Hub → Client | Agent turn has begun |
turn.completed | Hub → Client | Agent turn is complete |
permission.request | Hub → Client | Agent tool permission prompt (relayed from runtime) |
permission.response | Client → Hub | User's approval/denial (forwarded to runtime) |
history.response | Hub → Client | Replayed messages for a subscribed session |
session.closed | Hub → Client | Session has been closed (by user, admin, or idle reaper) |
error | Hub → Client | Error response (e.g. turn_in_progress, auth_failed) |
client.subscribe
Client → Hub — Subscribe to live updates for a session. The after_seq field enables reconnect replay: the hub sends all stored messages with a sequence number greater than after_seq.
{
"type": "client.subscribe",
"ts": "2024-01-15T10:30:00Z",
"payload": {
"session_id": "session-uuid",
"after_seq": 0
}
} error
Hub → Client — Error messages include a machine-readable code and a human-readable message.
{
"type": "error",
"session_id": "session-uuid",
"ts": "2024-01-15T10:30:05Z",
"payload": {
"code": "turn_in_progress",
"message": "A turn is already in progress for this session"
}
} Sequence Diagrams
Runtime Connection
Runtime Hub | | |---[WS connect /ws/runtime]--->| | | |---runtime.hello-------------->| | (token + endpoints) | | |---[validate token] | |---[register endpoints] |<--hello.ack-------------------| | (ok: true) | | | |<==== bidirectional channel ==>|
Session Lifecycle
User/Client Hub Runtime Agent
| | | |
|--POST /sessions--->| | |
| |--session.create--->| |
| | |---spawn/attach--->|
| |<-session.created---| |
|<--201 {session}----| | |
| | | |
|--WS: subscribe---->| | |
|--WS: user.message->| | |
| |--user.message----->| |
| | |---stdin---------->|
| |<-turn.started------| |
|<-WS: turn.started--| | |
| | |<--stdout----------|
| |<-agent.output------| |
|<-WS: agent.output--| | |
| | |<--exit/idle-------|
| |<-turn.completed----| |
|<-WS: turn.complete-| | |
| | | | Permission Flow
User/Client Hub Runtime Agent | | | | | | |<--tool call-------| | |<-perm.request------| | | | (tool, desc) | [blocked] | |<-WS: perm.request--| | | | [show banner] | | | |--WS: perm.response>| | | | (approved/denied)| | | | |--perm.response---->| | | | |---resume/deny---->| | | | | | | [60s timeout] | | | | [auto-deny if | | | | no response] | |
File Transfer Flow
User/Client Hub Runtime Agent
| | | |
| --- Upload (user to agent) --- | |
|--POST /files------>| | |
| (multipart) |--file.upload------>| |
| | (base64 over WS) |---file.input----->|
| | | |
| --- Download (agent to user) --- | |
| | |<--file.output------|
| |<-file.available----| |
| | (base64 over WS) | |
| |---store to disk--->| |
|<-WS: file message--| | |
|--GET /files/{id}-->| | |
|<--binary download--| | | Reconnect & Resume
Client Hub | | |--[connection lost]-| | | |--[WS reconnect]--->| |--subscribe-------->| | (after_seq: 42) | | |---[query messages > seq 42] |<-history.response--| | (missed msgs) | | | |<==== live stream ==>|