 b207f32d9e
			
		
	
	b207f32d9e
	
	
	
		
			
			- Add complete UCXL address parser with BNF grammar validation - Implement temporal navigation system with bounds checking - Create UCXI HTTP server with REST-like operations - Add comprehensive test suite with 87 passing tests - Integrate with existing BZZZ architecture (opt-in via config) - Support semantic addressing with wildcards and version control Core Features: - UCXL address format: ucxl://agent:role@project:task/temporal/path - Temporal segments: *^, ~~N, ^^N, *~, *~N with navigation logic - UCXI endpoints: GET/PUT/POST/DELETE/ANNOUNCE operations - Production-ready with error handling and graceful shutdown 🤖 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 bzzz:// 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 bzzz:// 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
 | |
| } |