Files
beads/internal/types/federation.go
emma bf92fda886 feat(types): add FederatedMessage type for inter-town protocol (bd-wkumz.11)
Add message types for federation protocol:
- work-handoff, query, reply, broadcast, ack, reject
- Sender trust and staked reputation fields
- Cryptographic signature support
- Comprehensive validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 23:15:14 -08:00

271 lines
8.3 KiB
Go

// Package types defines core data structures for the bd issue tracker.
package types
import (
"fmt"
"time"
)
// FederatedMessageType categorizes inter-town communication.
type FederatedMessageType string
// Federated message type constants
const (
// MsgWorkHandoff delegates work from one town to another.
MsgWorkHandoff FederatedMessageType = "work-handoff"
// MsgQuery requests information from a peer town.
// Used for issue lookups, status checks, and capability discovery.
MsgQuery FederatedMessageType = "query"
// MsgReply responds to a query message.
// Contains the requested data or an error explanation.
MsgReply FederatedMessageType = "reply"
// MsgBroadcast announces information to all peer towns.
// Used for status updates, capability changes, and town-wide notifications.
MsgBroadcast FederatedMessageType = "broadcast"
// MsgAck acknowledges receipt and acceptance of a message.
MsgAck FederatedMessageType = "ack"
// MsgReject indicates a message was received but rejected.
// Includes reason for rejection (invalid, unauthorized, capacity, etc.)
MsgReject FederatedMessageType = "reject"
)
// validMessageTypes is the set of allowed message type values
var validMessageTypes = map[FederatedMessageType]bool{
MsgWorkHandoff: true,
MsgQuery: true,
MsgReply: true,
MsgBroadcast: true,
MsgAck: true,
MsgReject: true,
}
// IsValid checks if the message type value is valid.
func (t FederatedMessageType) IsValid() bool {
return validMessageTypes[t]
}
// String returns the string representation of the message type.
func (t FederatedMessageType) String() string {
return string(t)
}
// FederatedMessage represents a message exchanged between federated towns.
// Messages are cryptographically signed for authenticity verification.
type FederatedMessage struct {
// ===== Core Identity =====
// ID is a unique identifier for this message (UUID or similar).
ID string `json:"id"`
// Type categorizes the message purpose.
Type FederatedMessageType `json:"type"`
// Timestamp is when the message was created.
Timestamp time.Time `json:"timestamp"`
// ===== Routing =====
// Sender identifies the originating town/entity.
Sender *EntityRef `json:"sender"`
// Recipient identifies the target town/entity (nil for broadcasts).
Recipient *EntityRef `json:"recipient,omitempty"`
// ReplyTo links this message to a previous message (for replies, acks, rejects).
ReplyTo string `json:"reply_to,omitempty"`
// ===== Payload =====
// Subject is a brief description of the message content.
Subject string `json:"subject,omitempty"`
// Payload contains the message-specific data (JSON-encoded).
// For work-handoff: issue data
// For query: query parameters
// For reply: response data
// For broadcast: announcement data
// For ack/reject: processing result
Payload string `json:"payload,omitempty"`
// BeadID references a specific bead if applicable.
BeadID string `json:"bead_id,omitempty"`
// ===== Security =====
// Signature is the cryptographic signature of the message content.
// Format: "<algorithm>:<base64-encoded-signature>"
// Example: "ed25519:ABC123..."
Signature string `json:"signature,omitempty"`
// SignerKeyID identifies which key was used to sign (for key rotation).
SignerKeyID string `json:"signer_key_id,omitempty"`
// ===== Rejection Details (for MsgReject) =====
// RejectReason explains why a message was rejected.
RejectReason string `json:"reject_reason,omitempty"`
// RejectCode is a machine-readable rejection code.
RejectCode RejectCode `json:"reject_code,omitempty"`
}
// RejectCode categorizes rejection reasons for machine processing.
type RejectCode string
// Rejection code constants
const (
// RejectInvalid indicates the message format or content is invalid.
RejectInvalid RejectCode = "invalid"
// RejectUnauthorized indicates the sender lacks permission.
RejectUnauthorized RejectCode = "unauthorized"
// RejectCapacity indicates the recipient is at capacity.
RejectCapacity RejectCode = "capacity"
// RejectNotFound indicates the referenced resource doesn't exist.
RejectNotFound RejectCode = "not_found"
// RejectTimeout indicates the message or referenced work has expired.
RejectTimeout RejectCode = "timeout"
// RejectDuplicate indicates this message was already processed.
RejectDuplicate RejectCode = "duplicate"
)
// validRejectCodes is the set of allowed reject code values
var validRejectCodes = map[RejectCode]bool{
RejectInvalid: true,
RejectUnauthorized: true,
RejectCapacity: true,
RejectNotFound: true,
RejectTimeout: true,
RejectDuplicate: true,
}
// IsValid checks if the reject code value is valid.
func (c RejectCode) IsValid() bool {
return validRejectCodes[c]
}
// String returns the string representation of the reject code.
func (c RejectCode) String() string {
return string(c)
}
// Validate checks if the FederatedMessage has valid field values.
func (m *FederatedMessage) Validate() error {
if m.ID == "" {
return fmt.Errorf("message ID is required")
}
if !m.Type.IsValid() {
return fmt.Errorf("invalid message type: %s", m.Type)
}
if m.Timestamp.IsZero() {
return fmt.Errorf("timestamp is required")
}
if m.Sender == nil || m.Sender.IsEmpty() {
return fmt.Errorf("sender is required")
}
// Type-specific validation
switch m.Type {
case MsgReply, MsgAck, MsgReject:
if m.ReplyTo == "" {
return fmt.Errorf("%s message requires reply_to field", m.Type)
}
}
// Additional validation for reject messages
if m.Type == MsgReject && m.RejectCode != "" && !m.RejectCode.IsValid() {
return fmt.Errorf("invalid reject code: %s", m.RejectCode)
}
return nil
}
// IsResponse returns true if this message type is a response to another message.
func (t FederatedMessageType) IsResponse() bool {
return t == MsgReply || t == MsgAck || t == MsgReject
}
// RequiresRecipient returns true if this message type requires a specific recipient.
func (t FederatedMessageType) RequiresRecipient() bool {
// Broadcasts don't require a specific recipient
return t != MsgBroadcast
}
// WorkHandoffPayload is the payload structure for work-handoff messages.
// Embedded in FederatedMessage.Payload as JSON.
type WorkHandoffPayload struct {
// Issue is the issue being handed off.
Issue *Issue `json:"issue"`
// Labels are the issue's labels.
Labels []string `json:"labels,omitempty"`
// Dependencies are the issue's dependencies.
Dependencies []*Dependency `json:"dependencies,omitempty"`
// Reason explains why the work is being handed off.
Reason string `json:"reason,omitempty"`
// Deadline is when the work should be completed.
Deadline *time.Time `json:"deadline,omitempty"`
// Priority override for the receiving town (optional).
PriorityOverride *int `json:"priority_override,omitempty"`
}
// QueryPayload is the payload structure for query messages.
type QueryPayload struct {
// QueryType identifies the kind of query.
// Examples: "issue-status", "capability-check", "bead-lookup"
QueryType string `json:"query_type"`
// Parameters contains query-specific parameters.
Parameters map[string]string `json:"parameters,omitempty"`
}
// ReplyPayload is the payload structure for reply messages.
type ReplyPayload struct {
// Success indicates whether the query was successful.
Success bool `json:"success"`
// Data contains the query response (type depends on query).
Data string `json:"data,omitempty"`
// Error contains error details if Success is false.
Error string `json:"error,omitempty"`
}
// AckPayload is the payload structure for acknowledgment messages.
type AckPayload struct {
// Accepted indicates whether the message was accepted for processing.
Accepted bool `json:"accepted"`
// ProcessingID is an optional ID for tracking the processing.
ProcessingID string `json:"processing_id,omitempty"`
// EstimatedCompletion is when the work is expected to complete.
EstimatedCompletion *time.Time `json:"estimated_completion,omitempty"`
}
// BroadcastPayload is the payload structure for broadcast messages.
type BroadcastPayload struct {
// BroadcastType categorizes the broadcast.
// Examples: "status-update", "capability-change", "town-announcement"
BroadcastType string `json:"broadcast_type"`
// Message is the broadcast content.
Message string `json:"message"`
// Metadata contains additional broadcast-specific data.
Metadata map[string]string `json:"metadata,omitempty"`
}