Files
bzzz/pkg/config/config.go
anthonyrawlins c8c5e918d5 feat: Implement comprehensive license enforcement and revenue protection
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>
2025-09-01 10:20:33 +10:00

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
}