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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user