package protocol import ( "encoding/json" "fmt" "strings" "time" ) const CurrentProtocolVersion = 1 // MessageType identifies a versioned agent/control-plane message. type MessageType string const ( // Agent → Control Plane TypeAgentHello MessageType = "agent.hello" TypeAgentHeartbeat MessageType = "agent.heartbeat" TypeAgentInventory MessageType = "agent.inventory" TypeServerState MessageType = "server.state" TypeServerMetrics MessageType = "server.metrics" TypeServerLog MessageType = "server.log" TypeServerOperationProgress MessageType = "server.operation.progress" TypeServerOperationComplete MessageType = "server.operation.completed" TypeServerOperationFailed MessageType = "server.operation.failed" // Control Plane → Agent TypeServerProvision MessageType = "server.provision" TypeServerInstall MessageType = "server.install" TypeServerStart MessageType = "server.start" TypeServerStop MessageType = "server.stop" TypeServerKill MessageType = "server.kill" TypeServerDelete MessageType = "server.delete" TypeServerInspect MessageType = "server.inspect" TypeServerCommand MessageType = "server.command" TypeServerBackupPrepare MessageType = "server.backup.prepare" TypeServerBackupRelease MessageType = "server.backup.release" TypeServerFilesList MessageType = "server.files.list" TypeServerFilesRead MessageType = "server.files.read" TypeServerFilesWrite MessageType = "server.files.write" TypeServerFilesUploadDone MessageType = "server.files.upload.complete" TypeServerWorldValidate MessageType = "server.world.validate" TypeServerMetricsSub MessageType = "server.metrics.subscribe" TypeServerLogsSubscribe MessageType = "server.logs.subscribe" ) var agentToControlTypes = map[MessageType]struct{}{ TypeAgentHello: {}, TypeAgentHeartbeat: {}, TypeAgentInventory: {}, TypeServerState: {}, TypeServerMetrics: {}, TypeServerLog: {}, TypeServerOperationProgress: {}, TypeServerOperationComplete: {}, TypeServerOperationFailed: {}, } var controlToAgentTypes = map[MessageType]struct{}{ TypeServerProvision: {}, TypeServerInstall: {}, TypeServerStart: {}, TypeServerStop: {}, TypeServerKill: {}, TypeServerDelete: {}, TypeServerInspect: {}, TypeServerCommand: {}, TypeServerBackupPrepare: {}, TypeServerBackupRelease: {}, TypeServerFilesList: {}, TypeServerFilesRead: {}, TypeServerFilesWrite: {}, TypeServerFilesUploadDone: {}, TypeServerWorldValidate: {}, TypeServerMetricsSub: {}, TypeServerLogsSubscribe: {}, } // Envelope is the common wrapper for all protocol messages. type Envelope struct { ProtocolVersion int `json:"protocolVersion"` MessageID string `json:"messageId"` CorrelationID string `json:"correlationId,omitempty"` Type MessageType `json:"type"` Timestamp time.Time `json:"timestamp"` Payload json.RawMessage `json:"payload"` } // OperationMeta is shared metadata for lifecycle operations. type OperationMeta struct { ServerID string `json:"serverId"` Generation int64 `json:"generation"` Deadline *time.Time `json:"deadline,omitempty"` IdempotencyKey string `json:"idempotencyKey,omitempty"` } // AgentHelloPayload is sent when the agent connects to the control plane. type AgentHelloPayload struct { NodeID string `json:"nodeId"` AgentVersion string `json:"agentVersion"` ProtocolVersion int `json:"protocolVersion"` Hostname string `json:"hostname,omitempty"` Capabilities []string `json:"capabilities,omitempty"` } // AgentHeartbeatPayload reports node health and resource usage. type AgentHeartbeatPayload struct { NodeID string `json:"nodeId"` CPUUsagePercent float64 `json:"cpuUsagePercent"` MemoryUsedBytes int64 `json:"memoryUsedBytes"` MemoryTotalBytes int64 `json:"memoryTotalBytes"` DiskUsedBytes int64 `json:"diskUsedBytes"` DiskTotalBytes int64 `json:"diskTotalBytes"` RunningServers int `json:"runningServers"` } // ServerStartPayload instructs the agent to start a game server. type ServerStartPayload struct { OperationMeta DesiredGeneration int64 `json:"desiredGeneration"` } // ServerStatePayload reports the observed server state. type ServerStatePayload struct { OperationMeta State string `json:"state"` ErrorCode string `json:"errorCode,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` } // Validate checks envelope fields and known message types. func (e *Envelope) Validate() error { if e == nil { return fmt.Errorf("envelope is nil") } if e.ProtocolVersion < 1 { return fmt.Errorf("protocolVersion must be >= 1") } if e.ProtocolVersion > CurrentProtocolVersion { return fmt.Errorf("unsupported protocolVersion %d", e.ProtocolVersion) } if strings.TrimSpace(e.MessageID) == "" { return fmt.Errorf("messageId is required") } if strings.TrimSpace(string(e.Type)) == "" { return fmt.Errorf("type is required") } if e.Timestamp.IsZero() { return fmt.Errorf("timestamp is required") } if !e.Timestamp.UTC().Equal(e.Timestamp) { return fmt.Errorf("timestamp must be UTC") } if _, ok := agentToControlTypes[e.Type]; !ok { if _, ok := controlToAgentTypes[e.Type]; !ok { return fmt.Errorf("unknown message type: %s", e.Type) } } if len(e.Payload) == 0 { return fmt.Errorf("payload is required") } if !json.Valid(e.Payload) { return fmt.Errorf("payload must be valid JSON") } return nil } // IsAgentToControl returns true when the message flows from agent to control plane. func (t MessageType) IsAgentToControl() bool { _, ok := agentToControlTypes[t] return ok } // IsControlToAgent returns true when the message flows from control plane to agent. func (t MessageType) IsControlToAgent() bool { _, ok := controlToAgentTypes[t] return ok } // DecodeEnvelope parses and validates a JSON envelope. func DecodeEnvelope(data []byte) (*Envelope, error) { var env Envelope if err := json.Unmarshal(data, &env); err != nil { return nil, fmt.Errorf("decode envelope: %w", err) } if err := env.Validate(); err != nil { return nil, err } return &env, nil } // NewEnvelope creates a validated outbound envelope. func NewEnvelope(msgType MessageType, messageID string, payload any) (*Envelope, error) { raw, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("marshal payload: %w", err) } env := &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: messageID, Type: msgType, Timestamp: time.Now().UTC(), Payload: raw, } if err := env.Validate(); err != nil { return nil, err } return env, nil }