Files
bzzz/api/setup_manager.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

2476 lines
77 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
"chorus.services/bzzz/pkg/config"
"chorus.services/bzzz/pkg/security"
"chorus.services/bzzz/repository"
)
// SetupManager handles the initial configuration setup for BZZZ
type SetupManager struct {
configPath string
factory repository.ProviderFactory
validator *security.SecurityValidator
}
// NewSetupManager creates a new setup manager
func NewSetupManager(configPath string) *SetupManager {
return &SetupManager{
configPath: configPath,
factory: &repository.DefaultProviderFactory{},
validator: security.NewSecurityValidator(),
}
}
// IsSetupRequired checks if initial setup is needed
func (s *SetupManager) IsSetupRequired() bool {
// Check if config file exists and is valid
if _, err := os.Stat(s.configPath); os.IsNotExist(err) {
return true
}
// Try to load and validate existing config
cfg, err := config.LoadConfig(s.configPath)
if err != nil {
return true
}
// Check if essential configuration is present
return cfg.Agent.ID == "" || cfg.WHOOSHAPI.BaseURL == ""
}
// SystemInfo holds system detection information
type SystemInfo struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
CPUCores int `json:"cpu_cores"`
Memory int64 `json:"memory_mb"`
GPUs []GPUInfo `json:"gpus"`
Network NetworkInfo `json:"network"`
Storage StorageInfo `json:"storage"`
Docker DockerInfo `json:"docker"`
}
// GPUInfo holds GPU detection information
type GPUInfo struct {
Name string `json:"name"`
Memory string `json:"memory"`
Driver string `json:"driver"`
Type string `json:"type"` // nvidia, amd, intel
}
// NetworkInfo holds network configuration
type NetworkInfo struct {
Hostname string `json:"hostname"`
Interfaces []string `json:"interfaces"`
PublicIP string `json:"public_ip,omitempty"`
PrivateIPs []string `json:"private_ips"`
DockerBridge string `json:"docker_bridge,omitempty"`
}
// StorageInfo holds storage information
type StorageInfo struct {
TotalSpace int64 `json:"total_space_gb"`
FreeSpace int64 `json:"free_space_gb"`
MountPath string `json:"mount_path"`
}
// DockerInfo holds Docker environment information
type DockerInfo struct {
Available bool `json:"available"`
Version string `json:"version,omitempty"`
ComposeAvailable bool `json:"compose_available"`
SwarmMode bool `json:"swarm_mode"`
}
// DetectSystemInfo performs comprehensive system detection
func (s *SetupManager) DetectSystemInfo() (*SystemInfo, error) {
info := &SystemInfo{
OS: runtime.GOOS,
Architecture: runtime.GOARCH,
CPUCores: runtime.NumCPU(),
}
// Detect memory
if memory, err := s.detectMemory(); err == nil {
info.Memory = memory
}
// Detect GPUs
if gpus, err := s.detectGPUs(); err == nil {
info.GPUs = gpus
}
// Detect network configuration
if network, err := s.detectNetwork(); err == nil {
info.Network = network
}
// Detect storage
if storage, err := s.detectStorage(); err == nil {
info.Storage = storage
}
// Detect Docker
if docker, err := s.detectDocker(); err == nil {
info.Docker = docker
}
return info, nil
}
// detectMemory detects system memory
func (s *SetupManager) detectMemory() (int64, error) {
switch runtime.GOOS {
case "linux":
// Read from /proc/meminfo
content, err := os.ReadFile("/proc/meminfo")
if err != nil {
return 0, err
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "MemTotal:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
kb, err := strconv.ParseInt(parts[1], 10, 64)
if err == nil {
return kb / 1024, nil // Convert KB to MB
}
}
}
}
case "darwin":
// Use sysctl on macOS
cmd := exec.Command("sysctl", "-n", "hw.memsize")
output, err := cmd.Output()
if err == nil {
bytes, err := strconv.ParseInt(strings.TrimSpace(string(output)), 10, 64)
if err == nil {
return bytes / (1024 * 1024), nil // Convert bytes to MB
}
}
}
return 0, fmt.Errorf("memory detection not supported on %s", runtime.GOOS)
}
// detectGPUs detects available GPUs
func (s *SetupManager) detectGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
// Try NVIDIA GPUs first
if nvidiaGPUs, err := s.detectNVIDIAGPUs(); err == nil {
gpus = append(gpus, nvidiaGPUs...)
}
// Try AMD GPUs
if amdGPUs, err := s.detectAMDGPUs(); err == nil {
gpus = append(gpus, amdGPUs...)
}
// Try Intel GPUs (basic detection)
if intelGPUs, err := s.detectIntelGPUs(); err == nil {
gpus = append(gpus, intelGPUs...)
}
return gpus, nil
}
// detectNVIDIAGPUs detects NVIDIA GPUs using nvidia-smi
func (s *SetupManager) detectNVIDIAGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
// Check if nvidia-smi is available
cmd := exec.Command("nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader,nounits")
output, err := cmd.Output()
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, ", ")
if len(parts) >= 3 {
gpu := GPUInfo{
Name: strings.TrimSpace(parts[0]),
Memory: strings.TrimSpace(parts[1]) + " MB",
Driver: strings.TrimSpace(parts[2]),
Type: "nvidia",
}
gpus = append(gpus, gpu)
}
}
return gpus, nil
}
// detectAMDGPUs detects AMD GPUs
func (s *SetupManager) detectAMDGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
// Try rocm-smi for AMD GPUs
cmd := exec.Command("rocm-smi", "--showproductname", "--showmeminfo", "vram")
output, err := cmd.Output()
if err != nil {
return nil, err
}
// Parse rocm-smi output (simplified)
if strings.Contains(string(output), "GPU") {
gpu := GPUInfo{
Name: "AMD GPU (detected)",
Memory: "Unknown",
Driver: "ROCm",
Type: "amd",
}
gpus = append(gpus, gpu)
}
return gpus, nil
}
// detectIntelGPUs detects Intel integrated GPUs
func (s *SetupManager) detectIntelGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
switch runtime.GOOS {
case "linux":
// Check for Intel GPU in /sys/class/drm
if _, err := os.Stat("/sys/class/drm/card0"); err == nil {
// Basic Intel GPU detection
gpu := GPUInfo{
Name: "Intel Integrated Graphics",
Memory: "Shared",
Driver: "i915",
Type: "intel",
}
gpus = append(gpus, gpu)
}
}
return gpus, nil
}
// detectNetwork detects network configuration
func (s *SetupManager) detectNetwork() (NetworkInfo, error) {
info := NetworkInfo{
PrivateIPs: []string{},
}
// Get hostname
if hostname, err := os.Hostname(); err == nil {
info.Hostname = hostname
}
// Detect network interfaces (simplified)
switch runtime.GOOS {
case "linux":
cmd := exec.Command("ip", "addr", "show")
output, err := cmd.Output()
if err == nil {
s.parseLinuxNetworkInfo(string(output), &info)
}
case "darwin":
cmd := exec.Command("ifconfig")
output, err := cmd.Output()
if err == nil {
s.parseDarwinNetworkInfo(string(output), &info)
}
}
return info, nil
}
// parseLinuxNetworkInfo parses Linux network info from ip command
func (s *SetupManager) parseLinuxNetworkInfo(output string, info *NetworkInfo) {
lines := strings.Split(output, "\n")
var currentInterface string
for _, line := range lines {
line = strings.TrimSpace(line)
// Interface line
if strings.Contains(line, ": ") && !strings.HasPrefix(line, "inet") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
currentInterface = strings.TrimSpace(parts[1])
if currentInterface != "lo" { // Skip loopback
info.Interfaces = append(info.Interfaces, currentInterface)
}
}
}
// IP address line
if strings.HasPrefix(line, "inet ") && currentInterface != "lo" {
parts := strings.Fields(line)
if len(parts) >= 2 {
ip := strings.Split(parts[1], "/")[0]
if s.isPrivateIP(ip) {
info.PrivateIPs = append(info.PrivateIPs, ip)
}
}
}
}
}
// parseDarwinNetworkInfo parses macOS network info from ifconfig
func (s *SetupManager) parseDarwinNetworkInfo(output string, info *NetworkInfo) {
lines := strings.Split(output, "\n")
var currentInterface string
for _, line := range lines {
// Interface line
if !strings.HasPrefix(line, "\t") && strings.Contains(line, ":") {
parts := strings.Split(line, ":")
if len(parts) >= 1 {
currentInterface = strings.TrimSpace(parts[0])
if currentInterface != "lo0" { // Skip loopback
info.Interfaces = append(info.Interfaces, currentInterface)
}
}
}
// IP address line
if strings.Contains(line, "inet ") && currentInterface != "lo0" {
parts := strings.Fields(strings.TrimSpace(line))
if len(parts) >= 2 {
ip := parts[1]
if s.isPrivateIP(ip) {
info.PrivateIPs = append(info.PrivateIPs, ip)
}
}
}
}
}
// isPrivateIP checks if an IP address is private
func (s *SetupManager) isPrivateIP(ip string) bool {
return strings.HasPrefix(ip, "192.168.") ||
strings.HasPrefix(ip, "10.") ||
strings.HasPrefix(ip, "172.16.") ||
strings.HasPrefix(ip, "172.17.") ||
strings.HasPrefix(ip, "172.18.") ||
strings.HasPrefix(ip, "172.19.") ||
strings.HasPrefix(ip, "172.20.") ||
strings.HasPrefix(ip, "172.21.") ||
strings.HasPrefix(ip, "172.22.") ||
strings.HasPrefix(ip, "172.23.") ||
strings.HasPrefix(ip, "172.24.") ||
strings.HasPrefix(ip, "172.25.") ||
strings.HasPrefix(ip, "172.26.") ||
strings.HasPrefix(ip, "172.27.") ||
strings.HasPrefix(ip, "172.28.") ||
strings.HasPrefix(ip, "172.29.") ||
strings.HasPrefix(ip, "172.30.") ||
strings.HasPrefix(ip, "172.31.")
}
// detectStorage detects storage information
func (s *SetupManager) detectStorage() (StorageInfo, error) {
info := StorageInfo{
MountPath: "/",
}
// Get current working directory for storage detection
wd, err := os.Getwd()
if err != nil {
wd = "/"
}
info.MountPath = wd
switch runtime.GOOS {
case "linux", "darwin":
cmd := exec.Command("df", "-BG", wd)
output, err := cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
if len(lines) >= 2 {
fields := strings.Fields(lines[1])
if len(fields) >= 4 {
// Parse total and available space
if total, err := strconv.ParseInt(strings.TrimSuffix(fields[1], "G"), 10, 64); err == nil {
info.TotalSpace = total
}
if free, err := strconv.ParseInt(strings.TrimSuffix(fields[3], "G"), 10, 64); err == nil {
info.FreeSpace = free
}
}
}
}
}
return info, nil
}
// detectDocker detects Docker environment
func (s *SetupManager) detectDocker() (DockerInfo, error) {
info := DockerInfo{}
// Check if docker command is available
cmd := exec.Command("docker", "--version")
output, err := cmd.Output()
if err == nil {
info.Available = true
info.Version = strings.TrimSpace(string(output))
}
// Check if docker compose is available (modern Docker includes compose as subcommand)
cmd = exec.Command("docker", "compose", "version")
if err := cmd.Run(); err == nil {
info.ComposeAvailable = true
} else {
// Fallback to legacy docker-compose for older systems
cmd = exec.Command("docker-compose", "--version")
if err := cmd.Run(); err == nil {
info.ComposeAvailable = true
}
}
// Check if Docker is in swarm mode
if info.Available {
cmd = exec.Command("docker", "info", "--format", "{{.Swarm.LocalNodeState}}")
output, err := cmd.Output()
if err == nil && strings.TrimSpace(string(output)) == "active" {
info.SwarmMode = true
}
}
return info, nil
}
// RepositoryConfig holds repository configuration for setup
type RepositoryConfig struct {
Provider string `json:"provider"`
BaseURL string `json:"baseURL,omitempty"`
AccessToken string `json:"accessToken"`
Owner string `json:"owner"`
Repository string `json:"repository"`
}
// ValidateRepositoryConfig validates repository configuration
func (s *SetupManager) ValidateRepositoryConfig(repoConfig *RepositoryConfig) error {
if repoConfig.Provider == "" {
return fmt.Errorf("provider is required")
}
if repoConfig.AccessToken == "" {
return fmt.Errorf("access token is required")
}
if repoConfig.Owner == "" {
return fmt.Errorf("owner is required")
}
if repoConfig.Repository == "" {
return fmt.Errorf("repository is required")
}
// Validate provider-specific requirements
switch strings.ToLower(repoConfig.Provider) {
case "gitea":
if repoConfig.BaseURL == "" {
return fmt.Errorf("base_url is required for Gitea")
}
case "github":
// GitHub uses default URL
if repoConfig.BaseURL == "" {
repoConfig.BaseURL = "https://api.github.com"
}
default:
return fmt.Errorf("unsupported provider: %s", repoConfig.Provider)
}
// Test connection to repository
return s.testRepositoryConnection(repoConfig)
}
// testRepositoryConnection tests connection to the repository
func (s *SetupManager) testRepositoryConnection(repoConfig *RepositoryConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
config := &repository.Config{
Provider: repoConfig.Provider,
BaseURL: repoConfig.BaseURL,
AccessToken: repoConfig.AccessToken,
Owner: repoConfig.Owner,
Repository: repoConfig.Repository,
TaskLabel: "bzzz-task",
InProgressLabel: "in-progress",
CompletedLabel: "completed",
BaseBranch: "main",
BranchPrefix: "bzzz/",
}
provider, err := s.factory.CreateProvider(ctx, config)
if err != nil {
return fmt.Errorf("failed to create provider: %w", err)
}
// Try to list tasks to test connection
_, err = provider.ListAvailableTasks()
if err != nil {
return fmt.Errorf("failed to connect to repository: %w", err)
}
return nil
}
// SetupConfig holds the complete setup configuration
type SetupConfig struct {
AgentID string `json:"agent_id"`
Capabilities []string `json:"capabilities"`
Models []string `json:"models"`
Repository *RepositoryConfig `json:"repository"`
Network map[string]interface{} `json:"network"`
Storage map[string]interface{} `json:"storage"`
Security map[string]interface{} `json:"security"`
}
// SaveConfiguration saves the setup configuration to file
func (s *SetupManager) SaveConfiguration(setupConfig *SetupConfig) error {
// Create configuration directory if it doesn't exist
configDir := filepath.Dir(s.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Load default configuration
cfg, err := config.LoadConfig("")
if err != nil {
// If loading fails, we'll create a minimal config
cfg = &config.Config{}
}
// Apply setup configuration
if setupConfig.AgentID != "" {
cfg.Agent.ID = setupConfig.AgentID
}
if len(setupConfig.Capabilities) > 0 {
cfg.Agent.Capabilities = setupConfig.Capabilities
}
if len(setupConfig.Models) > 0 {
cfg.Agent.Models = setupConfig.Models
}
// Configure repository if provided
if setupConfig.Repository != nil {
// This would integrate with the existing repository configuration
// For now, we'll store it in a way that can be used by the main application
}
// Save configuration to file
if err := config.SaveConfig(cfg, s.configPath); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}
return nil
}
// GetSupportedProviders returns list of supported repository providers
func (s *SetupManager) GetSupportedProviders() []string {
return s.factory.SupportedProviders()
}
// Machine represents a discovered network machine
type Machine struct {
IP string `json:"ip"`
Hostname string `json:"hostname"`
OS string `json:"os,omitempty"`
OSVersion string `json:"os_version,omitempty"`
SystemInfo map[string]interface{} `json:"system_info,omitempty"`
}
// DiscoverNetworkMachines scans the network subnet for available machines
func (s *SetupManager) DiscoverNetworkMachines(subnet string, sshKey string) ([]Machine, error) {
var machines []Machine
// Parse CIDR subnet
_, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return nil, fmt.Errorf("invalid subnet: %w", err)
}
// Create a semaphore to limit concurrent goroutines (max 50 for faster scanning)
sem := make(chan struct{}, 50)
var wg sync.WaitGroup
var mu sync.Mutex
// Context for early termination when we have enough results
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Generate list of IPs to scan
var ips []string
// Start from the network address
ip := make(net.IP, len(ipNet.IP))
copy(ip, ipNet.IP.Mask(ipNet.Mask))
for ipNet.Contains(ip) {
// Skip network and broadcast addresses
ipStr := ip.String()
if !ip.Equal(ipNet.IP) && !isBroadcast(ip, ipNet) {
ips = append(ips, ipStr)
}
// Increment IP
inc(ip)
// Limit total IPs to scan (avoid scanning entire /16)
if len(ips) >= 254 {
break
}
}
// Scan IPs with limited concurrency
for _, targetIP := range ips {
// Check if context is cancelled (early termination)
select {
case <-ctx.Done():
break
default:
}
wg.Add(1)
go func(ip string) {
defer wg.Done()
// Check context again
select {
case <-ctx.Done():
return
default:
}
// Acquire semaphore
sem <- struct{}{}
defer func() { <-sem }()
// Quick ping test with shorter timeout
pingCtx, pingCancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer pingCancel()
cmd := exec.CommandContext(pingCtx, "ping", "-c", "1", "-W", "300", ip)
if err := cmd.Run(); err == nil {
// Machine is pingable, try to get more info
machine := Machine{
IP: ip,
Hostname: s.getHostname(ip),
}
mu.Lock()
machines = append(machines, machine)
// Stop early if we have enough machines
if len(machines) >= 20 {
mu.Unlock()
cancel() // Signal other goroutines to stop
return
}
mu.Unlock()
}
}(targetIP)
}
wg.Wait()
return machines, nil
}
// inc increments IP address
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
// isBroadcast checks if IP is the broadcast address for the network
func isBroadcast(ip net.IP, ipNet *net.IPNet) bool {
if ip == nil || ipNet == nil {
return false
}
// Calculate broadcast address
broadcast := make(net.IP, len(ip))
copy(broadcast, ipNet.IP.Mask(ipNet.Mask))
// Set all host bits to 1
for i := range broadcast {
broadcast[i] |= ^ipNet.Mask[i]
}
return ip.Equal(broadcast)
}
// getHostname attempts to resolve hostname for IP
func (s *SetupManager) getHostname(ip string) string {
names, err := net.LookupAddr(ip)
if err != nil || len(names) == 0 {
return "Unknown"
}
return strings.TrimSuffix(names[0], ".")
}
// SSHTestResult represents the result of SSH connection test
type SSHTestResult struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
OS string `json:"os,omitempty"`
OSVersion string `json:"os_version,omitempty"`
SystemInfo map[string]interface{} `json:"system_info,omitempty"`
}
// TestSSHConnection tests SSH connectivity and gathers system info
func (s *SetupManager) TestSSHConnection(ip string, privateKey string, username string, password string, port int) (*SSHTestResult, error) {
result := &SSHTestResult{}
// SECURITY: Validate all input parameters with zero-trust approach
if err := s.validator.ValidateSSHConnectionRequest(ip, username, password, privateKey, port); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Security validation failed: %s", err.Error())
return result, nil
}
// Set default port if not provided
if port == 0 {
port = 22
}
// SSH client config with flexible authentication
var authMethods []ssh.AuthMethod
var authErrors []string
if privateKey != "" {
// Try private key authentication first
if signer, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
authMethods = append(authMethods, ssh.PublicKeys(signer))
} else {
authErrors = append(authErrors, fmt.Sprintf("Invalid SSH private key: %v", err))
}
}
if password != "" {
// Add password authentication
authMethods = append(authMethods, ssh.Password(password))
}
if len(authMethods) == 0 {
result.Success = false
result.Error = fmt.Sprintf("No valid authentication methods available. Errors: %v", strings.Join(authErrors, "; "))
return result, nil
}
config := &ssh.ClientConfig{
User: username,
Auth: authMethods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // For setup phase
Timeout: 10 * time.Second,
}
// Connect to SSH with detailed error reporting
address := fmt.Sprintf("%s:%d", ip, port)
client, err := ssh.Dial("tcp", address, config)
if err != nil {
result.Success = false
// Provide specific error messages based on error type
if strings.Contains(err.Error(), "connection refused") {
result.Error = fmt.Sprintf("SSH connection refused to %s:%d - SSH service may not be running or port blocked", ip, port)
} else if strings.Contains(err.Error(), "permission denied") {
result.Error = fmt.Sprintf("SSH authentication failed for user '%s' on %s:%d - check username/password/key", username, ip, port)
} else if strings.Contains(err.Error(), "no route to host") {
result.Error = fmt.Sprintf("No network route to host %s - check IP address and network connectivity", ip)
} else if strings.Contains(err.Error(), "timeout") {
result.Error = fmt.Sprintf("SSH connection timeout to %s:%d - host may be unreachable or SSH service slow", ip, port)
} else {
result.Error = fmt.Sprintf("SSH connection failed to %s@%s:%d - %v", username, ip, port, err)
}
return result, nil
}
defer client.Close()
result.Success = true
// Gather system information
session, err := client.NewSession()
if err == nil {
defer session.Close()
// Get OS info
if output, err := session.Output("uname -s"); err == nil {
result.OS = strings.TrimSpace(string(output))
}
// Get OS version
session, _ = client.NewSession()
if output, err := session.Output("lsb_release -d 2>/dev/null || cat /etc/os-release | head -1"); err == nil {
result.OSVersion = strings.TrimSpace(string(output))
}
session.Close()
// Get basic system info
session, _ = client.NewSession()
if output, err := session.Output("nproc && free -m | grep Mem | awk '{print $2}' && df -h / | tail -1 | awk '{print $4}'"); err == nil {
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
systemInfo := make(map[string]interface{})
if len(lines) >= 3 {
if cpu, err := strconv.Atoi(lines[0]); err == nil {
systemInfo["cpu"] = cpu
}
if mem, err := strconv.Atoi(lines[1]); err == nil {
systemInfo["memory"] = mem / 1024 // Convert MB to GB
}
systemInfo["disk"] = lines[2]
}
result.SystemInfo = systemInfo
}
session.Close()
}
return result, nil
}
// DeploymentResult represents the result of service deployment
type DeploymentResult struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Steps []DeploymentStep `json:"steps,omitempty"`
RollbackLog []string `json:"rollback_log,omitempty"`
SystemInfo *DeploymentSystemInfo `json:"system_info,omitempty"`
}
// DeploymentStep represents a single deployment step with detailed status
type DeploymentStep struct {
Name string `json:"name"`
Status string `json:"status"` // "pending", "running", "success", "failed"
Command string `json:"command,omitempty"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
Duration string `json:"duration,omitempty"`
Verified bool `json:"verified"`
}
// DeployServiceToMachine deploys BZZZ service to a remote machine with full verification
func (s *SetupManager) DeployServiceToMachine(ip string, privateKey string, username string, password string, port int, config interface{}) (*DeploymentResult, error) {
result := &DeploymentResult{
Steps: []DeploymentStep{},
RollbackLog: []string{},
}
// SECURITY: Validate all input parameters with zero-trust approach
if err := s.validator.ValidateSSHConnectionRequest(ip, username, password, privateKey, port); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Security validation failed: %s", err.Error())
return result, nil
}
// Set default port if not provided
if port == 0 {
port = 22
}
// SSH client config with flexible authentication
var authMethods []ssh.AuthMethod
var authErrors []string
if privateKey != "" {
// Try private key authentication first
if signer, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
authMethods = append(authMethods, ssh.PublicKeys(signer))
} else {
authErrors = append(authErrors, fmt.Sprintf("Invalid SSH private key: %v", err))
}
}
if password != "" {
// Add password authentication
authMethods = append(authMethods, ssh.Password(password))
}
if len(authMethods) == 0 {
result.Success = false
result.Error = fmt.Sprintf("No valid authentication methods available. Errors: %v", strings.Join(authErrors, "; "))
return result, nil
}
sshConfig := &ssh.ClientConfig{
User: username,
Auth: authMethods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 30 * time.Second,
}
// Connect to SSH with detailed error reporting
address := fmt.Sprintf("%s:%d", ip, port)
client, err := ssh.Dial("tcp", address, sshConfig)
if err != nil {
result.Success = false
// Provide specific error messages based on error type
if strings.Contains(err.Error(), "connection refused") {
result.Error = fmt.Sprintf("SSH connection refused to %s:%d - SSH service may not be running or port blocked", ip, port)
} else if strings.Contains(err.Error(), "permission denied") {
result.Error = fmt.Sprintf("SSH authentication failed for user '%s' on %s:%d - check username/password/key", username, ip, port)
} else if strings.Contains(err.Error(), "no route to host") {
result.Error = fmt.Sprintf("No network route to host %s - check IP address and network connectivity", ip)
} else if strings.Contains(err.Error(), "timeout") {
result.Error = fmt.Sprintf("SSH connection timeout to %s:%d - host may be unreachable or SSH service slow", ip, port)
} else {
result.Error = fmt.Sprintf("SSH connection failed to %s@%s:%d - %v", username, ip, port, err)
}
return result, nil
}
defer client.Close()
s.addStep(result, "SSH Connection", "success", "", "SSH connection established successfully", "", true)
// Execute deployment steps with verification
steps := []func(*ssh.Client, interface{}, string, *DeploymentResult) error{
s.verifiedPreDeploymentCheck,
s.verifiedStopExistingServices,
s.verifiedCopyBinary,
s.verifiedDeployConfiguration,
s.verifiedConfigureFirewall,
s.verifiedCreateSystemdService,
s.verifiedStartService,
s.verifiedPostDeploymentTest,
}
for _, step := range steps {
if err := step(client, config, password, result); err != nil {
result.Success = false
result.Error = err.Error()
s.performRollbackWithPassword(client, password, result)
return result, nil
}
}
result.Success = true
return result, nil
}
// addStep adds a deployment step to the result with timing information
func (s *SetupManager) addStep(result *DeploymentResult, name, status, command, output, error string, verified bool) {
step := DeploymentStep{
Name: name,
Status: status,
Command: command,
Output: output,
Error: error,
Verified: verified,
Duration: "", // Will be filled by the calling function if needed
}
result.Steps = append(result.Steps, step)
}
// executeSSHCommand executes a command via SSH and returns output, error
func (s *SetupManager) executeSSHCommand(client *ssh.Client, command string) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", fmt.Errorf("failed to create SSH session: %w", err)
}
defer session.Close()
var stdout, stderr strings.Builder
session.Stdout = &stdout
session.Stderr = &stderr
err = session.Run(command)
output := stdout.String()
if stderr.Len() > 0 {
output += "\n[STDERR]: " + stderr.String()
}
return output, err
}
// executeSudoCommand executes a command with sudo using the provided password, or tries passwordless sudo if no password
func (s *SetupManager) executeSudoCommand(client *ssh.Client, password string, command string) (string, error) {
// SECURITY: Sanitize command to prevent injection
safeCommand := s.validator.SanitizeForCommand(command)
if safeCommand != command {
return "", fmt.Errorf("command contained unsafe characters and was sanitized: original='%s', safe='%s'", command, safeCommand)
}
if password != "" {
// SECURITY: Use here-document to avoid password exposure in process list
// This keeps the password out of command line arguments and process lists
escapedPassword := strings.ReplaceAll(password, "'", "'\"'\"'")
secureCommand := fmt.Sprintf(`sudo -S %s <<'BZZZ_EOF'
%s
BZZZ_EOF`, safeCommand, escapedPassword)
return s.executeSSHCommand(client, secureCommand)
} else {
// Try passwordless sudo
sudoCommand := fmt.Sprintf("sudo -n %s", safeCommand)
return s.executeSSHCommand(client, sudoCommand)
}
}
// DeploymentSystemInfo holds information about the target system for deployment
type DeploymentSystemInfo struct {
OS string `json:"os"` // linux, darwin, freebsd, etc.
Distro string `json:"distro"` // ubuntu, centos, debian, etc.
ServiceMgr string `json:"service_mgr"` // systemd, sysv, openrc, launchd
Architecture string `json:"architecture"` // x86_64, arm64, etc.
BinaryPath string `json:"binary_path"` // Where to install binary
ServicePath string `json:"service_path"` // Where to install service file
}
// detectSystemInfo detects target system information
func (s *SetupManager) detectSystemInfo(client *ssh.Client) (*DeploymentSystemInfo, error) {
info := &DeploymentSystemInfo{}
// Detect OS
osOutput, err := s.executeSSHCommand(client, "uname -s")
if err != nil {
return nil, fmt.Errorf("failed to detect OS: %v", err)
}
info.OS = strings.ToLower(strings.TrimSpace(osOutput))
// Detect architecture
archOutput, err := s.executeSSHCommand(client, "uname -m")
if err != nil {
return nil, fmt.Errorf("failed to detect architecture: %v", err)
}
info.Architecture = strings.TrimSpace(archOutput)
// Detect distribution (Linux only)
if info.OS == "linux" {
if distroOutput, err := s.executeSSHCommand(client, "cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d= -f2 | tr -d '\"' || echo 'unknown'"); err == nil {
info.Distro = strings.TrimSpace(distroOutput)
}
}
// Detect service manager and set paths
if err := s.detectServiceManager(client, info); err != nil {
return nil, fmt.Errorf("failed to detect service manager: %v", err)
}
return info, nil
}
// detectServiceManager detects the service manager and sets appropriate paths
func (s *SetupManager) detectServiceManager(client *ssh.Client, info *DeploymentSystemInfo) error {
switch info.OS {
case "linux":
// Check for systemd
if _, err := s.executeSSHCommand(client, "which systemctl"); err == nil {
if pidOutput, err := s.executeSSHCommand(client, "ps -p 1 -o comm="); err == nil && strings.Contains(pidOutput, "systemd") {
info.ServiceMgr = "systemd"
info.ServicePath = "/etc/systemd/system"
info.BinaryPath = "/usr/local/bin"
return nil
}
}
// Check for OpenRC
if _, err := s.executeSSHCommand(client, "which rc-service"); err == nil {
info.ServiceMgr = "openrc"
info.ServicePath = "/etc/init.d"
info.BinaryPath = "/usr/local/bin"
return nil
}
// Check for SysV init
if _, err := s.executeSSHCommand(client, "ls /etc/init.d/ 2>/dev/null"); err == nil {
info.ServiceMgr = "sysv"
info.ServicePath = "/etc/init.d"
info.BinaryPath = "/usr/local/bin"
return nil
}
return fmt.Errorf("unsupported service manager on Linux")
case "darwin":
info.ServiceMgr = "launchd"
info.ServicePath = "/Library/LaunchDaemons"
info.BinaryPath = "/usr/local/bin"
return nil
case "freebsd":
info.ServiceMgr = "rc"
info.ServicePath = "/usr/local/etc/rc.d"
info.BinaryPath = "/usr/local/bin"
return nil
default:
return fmt.Errorf("unsupported operating system: %s", info.OS)
}
}
// verifiedPreDeploymentCheck checks system requirements and existing installations
func (s *SetupManager) verifiedPreDeploymentCheck(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Pre-deployment Check"
s.addStep(result, stepName, "running", "", "", "", false)
// Detect system information
sysInfo, err := s.detectSystemInfo(client)
if err != nil {
s.updateLastStep(result, "failed", "system detection", "", fmt.Sprintf("System detection failed: %v", err), false)
return fmt.Errorf("system detection failed: %v", err)
}
// Store system info for other steps to use
result.SystemInfo = sysInfo
// Check for existing BZZZ processes (informational only - cleanup step will handle)
output, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'No BZZZ processes found'")
if err != nil {
s.updateLastStep(result, "failed", "process check", output, fmt.Sprintf("Failed to check processes: %v", err), false)
return fmt.Errorf("pre-deployment check failed: %v", err)
}
// Log existing processes but don't fail - cleanup step will handle this
var processStatus string
if !strings.Contains(output, "No BZZZ processes found") {
processStatus = "Existing BZZZ processes detected (will be stopped in cleanup step)"
} else {
processStatus = "No existing BZZZ processes detected"
}
// Check for existing systemd services
output2, _ := s.executeSSHCommand(client, "systemctl status bzzz 2>/dev/null || echo 'No BZZZ service'")
// Check system requirements
output3, _ := s.executeSSHCommand(client, "uname -a && free -m && df -h /tmp")
combinedOutput := fmt.Sprintf("Process status: %s\n\nProcess details:\n%s\n\nService check:\n%s\n\nSystem info:\n%s", processStatus, output, output2, output3)
s.updateLastStep(result, "success", "", combinedOutput, "", true)
return nil
}
// verifiedStopExistingServices stops any existing BZZZ services
func (s *SetupManager) verifiedStopExistingServices(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Stop & Remove Existing Services"
s.addStep(result, stepName, "running", "", "", "", false)
// Stop systemd service if exists
cmd1 := "systemctl stop bzzz 2>/dev/null || echo 'No systemd service to stop'"
output1, _ := s.executeSudoCommand(client, password, cmd1)
// Disable systemd service if exists - separate command for better error tracking
cmd2a := "systemctl disable bzzz 2>/dev/null || echo 'No systemd service to disable'"
output2a, _ := s.executeSudoCommand(client, password, cmd2a)
// Remove service files
cmd2b := "rm -f /etc/systemd/system/bzzz.service ~/.config/systemd/user/bzzz.service 2>/dev/null || echo 'No service file to remove'"
output2b, _ := s.executeSudoCommand(client, password, cmd2b)
// Kill any remaining processes
cmd3 := "pkill -f bzzz || echo 'No processes to kill'"
output3, _ := s.executeSSHCommand(client, cmd3)
// Remove old binaries from standard locations
cmd4 := "rm -f /usr/local/bin/bzzz ~/bin/bzzz ~/bzzz 2>/dev/null || echo 'No old binaries to remove'"
output4, _ := s.executeSudoCommand(client, password, cmd4)
// Reload systemd after changes
cmd5 := "systemctl daemon-reload 2>/dev/null || echo 'Systemd reload completed'"
output5, _ := s.executeSudoCommand(client, password, cmd5)
// Verify no processes remain
output6, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'All BZZZ processes stopped'")
if err != nil {
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
output1, output2a, output2b, output3, output4, output5, output6)
s.updateLastStep(result, "failed", "cleanup verification", combinedOutput, fmt.Sprintf("Failed verification: %v", err), false)
return fmt.Errorf("failed to verify process cleanup: %v", err)
}
if !strings.Contains(output6, "All BZZZ processes stopped") {
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
output1, output2a, output2b, output3, output4, output5, output6)
s.updateLastStep(result, "failed", "process verification", combinedOutput, "BZZZ processes still running after cleanup", false)
return fmt.Errorf("failed to stop all BZZZ processes")
}
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
output1, output2a, output2b, output3, output4, output5, output6)
s.updateLastStep(result, "success", "stop + cleanup + verify", combinedOutput, "", true)
return nil
}
// updateLastStep updates the last step in the result
func (s *SetupManager) updateLastStep(result *DeploymentResult, status, command, output, error string, verified bool) {
if len(result.Steps) > 0 {
lastStep := &result.Steps[len(result.Steps)-1]
lastStep.Status = status
if command != "" {
lastStep.Command = command
}
if output != "" {
lastStep.Output = output
}
if error != "" {
lastStep.Error = error
}
lastStep.Verified = verified
}
}
// performRollbackWithPassword attempts to undo changes made during failed deployment using password
func (s *SetupManager) performRollbackWithPassword(client *ssh.Client, password string, result *DeploymentResult) {
result.RollbackLog = append(result.RollbackLog, "Starting rollback procedure...")
// Stop any services we might have started
if output, err := s.executeSudoCommand(client, password, "systemctl stop bzzz 2>/dev/null || echo 'No service to stop'"); err == nil {
result.RollbackLog = append(result.RollbackLog, "Stopped service: "+output)
}
// Remove systemd service
if output, err := s.executeSudoCommand(client, password, "systemctl disable bzzz 2>/dev/null; rm -f /etc/systemd/system/bzzz.service 2>/dev/null || echo 'No service file to remove'"); err == nil {
result.RollbackLog = append(result.RollbackLog, "Removed service: "+output)
}
// Remove binary
if output, err := s.executeSudoCommand(client, password, "rm -f /usr/local/bin/bzzz 2>/dev/null || echo 'No binary to remove'"); err == nil {
result.RollbackLog = append(result.RollbackLog, "Removed binary: "+output)
}
// Reload systemd
if output, err := s.executeSudoCommand(client, password, "systemctl daemon-reload"); err == nil {
result.RollbackLog = append(result.RollbackLog, "Reloaded systemd: "+output)
}
}
// performRollback attempts to rollback any changes made during failed deployment
func (s *SetupManager) performRollback(client *ssh.Client, result *DeploymentResult) {
result.RollbackLog = append(result.RollbackLog, "Starting rollback procedure...")
// Stop any services we might have started
if output, err := s.executeSSHCommand(client, "sudo -n systemctl stop bzzz 2>/dev/null || echo 'No service to stop'"); err == nil {
result.RollbackLog = append(result.RollbackLog, "Stopped service: "+output)
}
// Remove binaries we might have copied
if output, err := s.executeSSHCommand(client, "rm -f ~/bzzz /usr/local/bin/bzzz 2>/dev/null || echo 'No binaries to remove'"); err == nil {
result.RollbackLog = append(result.RollbackLog, "Removed binaries: "+output)
}
result.RollbackLog = append(result.RollbackLog, "Rollback completed")
}
// verifiedCopyBinary copies BZZZ binary and verifies installation
func (s *SetupManager) verifiedCopyBinary(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Copy Binary"
s.addStep(result, stepName, "running", "", "", "", false)
// Copy binary using existing function but with verification
if err := s.copyBinaryToMachineWithPassword(client, password); err != nil {
s.updateLastStep(result, "failed", "scp binary", "", err.Error(), false)
return fmt.Errorf("binary copy failed: %v", err)
}
// Verify binary was copied and is executable
checkCmd := "ls -la /usr/local/bin/bzzz ~/bin/bzzz 2>/dev/null || echo 'Binary not found in expected locations'"
output, err := s.executeSSHCommand(client, checkCmd)
if err != nil {
s.updateLastStep(result, "failed", checkCmd, output, fmt.Sprintf("Verification failed: %v", err), false)
return fmt.Errorf("binary verification failed: %v", err)
}
// Verify binary can execute (note: BZZZ doesn't have --version flag, use --help)
versionCmd := "timeout 3s /usr/local/bin/bzzz --help 2>&1 | head -n1 || timeout 3s ~/bin/bzzz --help 2>&1 | head -n1 || echo 'Binary not executable'"
versionOutput, _ := s.executeSSHCommand(client, versionCmd)
combinedOutput := fmt.Sprintf("File check:\n%s\n\nBinary test:\n%s", output, versionOutput)
if strings.Contains(output, "Binary not found") {
s.updateLastStep(result, "failed", checkCmd, combinedOutput, "Binary not found in expected locations", false)
return fmt.Errorf("binary installation verification failed")
}
s.updateLastStep(result, "success", "scp + verify", combinedOutput, "", true)
return nil
}
// verifiedDeployConfiguration deploys configuration and verifies correctness
func (s *SetupManager) verifiedDeployConfiguration(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Deploy Configuration"
s.addStep(result, stepName, "running", "", "", "", false)
// Generate and deploy configuration using existing function
if err := s.generateAndDeployConfig(client, "remote-host", config); err != nil {
s.updateLastStep(result, "failed", "deploy config", "", err.Error(), false)
return fmt.Errorf("configuration deployment failed: %v", err)
}
// Verify configuration file was created and is valid YAML
verifyCmd := "ls -la ~/.bzzz/config.yaml && echo '--- Config Preview ---' && head -20 ~/.bzzz/config.yaml"
output, err := s.executeSSHCommand(client, verifyCmd)
if err != nil {
s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Config verification failed: %v", err), false)
return fmt.Errorf("configuration verification failed: %v", err)
}
// Check if config contains expected sections for complex config structure
if !strings.Contains(output, "agent:") || !strings.Contains(output, "whoosh_api:") || !strings.Contains(output, "ai:") {
s.updateLastStep(result, "failed", verifyCmd, output, "Configuration missing required sections", false)
return fmt.Errorf("configuration incomplete - missing required sections")
}
s.updateLastStep(result, "success", "deploy + verify config", output, "", true)
return nil
}
// verifiedConfigureFirewall configures firewall and verifies rules
func (s *SetupManager) verifiedConfigureFirewall(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Configure Firewall"
s.addStep(result, stepName, "running", "", "", "", false)
// Configure firewall using existing function
if err := s.configureFirewall(client, config); err != nil {
s.updateLastStep(result, "failed", "configure firewall", "", err.Error(), false)
return fmt.Errorf("firewall configuration failed: %v", err)
}
// Verify firewall rules (this is informational, not critical)
verifyCmd := "ufw status 2>/dev/null || firewall-cmd --list-ports 2>/dev/null || echo 'Firewall status unavailable'"
output, _ := s.executeSudoCommand(client, password, verifyCmd)
s.updateLastStep(result, "success", "configure + verify firewall", output, "", true)
return nil
}
// verifiedCreateSystemdService creates systemd service and verifies configuration
func (s *SetupManager) verifiedCreateSystemdService(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Create SystemD Service"
s.addStep(result, stepName, "running", "", "", "", false)
// Create systemd service using password-based sudo
if err := s.createSystemdServiceWithPassword(client, config, password); err != nil {
s.updateLastStep(result, "failed", "create service", "", err.Error(), false)
return fmt.Errorf("systemd service creation failed: %v", err)
}
// Verify service file was created and contains correct paths
verifyCmd := "systemctl cat bzzz"
output, err := s.executeSudoCommand(client, password, verifyCmd)
if err != nil {
// Try to check if the service file exists another way
checkCmd := "ls -la /etc/systemd/system/bzzz.service"
checkOutput, checkErr := s.executeSudoCommand(client, password, checkCmd)
if checkErr != nil {
s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Service verification failed: %v. Service file check also failed: %v", err, checkErr), false)
return fmt.Errorf("systemd service verification failed: %v", err)
}
s.updateLastStep(result, "warning", verifyCmd, checkOutput, "Service file exists but systemctl cat failed, continuing", false)
}
// Verify service can be enabled
enableCmd := "systemctl enable bzzz"
enableOutput, enableErr := s.executeSudoCommand(client, password, enableCmd)
if enableErr != nil {
combinedOutput := fmt.Sprintf("Service file:\n%s\n\nEnable attempt:\n%s", output, enableOutput)
s.updateLastStep(result, "failed", enableCmd, combinedOutput, fmt.Sprintf("Failed to enable service: %v", enableErr), false)
return fmt.Errorf("failed to enable systemd service: %v", enableErr)
}
combinedOutput := fmt.Sprintf("Service file:\n%s\n\nService enabled:\n%s", output, enableOutput)
s.updateLastStep(result, "success", "create + enable service", combinedOutput, "", true)
return nil
}
// verifiedStartService starts the service and verifies it's running properly
func (s *SetupManager) verifiedStartService(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Start Service"
s.addStep(result, stepName, "running", "", "", "", false)
// Check if auto-start is enabled
configMap, ok := config.(map[string]interface{})
if !ok || configMap["autoStart"] != true {
s.updateLastStep(result, "success", "", "Auto-start disabled, skipping service start", "", true)
return nil
}
// Pre-flight checks before starting service
s.addStep(result, "Pre-Start Checks", "running", "", "", "", false)
// Check if config file exists and is readable by the service user
configCheck := "ls -la /home/*/bzzz/config.yaml 2>/dev/null || echo 'Config file not found'"
configOutput, _ := s.executeSSHCommand(client, configCheck)
// Check if binary is executable
binCheck := "ls -la /usr/local/bin/bzzz"
binOutput, _ := s.executeSudoCommand(client, password, binCheck)
preflightInfo := fmt.Sprintf("Binary check:\n%s\n\nConfig check:\n%s", binOutput, configOutput)
s.updateLastStep(result, "success", "pre-flight", preflightInfo, "Pre-start checks completed", false)
// Start the service
startCmd := "systemctl start bzzz"
startOutput, err := s.executeSudoCommand(client, password, startCmd)
if err != nil {
// Get detailed error information
statusCmd := "systemctl status bzzz"
statusOutput, _ := s.executeSudoCommand(client, password, statusCmd)
logsCmd := "journalctl -u bzzz --no-pager -n 20"
logsOutput, _ := s.executeSudoCommand(client, password, logsCmd)
// Combine all error information
detailedError := fmt.Sprintf("Start command output:\n%s\n\nService status:\n%s\n\nRecent logs:\n%s",
startOutput, statusOutput, logsOutput)
s.updateLastStep(result, "failed", startCmd, detailedError, fmt.Sprintf("Failed to start service: %v", err), false)
return fmt.Errorf("failed to start systemd service: %v", err)
}
// Wait for service to fully initialize (BZZZ needs time to start all subsystems)
time.Sleep(8 * time.Second)
// Verify service is running
statusCmd := "systemctl status bzzz"
statusOutput, _ := s.executeSSHCommand(client, statusCmd)
// Check if service is active
if !strings.Contains(statusOutput, "active (running)") {
// Get detailed logs to understand why service failed
logsCmd := "journalctl -u bzzz --no-pager -n 20"
logsOutput, _ := s.executeSudoCommand(client, password, logsCmd)
// Check if config file exists and is readable
configCheckCmd := "ls -la ~/.bzzz/config.yaml && head -5 ~/.bzzz/config.yaml"
configCheckOutput, _ := s.executeSSHCommand(client, configCheckCmd)
combinedOutput := fmt.Sprintf("Start attempt:\n%s\n\nStatus check:\n%s\n\nRecent logs:\n%s\n\nConfig check:\n%s",
startOutput, statusOutput, logsOutput, configCheckOutput)
s.updateLastStep(result, "failed", startCmd, combinedOutput, "Service failed to reach running state", false)
return fmt.Errorf("service is not running after start attempt")
}
combinedOutput := fmt.Sprintf("Service started:\n%s\n\nStatus verification:\n%s", startOutput, statusOutput)
s.updateLastStep(result, "success", startCmd+" + verify", combinedOutput, "", true)
return nil
}
// verifiedPostDeploymentTest performs final verification that deployment is functional
func (s *SetupManager) verifiedPostDeploymentTest(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
stepName := "Post-deployment Test"
s.addStep(result, stepName, "running", "", "", "", false)
// Test 1: Verify binary is executable
// Note: BZZZ binary doesn't have --version flag, so just check if it's executable and can start help
versionCmd := "if pgrep -f bzzz >/dev/null; then echo 'BZZZ process running'; else timeout 3s /usr/local/bin/bzzz --help 2>&1 | head -n1 || timeout 3s ~/bin/bzzz --help 2>&1 | head -n1 || echo 'Binary not executable'; fi"
versionOutput, _ := s.executeSSHCommand(client, versionCmd)
// Test 2: Verify service status
serviceCmd := "systemctl status bzzz --no-pager"
serviceOutput, _ := s.executeSSHCommand(client, serviceCmd)
// Test 3: Wait for API to be ready, then check if setup API is responding
// Poll for API readiness with timeout (up to 15 seconds)
var apiOutput string
apiReady := false
for i := 0; i < 15; i++ {
apiCmd := "curl -s -m 2 http://localhost:8090/api/setup/required 2>/dev/null"
output, err := s.executeSSHCommand(client, apiCmd)
if err == nil && !strings.Contains(output, "Connection refused") && !strings.Contains(output, "timeout") {
apiOutput = fmt.Sprintf("API ready (after %ds): %s", i+1, output)
apiReady = true
break
}
if i < 14 { // Don't sleep on the last iteration
time.Sleep(1 * time.Second)
}
}
if !apiReady {
apiOutput = "API not responding after 15s timeout"
}
// Test 4: Verify configuration is readable
configCmd := "test -r ~/.bzzz/config.yaml && echo 'Config readable' || echo 'Config not readable'"
configOutput, _ := s.executeSSHCommand(client, configCmd)
combinedOutput := fmt.Sprintf("Binary test:\n%s\n\nService test:\n%s\n\nAPI test:\n%s\n\nConfig test:\n%s",
versionOutput, serviceOutput, apiOutput, configOutput)
// Determine if tests passed and provide detailed failure information
// Binary test passes if BZZZ is running OR if help command succeeded
binaryFailed := strings.Contains(versionOutput, "Binary not executable") && !strings.Contains(versionOutput, "BZZZ process running")
configFailed := strings.Contains(configOutput, "Config not readable")
if binaryFailed || configFailed {
var failures []string
if binaryFailed {
failures = append(failures, "Binary not executable or accessible")
}
if configFailed {
failures = append(failures, "Config file not readable")
}
failureMsg := fmt.Sprintf("Tests failed: %s", strings.Join(failures, ", "))
s.updateLastStep(result, "failed", "post-deployment tests", combinedOutput, failureMsg, false)
return fmt.Errorf("post-deployment verification failed: %s", failureMsg)
}
s.updateLastStep(result, "success", "comprehensive verification", combinedOutput, "", true)
return nil
}
// copyBinaryToMachineWithPassword copies the BZZZ binary to remote machine using SCP protocol with sudo password
func (s *SetupManager) copyBinaryToMachineWithPassword(client *ssh.Client, password string) error {
// Read current binary
binaryPath, err := os.Executable()
if err != nil {
return err
}
binaryData, err := os.ReadFile(binaryPath)
if err != nil {
return err
}
// SCP protocol implementation
// Create SCP session
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Set up pipes
stdin, err := session.StdinPipe()
if err != nil {
return err
}
defer stdin.Close()
stdout, err := session.StdoutPipe()
if err != nil {
return err
}
// Start SCP receive command on remote host
remotePath := "~/bzzz"
go func() {
defer stdin.Close()
// Send SCP header: C<mode> <size> <filename>\n
header := fmt.Sprintf("C0755 %d bzzz\n", len(binaryData))
stdin.Write([]byte(header))
// Wait for acknowledgment
response := make([]byte, 1)
stdout.Read(response)
if response[0] != 0 {
return
}
// Send file content
stdin.Write(binaryData)
// Send final null byte
stdin.Write([]byte{0})
}()
// Execute SCP receive command
cmd := fmt.Sprintf("scp -t %s", remotePath)
if err := session.Run(cmd); err != nil {
return fmt.Errorf("failed to copy binary via SCP: %w", err)
}
// Make the binary executable
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("chmod +x ~/bzzz"); err != nil {
return fmt.Errorf("failed to make binary executable: %w", err)
}
// Try to move to /usr/local/bin with sudo, fall back to user bin if needed
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try to move to /usr/local/bin with sudo (with or without password), fall back to user bin if needed
var sudoCmd string
if password == "" {
// Try passwordless sudo first
sudoCmd = "sudo -n mv ~/bzzz /usr/local/bin/bzzz && sudo -n chmod +x /usr/local/bin/bzzz"
} else {
// Use password sudo
escapedPassword := strings.ReplaceAll(password, "'", "'\"'\"'")
sudoCmd = fmt.Sprintf("echo '%s' | sudo -S mv ~/bzzz /usr/local/bin/bzzz && echo '%s' | sudo -S chmod +x /usr/local/bin/bzzz",
escapedPassword, escapedPassword)
}
if err := session.Run(sudoCmd); err != nil {
// If sudo fails, create user bin directory and install there
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Create ~/bin directory and add to PATH if it doesn't exist
if err := session.Run("mkdir -p ~/bin && mv ~/bzzz ~/bin/bzzz && chmod +x ~/bin/bzzz"); err != nil {
return fmt.Errorf("failed to install binary to ~/bin: %w", err)
}
// Add ~/bin to PATH in .bashrc if not already there
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Run("grep -q 'export PATH=\"$HOME/bin:$PATH\"' ~/.bashrc || echo 'export PATH=\"$HOME/bin:$PATH\"' >> ~/.bashrc")
}
return nil
}
// copyBinaryToMachine copies the BZZZ binary to remote machine using SCP protocol (passwordless sudo)
func (s *SetupManager) copyBinaryToMachine(client *ssh.Client) error {
return s.copyBinaryToMachineWithPassword(client, "")
}
// createSystemdServiceWithPassword creates systemd service file using password sudo
func (s *SetupManager) createSystemdServiceWithPassword(client *ssh.Client, config interface{}, password string) error {
// Determine the correct binary path
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
// Check where the binary was installed
binaryPath := "/usr/local/bin/bzzz"
if err := session.Run("test -f /usr/local/bin/bzzz"); err != nil {
// If not in /usr/local/bin, it should be in ~/bin
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Stdout = &stdout
if err := session.Run("echo $HOME/bin/bzzz"); err == nil {
binaryPath = strings.TrimSpace(stdout.String())
}
}
// Get the actual username for the service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
var userBuilder strings.Builder
session.Stdout = &userBuilder
if err := session.Run("whoami"); err != nil {
return fmt.Errorf("failed to get username: %w", err)
}
username := strings.TrimSpace(userBuilder.String())
// Create service file with actual username
serviceFile := fmt.Sprintf(`[Unit]
Description=BZZZ P2P Task Coordination System
Documentation=https://chorus.services/docs/bzzz
After=network.target
[Service]
Type=simple
ExecStart=%s --config /home/%s/.bzzz/config.yaml
Restart=always
RestartSec=10
User=%s
Group=%s
[Install]
WantedBy=multi-user.target
`, binaryPath, username, username, username)
// Create service file in temp location first, then move with sudo
createCmd := fmt.Sprintf("cat > /tmp/bzzz.service << 'EOF'\n%sEOF", serviceFile)
if _, err := s.executeSSHCommand(client, createCmd); err != nil {
return fmt.Errorf("failed to create temp service file: %w", err)
}
// Move to systemd directory using password sudo
moveCmd := "mv /tmp/bzzz.service /etc/systemd/system/bzzz.service"
if _, err := s.executeSudoCommand(client, password, moveCmd); err != nil {
return fmt.Errorf("failed to install system service file: %w", err)
}
// Reload systemd to recognize new service
reloadCmd := "systemctl daemon-reload"
if _, err := s.executeSudoCommand(client, password, reloadCmd); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
return nil
}
// createSystemdService creates systemd service file
func (s *SetupManager) createSystemdService(client *ssh.Client, config interface{}) error {
// Determine the correct binary path
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
// Check where the binary was installed
binaryPath := "/usr/local/bin/bzzz"
if err := session.Run("test -f /usr/local/bin/bzzz"); err != nil {
// If not in /usr/local/bin, it should be in ~/bin
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Stdout = &stdout
if err := session.Run("echo $HOME/bin/bzzz"); err == nil {
binaryPath = strings.TrimSpace(stdout.String())
}
}
// Create service file that works for both system and user services
serviceFile := fmt.Sprintf(`[Unit]
Description=BZZZ P2P Task Coordination System
Documentation=https://chorus.services/docs/bzzz
After=network.target
[Service]
Type=simple
ExecStart=%s --config %%h/.bzzz/config.yaml
Restart=always
RestartSec=10
Environment=HOME=%%h
[Install]
WantedBy=default.target
`, binaryPath)
// Create service file using a more robust approach
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Create service file in temp location first, then move with sudo
cmd := fmt.Sprintf("cat > /tmp/bzzz.service << 'EOF'\n%sEOF", serviceFile)
if err := session.Run(cmd); err != nil {
return fmt.Errorf("failed to create temp service file: %w", err)
}
// Try to install as system service first, fall back to user service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try passwordless sudo for system service
if err := session.Run("sudo -n mv /tmp/bzzz.service /etc/systemd/system/bzzz.service"); err != nil {
// Sudo failed, create user-level service instead
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Create user systemd directory and install service there
if err := session.Run("mkdir -p ~/.config/systemd/user && mv /tmp/bzzz.service ~/.config/systemd/user/bzzz.service"); err != nil {
return fmt.Errorf("failed to install user service file: %w", err)
}
// Reload user systemd and enable service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("systemctl --user daemon-reload && systemctl --user enable bzzz"); err != nil {
return fmt.Errorf("failed to enable user bzzz service: %w", err)
}
// Enable lingering so user services start at boot
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Run("sudo -n loginctl enable-linger $(whoami) 2>/dev/null || true")
} else {
// System service installation succeeded, continue with system setup
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("sudo -n useradd -r -s /bin/false bzzz 2>/dev/null || true"); err != nil {
return fmt.Errorf("failed to create bzzz user: %w", err)
}
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("sudo -n mkdir -p /opt/bzzz && sudo -n chown bzzz:bzzz /opt/bzzz"); err != nil {
return fmt.Errorf("failed to create bzzz directory: %w", err)
}
// Reload systemd and enable service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("sudo -n systemctl daemon-reload && sudo -n systemctl enable bzzz"); err != nil {
return fmt.Errorf("failed to enable bzzz service: %w", err)
}
}
return nil
}
// startService starts the BZZZ service (system or user level)
func (s *SetupManager) startService(client *ssh.Client) error {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try system service first, fall back to user service
if err := session.Run("sudo -n systemctl start bzzz"); err != nil {
// Try user service instead
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
return session.Run("systemctl --user start bzzz")
}
return nil
}
// GenerateConfigForMachine generates the YAML configuration for a specific machine (for download/inspection)
func (s *SetupManager) GenerateConfigForMachine(machineIP string, config interface{}) (string, error) {
// Extract configuration from the setup data
configMap, ok := config.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config)
}
// Use machine IP to determine hostname (simplified)
hostname := strings.ReplaceAll(machineIP, ".", "-")
// Extract ports from configuration
ports := map[string]interface{}{
"api": 8080,
"mcp": 3000,
"webui": 8080,
"p2p": 7000,
}
// Override with configured ports if available
if portsConfig, exists := configMap["ports"]; exists {
if portsMap, ok := portsConfig.(map[string]interface{}); ok {
for key, value := range portsMap {
ports[key] = value
}
}
}
// Extract security configuration
securityConfig := map[string]interface{}{
"cluster_secret": "default-secret",
}
if security, exists := configMap["security"]; exists {
if securityMap, ok := security.(map[string]interface{}); ok {
if secret, exists := securityMap["clusterSecret"]; exists {
securityConfig["cluster_secret"] = secret
}
}
}
// Generate YAML configuration that matches the Go struct layout
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s
whoosh_api:
base_url: "https://whoosh.home.deepblack.cloud"
timeout: 30s
retry_count: 3
agent:
id: "%s-agent"
capabilities: ["general", "reasoning", "task-coordination"]
poll_interval: 30s
max_tasks: 3
models: ["phi3", "llama3.1"]
specialization: "general_developer"
model_selection_webhook: "https://n8n.home.deepblack.cloud/webhook/model-selection"
default_reasoning_model: "phi3"
sandbox_image: "registry.home.deepblack.cloud/bzzz-sandbox:latest"
role: ""
system_prompt: ""
reports_to: []
expertise: []
deliverables: []
collaboration:
preferred_message_types: []
auto_subscribe_to_roles: []
auto_subscribe_to_expertise: []
response_timeout_seconds: 0
max_collaboration_depth: 0
escalation_threshold: 0
custom_topic_subscriptions: []
github:
token_file: ""
user_agent: "Bzzz-P2P-Agent/1.0"
timeout: 30s
rate_limit: true
assignee: ""
p2p:
service_tag: "bzzz-peer-discovery"
bzzz_topic: "bzzz/coordination/v1"
hmmm_topic: "hmmm/meta-discussion/v1"
discovery_timeout: 10s
escalation_webhook: "https://n8n.home.deepblack.cloud/webhook-test/human-escalation"
escalation_keywords: ["stuck", "help", "human", "escalate", "clarification needed", "manual intervention"]
conversation_limit: 10
logging:
level: "info"
format: "text"
output: "stdout"
structured: false
slurp:
enabled: true
base_url: ""
api_key: ""
timeout: 30s
retry_count: 3
max_concurrent_requests: 10
request_queue_size: 100
v2:
enabled: false
protocol_version: "2.0.0"
uri_resolution:
cache_ttl: 5m0s
max_peers_per_result: 5
default_strategy: "best_match"
resolution_timeout: 30s
dht:
enabled: false
bootstrap_peers: []
mode: "auto"
protocol_prefix: "/bzzz"
bootstrap_timeout: 30s
discovery_interval: 1m0s
auto_bootstrap: false
semantic_addressing:
enable_wildcards: true
default_agent: "any"
default_role: "any"
default_project: "any"
enable_role_hierarchy: true
feature_flags:
uri_protocol: false
semantic_addressing: false
dht_discovery: false
advanced_resolution: false
ucxl:
enabled: false
server:
port: 8081
base_path: "/bzzz"
enabled: true
resolution:
cache_ttl: 5m0s
enable_wildcards: true
max_results: 50
storage:
type: "filesystem"
directory: "/tmp/bzzz-ucxl-storage"
max_size: 104857600
p2p_integration:
enable_announcement: true
enable_discovery: true
announcement_topic: "bzzz/ucxl/announcement/v1"
discovery_timeout: 30s
security:
admin_key_shares:
threshold: 3
total_shares: 5
election_config:
heartbeat_timeout: 5s
discovery_timeout: 30s
election_timeout: 15s
max_discovery_attempts: 6
discovery_backoff: 5s
minimum_quorum: 3
consensus_algorithm: "raft"
split_brain_detection: true
conflict_resolution: "highest_uptime"
key_rotation_days: 90
audit_logging: true
audit_path: ".bzzz/security-audit.log"
ai:
ollama:
endpoint: "http://192.168.1.27:11434"
timeout: 30s
models: ["phi3", "llama3.1"]
openai:
api_key: ""
endpoint: "https://api.openai.com/v1"
timeout: 30s
`, hostname, hostname)
return configYAML, nil
}
// GenerateConfigForMachineSimple generates a simple BZZZ configuration that matches the working config structure
// REVENUE CRITICAL: This method now properly processes license data to enable revenue protection
func (s *SetupManager) GenerateConfigForMachineSimple(machineIP string, config interface{}) (string, error) {
// CRITICAL FIX: Extract license data from setup configuration - this was being ignored!
// This fix enables revenue protection by ensuring license data is saved in configuration
configMap, ok := config.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T", config)
}
// Use machine IP to determine hostname (simplified)
hostname := strings.ReplaceAll(machineIP, ".", "-")
// REVENUE CRITICAL: Extract license data from setup configuration
// This ensures license data collected during setup is actually saved in the configuration
var licenseData map[string]interface{}
if license, exists := configMap["license"]; exists {
if licenseMap, ok := license.(map[string]interface{}); ok {
licenseData = licenseMap
}
}
// Validate license data exists - FAIL CLOSED DESIGN
if licenseData == nil {
return "", fmt.Errorf("REVENUE PROTECTION: License data missing from setup configuration - BZZZ cannot be deployed without valid licensing")
}
// Extract required license fields with validation
email, _ := licenseData["email"].(string)
licenseKey, _ := licenseData["licenseKey"].(string)
orgName, _ := licenseData["organizationName"].(string)
if email == "" || licenseKey == "" {
return "", fmt.Errorf("REVENUE PROTECTION: Email and license key are required - cannot deploy BZZZ without valid licensing")
}
// Generate unique cluster ID for license binding (prevents license sharing across clusters)
clusterID := fmt.Sprintf("cluster-%s-%d", hostname, time.Now().Unix())
// Generate YAML configuration with FULL license integration for revenue protection
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s - REVENUE PROTECTED
# Generated at %s with license validation
whoosh_api:
base_url: "https://whoosh.home.deepblack.cloud"
api_key: ""
timeout: 30s
retry_count: 3
agent:
id: "%s-agent"
capabilities: ["general"]
poll_interval: 30s
max_tasks: 2
models: []
specialization: ""
model_selection_webhook: ""
default_reasoning_model: ""
sandbox_image: ""
role: ""
system_prompt: ""
reports_to: []
expertise: []
deliverables: []
collaboration:
preferred_message_types: []
auto_subscribe_to_roles: []
auto_subscribe_to_expertise: []
response_timeout_seconds: 0
max_collaboration_depth: 0
escalation_threshold: 0
custom_topic_subscriptions: []
github:
token_file: ""
user_agent: "BZZZ-Agent/1.0"
timeout: 30s
rate_limit: true
assignee: ""
p2p:
service_tag: "bzzz-peer-discovery"
bzzz_topic: "bzzz/coordination/v1"
hmmm_topic: "hmmm/meta-discussion/v1"
discovery_timeout: 10s
escalation_webhook: ""
escalation_keywords: []
conversation_limit: 10
logging:
level: "info"
format: "text"
output: "stdout"
structured: false
slurp:
enabled: false
base_url: ""
api_key: ""
timeout: 30s
retry_count: 3
max_concurrent_requests: 10
request_queue_size: 100
v2:
enabled: false
protocol_version: "2.0.0"
uri_resolution:
cache_ttl: 5m0s
max_peers_per_result: 5
default_strategy: "best_match"
resolution_timeout: 30s
dht:
enabled: false
bootstrap_peers: []
mode: "auto"
protocol_prefix: "/bzzz"
bootstrap_timeout: 30s
discovery_interval: 1m0s
auto_bootstrap: false
semantic_addressing:
enable_wildcards: true
default_agent: "any"
default_role: "any"
default_project: "any"
enable_role_hierarchy: true
feature_flags:
uri_protocol: false
semantic_addressing: false
dht_discovery: false
advanced_resolution: false
ucxl:
enabled: false
server:
port: 8081
base_path: "/bzzz"
enabled: false
resolution:
cache_ttl: 5m0s
enable_wildcards: true
max_results: 50
storage:
type: "filesystem"
directory: "/tmp/bzzz-ucxl-storage"
max_size: 104857600
p2p_integration:
enable_announcement: false
enable_discovery: false
announcement_topic: "bzzz/ucxl/announcement/v1"
discovery_timeout: 30s
security:
admin_key_shares:
threshold: 3
total_shares: 5
election_config:
heartbeat_timeout: 5s
discovery_timeout: 30s
election_timeout: 15s
max_discovery_attempts: 6
discovery_backoff: 5s
minimum_quorum: 3
consensus_algorithm: "raft"
split_brain_detection: true
conflict_resolution: "highest_uptime"
key_rotation_days: 90
audit_logging: false
audit_path: ""
ai:
ollama:
endpoint: ""
timeout: 30s
models: []
openai:
api_key: ""
endpoint: "https://api.openai.com/v1"
timeout: 30s
# REVENUE CRITICAL: License configuration enables revenue protection
license:
email: "%s"
license_key: "%s"
organization_name: "%s"
cluster_id: "%s"
cluster_name: "%s-cluster"
kaching_url: "https://kaching.chorus.services"
heartbeat_minutes: 60
grace_period_hours: 24
last_validated: "%s"
validation_token: ""
license_type: ""
max_nodes: 0
expires_at: "0001-01-01T00:00:00Z"
is_active: true
`, hostname, time.Now().Format(time.RFC3339), email, licenseKey, orgName, clusterID, hostname, time.Now().Format(time.RFC3339))
return configYAML, nil
}
// generateAndDeployConfig generates node-specific config.yaml and deploys it
func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string, config interface{}) error {
// Get hostname for unique agent ID
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
if err := session.Run("hostname"); err != nil {
return fmt.Errorf("failed to get hostname: %w", err)
}
hostname := strings.TrimSpace(stdout.String())
// Generate YAML configuration using the shared method
configYAML, err := s.GenerateConfigForMachineSimple(hostname, config)
if err != nil {
return fmt.Errorf("failed to generate config: %w", err)
}
// Create configuration directory
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("mkdir -p ~/.bzzz ~/.bzzz/data ~/.bzzz/logs"); err != nil {
return fmt.Errorf("failed to create config directories: %w", err)
}
// Deploy configuration file
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
return err
}
go func() {
defer stdin.Close()
stdin.Write([]byte(configYAML))
}()
if err := session.Run("cat > ~/.bzzz/config.yaml"); err != nil {
return fmt.Errorf("failed to deploy config file: %w", err)
}
return nil
}
// configureFirewall configures firewall rules for BZZZ ports
func (s *SetupManager) configureFirewall(client *ssh.Client, config interface{}) error {
// Extract ports from configuration
configMap, ok := config.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid configuration format in firewall: expected map[string]interface{}, got %T: %+v", config, config)
}
ports := []string{"22"} // Always include SSH
// Add BZZZ ports
if portsConfig, exists := configMap["ports"]; exists {
if portsMap, ok := portsConfig.(map[string]interface{}); ok {
for _, value := range portsMap {
if portStr := fmt.Sprintf("%v", value); portStr != "" {
ports = append(ports, portStr)
}
}
}
}
// Detect firewall system and configure rules
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try ufw first (Ubuntu/Debian)
if err := session.Run("which ufw > /dev/null 2>&1"); err == nil {
return s.configureUFW(client, ports)
}
// Try firewalld (RHEL/CentOS/Fedora)
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("which firewall-cmd > /dev/null 2>&1"); err == nil {
return s.configureFirewalld(client, ports)
}
// If no firewall detected, that's okay - just log it
return nil
}
// configureUFW configures UFW firewall rules
func (s *SetupManager) configureUFW(client *ssh.Client, ports []string) error {
for _, port := range ports {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try with sudo, ignore failures for non-sudo users
cmd := fmt.Sprintf("sudo -n ufw allow %s 2>/dev/null || true", port)
session.Run(cmd)
}
return nil
}
// configureFirewalld configures firewalld rules
func (s *SetupManager) configureFirewalld(client *ssh.Client, ports []string) error {
for _, port := range ports {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try with sudo, ignore failures for non-sudo users
cmd := fmt.Sprintf("sudo -n firewall-cmd --permanent --add-port=%s/tcp 2>/dev/null || true", port)
session.Run(cmd)
}
// Reload firewall rules
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Run("sudo -n firewall-cmd --reload 2>/dev/null || true")
return nil
}
// ValidateOllamaEndpoint tests if an Ollama endpoint is accessible and returns available models
func (s *SetupManager) ValidateOllamaEndpoint(endpoint string) (bool, []string, error) {
if endpoint == "" {
return false, nil, fmt.Errorf("endpoint cannot be empty")
}
// Ensure endpoint has proper format
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
endpoint = "http://" + endpoint
}
// Create HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
// Test connection to /api/tags endpoint
apiURL := strings.TrimRight(endpoint, "/") + "/api/tags"
resp, err := client.Get(apiURL)
if err != nil {
return false, nil, fmt.Errorf("failed to connect to Ollama API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, nil, fmt.Errorf("Ollama API returned status %d", resp.StatusCode)
}
// Parse the response to get available models
var tagsResponse struct {
Models []struct {
Name string `json:"name"`
} `json:"models"`
}
if err := json.NewDecoder(resp.Body).Decode(&tagsResponse); err != nil {
return false, nil, fmt.Errorf("failed to decode Ollama response: %w", err)
}
// Extract model names
var models []string
for _, model := range tagsResponse.Models {
models = append(models, model.Name)
}
return true, models, nil
}