CRITICAL REVENUE PROTECTION: Fix $0 recurring revenue by enforcing BZZZ licensing This commit implements Phase 2A license enforcement, transforming BZZZ from having zero license validation to comprehensive revenue protection integrated with KACHING license authority. KEY BUSINESS IMPACT: • PREVENTS unlimited free usage - BZZZ now requires valid licensing to operate • ENABLES real-time license control - licenses can be suspended immediately via KACHING • PROTECTS against license sharing - unique cluster IDs bind licenses to specific deployments • ESTABLISHES recurring revenue foundation - licensing is now technically enforced CRITICAL FIXES: 1. Setup Manager Revenue Protection (api/setup_manager.go): - FIXED: License data was being completely discarded during setup (line 2085) - NOW: License data is extracted, validated, and saved to configuration - IMPACT: Closes $0 recurring revenue loophole - licenses are now required for deployment 2. Configuration System Integration (pkg/config/config.go): - ADDED: Complete LicenseConfig struct with KACHING integration fields - ADDED: License validation in config validation pipeline - IMPACT: Makes licensing a core requirement, not optional 3. Runtime License Enforcement (main.go): - ADDED: License validation before P2P node initialization (line 175) - ADDED: Fail-closed design - BZZZ exits if license validation fails - ADDED: Grace period support for offline operations - IMPACT: Prevents unlicensed BZZZ instances from starting 4. KACHING License Authority Integration: - REPLACED: Mock license validation (hardcoded BZZZ-2025-DEMO-EVAL-001) - ADDED: Real-time KACHING API integration for license activation - ADDED: Cluster ID generation for license binding - IMPACT: Enables centralized license management and immediate suspension 5. Frontend License Validation Enhancement: - UPDATED: License validation UI to indicate KACHING integration - MAINTAINED: Existing UX while adding revenue protection backend - IMPACT: Users now see real license validation, not mock responses TECHNICAL DETAILS: • Version bump: 1.0.8 → 1.1.0 (significant license enforcement features) • Fail-closed security design: System stops rather than degrading on license issues • Unique cluster ID generation prevents license sharing across deployments • Grace period support (24h default) for offline/network issue scenarios • Comprehensive error handling and user guidance for license issues TESTING REQUIREMENTS: • Test that BZZZ refuses to start without valid license configuration • Verify license data is properly saved during setup (no longer discarded) • Test KACHING integration for license activation and validation • Confirm cluster ID uniqueness and license binding DEPLOYMENT IMPACT: • Existing BZZZ deployments will require license configuration on next restart • Setup process now enforces license validation before deployment • Invalid/missing licenses will prevent BZZZ startup (revenue protection) This implementation establishes the foundation for recurring revenue by making valid licensing technically required for BZZZ operation. 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
869 lines
30 KiB
Go
869 lines
30 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"chorus.services/bzzz/pkg/security"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// LicenseConfig holds license verification and runtime enforcement configuration
|
|
// BUSINESS CRITICAL: This config enables revenue protection by enforcing valid licenses
|
|
type LicenseConfig struct {
|
|
// Core license identification - REQUIRED for all BZZZ operations
|
|
Email string `yaml:"email" json:"email"` // Licensed user email
|
|
LicenseKey string `yaml:"license_key" json:"license_key"` // Unique license key
|
|
|
|
// Organization binding (optional but recommended for enterprise)
|
|
OrganizationName string `yaml:"organization_name,omitempty" json:"organization_name,omitempty"`
|
|
|
|
// Cluster identity and binding - prevents license sharing across clusters
|
|
ClusterID string `yaml:"cluster_id" json:"cluster_id"` // Unique cluster identifier
|
|
ClusterName string `yaml:"cluster_name,omitempty" json:"cluster_name,omitempty"` // Human-readable cluster name
|
|
|
|
// KACHING license authority integration
|
|
KachingURL string `yaml:"kaching_url" json:"kaching_url"` // KACHING server URL
|
|
HeartbeatMinutes int `yaml:"heartbeat_minutes" json:"heartbeat_minutes"` // License heartbeat interval
|
|
GracePeriodHours int `yaml:"grace_period_hours" json:"grace_period_hours"` // Offline grace period
|
|
|
|
// Runtime state tracking
|
|
LastValidated time.Time `yaml:"last_validated,omitempty" json:"last_validated,omitempty"`
|
|
ValidationToken string `yaml:"validation_token,omitempty" json:"validation_token,omitempty"` // Current auth token
|
|
|
|
// License details (populated by KACHING validation)
|
|
LicenseType string `yaml:"license_type,omitempty" json:"license_type,omitempty"` // e.g., "standard", "enterprise"
|
|
MaxNodes int `yaml:"max_nodes,omitempty" json:"max_nodes,omitempty"` // Maximum allowed nodes
|
|
ExpiresAt time.Time `yaml:"expires_at,omitempty" json:"expires_at,omitempty"` // License expiration
|
|
IsActive bool `yaml:"is_active" json:"is_active"` // Current license status
|
|
}
|
|
|
|
// SecurityConfig holds cluster security and election configuration
|
|
type SecurityConfig struct {
|
|
// Admin key sharing
|
|
AdminKeyShares ShamirShare `yaml:"admin_key_shares" json:"admin_key_shares"`
|
|
ElectionConfig ElectionConfig `yaml:"election_config" json:"election_config"`
|
|
|
|
// Key management
|
|
KeyRotationDays int `yaml:"key_rotation_days,omitempty" json:"key_rotation_days,omitempty"`
|
|
AuditLogging bool `yaml:"audit_logging" json:"audit_logging"`
|
|
AuditPath string `yaml:"audit_path,omitempty" json:"audit_path,omitempty"`
|
|
}
|
|
|
|
// AIConfig holds AI/LLM integration settings
|
|
type AIConfig struct {
|
|
Ollama OllamaConfig `yaml:"ollama"`
|
|
OpenAI OpenAIConfig `yaml:"openai"`
|
|
}
|
|
|
|
// OllamaConfig holds Ollama API configuration
|
|
type OllamaConfig struct {
|
|
Endpoint string `yaml:"endpoint"`
|
|
Timeout time.Duration `yaml:"timeout"`
|
|
Models []string `yaml:"models"`
|
|
}
|
|
|
|
// OpenAIConfig holds OpenAI API configuration
|
|
type OpenAIConfig struct {
|
|
APIKey string `yaml:"api_key"`
|
|
Endpoint string `yaml:"endpoint"`
|
|
Timeout time.Duration `yaml:"timeout"`
|
|
}
|
|
|
|
// Config represents the complete configuration for a Bzzz agent
|
|
type Config struct {
|
|
WHOOSHAPI WHOOSHAPIConfig `yaml:"whoosh_api"`
|
|
Agent AgentConfig `yaml:"agent"`
|
|
GitHub GitHubConfig `yaml:"github"`
|
|
P2P P2PConfig `yaml:"p2p"`
|
|
Logging LoggingConfig `yaml:"logging"`
|
|
Slurp SlurpConfig `yaml:"slurp"`
|
|
V2 V2Config `yaml:"v2"` // BZZZ v2 protocol settings
|
|
UCXL UCXLConfig `yaml:"ucxl"` // UCXL protocol settings
|
|
Security SecurityConfig `yaml:"security"` // Cluster security and elections
|
|
AI AIConfig `yaml:"ai"` // AI/LLM integration settings
|
|
License LicenseConfig `yaml:"license"` // License verification and enforcement - REVENUE CRITICAL
|
|
}
|
|
|
|
// WHOOSHAPIConfig holds WHOOSH system integration settings
|
|
type WHOOSHAPIConfig struct {
|
|
BaseURL string `yaml:"base_url"`
|
|
APIKey string `yaml:"api_key"`
|
|
Timeout time.Duration `yaml:"timeout"`
|
|
RetryCount int `yaml:"retry_count"`
|
|
}
|
|
|
|
// CollaborationConfig holds role-based collaboration settings
|
|
type CollaborationConfig struct {
|
|
PreferredMessageTypes []string `yaml:"preferred_message_types"`
|
|
AutoSubscribeToRoles []string `yaml:"auto_subscribe_to_roles"`
|
|
AutoSubscribeToExpertise []string `yaml:"auto_subscribe_to_expertise"`
|
|
ResponseTimeoutSeconds int `yaml:"response_timeout_seconds"`
|
|
MaxCollaborationDepth int `yaml:"max_collaboration_depth"`
|
|
EscalationThreshold int `yaml:"escalation_threshold"`
|
|
CustomTopicSubscriptions []string `yaml:"custom_topic_subscriptions"`
|
|
}
|
|
|
|
// AgentConfig holds agent-specific configuration
|
|
type AgentConfig struct {
|
|
ID string `yaml:"id"`
|
|
Capabilities []string `yaml:"capabilities"`
|
|
PollInterval time.Duration `yaml:"poll_interval"`
|
|
MaxTasks int `yaml:"max_tasks"`
|
|
Models []string `yaml:"models"`
|
|
Specialization string `yaml:"specialization"`
|
|
ModelSelectionWebhook string `yaml:"model_selection_webhook"`
|
|
DefaultReasoningModel string `yaml:"default_reasoning_model"`
|
|
SandboxImage string `yaml:"sandbox_image"`
|
|
|
|
// Role-based configuration from Bees-AgenticWorkers
|
|
Role string `yaml:"role"`
|
|
SystemPrompt string `yaml:"system_prompt"`
|
|
ReportsTo []string `yaml:"reports_to"`
|
|
Expertise []string `yaml:"expertise"`
|
|
Deliverables []string `yaml:"deliverables"`
|
|
|
|
// Role-based collaboration settings
|
|
CollaborationSettings CollaborationConfig `yaml:"collaboration"`
|
|
}
|
|
|
|
// GitHubConfig holds GitHub integration settings
|
|
type GitHubConfig struct {
|
|
TokenFile string `yaml:"token_file"`
|
|
UserAgent string `yaml:"user_agent"`
|
|
Timeout time.Duration `yaml:"timeout"`
|
|
RateLimit bool `yaml:"rate_limit"`
|
|
Assignee string `yaml:"assignee"`
|
|
}
|
|
|
|
// P2PConfig holds P2P networking configuration
|
|
type P2PConfig struct {
|
|
ServiceTag string `yaml:"service_tag"`
|
|
BzzzTopic string `yaml:"bzzz_topic"`
|
|
HmmmTopic string `yaml:"hmmm_topic"`
|
|
DiscoveryTimeout time.Duration `yaml:"discovery_timeout"`
|
|
|
|
// Human escalation settings
|
|
EscalationWebhook string `yaml:"escalation_webhook"`
|
|
EscalationKeywords []string `yaml:"escalation_keywords"`
|
|
ConversationLimit int `yaml:"conversation_limit"`
|
|
}
|
|
|
|
// LoggingConfig holds logging configuration
|
|
type LoggingConfig struct {
|
|
Level string `yaml:"level"`
|
|
Format string `yaml:"format"`
|
|
Output string `yaml:"output"`
|
|
Structured bool `yaml:"structured"`
|
|
}
|
|
|
|
// V2Config holds BZZZ v2 protocol configuration
|
|
type V2Config struct {
|
|
// Enable v2 protocol features
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
|
|
// Protocol version
|
|
ProtocolVersion string `yaml:"protocol_version" json:"protocol_version"`
|
|
|
|
// URI resolution settings
|
|
URIResolution URIResolutionConfig `yaml:"uri_resolution" json:"uri_resolution"`
|
|
|
|
// DHT settings
|
|
DHT DHTConfig `yaml:"dht" json:"dht"`
|
|
|
|
// Semantic addressing
|
|
SemanticAddressing SemanticAddressingConfig `yaml:"semantic_addressing" json:"semantic_addressing"`
|
|
|
|
// Feature flags
|
|
FeatureFlags map[string]bool `yaml:"feature_flags" json:"feature_flags"`
|
|
}
|
|
|
|
// URIResolutionConfig holds URI resolution settings
|
|
type URIResolutionConfig struct {
|
|
CacheTTL time.Duration `yaml:"cache_ttl" json:"cache_ttl"`
|
|
MaxPeersPerResult int `yaml:"max_peers_per_result" json:"max_peers_per_result"`
|
|
DefaultStrategy string `yaml:"default_strategy" json:"default_strategy"`
|
|
ResolutionTimeout time.Duration `yaml:"resolution_timeout" json:"resolution_timeout"`
|
|
}
|
|
|
|
// DHTConfig holds DHT-specific configuration
|
|
type DHTConfig struct {
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
BootstrapPeers []string `yaml:"bootstrap_peers" json:"bootstrap_peers"`
|
|
Mode string `yaml:"mode" json:"mode"` // "client", "server", "auto"
|
|
ProtocolPrefix string `yaml:"protocol_prefix" json:"protocol_prefix"`
|
|
BootstrapTimeout time.Duration `yaml:"bootstrap_timeout" json:"bootstrap_timeout"`
|
|
DiscoveryInterval time.Duration `yaml:"discovery_interval" json:"discovery_interval"`
|
|
AutoBootstrap bool `yaml:"auto_bootstrap" json:"auto_bootstrap"`
|
|
}
|
|
|
|
// SemanticAddressingConfig holds semantic addressing settings
|
|
type SemanticAddressingConfig struct {
|
|
EnableWildcards bool `yaml:"enable_wildcards" json:"enable_wildcards"`
|
|
DefaultAgent string `yaml:"default_agent" json:"default_agent"`
|
|
DefaultRole string `yaml:"default_role" json:"default_role"`
|
|
DefaultProject string `yaml:"default_project" json:"default_project"`
|
|
EnableRoleHierarchy bool `yaml:"enable_role_hierarchy" json:"enable_role_hierarchy"`
|
|
}
|
|
|
|
// UCXLConfig holds UCXL protocol configuration
|
|
type UCXLConfig struct {
|
|
// Enable UCXL protocol
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
|
|
// UCXI server configuration
|
|
Server UCXIServerConfig `yaml:"server" json:"server"`
|
|
|
|
// Address resolution settings
|
|
Resolution UCXLResolutionConfig `yaml:"resolution" json:"resolution"`
|
|
|
|
// Storage settings
|
|
Storage UCXLStorageConfig `yaml:"storage" json:"storage"`
|
|
|
|
// P2P integration settings
|
|
P2PIntegration UCXLP2PConfig `yaml:"p2p_integration" json:"p2p_integration"`
|
|
}
|
|
|
|
// UCXIServerConfig holds UCXI server settings
|
|
type UCXIServerConfig struct {
|
|
Port int `yaml:"port" json:"port"`
|
|
BasePath string `yaml:"base_path" json:"base_path"`
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
}
|
|
|
|
// UCXLResolutionConfig holds address resolution settings
|
|
type UCXLResolutionConfig struct {
|
|
CacheTTL time.Duration `yaml:"cache_ttl" json:"cache_ttl"`
|
|
EnableWildcards bool `yaml:"enable_wildcards" json:"enable_wildcards"`
|
|
MaxResults int `yaml:"max_results" json:"max_results"`
|
|
}
|
|
|
|
// UCXLStorageConfig holds storage settings
|
|
type UCXLStorageConfig struct {
|
|
Type string `yaml:"type" json:"type"` // "filesystem", "memory"
|
|
Directory string `yaml:"directory" json:"directory"`
|
|
MaxSize int64 `yaml:"max_size" json:"max_size"` // in bytes
|
|
}
|
|
|
|
// UCXLP2PConfig holds P2P integration settings
|
|
type UCXLP2PConfig struct {
|
|
EnableAnnouncement bool `yaml:"enable_announcement" json:"enable_announcement"`
|
|
EnableDiscovery bool `yaml:"enable_discovery" json:"enable_discovery"`
|
|
AnnouncementTopic string `yaml:"announcement_topic" json:"announcement_topic"`
|
|
DiscoveryTimeout time.Duration `yaml:"discovery_timeout" json:"discovery_timeout"`
|
|
}
|
|
|
|
|
|
// LoadConfig loads configuration from file, environment variables, and defaults
|
|
func LoadConfig(configPath string) (*Config, error) {
|
|
// Start with defaults
|
|
config := getDefaultConfig()
|
|
|
|
// Load from file if it exists
|
|
if configPath != "" && fileExists(configPath) {
|
|
if err := loadFromFile(config, configPath); err != nil {
|
|
return nil, fmt.Errorf("failed to load config file: %w", err)
|
|
}
|
|
}
|
|
|
|
// Override with environment variables
|
|
if err := loadFromEnv(config); err != nil {
|
|
return nil, fmt.Errorf("failed to load environment variables: %w", err)
|
|
}
|
|
|
|
// Validate configuration
|
|
if err := validateConfig(config); err != nil {
|
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// getDefaultConfig returns the default configuration
|
|
func getDefaultConfig() *Config {
|
|
return &Config{
|
|
WHOOSHAPI: WHOOSHAPIConfig{
|
|
BaseURL: "https://whoosh.home.deepblack.cloud",
|
|
Timeout: 30 * time.Second,
|
|
RetryCount: 3,
|
|
},
|
|
Agent: AgentConfig{
|
|
Capabilities: []string{"general", "reasoning", "task-coordination"},
|
|
PollInterval: 30 * time.Second,
|
|
MaxTasks: 3,
|
|
Models: []string{"phi3", "llama3.1"},
|
|
Specialization: "general_developer",
|
|
ModelSelectionWebhook: "https://n8n.home.deepblack.cloud/webhook/model-selection",
|
|
DefaultReasoningModel: "phi3",
|
|
SandboxImage: "registry.home.deepblack.cloud/tony/bzzz-sandbox:latest",
|
|
},
|
|
GitHub: GitHubConfig{
|
|
TokenFile: "/home/tony/chorus/business/secrets/gh-token",
|
|
UserAgent: "Bzzz-P2P-Agent/1.0",
|
|
Timeout: 30 * time.Second,
|
|
RateLimit: true,
|
|
Assignee: "anthonyrawlins",
|
|
},
|
|
P2P: P2PConfig{
|
|
ServiceTag: "bzzz-peer-discovery",
|
|
BzzzTopic: "bzzz/coordination/v1",
|
|
HmmmTopic: "hmmm/meta-discussion/v1",
|
|
DiscoveryTimeout: 10 * time.Second,
|
|
EscalationWebhook: "https://n8n.home.deepblack.cloud/webhook-test/human-escalation",
|
|
EscalationKeywords: []string{"stuck", "help", "human", "escalate", "clarification needed", "manual intervention"},
|
|
ConversationLimit: 10,
|
|
},
|
|
Logging: LoggingConfig{
|
|
Level: "info",
|
|
Format: "text",
|
|
Output: "stdout",
|
|
Structured: false,
|
|
},
|
|
Slurp: GetDefaultSlurpConfig(),
|
|
UCXL: UCXLConfig{
|
|
Enabled: false, // Disabled by default
|
|
Server: UCXIServerConfig{
|
|
Port: 8081,
|
|
BasePath: "/bzzz",
|
|
Enabled: true,
|
|
},
|
|
Resolution: UCXLResolutionConfig{
|
|
CacheTTL: 5 * time.Minute,
|
|
EnableWildcards: true,
|
|
MaxResults: 50,
|
|
},
|
|
Storage: UCXLStorageConfig{
|
|
Type: "filesystem",
|
|
Directory: "/tmp/bzzz-ucxl-storage",
|
|
MaxSize: 100 * 1024 * 1024, // 100MB
|
|
},
|
|
P2PIntegration: UCXLP2PConfig{
|
|
EnableAnnouncement: true,
|
|
EnableDiscovery: true,
|
|
AnnouncementTopic: "bzzz/ucxl/announcement/v1",
|
|
DiscoveryTimeout: 30 * time.Second,
|
|
},
|
|
},
|
|
Security: SecurityConfig{
|
|
AdminKeyShares: ShamirShare{
|
|
Threshold: 3,
|
|
TotalShares: 5,
|
|
},
|
|
ElectionConfig: ElectionConfig{
|
|
HeartbeatTimeout: 5 * time.Second,
|
|
DiscoveryTimeout: 30 * time.Second,
|
|
ElectionTimeout: 15 * time.Second,
|
|
MaxDiscoveryAttempts: 6,
|
|
DiscoveryBackoff: 5 * time.Second,
|
|
MinimumQuorum: 3,
|
|
ConsensusAlgorithm: "raft",
|
|
SplitBrainDetection: true,
|
|
ConflictResolution: "highest_uptime",
|
|
},
|
|
KeyRotationDays: 90,
|
|
AuditLogging: true,
|
|
AuditPath: ".bzzz/security-audit.log",
|
|
},
|
|
V2: V2Config{
|
|
Enabled: false, // Disabled by default for backward compatibility
|
|
ProtocolVersion: "2.0.0",
|
|
URIResolution: URIResolutionConfig{
|
|
CacheTTL: 5 * time.Minute,
|
|
MaxPeersPerResult: 5,
|
|
DefaultStrategy: "best_match",
|
|
ResolutionTimeout: 30 * time.Second,
|
|
},
|
|
DHT: DHTConfig{
|
|
Enabled: false, // Disabled by default
|
|
BootstrapPeers: []string{},
|
|
Mode: "auto",
|
|
ProtocolPrefix: "/bzzz",
|
|
BootstrapTimeout: 30 * time.Second,
|
|
DiscoveryInterval: 60 * time.Second,
|
|
AutoBootstrap: false,
|
|
},
|
|
SemanticAddressing: SemanticAddressingConfig{
|
|
EnableWildcards: true,
|
|
DefaultAgent: "any",
|
|
DefaultRole: "any",
|
|
DefaultProject: "any",
|
|
EnableRoleHierarchy: true,
|
|
},
|
|
FeatureFlags: map[string]bool{
|
|
"uri_protocol": false,
|
|
"semantic_addressing": false,
|
|
"dht_discovery": false,
|
|
"advanced_resolution": false,
|
|
},
|
|
},
|
|
AI: AIConfig{
|
|
Ollama: OllamaConfig{
|
|
Endpoint: "http://localhost:11434",
|
|
Timeout: 30 * time.Second,
|
|
Models: []string{"phi3", "llama3.1"},
|
|
},
|
|
OpenAI: OpenAIConfig{
|
|
APIKey: "",
|
|
Endpoint: "https://api.openai.com/v1",
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
},
|
|
// REVENUE CRITICAL: License configuration defaults
|
|
// These settings ensure BZZZ can only run with valid licensing from KACHING
|
|
License: LicenseConfig{
|
|
KachingURL: "https://kaching.chorus.services", // KACHING license authority server
|
|
HeartbeatMinutes: 60, // Check license validity every hour
|
|
GracePeriodHours: 24, // Allow 24 hours offline before enforcement
|
|
IsActive: false, // Default to inactive - MUST be validated during setup
|
|
// Note: Email, LicenseKey, and ClusterID are required and will be set during setup
|
|
// Leaving them empty in defaults forces setup process to collect them
|
|
},
|
|
}
|
|
}
|
|
|
|
// loadFromFile loads configuration from a YAML file with zero-trust validation
|
|
func loadFromFile(config *Config, filePath string) error {
|
|
// SECURITY: Validate file path to prevent directory traversal
|
|
validator := security.NewSecurityValidator()
|
|
if err := validator.ValidateFilePath(filePath); err != nil {
|
|
return fmt.Errorf("security validation failed for config path: %w", err)
|
|
}
|
|
|
|
data, err := ioutil.ReadFile(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
// SECURITY: Limit file size to prevent memory exhaustion attacks
|
|
maxConfigSize := 1024 * 1024 // 1MB limit for config files
|
|
if len(data) > maxConfigSize {
|
|
return fmt.Errorf("config file too large: %d bytes exceeds limit of %d bytes", len(data), maxConfigSize)
|
|
}
|
|
|
|
if err := yaml.Unmarshal(data, config); err != nil {
|
|
return fmt.Errorf("failed to parse YAML config: %w", err)
|
|
}
|
|
|
|
// SECURITY: Validate all configuration values after loading
|
|
if err := validateConfigSecurity(config, validator); err != nil {
|
|
return fmt.Errorf("security validation failed for loaded config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadFromEnv loads configuration from environment variables
|
|
func loadFromEnv(config *Config) error {
|
|
// WHOOSH API configuration
|
|
if url := os.Getenv("BZZZ_WHOOSH_API_URL"); url != "" {
|
|
config.WHOOSHAPI.BaseURL = url
|
|
}
|
|
if apiKey := os.Getenv("BZZZ_WHOOSH_API_KEY"); apiKey != "" {
|
|
config.WHOOSHAPI.APIKey = apiKey
|
|
}
|
|
|
|
// Agent configuration
|
|
if agentID := os.Getenv("BZZZ_AGENT_ID"); agentID != "" {
|
|
config.Agent.ID = agentID
|
|
}
|
|
if capabilities := os.Getenv("BZZZ_AGENT_CAPABILITIES"); capabilities != "" {
|
|
config.Agent.Capabilities = strings.Split(capabilities, ",")
|
|
}
|
|
if specialization := os.Getenv("BZZZ_AGENT_SPECIALIZATION"); specialization != "" {
|
|
config.Agent.Specialization = specialization
|
|
}
|
|
if modelWebhook := os.Getenv("BZZZ_MODEL_SELECTION_WEBHOOK"); modelWebhook != "" {
|
|
config.Agent.ModelSelectionWebhook = modelWebhook
|
|
}
|
|
|
|
// GitHub configuration
|
|
if tokenFile := os.Getenv("BZZZ_GITHUB_TOKEN_FILE"); tokenFile != "" {
|
|
config.GitHub.TokenFile = tokenFile
|
|
}
|
|
|
|
// P2P configuration
|
|
if webhook := os.Getenv("BZZZ_ESCALATION_WEBHOOK"); webhook != "" {
|
|
config.P2P.EscalationWebhook = webhook
|
|
}
|
|
|
|
// Logging configuration
|
|
if level := os.Getenv("BZZZ_LOG_LEVEL"); level != "" {
|
|
config.Logging.Level = level
|
|
}
|
|
|
|
// SLURP configuration
|
|
if slurpURL := os.Getenv("BZZZ_SLURP_URL"); slurpURL != "" {
|
|
config.Slurp.BaseURL = slurpURL
|
|
}
|
|
if slurpKey := os.Getenv("BZZZ_SLURP_API_KEY"); slurpKey != "" {
|
|
config.Slurp.APIKey = slurpKey
|
|
}
|
|
if slurpEnabled := os.Getenv("BZZZ_SLURP_ENABLED"); slurpEnabled == "true" {
|
|
config.Slurp.Enabled = true
|
|
}
|
|
|
|
// UCXL protocol configuration
|
|
if ucxlEnabled := os.Getenv("BZZZ_UCXL_ENABLED"); ucxlEnabled == "true" {
|
|
config.UCXL.Enabled = true
|
|
}
|
|
if ucxiPort := os.Getenv("BZZZ_UCXI_PORT"); ucxiPort != "" {
|
|
// Would need strconv.Atoi but keeping simple for now
|
|
// In production, add proper integer parsing
|
|
}
|
|
|
|
// V2 protocol configuration
|
|
if v2Enabled := os.Getenv("BZZZ_V2_ENABLED"); v2Enabled == "true" {
|
|
config.V2.Enabled = true
|
|
}
|
|
if dhtEnabled := os.Getenv("BZZZ_DHT_ENABLED"); dhtEnabled == "true" {
|
|
config.V2.DHT.Enabled = true
|
|
}
|
|
if dhtMode := os.Getenv("BZZZ_DHT_MODE"); dhtMode != "" {
|
|
config.V2.DHT.Mode = dhtMode
|
|
}
|
|
if bootstrapPeers := os.Getenv("BZZZ_DHT_BOOTSTRAP_PEERS"); bootstrapPeers != "" {
|
|
config.V2.DHT.BootstrapPeers = strings.Split(bootstrapPeers, ",")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateConfig validates the configuration values
|
|
func validateConfig(config *Config) error {
|
|
// Validate required fields
|
|
if config.WHOOSHAPI.BaseURL == "" {
|
|
return fmt.Errorf("whoosh_api.base_url is required")
|
|
}
|
|
|
|
// Note: Agent.ID can be empty - it will be auto-generated from node ID in main.go
|
|
|
|
if len(config.Agent.Capabilities) == 0 {
|
|
return fmt.Errorf("agent.capabilities cannot be empty")
|
|
}
|
|
|
|
if config.Agent.PollInterval <= 0 {
|
|
return fmt.Errorf("agent.poll_interval must be positive")
|
|
}
|
|
|
|
if config.Agent.MaxTasks <= 0 {
|
|
return fmt.Errorf("agent.max_tasks must be positive")
|
|
}
|
|
|
|
// Validate GitHub token file exists if specified
|
|
if config.GitHub.TokenFile != "" && !fileExists(config.GitHub.TokenFile) {
|
|
return fmt.Errorf("github token file does not exist: %s", config.GitHub.TokenFile)
|
|
}
|
|
|
|
// Validate SLURP configuration
|
|
if err := ValidateSlurpConfig(config.Slurp); err != nil {
|
|
return fmt.Errorf("slurp configuration invalid: %w", err)
|
|
}
|
|
|
|
// REVENUE CRITICAL: Validate license configuration
|
|
// This prevents BZZZ from running without valid licensing
|
|
if err := validateLicenseConfig(config.License); err != nil {
|
|
return fmt.Errorf("license configuration invalid: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateLicenseConfig ensures license configuration meets revenue protection requirements
|
|
// BUSINESS CRITICAL: This function enforces license requirements that protect revenue
|
|
func validateLicenseConfig(license LicenseConfig) error {
|
|
// Check core license identification fields
|
|
if license.Email == "" {
|
|
return fmt.Errorf("license email is required - BZZZ cannot run without valid licensing")
|
|
}
|
|
|
|
if license.LicenseKey == "" {
|
|
return fmt.Errorf("license key is required - BZZZ cannot run without valid licensing")
|
|
}
|
|
|
|
if license.ClusterID == "" {
|
|
return fmt.Errorf("cluster ID is required - BZZZ cannot run without cluster binding")
|
|
}
|
|
|
|
// Validate KACHING integration settings
|
|
if license.KachingURL == "" {
|
|
return fmt.Errorf("KACHING URL is required for license validation")
|
|
}
|
|
|
|
// Validate URL format
|
|
if err := validateURL(license.KachingURL); err != nil {
|
|
return fmt.Errorf("invalid KACHING URL: %w", err)
|
|
}
|
|
|
|
// Validate heartbeat and grace period settings
|
|
if license.HeartbeatMinutes <= 0 {
|
|
return fmt.Errorf("heartbeat interval must be positive (recommended: 60 minutes)")
|
|
}
|
|
|
|
if license.GracePeriodHours <= 0 {
|
|
return fmt.Errorf("grace period must be positive (recommended: 24 hours)")
|
|
}
|
|
|
|
// FAIL-CLOSED DESIGN: License must be explicitly marked as active
|
|
// This ensures setup process validates license before allowing operations
|
|
if !license.IsActive {
|
|
return fmt.Errorf("license is not active - run setup to validate with KACHING license authority")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SaveConfig saves the configuration to a YAML file
|
|
func SaveConfig(config *Config, filePath string) error {
|
|
data, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal config to YAML: %w", err)
|
|
}
|
|
|
|
if err := ioutil.WriteFile(filePath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write config file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetGitHubToken reads the GitHub token from the configured file
|
|
func (c *Config) GetGitHubToken() (string, error) {
|
|
if c.GitHub.TokenFile == "" {
|
|
return "", fmt.Errorf("no GitHub token file configured")
|
|
}
|
|
|
|
tokenBytes, err := ioutil.ReadFile(c.GitHub.TokenFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read GitHub token: %w", err)
|
|
}
|
|
|
|
return strings.TrimSpace(string(tokenBytes)), nil
|
|
}
|
|
|
|
// fileExists checks if a file exists
|
|
func fileExists(filePath string) bool {
|
|
_, err := os.Stat(filePath)
|
|
return err == nil
|
|
}
|
|
|
|
// GenerateDefaultConfigFile creates a default configuration file
|
|
func GenerateDefaultConfigFile(filePath string) error {
|
|
config := getDefaultConfig()
|
|
return SaveConfig(config, filePath)
|
|
}
|
|
|
|
// IsSetupRequired checks if BZZZ needs to be set up (no valid configuration exists)
|
|
func IsSetupRequired(configPath string) bool {
|
|
// Check if config file exists
|
|
if !fileExists(configPath) {
|
|
return true
|
|
}
|
|
|
|
// Try to load the configuration
|
|
_, err := LoadConfig(configPath)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
|
|
// Configuration exists and is valid
|
|
return false
|
|
}
|
|
|
|
// IsValidConfiguration checks if a configuration is valid for production use
|
|
func IsValidConfiguration(config *Config) bool {
|
|
// Check essential configuration elements
|
|
if config.Agent.ID == "" {
|
|
return false
|
|
}
|
|
|
|
if len(config.Agent.Capabilities) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Configuration appears valid
|
|
return true
|
|
}
|
|
|
|
// validateConfigSecurity performs zero-trust validation on all configuration values
|
|
func validateConfigSecurity(config *Config, validator *security.SecurityValidator) error {
|
|
// Validate Agent configuration
|
|
if config.Agent.ID != "" {
|
|
// Agent IDs should be alphanumeric identifiers
|
|
if len(config.Agent.ID) > 64 {
|
|
return fmt.Errorf("agent ID too long (max 64 characters): %s", config.Agent.ID)
|
|
}
|
|
if !isAlphanumericWithDashes(config.Agent.ID) {
|
|
return fmt.Errorf("agent ID contains invalid characters: %s", config.Agent.ID)
|
|
}
|
|
}
|
|
|
|
// Validate specialization
|
|
if config.Agent.Specialization != "" {
|
|
if len(config.Agent.Specialization) > 64 {
|
|
return fmt.Errorf("specialization too long (max 64 characters)")
|
|
}
|
|
if !isAlphanumericWithUnderscore(config.Agent.Specialization) {
|
|
return fmt.Errorf("specialization contains invalid characters: %s", config.Agent.Specialization)
|
|
}
|
|
}
|
|
|
|
// Validate role
|
|
if config.Agent.Role != "" {
|
|
if len(config.Agent.Role) > 32 {
|
|
return fmt.Errorf("role too long (max 32 characters)")
|
|
}
|
|
if !isAlphanumericWithUnderscore(config.Agent.Role) {
|
|
return fmt.Errorf("role contains invalid characters: %s", config.Agent.Role)
|
|
}
|
|
}
|
|
|
|
// Validate capabilities list
|
|
if len(config.Agent.Capabilities) > 20 {
|
|
return fmt.Errorf("too many capabilities (max 20)")
|
|
}
|
|
for _, capability := range config.Agent.Capabilities {
|
|
if len(capability) > 32 {
|
|
return fmt.Errorf("capability name too long (max 32 characters): %s", capability)
|
|
}
|
|
if !isAlphanumericWithUnderscore(capability) {
|
|
return fmt.Errorf("capability contains invalid characters: %s", capability)
|
|
}
|
|
}
|
|
|
|
// Validate models list
|
|
if len(config.Agent.Models) > 50 {
|
|
return fmt.Errorf("too many models (max 50)")
|
|
}
|
|
for _, model := range config.Agent.Models {
|
|
if len(model) > 64 {
|
|
return fmt.Errorf("model name too long (max 64 characters): %s", model)
|
|
}
|
|
if !isAlphanumericWithDots(model) {
|
|
return fmt.Errorf("model name contains invalid characters: %s", model)
|
|
}
|
|
}
|
|
|
|
// Validate URLs and webhooks
|
|
if config.WHOOSHAPI.BaseURL != "" {
|
|
if err := validateURL(config.WHOOSHAPI.BaseURL); err != nil {
|
|
return fmt.Errorf("invalid WHOOSH API URL: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Agent.ModelSelectionWebhook != "" {
|
|
if err := validateURL(config.Agent.ModelSelectionWebhook); err != nil {
|
|
return fmt.Errorf("invalid model selection webhook URL: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.P2P.EscalationWebhook != "" {
|
|
if err := validateURL(config.P2P.EscalationWebhook); err != nil {
|
|
return fmt.Errorf("invalid escalation webhook URL: %w", err)
|
|
}
|
|
}
|
|
|
|
// Validate file paths
|
|
if config.GitHub.TokenFile != "" {
|
|
if err := validator.ValidateFilePath(config.GitHub.TokenFile); err != nil {
|
|
return fmt.Errorf("invalid GitHub token file path: %w", err)
|
|
}
|
|
}
|
|
|
|
if config.Security.AuditPath != "" {
|
|
if err := validator.ValidateFilePath(config.Security.AuditPath); err != nil {
|
|
return fmt.Errorf("invalid audit path: %w", err)
|
|
}
|
|
}
|
|
|
|
// Validate numeric limits
|
|
if config.Agent.MaxTasks < 1 || config.Agent.MaxTasks > 100 {
|
|
return fmt.Errorf("invalid max tasks: must be between 1 and 100, got %d", config.Agent.MaxTasks)
|
|
}
|
|
|
|
if config.WHOOSHAPI.RetryCount < 0 || config.WHOOSHAPI.RetryCount > 10 {
|
|
return fmt.Errorf("invalid retry count: must be between 0 and 10, got %d", config.WHOOSHAPI.RetryCount)
|
|
}
|
|
|
|
if config.Security.KeyRotationDays < 0 || config.Security.KeyRotationDays > 365 {
|
|
return fmt.Errorf("invalid key rotation days: must be between 0 and 365, got %d", config.Security.KeyRotationDays)
|
|
}
|
|
|
|
// Validate timeout durations (prevent extremely long timeouts that could cause DoS)
|
|
maxTimeout := 300 * time.Second // 5 minutes max
|
|
if config.WHOOSHAPI.Timeout > maxTimeout {
|
|
return fmt.Errorf("WHOOSH API timeout too long (max 5 minutes): %v", config.WHOOSHAPI.Timeout)
|
|
}
|
|
if config.GitHub.Timeout > maxTimeout {
|
|
return fmt.Errorf("GitHub timeout too long (max 5 minutes): %v", config.GitHub.Timeout)
|
|
}
|
|
if config.Agent.PollInterval > 3600*time.Second {
|
|
return fmt.Errorf("poll interval too long (max 1 hour): %v", config.Agent.PollInterval)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper validation functions
|
|
|
|
func isAlphanumericWithDashes(s string) bool {
|
|
if len(s) == 0 {
|
|
return false
|
|
}
|
|
for _, char := range s {
|
|
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
|
|
(char >= '0' && char <= '9') || char == '-' || char == '_') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isAlphanumericWithUnderscore(s string) bool {
|
|
if len(s) == 0 {
|
|
return false
|
|
}
|
|
for _, char := range s {
|
|
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
|
|
(char >= '0' && char <= '9') || char == '_') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isAlphanumericWithDots(s string) bool {
|
|
if len(s) == 0 {
|
|
return false
|
|
}
|
|
for _, char := range s {
|
|
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
|
|
(char >= '0' && char <= '9') || char == '.' || char == '-' || char == '_') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validateURL(url string) error {
|
|
if len(url) > 2048 {
|
|
return fmt.Errorf("URL too long (max 2048 characters)")
|
|
}
|
|
|
|
// Basic URL validation - should start with http:// or https://
|
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
|
return fmt.Errorf("URL must start with http:// or https://")
|
|
}
|
|
|
|
// Check for dangerous characters that could be used for injection
|
|
dangerousChars := []string{"`", "$", ";", "|", "&", "<", ">", "\n", "\r"}
|
|
for _, char := range dangerousChars {
|
|
if strings.Contains(url, char) {
|
|
return fmt.Errorf("URL contains dangerous characters")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
} |