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:
578
pkg/ucxi/server.go
Normal file
578
pkg/ucxi/server.go
Normal file
@@ -0,0 +1,578 @@
|
||||
package ucxi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anthonyrawlins/bzzz/pkg/ucxl"
|
||||
)
|
||||
|
||||
// Server represents a UCXI HTTP server for UCXL operations
|
||||
type Server struct {
|
||||
// HTTP server configuration
|
||||
server *http.Server
|
||||
port int
|
||||
basePath string
|
||||
|
||||
// Address resolution
|
||||
resolver AddressResolver
|
||||
|
||||
// Content storage
|
||||
storage ContentStorage
|
||||
|
||||
// Temporal navigation
|
||||
navigators map[string]*ucxl.TemporalNavigator
|
||||
navMutex sync.RWMutex
|
||||
|
||||
// Server state
|
||||
running bool
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// Middleware and logging
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// AddressResolver interface for resolving UCXL addresses to actual content
|
||||
type AddressResolver interface {
|
||||
Resolve(ctx context.Context, addr *ucxl.Address) (*ResolvedContent, error)
|
||||
Announce(ctx context.Context, addr *ucxl.Address, content *Content) error
|
||||
Discover(ctx context.Context, pattern *ucxl.Address) ([]*ResolvedContent, error)
|
||||
}
|
||||
|
||||
// ContentStorage interface for storing and retrieving content
|
||||
type ContentStorage interface {
|
||||
Store(ctx context.Context, key string, content *Content) error
|
||||
Retrieve(ctx context.Context, key string) (*Content, error)
|
||||
Delete(ctx context.Context, key string) error
|
||||
List(ctx context.Context, prefix string) ([]string, error)
|
||||
}
|
||||
|
||||
// Logger interface for server logging
|
||||
type Logger interface {
|
||||
Info(msg string, fields ...interface{})
|
||||
Warn(msg string, fields ...interface{})
|
||||
Error(msg string, fields ...interface{})
|
||||
Debug(msg string, fields ...interface{})
|
||||
}
|
||||
|
||||
// Content represents content stored at a UCXL address
|
||||
type Content struct {
|
||||
Data []byte `json:"data"`
|
||||
ContentType string `json:"content_type"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Version int `json:"version"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// ResolvedContent represents content resolved from a UCXL address
|
||||
type ResolvedContent struct {
|
||||
Address *ucxl.Address `json:"address"`
|
||||
Content *Content `json:"content"`
|
||||
Source string `json:"source"` // Source node/peer ID
|
||||
Resolved time.Time `json:"resolved"` // Resolution timestamp
|
||||
TTL time.Duration `json:"ttl"` // Time to live for caching
|
||||
}
|
||||
|
||||
// Response represents a standardized UCXI response
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// ServerConfig holds server configuration
|
||||
type ServerConfig struct {
|
||||
Port int `json:"port"`
|
||||
BasePath string `json:"base_path"`
|
||||
Resolver AddressResolver `json:"-"`
|
||||
Storage ContentStorage `json:"-"`
|
||||
Logger Logger `json:"-"`
|
||||
}
|
||||
|
||||
// NewServer creates a new UCXI server
|
||||
func NewServer(config ServerConfig) *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Server{
|
||||
port: config.Port,
|
||||
basePath: strings.TrimSuffix(config.BasePath, "/"),
|
||||
resolver: config.Resolver,
|
||||
storage: config.Storage,
|
||||
logger: config.Logger,
|
||||
navigators: make(map[string]*ucxl.TemporalNavigator),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the UCXI HTTP server
|
||||
func (s *Server) Start() error {
|
||||
if s.running {
|
||||
return fmt.Errorf("server is already running")
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Register routes
|
||||
s.registerRoutes(mux)
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", s.port),
|
||||
Handler: s.withMiddleware(mux),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
s.running = true
|
||||
s.logger.Info("Starting UCXI server", "port", s.port, "base_path", s.basePath)
|
||||
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop stops the UCXI HTTP server
|
||||
func (s *Server) Stop() error {
|
||||
if !s.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("Stopping UCXI server")
|
||||
s.cancel()
|
||||
s.running = false
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// registerRoutes registers all UCXI HTTP routes
|
||||
func (s *Server) registerRoutes(mux *http.ServeMux) {
|
||||
prefix := s.basePath + "/ucxi/v1"
|
||||
|
||||
// Content operations
|
||||
mux.HandleFunc(prefix+"/get", s.handleGet)
|
||||
mux.HandleFunc(prefix+"/put", s.handlePut)
|
||||
mux.HandleFunc(prefix+"/post", s.handlePost)
|
||||
mux.HandleFunc(prefix+"/delete", s.handleDelete)
|
||||
|
||||
// Discovery and announcement
|
||||
mux.HandleFunc(prefix+"/announce", s.handleAnnounce)
|
||||
mux.HandleFunc(prefix+"/discover", s.handleDiscover)
|
||||
|
||||
// Temporal navigation
|
||||
mux.HandleFunc(prefix+"/navigate", s.handleNavigate)
|
||||
|
||||
// Server status and health
|
||||
mux.HandleFunc(prefix+"/health", s.handleHealth)
|
||||
mux.HandleFunc(prefix+"/status", s.handleStatus)
|
||||
}
|
||||
|
||||
// handleGet handles GET requests for retrieving content
|
||||
func (s *Server) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
addressStr := r.URL.Query().Get("address")
|
||||
if addressStr == "" {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Missing address parameter", "")
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := ucxl.Parse(addressStr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the address
|
||||
resolved, err := s.resolver.Resolve(r.Context(), addr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusNotFound, "Failed to resolve address", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, resolved)
|
||||
}
|
||||
|
||||
// handlePut handles PUT requests for storing content
|
||||
func (s *Server) handlePut(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
addressStr := r.URL.Query().Get("address")
|
||||
if addressStr == "" {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Missing address parameter", "")
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := ucxl.Parse(addressStr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read content from request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Failed to read request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
content := &Content{
|
||||
Data: body,
|
||||
ContentType: r.Header.Get("Content-Type"),
|
||||
Metadata: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Author: r.Header.Get("X-Author"),
|
||||
}
|
||||
|
||||
// Copy custom metadata from headers
|
||||
for key, values := range r.Header {
|
||||
if strings.HasPrefix(key, "X-Meta-") {
|
||||
metaKey := strings.TrimPrefix(key, "X-Meta-")
|
||||
if len(values) > 0 {
|
||||
content.Metadata[metaKey] = values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the content
|
||||
key := s.generateStorageKey(addr)
|
||||
if err := s.storage.Store(r.Context(), key, content); err != nil {
|
||||
s.writeErrorResponse(w, http.StatusInternalServerError, "Failed to store content", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Announce the content
|
||||
if err := s.resolver.Announce(r.Context(), addr, content); err != nil {
|
||||
s.logger.Warn("Failed to announce content", "error", err.Error(), "address", addr.String())
|
||||
// Don't fail the request if announcement fails
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"address": addr.String(),
|
||||
"key": key,
|
||||
"stored": true,
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, response)
|
||||
}
|
||||
|
||||
// handlePost handles POST requests for updating content
|
||||
func (s *Server) handlePost(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
// POST is similar to PUT but may have different semantics
|
||||
// For now, delegate to PUT handler
|
||||
s.handlePut(w, r)
|
||||
}
|
||||
|
||||
// handleDelete handles DELETE requests for removing content
|
||||
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
addressStr := r.URL.Query().Get("address")
|
||||
if addressStr == "" {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Missing address parameter", "")
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := ucxl.Parse(addressStr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
key := s.generateStorageKey(addr)
|
||||
if err := s.storage.Delete(r.Context(), key); err != nil {
|
||||
s.writeErrorResponse(w, http.StatusInternalServerError, "Failed to delete content", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"address": addr.String(),
|
||||
"key": key,
|
||||
"deleted": true,
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, response)
|
||||
}
|
||||
|
||||
// handleAnnounce handles content announcement requests
|
||||
func (s *Server) handleAnnounce(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Address string `json:"address"`
|
||||
Content Content `json:"content"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid JSON request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := ucxl.Parse(request.Address)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.resolver.Announce(r.Context(), addr, &request.Content); err != nil {
|
||||
s.writeErrorResponse(w, http.StatusInternalServerError, "Failed to announce content", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"address": addr.String(),
|
||||
"announced": true,
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, response)
|
||||
}
|
||||
|
||||
// handleDiscover handles content discovery requests
|
||||
func (s *Server) handleDiscover(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
pattern := r.URL.Query().Get("pattern")
|
||||
if pattern == "" {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Missing pattern parameter", "")
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := ucxl.Parse(pattern)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL pattern", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
results, err := s.resolver.Discover(r.Context(), addr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusInternalServerError, "Discovery failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, results)
|
||||
}
|
||||
|
||||
// handleNavigate handles temporal navigation requests
|
||||
func (s *Server) handleNavigate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
var request struct {
|
||||
Address string `json:"address"`
|
||||
TemporalSegment string `json:"temporal_segment"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid JSON request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := ucxl.Parse(request.Address)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get or create navigator for this address context
|
||||
navKey := s.generateNavigatorKey(addr)
|
||||
navigator := s.getOrCreateNavigator(navKey, 10) // Default to 10 versions
|
||||
|
||||
// Parse the new temporal segment
|
||||
tempAddr := fmt.Sprintf("ucxl://temp:temp@temp:temp/%s", request.TemporalSegment)
|
||||
tempParsed, err := ucxl.Parse(tempAddr)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Invalid temporal segment", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Perform navigation
|
||||
result, err := navigator.Navigate(tempParsed.TemporalSegment)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, http.StatusBadRequest, "Navigation failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, result)
|
||||
}
|
||||
|
||||
// handleHealth handles health check requests
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
health := map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"running": s.running,
|
||||
"uptime": time.Now().UTC(),
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, health)
|
||||
}
|
||||
|
||||
// handleStatus handles server status requests
|
||||
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
|
||||
return
|
||||
}
|
||||
|
||||
s.navMutex.RLock()
|
||||
navigatorCount := len(s.navigators)
|
||||
s.navMutex.RUnlock()
|
||||
|
||||
status := map[string]interface{}{
|
||||
"server": map[string]interface{}{
|
||||
"port": s.port,
|
||||
"base_path": s.basePath,
|
||||
"running": s.running,
|
||||
},
|
||||
"navigators": map[string]interface{}{
|
||||
"active_count": navigatorCount,
|
||||
},
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
s.writeSuccessResponse(w, status)
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
// generateStorageKey generates a storage key from a UCXL address
|
||||
func (s *Server) generateStorageKey(addr *ucxl.Address) string {
|
||||
return fmt.Sprintf("%s:%s@%s:%s/%s",
|
||||
addr.Agent, addr.Role, addr.Project, addr.Task, addr.TemporalSegment.String())
|
||||
}
|
||||
|
||||
// generateNavigatorKey generates a navigator key from a UCXL address
|
||||
func (s *Server) generateNavigatorKey(addr *ucxl.Address) string {
|
||||
return fmt.Sprintf("%s:%s@%s:%s", addr.Agent, addr.Role, addr.Project, addr.Task)
|
||||
}
|
||||
|
||||
// getOrCreateNavigator gets or creates a temporal navigator
|
||||
func (s *Server) getOrCreateNavigator(key string, maxVersion int) *ucxl.TemporalNavigator {
|
||||
s.navMutex.Lock()
|
||||
defer s.navMutex.Unlock()
|
||||
|
||||
if navigator, exists := s.navigators[key]; exists {
|
||||
return navigator
|
||||
}
|
||||
|
||||
navigator := ucxl.NewTemporalNavigator(maxVersion)
|
||||
s.navigators[key] = navigator
|
||||
return navigator
|
||||
}
|
||||
|
||||
// withMiddleware wraps the handler with common middleware
|
||||
func (s *Server) withMiddleware(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Add CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Author, X-Meta-*")
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Set content type to JSON by default
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Log request
|
||||
start := time.Now()
|
||||
s.logger.Debug("Request", "method", r.Method, "url", r.URL.String(), "remote", r.RemoteAddr)
|
||||
|
||||
// Call the handler
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
// Log response
|
||||
duration := time.Since(start)
|
||||
s.logger.Debug("Response", "duration", duration.String())
|
||||
})
|
||||
}
|
||||
|
||||
// writeSuccessResponse writes a successful JSON response
|
||||
func (s *Server) writeSuccessResponse(w http.ResponseWriter, data interface{}) {
|
||||
response := Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Timestamp: time.Now().UTC(),
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// writeErrorResponse writes an error JSON response
|
||||
func (s *Server) writeErrorResponse(w http.ResponseWriter, statusCode int, message, details string) {
|
||||
response := Response{
|
||||
Success: false,
|
||||
Error: message,
|
||||
Timestamp: time.Now().UTC(),
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
if details != "" {
|
||||
response.Data = map[string]string{"details": details}
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Simple logger implementation
|
||||
type SimpleLogger struct{}
|
||||
|
||||
func (l SimpleLogger) Info(msg string, fields ...interface{}) { log.Printf("INFO: %s %v", msg, fields) }
|
||||
func (l SimpleLogger) Warn(msg string, fields ...interface{}) { log.Printf("WARN: %s %v", msg, fields) }
|
||||
func (l SimpleLogger) Error(msg string, fields ...interface{}) { log.Printf("ERROR: %s %v", msg, fields) }
|
||||
func (l SimpleLogger) Debug(msg string, fields ...interface{}) { log.Printf("DEBUG: %s %v", msg, fields) }
|
||||
Reference in New Issue
Block a user