Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
551 lines
13 KiB
Go
551 lines
13 KiB
Go
package protocol
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
"github.com/libp2p/go-libp2p/core/peerstore"
|
|
)
|
|
|
|
// PeerCapability represents the capabilities of a peer
|
|
type PeerCapability struct {
|
|
PeerID peer.ID `json:"peer_id"`
|
|
Agent string `json:"agent"`
|
|
Role string `json:"role"`
|
|
Capabilities []string `json:"capabilities"`
|
|
Models []string `json:"models"`
|
|
Specialization string `json:"specialization"`
|
|
Project string `json:"project"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
Status string `json:"status"` // "online", "busy", "offline"
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
// PeerAddress represents a resolved peer address
|
|
type PeerAddress struct {
|
|
PeerID peer.ID `json:"peer_id"`
|
|
Addresses []string `json:"addresses"`
|
|
Priority int `json:"priority"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
|
|
// ResolutionResult represents the result of address resolution
|
|
type ResolutionResult struct {
|
|
URI *BzzzURI `json:"uri"`
|
|
Peers []*PeerAddress `json:"peers"`
|
|
ResolvedAt time.Time `json:"resolved_at"`
|
|
ResolutionTTL time.Duration `json:"ttl"`
|
|
Strategy string `json:"strategy"`
|
|
}
|
|
|
|
// ResolutionStrategy defines how to resolve addresses
|
|
type ResolutionStrategy string
|
|
|
|
const (
|
|
StrategyExact ResolutionStrategy = "exact" // Exact match only
|
|
StrategyBestMatch ResolutionStrategy = "best_match" // Best available match
|
|
StrategyLoadBalance ResolutionStrategy = "load_balance" // Load balance among matches
|
|
StrategyPriority ResolutionStrategy = "priority" // Highest priority first
|
|
)
|
|
|
|
// Resolver handles semantic address resolution
|
|
type Resolver struct {
|
|
// Peer capability registry
|
|
capabilities map[peer.ID]*PeerCapability
|
|
capMutex sync.RWMutex
|
|
|
|
// Address resolution cache
|
|
cache map[string]*ResolutionResult
|
|
cacheMutex sync.RWMutex
|
|
cacheTTL time.Duration
|
|
|
|
// Configuration
|
|
defaultStrategy ResolutionStrategy
|
|
maxPeersPerResult int
|
|
|
|
// Peerstore for address information
|
|
peerstore peerstore.Peerstore
|
|
}
|
|
|
|
// NewResolver creates a new semantic address resolver
|
|
func NewResolver(peerstore peerstore.Peerstore, opts ...ResolverOption) *Resolver {
|
|
r := &Resolver{
|
|
capabilities: make(map[peer.ID]*PeerCapability),
|
|
cache: make(map[string]*ResolutionResult),
|
|
cacheTTL: 5 * time.Minute,
|
|
defaultStrategy: StrategyBestMatch,
|
|
maxPeersPerResult: 5,
|
|
peerstore: peerstore,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(r)
|
|
}
|
|
|
|
// Start background cleanup
|
|
go r.startCleanup()
|
|
|
|
return r
|
|
}
|
|
|
|
// ResolverOption configures the resolver
|
|
type ResolverOption func(*Resolver)
|
|
|
|
// WithCacheTTL sets the cache TTL
|
|
func WithCacheTTL(ttl time.Duration) ResolverOption {
|
|
return func(r *Resolver) {
|
|
r.cacheTTL = ttl
|
|
}
|
|
}
|
|
|
|
// WithDefaultStrategy sets the default resolution strategy
|
|
func WithDefaultStrategy(strategy ResolutionStrategy) ResolverOption {
|
|
return func(r *Resolver) {
|
|
r.defaultStrategy = strategy
|
|
}
|
|
}
|
|
|
|
// WithMaxPeersPerResult sets the maximum peers per result
|
|
func WithMaxPeersPerResult(max int) ResolverOption {
|
|
return func(r *Resolver) {
|
|
r.maxPeersPerResult = max
|
|
}
|
|
}
|
|
|
|
// RegisterPeer registers a peer's capabilities
|
|
func (r *Resolver) RegisterPeer(peerID peer.ID, capability *PeerCapability) {
|
|
r.capMutex.Lock()
|
|
defer r.capMutex.Unlock()
|
|
|
|
capability.PeerID = peerID
|
|
capability.LastSeen = time.Now()
|
|
r.capabilities[peerID] = capability
|
|
|
|
// Clear relevant cache entries
|
|
r.invalidateCache()
|
|
}
|
|
|
|
// UnregisterPeer removes a peer from the registry
|
|
func (r *Resolver) UnregisterPeer(peerID peer.ID) {
|
|
r.capMutex.Lock()
|
|
defer r.capMutex.Unlock()
|
|
|
|
delete(r.capabilities, peerID)
|
|
|
|
// Clear relevant cache entries
|
|
r.invalidateCache()
|
|
}
|
|
|
|
// UpdatePeerStatus updates a peer's status
|
|
func (r *Resolver) UpdatePeerStatus(peerID peer.ID, status string) {
|
|
r.capMutex.Lock()
|
|
defer r.capMutex.Unlock()
|
|
|
|
if cap, exists := r.capabilities[peerID]; exists {
|
|
cap.Status = status
|
|
cap.LastSeen = time.Now()
|
|
}
|
|
}
|
|
|
|
// Resolve resolves a CHORUS:// URI to peer addresses
|
|
func (r *Resolver) Resolve(ctx context.Context, uri *BzzzURI, strategy ...ResolutionStrategy) (*ResolutionResult, error) {
|
|
if uri == nil {
|
|
return nil, fmt.Errorf("nil URI")
|
|
}
|
|
|
|
// Determine strategy
|
|
resolveStrategy := r.defaultStrategy
|
|
if len(strategy) > 0 {
|
|
resolveStrategy = strategy[0]
|
|
}
|
|
|
|
// Check cache first
|
|
cacheKey := r.getCacheKey(uri, resolveStrategy)
|
|
if result := r.getFromCache(cacheKey); result != nil {
|
|
return result, nil
|
|
}
|
|
|
|
// Perform resolution
|
|
result, err := r.resolveURI(ctx, uri, resolveStrategy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache result
|
|
r.cacheResult(cacheKey, result)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ResolveString resolves a CHORUS:// URI string to peer addresses
|
|
func (r *Resolver) ResolveString(ctx context.Context, uriStr string, strategy ...ResolutionStrategy) (*ResolutionResult, error) {
|
|
uri, err := ParseBzzzURI(uriStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse URI: %w", err)
|
|
}
|
|
|
|
return r.Resolve(ctx, uri, strategy...)
|
|
}
|
|
|
|
// resolveURI performs the actual URI resolution
|
|
func (r *Resolver) resolveURI(ctx context.Context, uri *BzzzURI, strategy ResolutionStrategy) (*ResolutionResult, error) {
|
|
r.capMutex.RLock()
|
|
defer r.capMutex.RUnlock()
|
|
|
|
var matchingPeers []*PeerCapability
|
|
|
|
// Find matching peers
|
|
for _, cap := range r.capabilities {
|
|
if r.peerMatches(cap, uri) {
|
|
matchingPeers = append(matchingPeers, cap)
|
|
}
|
|
}
|
|
|
|
if len(matchingPeers) == 0 {
|
|
return &ResolutionResult{
|
|
URI: uri,
|
|
Peers: []*PeerAddress{},
|
|
ResolvedAt: time.Now(),
|
|
ResolutionTTL: r.cacheTTL,
|
|
Strategy: string(strategy),
|
|
}, nil
|
|
}
|
|
|
|
// Apply resolution strategy
|
|
selectedPeers := r.applyStrategy(matchingPeers, strategy)
|
|
|
|
// Convert to peer addresses
|
|
var peerAddresses []*PeerAddress
|
|
for i, cap := range selectedPeers {
|
|
if i >= r.maxPeersPerResult {
|
|
break
|
|
}
|
|
|
|
addr := &PeerAddress{
|
|
PeerID: cap.PeerID,
|
|
Priority: r.calculatePriority(cap, uri),
|
|
Metadata: map[string]interface{}{
|
|
"agent": cap.Agent,
|
|
"role": cap.Role,
|
|
"specialization": cap.Specialization,
|
|
"status": cap.Status,
|
|
"last_seen": cap.LastSeen,
|
|
},
|
|
}
|
|
|
|
// Get addresses from peerstore
|
|
if r.peerstore != nil {
|
|
addrs := r.peerstore.Addrs(cap.PeerID)
|
|
for _, ma := range addrs {
|
|
addr.Addresses = append(addr.Addresses, ma.String())
|
|
}
|
|
}
|
|
|
|
peerAddresses = append(peerAddresses, addr)
|
|
}
|
|
|
|
return &ResolutionResult{
|
|
URI: uri,
|
|
Peers: peerAddresses,
|
|
ResolvedAt: time.Now(),
|
|
ResolutionTTL: r.cacheTTL,
|
|
Strategy: string(strategy),
|
|
}, nil
|
|
}
|
|
|
|
// peerMatches checks if a peer matches the URI criteria
|
|
func (r *Resolver) peerMatches(cap *PeerCapability, uri *BzzzURI) bool {
|
|
// Check if peer is online
|
|
if cap.Status == "offline" {
|
|
return false
|
|
}
|
|
|
|
// Check agent match
|
|
if !IsWildcard(uri.Agent) && !componentMatches(uri.Agent, cap.Agent) {
|
|
return false
|
|
}
|
|
|
|
// Check role match
|
|
if !IsWildcard(uri.Role) && !componentMatches(uri.Role, cap.Role) {
|
|
return false
|
|
}
|
|
|
|
// Check project match (if specified in metadata)
|
|
if !IsWildcard(uri.Project) {
|
|
if project, exists := cap.Metadata["project"]; exists {
|
|
if !componentMatches(uri.Project, project) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check task capabilities (if peer has relevant capabilities)
|
|
if !IsWildcard(uri.Task) {
|
|
taskMatches := false
|
|
for _, capability := range cap.Capabilities {
|
|
if componentMatches(uri.Task, capability) {
|
|
taskMatches = true
|
|
break
|
|
}
|
|
}
|
|
if !taskMatches {
|
|
// Also check specialization
|
|
if !componentMatches(uri.Task, cap.Specialization) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// applyStrategy applies the resolution strategy to matching peers
|
|
func (r *Resolver) applyStrategy(peers []*PeerCapability, strategy ResolutionStrategy) []*PeerCapability {
|
|
switch strategy {
|
|
case StrategyExact:
|
|
// Return only exact matches (already filtered)
|
|
return peers
|
|
|
|
case StrategyPriority:
|
|
// Sort by priority (calculated based on specificity and status)
|
|
return r.sortByPriority(peers)
|
|
|
|
case StrategyLoadBalance:
|
|
// Sort by load (prefer less busy peers)
|
|
return r.sortByLoad(peers)
|
|
|
|
case StrategyBestMatch:
|
|
fallthrough
|
|
default:
|
|
// Sort by best match score
|
|
return r.sortByMatch(peers)
|
|
}
|
|
}
|
|
|
|
// sortByPriority sorts peers by priority score
|
|
func (r *Resolver) sortByPriority(peers []*PeerCapability) []*PeerCapability {
|
|
// Simple priority: online > working > busy, then by last seen
|
|
result := make([]*PeerCapability, len(peers))
|
|
copy(result, peers)
|
|
|
|
// Sort by status priority and recency
|
|
for i := 0; i < len(result)-1; i++ {
|
|
for j := i + 1; j < len(result); j++ {
|
|
iPriority := r.getStatusPriority(result[i].Status)
|
|
jPriority := r.getStatusPriority(result[j].Status)
|
|
|
|
if iPriority < jPriority ||
|
|
(iPriority == jPriority && result[i].LastSeen.Before(result[j].LastSeen)) {
|
|
result[i], result[j] = result[j], result[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// sortByLoad sorts peers by current load (prefer less busy)
|
|
func (r *Resolver) sortByLoad(peers []*PeerCapability) []*PeerCapability {
|
|
result := make([]*PeerCapability, len(peers))
|
|
copy(result, peers)
|
|
|
|
// Sort by status (ready > working > busy)
|
|
for i := 0; i < len(result)-1; i++ {
|
|
for j := i + 1; j < len(result); j++ {
|
|
iLoad := r.getLoadScore(result[i].Status)
|
|
jLoad := r.getLoadScore(result[j].Status)
|
|
|
|
if iLoad > jLoad {
|
|
result[i], result[j] = result[j], result[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// sortByMatch sorts peers by match quality
|
|
func (r *Resolver) sortByMatch(peers []*PeerCapability) []*PeerCapability {
|
|
result := make([]*PeerCapability, len(peers))
|
|
copy(result, peers)
|
|
|
|
// Simple sorting - prefer online status and recent activity
|
|
for i := 0; i < len(result)-1; i++ {
|
|
for j := i + 1; j < len(result); j++ {
|
|
if r.getMatchScore(result[i]) < r.getMatchScore(result[j]) {
|
|
result[i], result[j] = result[j], result[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Helper functions for scoring
|
|
func (r *Resolver) getStatusPriority(status string) int {
|
|
switch status {
|
|
case "ready":
|
|
return 3
|
|
case "working":
|
|
return 2
|
|
case "busy":
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func (r *Resolver) getLoadScore(status string) int {
|
|
switch status {
|
|
case "ready":
|
|
return 0 // Lowest load
|
|
case "working":
|
|
return 1
|
|
case "busy":
|
|
return 2 // Highest load
|
|
default:
|
|
return 3
|
|
}
|
|
}
|
|
|
|
func (r *Resolver) getMatchScore(cap *PeerCapability) int {
|
|
score := 0
|
|
|
|
// Status contribution
|
|
score += r.getStatusPriority(cap.Status) * 10
|
|
|
|
// Recency contribution (more recent = higher score)
|
|
timeSince := time.Since(cap.LastSeen)
|
|
if timeSince < time.Minute {
|
|
score += 5
|
|
} else if timeSince < time.Hour {
|
|
score += 3
|
|
} else if timeSince < 24*time.Hour {
|
|
score += 1
|
|
}
|
|
|
|
// Capability count contribution
|
|
score += len(cap.Capabilities)
|
|
|
|
return score
|
|
}
|
|
|
|
// calculatePriority calculates priority for a peer address
|
|
func (r *Resolver) calculatePriority(cap *PeerCapability, uri *BzzzURI) int {
|
|
priority := 0
|
|
|
|
// Exact matches get higher priority
|
|
if cap.Agent == uri.Agent {
|
|
priority += 4
|
|
}
|
|
if cap.Role == uri.Role {
|
|
priority += 3
|
|
}
|
|
if cap.Specialization == uri.Task {
|
|
priority += 2
|
|
}
|
|
|
|
// Status-based priority
|
|
priority += r.getStatusPriority(cap.Status)
|
|
|
|
return priority
|
|
}
|
|
|
|
// Cache management
|
|
func (r *Resolver) getCacheKey(uri *BzzzURI, strategy ResolutionStrategy) string {
|
|
return fmt.Sprintf("%s:%s", uri.String(), strategy)
|
|
}
|
|
|
|
func (r *Resolver) getFromCache(key string) *ResolutionResult {
|
|
r.cacheMutex.RLock()
|
|
defer r.cacheMutex.RUnlock()
|
|
|
|
if result, exists := r.cache[key]; exists {
|
|
// Check if result is still valid
|
|
if time.Since(result.ResolvedAt) < result.ResolutionTTL {
|
|
return result
|
|
}
|
|
|
|
// Remove expired entry
|
|
delete(r.cache, key)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Resolver) cacheResult(key string, result *ResolutionResult) {
|
|
r.cacheMutex.Lock()
|
|
defer r.cacheMutex.Unlock()
|
|
|
|
r.cache[key] = result
|
|
}
|
|
|
|
func (r *Resolver) invalidateCache() {
|
|
r.cacheMutex.Lock()
|
|
defer r.cacheMutex.Unlock()
|
|
|
|
// Clear entire cache on capability changes
|
|
r.cache = make(map[string]*ResolutionResult)
|
|
}
|
|
|
|
// startCleanup starts background cache cleanup
|
|
func (r *Resolver) startCleanup() {
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
r.cleanupCache()
|
|
}
|
|
}
|
|
|
|
func (r *Resolver) cleanupCache() {
|
|
r.cacheMutex.Lock()
|
|
defer r.cacheMutex.Unlock()
|
|
|
|
now := time.Now()
|
|
for key, result := range r.cache {
|
|
if now.Sub(result.ResolvedAt) > result.ResolutionTTL {
|
|
delete(r.cache, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetPeerCapabilities returns all registered peer capabilities
|
|
func (r *Resolver) GetPeerCapabilities() map[peer.ID]*PeerCapability {
|
|
r.capMutex.RLock()
|
|
defer r.capMutex.RUnlock()
|
|
|
|
result := make(map[peer.ID]*PeerCapability)
|
|
for id, cap := range r.capabilities {
|
|
result[id] = cap
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetPeerCapability returns a specific peer's capabilities
|
|
func (r *Resolver) GetPeerCapability(peerID peer.ID) (*PeerCapability, bool) {
|
|
r.capMutex.RLock()
|
|
defer r.capMutex.RUnlock()
|
|
|
|
cap, exists := r.capabilities[peerID]
|
|
return cap, exists
|
|
}
|
|
|
|
// Close shuts down the resolver
|
|
func (r *Resolver) Close() error {
|
|
// Clear all data
|
|
r.capMutex.Lock()
|
|
r.capabilities = make(map[peer.ID]*PeerCapability)
|
|
r.capMutex.Unlock()
|
|
|
|
r.cacheMutex.Lock()
|
|
r.cache = make(map[string]*ResolutionResult)
|
|
r.cacheMutex.Unlock()
|
|
|
|
return nil
|
|
} |