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>
This commit is contained in:
emma
2026-01-20 23:15:14 -08:00
committed by Steve Yegge
parent cff58c4639
commit bf92fda886
2 changed files with 636 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
// 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"`
}

View File

@@ -0,0 +1,366 @@
package types
import (
"testing"
"time"
)
func TestFederatedMessageTypeIsValid(t *testing.T) {
tests := []struct {
msgType FederatedMessageType
valid bool
}{
{MsgWorkHandoff, true},
{MsgQuery, true},
{MsgReply, true},
{MsgBroadcast, true},
{MsgAck, true},
{MsgReject, true},
{FederatedMessageType(""), false},
{FederatedMessageType("invalid"), false},
{FederatedMessageType("work_handoff"), false}, // underscore not dash
}
for _, tt := range tests {
t.Run(string(tt.msgType), func(t *testing.T) {
if got := tt.msgType.IsValid(); got != tt.valid {
t.Errorf("FederatedMessageType(%q).IsValid() = %v, want %v", tt.msgType, got, tt.valid)
}
})
}
}
func TestFederatedMessageTypeIsResponse(t *testing.T) {
tests := []struct {
msgType FederatedMessageType
isResponse bool
}{
{MsgWorkHandoff, false},
{MsgQuery, false},
{MsgReply, true},
{MsgBroadcast, false},
{MsgAck, true},
{MsgReject, true},
}
for _, tt := range tests {
t.Run(string(tt.msgType), func(t *testing.T) {
if got := tt.msgType.IsResponse(); got != tt.isResponse {
t.Errorf("FederatedMessageType(%q).IsResponse() = %v, want %v", tt.msgType, got, tt.isResponse)
}
})
}
}
func TestFederatedMessageTypeRequiresRecipient(t *testing.T) {
tests := []struct {
msgType FederatedMessageType
requiresRecipient bool
}{
{MsgWorkHandoff, true},
{MsgQuery, true},
{MsgReply, true},
{MsgBroadcast, false}, // broadcasts don't require specific recipient
{MsgAck, true},
{MsgReject, true},
}
for _, tt := range tests {
t.Run(string(tt.msgType), func(t *testing.T) {
if got := tt.msgType.RequiresRecipient(); got != tt.requiresRecipient {
t.Errorf("FederatedMessageType(%q).RequiresRecipient() = %v, want %v", tt.msgType, got, tt.requiresRecipient)
}
})
}
}
func TestRejectCodeIsValid(t *testing.T) {
tests := []struct {
code RejectCode
valid bool
}{
{RejectInvalid, true},
{RejectUnauthorized, true},
{RejectCapacity, true},
{RejectNotFound, true},
{RejectTimeout, true},
{RejectDuplicate, true},
{RejectCode(""), false},
{RejectCode("unknown"), false},
}
for _, tt := range tests {
t.Run(string(tt.code), func(t *testing.T) {
if got := tt.code.IsValid(); got != tt.valid {
t.Errorf("RejectCode(%q).IsValid() = %v, want %v", tt.code, got, tt.valid)
}
})
}
}
func TestFederatedMessageValidation(t *testing.T) {
validSender := &EntityRef{
Name: "town-alpha",
Platform: "gastown",
Org: "acme",
ID: "town-alpha",
}
tests := []struct {
name string
msg FederatedMessage
wantErr bool
errMsg string
}{
{
name: "valid work-handoff message",
msg: FederatedMessage{
ID: "msg-001",
Type: MsgWorkHandoff,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: false,
},
{
name: "valid query message",
msg: FederatedMessage{
ID: "msg-002",
Type: MsgQuery,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: false,
},
{
name: "valid broadcast message",
msg: FederatedMessage{
ID: "msg-003",
Type: MsgBroadcast,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: false,
},
{
name: "valid reply message with reply_to",
msg: FederatedMessage{
ID: "msg-004",
Type: MsgReply,
Timestamp: time.Now(),
Sender: validSender,
ReplyTo: "msg-002",
},
wantErr: false,
},
{
name: "valid ack message with reply_to",
msg: FederatedMessage{
ID: "msg-005",
Type: MsgAck,
Timestamp: time.Now(),
Sender: validSender,
ReplyTo: "msg-001",
},
wantErr: false,
},
{
name: "valid reject message with reply_to",
msg: FederatedMessage{
ID: "msg-006",
Type: MsgReject,
Timestamp: time.Now(),
Sender: validSender,
ReplyTo: "msg-001",
RejectCode: RejectCapacity,
},
wantErr: false,
},
{
name: "missing ID",
msg: FederatedMessage{
Type: MsgWorkHandoff,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: true,
errMsg: "message ID is required",
},
{
name: "invalid message type",
msg: FederatedMessage{
ID: "msg-007",
Type: FederatedMessageType("invalid"),
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: true,
errMsg: "invalid message type",
},
{
name: "missing timestamp",
msg: FederatedMessage{
ID: "msg-008",
Type: MsgWorkHandoff,
Sender: validSender,
},
wantErr: true,
errMsg: "timestamp is required",
},
{
name: "missing sender",
msg: FederatedMessage{
ID: "msg-009",
Type: MsgWorkHandoff,
Timestamp: time.Now(),
},
wantErr: true,
errMsg: "sender is required",
},
{
name: "empty sender",
msg: FederatedMessage{
ID: "msg-010",
Type: MsgWorkHandoff,
Timestamp: time.Now(),
Sender: &EntityRef{},
},
wantErr: true,
errMsg: "sender is required",
},
{
name: "reply without reply_to",
msg: FederatedMessage{
ID: "msg-014",
Type: MsgReply,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: true,
errMsg: "reply message requires reply_to field",
},
{
name: "ack without reply_to",
msg: FederatedMessage{
ID: "msg-015",
Type: MsgAck,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: true,
errMsg: "ack message requires reply_to field",
},
{
name: "reject without reply_to",
msg: FederatedMessage{
ID: "msg-016",
Type: MsgReject,
Timestamp: time.Now(),
Sender: validSender,
},
wantErr: true,
errMsg: "reject message requires reply_to field",
},
{
name: "reject with invalid reject code",
msg: FederatedMessage{
ID: "msg-017",
Type: MsgReject,
Timestamp: time.Now(),
Sender: validSender,
ReplyTo: "msg-001",
RejectCode: RejectCode("unknown"),
},
wantErr: true,
errMsg: "invalid reject code",
},
{
name: "valid message with signature",
msg: FederatedMessage{
ID: "msg-019",
Type: MsgWorkHandoff,
Timestamp: time.Now(),
Sender: validSender,
Signature: "ed25519:ABC123...",
SignerKeyID: "key-001",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.msg.Validate()
if tt.wantErr {
if err == nil {
t.Errorf("FederatedMessage.Validate() expected error containing %q, got nil", tt.errMsg)
} else if tt.errMsg != "" && !containsString(err.Error(), tt.errMsg) {
t.Errorf("FederatedMessage.Validate() error = %q, want error containing %q", err.Error(), tt.errMsg)
}
} else {
if err != nil {
t.Errorf("FederatedMessage.Validate() unexpected error = %v", err)
}
}
})
}
}
func TestFederatedMessageTypeString(t *testing.T) {
tests := []struct {
msgType FederatedMessageType
want string
}{
{MsgWorkHandoff, "work-handoff"},
{MsgQuery, "query"},
{MsgReply, "reply"},
{MsgBroadcast, "broadcast"},
{MsgAck, "ack"},
{MsgReject, "reject"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
if got := tt.msgType.String(); got != tt.want {
t.Errorf("FederatedMessageType.String() = %v, want %v", got, tt.want)
}
})
}
}
func TestRejectCodeString(t *testing.T) {
tests := []struct {
code RejectCode
want string
}{
{RejectInvalid, "invalid"},
{RejectUnauthorized, "unauthorized"},
{RejectCapacity, "capacity"},
{RejectNotFound, "not_found"},
{RejectTimeout, "timeout"},
{RejectDuplicate, "duplicate"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
if got := tt.code.String(); got != tt.want {
t.Errorf("RejectCode.String() = %v, want %v", got, tt.want)
}
})
}
}
// containsString checks if s contains substr
func containsString(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}