Implement UCXL Protocol Foundation (Phase 1)
- 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>
This commit is contained in:
551
pkg/protocol/resolver.go
Normal file
551
pkg/protocol/resolver.go
Normal file
@@ -0,0 +1,551 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user