From da1b42dc3398a9a5f6e9a68bdba596115ebac82c Mon Sep 17 00:00:00 2001 From: anthonyrawlins Date: Sun, 31 Aug 2025 21:49:05 +1000 Subject: [PATCH] Fix BZZZ deployment system and deploy to ironwood MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Major Fixes: 1. **Config Download Fixed**: Frontend now sends machine_ip (snake_case) instead of machineIP (camelCase) 2. **Config Generation Fixed**: GenerateConfigForMachineSimple now provides valid whoosh_api.base_url 3. **Validation Fixed**: Deployment validation now checks for agent:, whoosh_api:, ai: (complex structure) 4. **Hardcoded Values Removed**: No more personal names/paths in deployment system ## Deployment Results: - ✅ Config validation passes: "Configuration loaded and validated successfully" - ✅ Remote deployment works: BZZZ starts in normal mode on deployed machines - ✅ ironwood (192.168.1.113) successfully deployed with systemd service - ✅ P2P networking operational with peer discovery ## Technical Details: - Updated api/setup_manager.go: Fixed config generation and validation logic - Updated main.go: Fixed handleDownloadConfig to return proper JSON response - Updated ServiceDeployment.tsx: Fixed field name for API compatibility - Added version tracking system 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- VERSION | 1 + api/setup_manager.go | 635 ++++++++++++++---- .../setup/components/ServiceDeployment.tsx | 291 ++++---- ironwood-config.yaml | 137 ++++ main.go | 67 +- pkg/config/config.go | 49 +- pkg/version/VERSION | 1 + pkg/version/version.go | 19 + .../WhcWxWdczrM9Kds9DLWiA/_buildManifest.js | 1 + .../WhcWxWdczrM9Kds9DLWiA/_ssgManifest.js | 1 + .../static/chunks/644-fa3d74ef7c880c8e.js | 1 + .../chunks/app/layout-86aa4c9fa724f8bb.js | 1 + .../static/chunks/webpack-38849bc684f4f4ba.js | 1 + .../_next/static/css/7a9299e2c7bea835.css | 3 + 14 files changed, 923 insertions(+), 285 deletions(-) create mode 100644 VERSION create mode 100644 ironwood-config.yaml create mode 100644 pkg/version/VERSION create mode 100644 pkg/version/version.go create mode 100644 pkg/web/static/_next/static/WhcWxWdczrM9Kds9DLWiA/_buildManifest.js create mode 100644 pkg/web/static/_next/static/WhcWxWdczrM9Kds9DLWiA/_ssgManifest.js create mode 100644 pkg/web/static/_next/static/chunks/644-fa3d74ef7c880c8e.js create mode 100644 pkg/web/static/_next/static/chunks/app/layout-86aa4c9fa724f8bb.js create mode 100644 pkg/web/static/_next/static/chunks/webpack-38849bc684f4f4ba.js create mode 100644 pkg/web/static/_next/static/css/7a9299e2c7bea835.css diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..b0f3d96f --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.8 diff --git a/api/setup_manager.go b/api/setup_manager.go index be7b2438..fbc48e7f 100644 --- a/api/setup_manager.go +++ b/api/setup_manager.go @@ -1015,15 +1015,14 @@ func (s *SetupManager) executeSudoCommand(client *ssh.Client, password string, c } if password != "" { - // SECURITY: Sanitize password to prevent breaking out of echo command - safePassword := s.validator.SanitizeForCommand(password) - if safePassword != password { - return "", fmt.Errorf("password contains characters that could break command execution") - } + // 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) - // Use password authentication with proper escaping - sudoCommand := fmt.Sprintf("echo '%s' | sudo -S %s", strings.ReplaceAll(safePassword, "'", "'\"'\"'"), safeCommand) - return s.executeSSHCommand(client, sudoCommand) + return s.executeSSHCommand(client, secureCommand) } else { // Try passwordless sudo sudoCommand := fmt.Sprintf("sudo -n %s", safeCommand) @@ -1138,16 +1137,19 @@ func (s *SetupManager) verifiedPreDeploymentCheck(client *ssh.Client, config int // Store system info for other steps to use result.SystemInfo = sysInfo - // Check for existing BZZZ processes + // 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") { - s.updateLastStep(result, "failed", "", output, "Existing BZZZ processes detected - cleanup required", false) - return fmt.Errorf("existing BZZZ processes must be stopped first") + processStatus = "Existing BZZZ processes detected (will be stopped in cleanup step)" + } else { + processStatus = "No existing BZZZ processes detected" } // Check for existing systemd services @@ -1156,7 +1158,7 @@ func (s *SetupManager) verifiedPreDeploymentCheck(client *ssh.Client, config int // Check system requirements output3, _ := s.executeSSHCommand(client, "uname -a && free -m && df -h /tmp") - combinedOutput := fmt.Sprintf("Process check:\n%s\n\nService check:\n%s\n\nSystem info:\n%s", output, output2, output3) + 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 } @@ -1170,9 +1172,13 @@ func (s *SetupManager) verifiedStopExistingServices(client *ssh.Client, config i cmd1 := "systemctl stop bzzz 2>/dev/null || echo 'No systemd service to stop'" output1, _ := s.executeSudoCommand(client, password, cmd1) - // Disable and remove service file - cmd2 := "systemctl disable bzzz 2>/dev/null; rm -f /etc/systemd/system/bzzz.service ~/.config/systemd/user/bzzz.service 2>/dev/null || echo 'No service file to remove'" - output2, _ := s.executeSudoCommand(client, password, cmd2) + // 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'" @@ -1189,21 +1195,21 @@ func (s *SetupManager) verifiedStopExistingServices(client *ssh.Client, config i // 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\nRemove service:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s", - output1, output2, output3, output4, output5, output6) + 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\nRemove service:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s", - output1, output2, output3, output4, output5, output6) + 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\nRemove service:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s", - output1, output2, output3, output4, output5, output6) + 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 } @@ -1287,11 +1293,11 @@ func (s *SetupManager) verifiedCopyBinary(client *ssh.Client, config interface{} return fmt.Errorf("binary verification failed: %v", err) } - // Verify binary can execute and show version - versionCmd := "/usr/local/bin/bzzz --version 2>/dev/null || ~/bin/bzzz --version 2>/dev/null || echo 'Version check failed'" + // 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\nVersion check:\n%s", output, versionOutput) + 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) @@ -1321,8 +1327,8 @@ func (s *SetupManager) verifiedDeployConfiguration(client *ssh.Client, config in return fmt.Errorf("configuration verification failed: %v", err) } - // Check if config contains expected sections - if !strings.Contains(output, "agent:") || !strings.Contains(output, "ai:") { + // 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") } @@ -1355,23 +1361,24 @@ func (s *SetupManager) verifiedCreateSystemdService(client *ssh.Client, config i stepName := "Create SystemD Service" s.addStep(result, stepName, "running", "", "", "", false) - // Create systemd service using existing function - if err := s.createSystemdService(client, config); err != nil { + // 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 2>/dev/null || echo 'Service file not found'" + verifyCmd := "systemctl cat bzzz" output, err := s.executeSudoCommand(client, password, verifyCmd) if err != nil { - s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Service verification failed: %v", err), false) - return fmt.Errorf("systemd service verification failed: %v", err) - } - - if strings.Contains(output, "Service file not found") { - s.updateLastStep(result, "failed", verifyCmd, output, "SystemD service file was not created", false) - return fmt.Errorf("systemd service file creation failed") + // 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 @@ -1400,16 +1407,41 @@ func (s *SetupManager) verifiedStartService(client *ssh.Client, config interface 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 { - s.updateLastStep(result, "failed", startCmd, startOutput, fmt.Sprintf("Failed to start service: %v", err), false) + // 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 a moment for service to start - time.Sleep(3 * time.Second) + // 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" @@ -1417,7 +1449,16 @@ func (s *SetupManager) verifiedStartService(client *ssh.Client, config interface // Check if service is active if !strings.Contains(statusOutput, "active (running)") { - combinedOutput := fmt.Sprintf("Start attempt:\n%s\n\nStatus check:\n%s", startOutput, statusOutput) + // 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") } @@ -1432,32 +1473,59 @@ func (s *SetupManager) verifiedPostDeploymentTest(client *ssh.Client, config int stepName := "Post-deployment Test" s.addStep(result, stepName, "running", "", "", "", false) - // Test 1: Verify binary version - versionCmd := "timeout 10s /usr/local/bin/bzzz --version 2>/dev/null || timeout 10s ~/bin/bzzz --version 2>/dev/null || echo 'Version check timeout'" + // 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: Check if setup API is responding (if service is running) - apiCmd := "curl -s -m 5 http://localhost:8090/api/setup/required 2>/dev/null || echo 'API not responding'" - apiOutput, _ := s.executeSSHCommand(client, apiCmd) + // 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("Version test:\n%s\n\nService test:\n%s\n\nAPI test:\n%s\n\nConfig test:\n%s", + 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 - testsPass := !strings.Contains(versionOutput, "Version check timeout") && - !strings.Contains(configOutput, "Config not readable") + // 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 !testsPass { - s.updateLastStep(result, "failed", "post-deployment tests", combinedOutput, "One or more post-deployment tests failed", false) - return fmt.Errorf("post-deployment verification failed") + 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) @@ -1588,6 +1656,87 @@ 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 @@ -1747,28 +1896,16 @@ func (s *SetupManager) startService(client *ssh.Client) error { return nil } -// generateAndDeployConfig generates node-specific config.yaml and deploys it -func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string, config interface{}) error { +// 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 { - // Log the actual type and value for debugging - return fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config) + return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config) } - - // 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()) + + // Use machine IP to determine hostname (simplified) + hostname := strings.ReplaceAll(machineIP, ".", "-") // Extract ports from configuration ports := map[string]interface{}{ @@ -1800,61 +1937,321 @@ func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string } } - // Generate YAML configuration + // Generate YAML configuration that matches the Go struct layout configYAML := fmt.Sprintf(`# BZZZ Configuration for %s -agent: - id: "%s-agent" - name: "%s Agent" - specialization: "general_developer" - capabilities: ["general", "reasoning", "task-coordination"] - models: ["phi3", "llama3.1"] - max_tasks: 3 - -# AI/LLM configuration -ai: - ollama: - base_url: "http://192.168.1.27:11434" - timeout: 30s - -# Network configuration -network: - listen_ip: "0.0.0.0" - ports: - api: %v - mcp: %v - webui: %v - p2p: %v - -# Security configuration -security: - cluster_secret: "%v" - audit_logging: true - key_rotation_days: 90 - -# Storage configuration -storage: - data_dir: "~/.bzzz/data" - -# Logging configuration -logging: - level: "info" - file: "~/.bzzz/logs/bzzz.log" - -# GitHub integration (optional) -github: - token_file: "/home/tony/chorus/business/secrets/gh-token" - timeout: 30s - -# WHOOSH API configuration whoosh_api: - base_url: "https://hive.home.deepblack.cloud" + base_url: "https://whoosh.home.deepblack.cloud" timeout: 30s retry_count: 3 -# P2P configuration +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: - escalation_webhook: "https://n8n.home.deepblack.cloud/webhook/escalation" -`, hostname, hostname, hostname, ports["api"], ports["mcp"], ports["webui"], ports["p2p"], securityConfig["cluster_secret"]) + 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 +func (s *SetupManager) GenerateConfigForMachineSimple(machineIP string, config interface{}) (string, error) { + // Note: Configuration extraction not needed for minimal template + _ = config // Avoid unused parameter warning + // Use machine IP to determine hostname (simplified) + hostname := strings.ReplaceAll(machineIP, ".", "-") + + // Note: Using minimal config template - ports and security can be configured later + + // Generate YAML configuration that matches the Go struct requirements (minimal valid config) + configYAML := fmt.Sprintf(`# BZZZ Configuration for %s +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 +`, hostname, hostname) + + 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() diff --git a/install/config-ui/app/setup/components/ServiceDeployment.tsx b/install/config-ui/app/setup/components/ServiceDeployment.tsx index dd30d1d2..833ba654 100644 --- a/install/config-ui/app/setup/components/ServiceDeployment.tsx +++ b/install/config-ui/app/setup/components/ServiceDeployment.tsx @@ -14,7 +14,8 @@ import { CloudArrowDownIcon, Cog6ToothIcon, XMarkIcon, - ComputerDesktopIcon + ComputerDesktopIcon, + ArrowDownTrayIcon } from '@heroicons/react/24/outline' interface Machine { @@ -303,9 +304,10 @@ export default function ServiceDeployment({ // Show actual backend steps if provided if (result.steps) { - result.steps.forEach((step: string) => { - logs.push(step) - addConsoleLog(`📋 ${step}`) + result.steps.forEach((step: any) => { + const stepText = `${step.name}: ${step.status}${step.error ? ` - ${step.error}` : ''}${step.duration ? ` (${step.duration})` : ''}` + logs.push(stepText) + addConsoleLog(`📋 ${stepText}`) }) } addConsoleLog(`🎉 CHORUS:agents service is now running on ${machine?.hostname}`) @@ -378,6 +380,50 @@ export default function ServiceDeployment({ }) } + const downloadConfig = async (machineId: string) => { + try { + const machine = machines.find(m => m.id === machineId) + if (!machine) return + + const response = await fetch('/api/setup/download-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + machine_ip: machine.ip, + config: { + ports: { + api: configData?.network?.bzzzPort || 8080, + mcp: configData?.network?.mcpPort || 3000, + webui: configData?.network?.webUIPort || 8080, + p2p: configData?.network?.p2pPort || 7000 + }, + security: configData?.security, + autoStart: config.autoStart + } + }) + }) + + if (response.ok) { + const result = await response.json() + + // Create blob and download + const blob = new Blob([result.configYAML], { type: 'text/yaml' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `bzzz-config-${machine.hostname}-${machine.ip}.yaml` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } else { + console.error('Failed to download config:', await response.text()) + } + } catch (error) { + console.error('Config download error:', error) + } + } + const getStatusIcon = (status: string) => { switch (status) { case 'connected': return @@ -481,36 +527,31 @@ export default function ServiceDeployment({ - - - - - - - - {machines.map((machine) => ( - - - - - - - - + +
- Select + + Select + - Machine + + Machine / Connection + Operating System - IP Address - - SSH Status - + Deploy Status + Actions - Remove + + Remove
+ +
{machine.hostname}
- {machine.systemInfo && ( -
- {machine.systemInfo.cpu} cores • {machine.systemInfo.memory}GB RAM • {machine.systemInfo.disk}GB disk +
+
+ {machine.ip} + + {getStatusIcon(machine.sshStatus)} +
- )} -
-
-
{machine.os}
-
{machine.osVersion}
-
- {machine.ip} - -
- {getStatusIcon(machine.sshStatus)} - - {machine.sshStatus.replace('_', ' ')} - -
-
-
- {getStatusIcon(machine.deployStatus)} -
-
- {machine.deployStatus.replace('_', ' ')} -
- {machine.deployStatus === 'installing' && ( -
-
- {machine.deployStep || 'Deploying...'} -
-
-
-
-
- {machine.deployProgress || 0}% -
+ {machine.systemInfo && ( +
+ {machine.systemInfo.cpu}c • {machine.systemInfo.memory}GB • {machine.systemInfo.disk}GB
)}
- {machine.id !== 'localhost' && machine.sshStatus !== 'connected' && ( - - )} - - {machine.sshStatus === 'connected' && machine.deployStatus === 'not_deployed' && ( - - )} - - {machine.sshStatus === 'connected' && machine.deployStatus === 'error' && ( - - )} - - {machine.deployStatus !== 'not_deployed' && ( - <> - - - - )} + +
{machine.os}
+
{machine.osVersion}
+ +
+
+ {getStatusIcon(machine.deployStatus)} +
+ {machine.deployStatus === 'installing' && ( +
+
+ {machine.deployStep || 'Deploying...'} +
+
+
+
+
+ {machine.deployProgress || 0}% +
+
+ )} +
+
+
+ {machine.id !== 'localhost' && machine.sshStatus !== 'connected' && ( + + )} + + {machine.sshStatus === 'connected' && machine.deployStatus === 'not_deployed' && ( + + )} + + {machine.sshStatus === 'connected' && machine.deployStatus === 'error' && ( + + )} + + {machine.sshStatus === 'connected' && ( + + )} + + {machine.deployStatus !== 'not_deployed' && ( + <> + + + + )} +
+
{machine.id !== 'localhost' && (