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:
326
pkg/protocol/uri.go
Normal file
326
pkg/protocol/uri.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BzzzURI represents a parsed bzzz:// URI with semantic addressing
|
||||
// Grammar: bzzz://[agent]:[role]@[project]:[task]/[path][?query][#fragment]
|
||||
type BzzzURI struct {
|
||||
// Core addressing components
|
||||
Agent string // Agent identifier (e.g., "claude", "any", "*")
|
||||
Role string // Agent role (e.g., "frontend", "backend", "architect")
|
||||
Project string // Project context (e.g., "chorus", "bzzz")
|
||||
Task string // Task identifier (e.g., "implement", "review", "test", "*")
|
||||
|
||||
// Resource path
|
||||
Path string // Resource path (e.g., "/src/main.go", "/docs/api.md")
|
||||
|
||||
// Standard URI components
|
||||
Query string // Query parameters
|
||||
Fragment string // Fragment identifier
|
||||
|
||||
// Original raw URI string
|
||||
Raw string
|
||||
}
|
||||
|
||||
// URI grammar constants
|
||||
const (
|
||||
BzzzScheme = "bzzz"
|
||||
|
||||
// Special identifiers
|
||||
AnyAgent = "any"
|
||||
AnyRole = "any"
|
||||
AnyProject = "any"
|
||||
AnyTask = "any"
|
||||
Wildcard = "*"
|
||||
)
|
||||
|
||||
// Validation patterns
|
||||
var (
|
||||
// Component validation patterns
|
||||
agentPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
||||
rolePattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
||||
projectPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
||||
taskPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
||||
pathPattern = regexp.MustCompile(`^/[a-zA-Z0-9\-_/\.]*$|^$`)
|
||||
|
||||
// Full URI pattern for validation
|
||||
bzzzURIPattern = regexp.MustCompile(`^bzzz://([a-zA-Z0-9\-_*]|any):([a-zA-Z0-9\-_*]|any)@([a-zA-Z0-9\-_*]|any):([a-zA-Z0-9\-_*]|any)(/[a-zA-Z0-9\-_/\.]*)?(\?[^#]*)?(\#.*)?$`)
|
||||
)
|
||||
|
||||
// ParseBzzzURI parses a bzzz:// URI string into a BzzzURI struct
|
||||
func ParseBzzzURI(uri string) (*BzzzURI, error) {
|
||||
if uri == "" {
|
||||
return nil, fmt.Errorf("empty URI")
|
||||
}
|
||||
|
||||
// Basic scheme validation
|
||||
if !strings.HasPrefix(uri, BzzzScheme+"://") {
|
||||
return nil, fmt.Errorf("invalid scheme: expected '%s'", BzzzScheme)
|
||||
}
|
||||
|
||||
// Use Go's standard URL parser for basic parsing
|
||||
parsedURL, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URI: %w", err)
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != BzzzScheme {
|
||||
return nil, fmt.Errorf("invalid scheme: expected '%s', got '%s'", BzzzScheme, parsedURL.Scheme)
|
||||
}
|
||||
|
||||
// Parse the authority part (user:pass@host:port becomes agent:role@project:task)
|
||||
userInfo := parsedURL.User
|
||||
if userInfo == nil {
|
||||
return nil, fmt.Errorf("missing agent:role information")
|
||||
}
|
||||
|
||||
username := userInfo.Username()
|
||||
password, hasPassword := userInfo.Password()
|
||||
if !hasPassword {
|
||||
return nil, fmt.Errorf("missing role information")
|
||||
}
|
||||
|
||||
agent := username
|
||||
role := password
|
||||
|
||||
// Parse host:port as project:task
|
||||
hostPort := parsedURL.Host
|
||||
if hostPort == "" {
|
||||
return nil, fmt.Errorf("missing project:task information")
|
||||
}
|
||||
|
||||
// Split host:port to get project:task
|
||||
parts := strings.Split(hostPort, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid project:task format: expected 'project:task'")
|
||||
}
|
||||
|
||||
project := parts[0]
|
||||
task := parts[1]
|
||||
|
||||
// Create BzzzURI instance
|
||||
bzzzURI := &BzzzURI{
|
||||
Agent: agent,
|
||||
Role: role,
|
||||
Project: project,
|
||||
Task: task,
|
||||
Path: parsedURL.Path,
|
||||
Query: parsedURL.RawQuery,
|
||||
Fragment: parsedURL.Fragment,
|
||||
Raw: uri,
|
||||
}
|
||||
|
||||
// Validate components
|
||||
if err := bzzzURI.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
return bzzzURI, nil
|
||||
}
|
||||
|
||||
// Validate validates all components of the BzzzURI
|
||||
func (u *BzzzURI) Validate() error {
|
||||
// Validate agent
|
||||
if u.Agent == "" {
|
||||
return fmt.Errorf("agent cannot be empty")
|
||||
}
|
||||
if !agentPattern.MatchString(u.Agent) {
|
||||
return fmt.Errorf("invalid agent format: '%s'", u.Agent)
|
||||
}
|
||||
|
||||
// Validate role
|
||||
if u.Role == "" {
|
||||
return fmt.Errorf("role cannot be empty")
|
||||
}
|
||||
if !rolePattern.MatchString(u.Role) {
|
||||
return fmt.Errorf("invalid role format: '%s'", u.Role)
|
||||
}
|
||||
|
||||
// Validate project
|
||||
if u.Project == "" {
|
||||
return fmt.Errorf("project cannot be empty")
|
||||
}
|
||||
if !projectPattern.MatchString(u.Project) {
|
||||
return fmt.Errorf("invalid project format: '%s'", u.Project)
|
||||
}
|
||||
|
||||
// Validate task
|
||||
if u.Task == "" {
|
||||
return fmt.Errorf("task cannot be empty")
|
||||
}
|
||||
if !taskPattern.MatchString(u.Task) {
|
||||
return fmt.Errorf("invalid task format: '%s'", u.Task)
|
||||
}
|
||||
|
||||
// Validate path (optional)
|
||||
if u.Path != "" && !pathPattern.MatchString(u.Path) {
|
||||
return fmt.Errorf("invalid path format: '%s'", u.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the canonical string representation of the BzzzURI
|
||||
func (u *BzzzURI) String() string {
|
||||
uri := fmt.Sprintf("%s://%s:%s@%s:%s", BzzzScheme, u.Agent, u.Role, u.Project, u.Task)
|
||||
|
||||
if u.Path != "" {
|
||||
uri += u.Path
|
||||
}
|
||||
|
||||
if u.Query != "" {
|
||||
uri += "?" + u.Query
|
||||
}
|
||||
|
||||
if u.Fragment != "" {
|
||||
uri += "#" + u.Fragment
|
||||
}
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
// Normalize normalizes the URI components for consistent addressing
|
||||
func (u *BzzzURI) Normalize() {
|
||||
// Convert empty wildcards to standard wildcard
|
||||
if u.Agent == "" {
|
||||
u.Agent = Wildcard
|
||||
}
|
||||
if u.Role == "" {
|
||||
u.Role = Wildcard
|
||||
}
|
||||
if u.Project == "" {
|
||||
u.Project = Wildcard
|
||||
}
|
||||
if u.Task == "" {
|
||||
u.Task = Wildcard
|
||||
}
|
||||
|
||||
// Normalize to lowercase for consistency
|
||||
u.Agent = strings.ToLower(u.Agent)
|
||||
u.Role = strings.ToLower(u.Role)
|
||||
u.Project = strings.ToLower(u.Project)
|
||||
u.Task = strings.ToLower(u.Task)
|
||||
|
||||
// Clean path
|
||||
if u.Path != "" && !strings.HasPrefix(u.Path, "/") {
|
||||
u.Path = "/" + u.Path
|
||||
}
|
||||
}
|
||||
|
||||
// IsWildcard checks if a component is a wildcard or "any"
|
||||
func IsWildcard(component string) bool {
|
||||
return component == Wildcard || component == AnyAgent || component == AnyRole ||
|
||||
component == AnyProject || component == AnyTask
|
||||
}
|
||||
|
||||
// Matches checks if this URI matches another URI (with wildcard support)
|
||||
func (u *BzzzURI) Matches(other *BzzzURI) bool {
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check each component with wildcard support
|
||||
if !componentMatches(u.Agent, other.Agent) {
|
||||
return false
|
||||
}
|
||||
if !componentMatches(u.Role, other.Role) {
|
||||
return false
|
||||
}
|
||||
if !componentMatches(u.Project, other.Project) {
|
||||
return false
|
||||
}
|
||||
if !componentMatches(u.Task, other.Task) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Path matching (exact or wildcard)
|
||||
if u.Path != "" && other.Path != "" && u.Path != other.Path {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// componentMatches checks if two components match (with wildcard support)
|
||||
func componentMatches(a, b string) bool {
|
||||
// Exact match
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
// Wildcard matching
|
||||
if IsWildcard(a) || IsWildcard(b) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSelectorPriority returns a priority score for URI matching (higher = more specific)
|
||||
func (u *BzzzURI) GetSelectorPriority() int {
|
||||
priority := 0
|
||||
|
||||
// More specific components get higher priority
|
||||
if !IsWildcard(u.Agent) {
|
||||
priority += 8
|
||||
}
|
||||
if !IsWildcard(u.Role) {
|
||||
priority += 4
|
||||
}
|
||||
if !IsWildcard(u.Project) {
|
||||
priority += 2
|
||||
}
|
||||
if !IsWildcard(u.Task) {
|
||||
priority += 1
|
||||
}
|
||||
|
||||
// Path specificity adds priority
|
||||
if u.Path != "" && u.Path != "/" {
|
||||
priority += 1
|
||||
}
|
||||
|
||||
return priority
|
||||
}
|
||||
|
||||
// ToAddress returns a simplified address representation for P2P routing
|
||||
func (u *BzzzURI) ToAddress() string {
|
||||
return fmt.Sprintf("%s:%s@%s:%s", u.Agent, u.Role, u.Project, u.Task)
|
||||
}
|
||||
|
||||
// ValidateBzzzURIString validates a bzzz:// URI string without parsing
|
||||
func ValidateBzzzURIString(uri string) error {
|
||||
if uri == "" {
|
||||
return fmt.Errorf("empty URI")
|
||||
}
|
||||
|
||||
if !bzzzURIPattern.MatchString(uri) {
|
||||
return fmt.Errorf("invalid bzzz:// URI format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBzzzURI creates a new BzzzURI with the given components
|
||||
func NewBzzzURI(agent, role, project, task, path string) *BzzzURI {
|
||||
uri := &BzzzURI{
|
||||
Agent: agent,
|
||||
Role: role,
|
||||
Project: project,
|
||||
Task: task,
|
||||
Path: path,
|
||||
}
|
||||
uri.Normalize()
|
||||
return uri
|
||||
}
|
||||
|
||||
// ParseAddress parses a simplified address format (agent:role@project:task)
|
||||
func ParseAddress(addr string) (*BzzzURI, error) {
|
||||
// Convert simplified address to full URI
|
||||
fullURI := BzzzScheme + "://" + addr
|
||||
return ParseBzzzURI(fullURI)
|
||||
}
|
||||
Reference in New Issue
Block a user