Enhance deployment system with retry functionality and improved UX

Major Improvements:
- Added retry deployment buttons in machine list for failed deployments
- Added retry button in SSH console modal footer for enhanced UX
- Enhanced deployment process with comprehensive cleanup of existing services
- Improved binary installation with password-based sudo authentication
- Updated configuration generation to include all required sections (agent, ai, network, security)
- Fixed deployment verification and error handling

Security Enhancements:
- Enhanced verifiedStopExistingServices with thorough cleanup process
- Improved binary copying with proper sudo authentication
- Added comprehensive configuration validation

UX Improvements:
- Users can retry deployments without re-running machine discovery
- Retry buttons available from both machine list and console modal
- Real-time deployment progress with detailed console output
- Clear error states with actionable retry options

Technical Changes:
- Modified ServiceDeployment.tsx with retry button components
- Enhanced api/setup_manager.go with improved deployment functions
- Updated main.go with command line argument support (--config, --setup)
- Added comprehensive zero-trust security validation system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-08-31 10:23:27 +10:00
parent df4d98bf30
commit be761cfe20
234 changed files with 7508 additions and 38528 deletions

View File

@@ -7,6 +7,7 @@ import (
"strings"
"time"
"chorus.services/bzzz/pkg/security"
"gopkg.in/yaml.v2"
)
@@ -22,6 +23,26 @@ type SecurityConfig struct {
AuditPath string `yaml:"audit_path,omitempty" json:"audit_path,omitempty"`
}
// AIConfig holds AI/LLM integration settings
type AIConfig struct {
Ollama OllamaConfig `yaml:"ollama"`
OpenAI OpenAIConfig `yaml:"openai"`
}
// OllamaConfig holds Ollama API configuration
type OllamaConfig struct {
Endpoint string `yaml:"endpoint"`
Timeout time.Duration `yaml:"timeout"`
Models []string `yaml:"models"`
}
// OpenAIConfig holds OpenAI API configuration
type OpenAIConfig struct {
APIKey string `yaml:"api_key"`
Endpoint string `yaml:"endpoint"`
Timeout time.Duration `yaml:"timeout"`
}
// Config represents the complete configuration for a Bzzz agent
type Config struct {
WHOOSHAPI WHOOSHAPIConfig `yaml:"hive_api"`
@@ -34,6 +55,7 @@ type Config struct {
V2 V2Config `yaml:"v2"` // BZZZ v2 protocol settings
UCXL UCXLConfig `yaml:"ucxl"` // UCXL protocol settings
Security SecurityConfig `yaml:"security"` // Cluster security and elections
AI AIConfig `yaml:"ai"` // AI/LLM integration settings
}
// WHOOSHAPIConfig holds WHOOSH system integration settings
@@ -385,20 +407,49 @@ func getDefaultConfig() *Config {
"advanced_resolution": false,
},
},
AI: AIConfig{
Ollama: OllamaConfig{
Endpoint: "http://localhost:11434",
Timeout: 30 * time.Second,
Models: []string{"phi3", "llama3.1"},
},
OpenAI: OpenAIConfig{
APIKey: "",
Endpoint: "https://api.openai.com/v1",
Timeout: 30 * time.Second,
},
},
}
}
// loadFromFile loads configuration from a YAML file
// loadFromFile loads configuration from a YAML file with zero-trust validation
func loadFromFile(config *Config, filePath string) error {
// SECURITY: Validate file path to prevent directory traversal
validator := security.NewSecurityValidator()
if err := validator.ValidateFilePath(filePath); err != nil {
return fmt.Errorf("security validation failed for config path: %w", err)
}
data, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// SECURITY: Limit file size to prevent memory exhaustion attacks
maxConfigSize := 1024 * 1024 // 1MB limit for config files
if len(data) > maxConfigSize {
return fmt.Errorf("config file too large: %d bytes exceeds limit of %d bytes", len(data), maxConfigSize)
}
if err := yaml.Unmarshal(data, config); err != nil {
return fmt.Errorf("failed to parse YAML config: %w", err)
}
// SECURITY: Validate all configuration values after loading
if err := validateConfigSecurity(config, validator); err != nil {
return fmt.Errorf("security validation failed for loaded config: %w", err)
}
return nil
}
@@ -582,4 +633,185 @@ func IsValidConfiguration(config *Config) bool {
// Configuration appears valid
return true
}
// validateConfigSecurity performs zero-trust validation on all configuration values
func validateConfigSecurity(config *Config, validator *security.SecurityValidator) error {
// Validate Agent configuration
if config.Agent.ID != "" {
// Agent IDs should be alphanumeric identifiers
if len(config.Agent.ID) > 64 {
return fmt.Errorf("agent ID too long (max 64 characters): %s", config.Agent.ID)
}
if !isAlphanumericWithDashes(config.Agent.ID) {
return fmt.Errorf("agent ID contains invalid characters: %s", config.Agent.ID)
}
}
// Validate specialization
if config.Agent.Specialization != "" {
if len(config.Agent.Specialization) > 64 {
return fmt.Errorf("specialization too long (max 64 characters)")
}
if !isAlphanumericWithUnderscore(config.Agent.Specialization) {
return fmt.Errorf("specialization contains invalid characters: %s", config.Agent.Specialization)
}
}
// Validate role
if config.Agent.Role != "" {
if len(config.Agent.Role) > 32 {
return fmt.Errorf("role too long (max 32 characters)")
}
if !isAlphanumericWithUnderscore(config.Agent.Role) {
return fmt.Errorf("role contains invalid characters: %s", config.Agent.Role)
}
}
// Validate capabilities list
if len(config.Agent.Capabilities) > 20 {
return fmt.Errorf("too many capabilities (max 20)")
}
for _, capability := range config.Agent.Capabilities {
if len(capability) > 32 {
return fmt.Errorf("capability name too long (max 32 characters): %s", capability)
}
if !isAlphanumericWithUnderscore(capability) {
return fmt.Errorf("capability contains invalid characters: %s", capability)
}
}
// Validate models list
if len(config.Agent.Models) > 50 {
return fmt.Errorf("too many models (max 50)")
}
for _, model := range config.Agent.Models {
if len(model) > 64 {
return fmt.Errorf("model name too long (max 64 characters): %s", model)
}
if !isAlphanumericWithDots(model) {
return fmt.Errorf("model name contains invalid characters: %s", model)
}
}
// Validate URLs and webhooks
if config.WHOOSHAPI.BaseURL != "" {
if err := validateURL(config.WHOOSHAPI.BaseURL); err != nil {
return fmt.Errorf("invalid WHOOSH API URL: %w", err)
}
}
if config.Agent.ModelSelectionWebhook != "" {
if err := validateURL(config.Agent.ModelSelectionWebhook); err != nil {
return fmt.Errorf("invalid model selection webhook URL: %w", err)
}
}
if config.P2P.EscalationWebhook != "" {
if err := validateURL(config.P2P.EscalationWebhook); err != nil {
return fmt.Errorf("invalid escalation webhook URL: %w", err)
}
}
// Validate file paths
if config.GitHub.TokenFile != "" {
if err := validator.ValidateFilePath(config.GitHub.TokenFile); err != nil {
return fmt.Errorf("invalid GitHub token file path: %w", err)
}
}
if config.Security.AuditPath != "" {
if err := validator.ValidateFilePath(config.Security.AuditPath); err != nil {
return fmt.Errorf("invalid audit path: %w", err)
}
}
// Validate numeric limits
if config.Agent.MaxTasks < 1 || config.Agent.MaxTasks > 100 {
return fmt.Errorf("invalid max tasks: must be between 1 and 100, got %d", config.Agent.MaxTasks)
}
if config.WHOOSHAPI.RetryCount < 0 || config.WHOOSHAPI.RetryCount > 10 {
return fmt.Errorf("invalid retry count: must be between 0 and 10, got %d", config.WHOOSHAPI.RetryCount)
}
if config.Security.KeyRotationDays < 0 || config.Security.KeyRotationDays > 365 {
return fmt.Errorf("invalid key rotation days: must be between 0 and 365, got %d", config.Security.KeyRotationDays)
}
// Validate timeout durations (prevent extremely long timeouts that could cause DoS)
maxTimeout := 300 * time.Second // 5 minutes max
if config.WHOOSHAPI.Timeout > maxTimeout {
return fmt.Errorf("WHOOSH API timeout too long (max 5 minutes): %v", config.WHOOSHAPI.Timeout)
}
if config.GitHub.Timeout > maxTimeout {
return fmt.Errorf("GitHub timeout too long (max 5 minutes): %v", config.GitHub.Timeout)
}
if config.Agent.PollInterval > 3600*time.Second {
return fmt.Errorf("poll interval too long (max 1 hour): %v", config.Agent.PollInterval)
}
return nil
}
// Helper validation functions
func isAlphanumericWithDashes(s string) bool {
if len(s) == 0 {
return false
}
for _, char := range s {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '-' || char == '_') {
return false
}
}
return true
}
func isAlphanumericWithUnderscore(s string) bool {
if len(s) == 0 {
return false
}
for _, char := range s {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '_') {
return false
}
}
return true
}
func isAlphanumericWithDots(s string) bool {
if len(s) == 0 {
return false
}
for _, char := range s {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '.' || char == '-' || char == '_') {
return false
}
}
return true
}
func validateURL(url string) error {
if len(url) > 2048 {
return fmt.Errorf("URL too long (max 2048 characters)")
}
// Basic URL validation - should start with http:// or https://
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return fmt.Errorf("URL must start with http:// or https://")
}
// Check for dangerous characters that could be used for injection
dangerousChars := []string{"`", "$", ";", "|", "&", "<", ">", "\n", "\r"}
for _, char := range dangerousChars {
if strings.Contains(url, char) {
return fmt.Errorf("URL contains dangerous characters")
}
}
return nil
}