Nimbusv1.0.0

Protocol / Message Reference

Shared message types for controller-agent WebSocket communication in the Nimbus cluster protocol.

The nimbus-protocol module defines the shared message types exchanged between the Nimbus controller and agent nodes over WebSocket. It is a dependency of both nimbus-core and nimbus-agent.

Overview

All cluster communication uses JSON-serialized messages with a type discriminator field. The protocol is defined in two files:

  • dev.nimbuspowered.nimbus.protocol.ClusterMessage -- Sealed class containing all message types
  • dev.nimbuspowered.nimbus.protocol.ServiceHeartbeat / RemoteFileEntry -- Data classes used inside messages
  • dev.nimbuspowered.nimbus.protocol.StateManifest / StateFileEntry -- State-sync manifest types used by the controller's state-sync REST API (not part of the WebSocket protocol; served over the TLS cluster port)
  • dev.nimbuspowered.nimbus.protocol.clusterJson -- Pre-configured kotlinx.serialization.json.Json instance
Kotlin
val clusterJson = Json {
    ignoreUnknownKeys = true
    encodeDefaults = true
    classDiscriminator = "type"
}

Message types

Controller to Agent

TypeClassPurpose
AUTH_RESPONSEAuthResponseAccept/reject agent authentication
START_SERVICEStartServiceLaunch a new service on the agent
STOP_SERVICEStopServiceStop a running service
SEND_COMMANDSendCommandForward a console command to a service
HEARTBEAT_REQUESTHeartbeatRequestRequest system metrics from agent
TEMPLATE_INFOTemplateInfoTemplate metadata (name, hash, download URL, size)
SHUTDOWN_AGENTShutdownAgentGracefully shut down the agent
DISCARD_SYNC_WORKDIRDiscardSyncWorkdirTell the agent to delete the local working directory for a sync-enabled service (fields: serviceName)
FILE_LIST_REQUESTFileListRequestRequest a directory listing from the agent for remote file management
FILE_READ_REQUESTFileReadRequestRequest the contents of a file on the agent
FILE_WRITE_REQUESTFileWriteRequestWrite or overwrite a file on the agent
FILE_DELETE_REQUESTFileDeleteRequestDelete a file or directory on the agent

Agent to Controller (file management responses)

TypeClassPurpose
FILE_LIST_RESPONSEFileListResponseReturns directory listing for FileListRequest
FILE_READ_RESPONSEFileReadResponseReturns file contents for FileReadRequest
FILE_WRITE_RESPONSEFileWriteResponseAcknowledges or reports error for FileWriteRequest
FILE_DELETE_RESPONSEFileDeleteResponseAcknowledges or reports error for FileDeleteRequest

Agent to Controller

TypeClassPurpose
AUTH_REQUESTAuthRequestAuthenticate with token, node info, and resources
HEARTBEAT_RESPONSEHeartbeatResponseCPU, memory, and per-service status
SERVICE_STATE_CHANGEDServiceStateChangedReport service state transition
SERVICE_STDOUTServiceStdoutForward a stdout line from a service
SERVICE_PLAYER_COUNTServicePlayerCountReport player count for a service
COMMAND_RESULTCommandResultResult of a forwarded command
TEMPLATE_REQUESTTemplateRequestRequest a template download
LOG_MESSAGELogMessageForward a log message to the controller

Key message details

StartService

The most complex message -- contains everything needed to launch a service: serviceName, groupName, port, templateName, templateHash, software, version, memory, jvmArgs, jarName, forwardingMode, forwardingSecret, isStatic, isModded, apiUrl, apiToken, nimbusProperties, javaVersion, bedrockPort, bedrockEnabled, and more. See ClusterMessage.kt for the full field list.

The apiToken sent to game servers is a restricted service token (HMAC-derived from the admin token), limiting API access to service-level endpoints only. Velocity proxy services receive the full admin token for bridge plugin access.

AuthRequest

Sent by the agent after connecting. Fields: token, nodeName, maxMemory, maxServices, currentServices, agentVersion, os, arch, hostname, osVersion, cpuModel, availableProcessors, systemMemoryTotalMb, javaVersion, javaVendor, publicHost, runningServices, protocolVersion.

publicHost is the publicly reachable IP or hostname reported by the agent for player routing. runningServices is a list of services the agent was already managing before the (re-)connection, used during the controller's reconciliation window to avoid duplicate restarts.

Protocol version

Both AuthRequest and AuthResponse carry an integer protocolVersion. The current value is exposed as the compile-time constant ClusterMessage.CURRENT_PROTOCOL_VERSION (in 0.13.0: 1).

During the handshake, the controller compares the agent's reported version to its own before checking the auth token. On mismatch, the controller replies with AuthResponse(accepted = false, reason = "protocol version mismatch: agent=X, controller=Y", protocolVersion = <controller>) and closes the WebSocket with VIOLATED_POLICY. The ordering is intentional: when both versions and tokens disagree, the operator sees the real cause (incompatible builds) instead of a misleading token error.

The agent recognises a version-mismatch reject via a local compare of the advertised versions, logs a single ERROR, and exits the reconnect loop immediately -- no exponential backoff, no silent retry churn. Incompatible deployments fail loud and fast; the fix is to bring both controller and agent to matching Nimbus versions and restart.

Both fields default to 1 on the wire. Combined with clusterJson.ignoreUnknownKeys = true, this preserves the rolling-upgrade path between 0.12.x and 0.13.0 nodes that don't yet emit the field.

When to bump CURRENT_PROTOCOL_VERSION. Only when an existing ClusterMessage field changes semantics, is renamed, or is removed. Adding a new @SerialName'd message type, or adding a new optional field to an existing type with a sensible default, does not require a bump -- older peers ignore unknown keys and fall through to the default.

ServiceHeartbeat

Included in HeartbeatResponse.services -- per-service status with serviceName, groupName, state, port, pid, playerCount, and optional customState.

ServiceHandle interface

The ServiceHandle interface (dev.nimbuspowered.nimbus.service.ServiceHandle in nimbus-core) abstracts over local and remote processes with methods for stdoutLines, isAlive(), pid(), sendCommand(), waitForReady(), stopGracefully(), awaitExit(), and destroy(). For remote services, the controller translates these calls into ClusterMessage instances sent over the WebSocket.

Message flow examples

Starting a service on a remote node

Directory Structure
Controller                          Agent
    │                                 │
    ├── StartService ───────────────→ │
    │                                 ├── ensureTemplate() (download if hash mismatch)
    │                                 ├── copyTemplate() + patchConfigs()
    │                                 ├── launch JVM process
    │   ←── ServiceStateChanged ──────┤  (state=STARTING)
    │                                 ├── monitor stdout for ready pattern
    │   ←── ServiceStdout ────────────┤  (forwarded log lines)
    │   ←── ServiceStateChanged ──────┤  (state=READY)
    │                                 │

Heartbeat cycle

Directory Structure
Controller                          Agent
    │                                 │
    ├── HeartbeatRequest ───────────→ │
    │   ←── HeartbeatResponse ────────┤  (cpu, memory, service list)
    │                                 │

Graceful shutdown

Directory Structure
Controller                          Agent
    │                                 │
    ├── ShutdownAgent ──────────────→ │
    │                                 ├── stopAll() (send "stop" to each service)
    │                                 ├── close connections
    │                                 └── exit

Next steps