package api import ( "encoding/json" "fmt" ) // MessageType enumerates every kind of envelope that can flow over // the agent ↔ server WebSocket. Keeping these as string constants // (not iota ints) makes traffic readable in logs and packet captures. type MessageType string // Agent → server message types. const ( MsgHello MessageType = "hello" MsgHeartbeat MessageType = "heartbeat" MsgJobStarted MessageType = "job.started" MsgJobProgress MessageType = "job.progress" MsgJobFinished MessageType = "job.finished" MsgSnapshotsRpt MessageType = "snapshots.report" MsgRepoStats MessageType = "repo.stats" MsgLogStream MessageType = "log.stream" MsgScheduleAck MessageType = "schedule.ack" MsgScheduleFire MessageType = "schedule.fire" // agent: a local cron entry fired, please dispatch a job MsgCommandResult MessageType = "command.result" // ack for command.run MsgTreeListResult MessageType = "tree.list.result" // reply to a server-driven tree.list MsgError MessageType = "error" ) // Server → agent message types. const ( MsgCommandRun MessageType = "command.run" MsgCommandCancel MessageType = "command.cancel" MsgScheduleSet MessageType = "schedule.set" MsgConfigUpdate MessageType = "config.update" MsgAgentUpdateAvail MessageType = "agent.update.available" MsgTreeList MessageType = "tree.list" // sync RPC: list a snapshot's children ) // Envelope is the framing for every WS message in either direction. // Payload is parsed into the concrete struct chosen by Type. // // ID is set on RPC-style messages (command.run / command.result) so // responses can be correlated. For one-shot pushes (heartbeat, // job.progress) it is empty. type Envelope struct { Type MessageType `json:"type"` ID string `json:"id,omitempty"` Payload json.RawMessage `json:"payload,omitempty"` } // Marshal builds an envelope from a concrete payload struct. func Marshal(t MessageType, id string, payload any) (Envelope, error) { if payload == nil { return Envelope{Type: t, ID: id}, nil } raw, err := json.Marshal(payload) if err != nil { return Envelope{}, fmt.Errorf("marshal %s payload: %w", t, err) } return Envelope{Type: t, ID: id, Payload: raw}, nil } // UnmarshalPayload decodes the envelope's payload into v. func (e Envelope) UnmarshalPayload(v any) error { if len(e.Payload) == 0 { return nil } return json.Unmarshal(e.Payload, v) } // ErrorCode enumerates error reasons surfaced over the wire. // These are stable identifiers; client code may switch on them. type ErrorCode string // Stable ErrorCode values surfaced over the wire. Clients switch on // these; renaming requires a wire-version bump. const ( ErrProtocolTooOld ErrorCode = "protocol_too_old" ErrProtocolTooNew ErrorCode = "protocol_too_new" ErrUnauthorized ErrorCode = "unauthorized" ErrBadRequest ErrorCode = "bad_request" ErrInternal ErrorCode = "internal" ) // ErrorPayload is the body of an `error` envelope. type ErrorPayload struct { Code ErrorCode `json:"code"` Message string `json:"message"` HelpURL string `json:"help_url,omitempty"` }