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>
310 lines
8.6 KiB
Go
310 lines
8.6 KiB
Go
package runtime
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"chorus.services/bzzz/pkg/ucxl"
|
|
"chorus.services/bzzz/pubsub"
|
|
)
|
|
|
|
// TaskTracker implements the SimpleTaskTracker interface
|
|
type TaskTracker struct {
|
|
maxTasks int
|
|
activeTasks map[string]bool
|
|
decisionPublisher *ucxl.DecisionPublisher
|
|
pubsub *pubsub.PubSub
|
|
nodeID string
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewTaskTracker creates a new task tracker
|
|
func NewTaskTracker(maxTasks int, nodeID string, ps *pubsub.PubSub) SimpleTaskTracker {
|
|
return &TaskTracker{
|
|
maxTasks: maxTasks,
|
|
activeTasks: make(map[string]bool),
|
|
pubsub: ps,
|
|
nodeID: nodeID,
|
|
}
|
|
}
|
|
|
|
// GetActiveTasks returns list of active task IDs
|
|
func (t *TaskTracker) GetActiveTasks() []string {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
tasks := make([]string, 0, len(t.activeTasks))
|
|
for taskID := range t.activeTasks {
|
|
tasks = append(tasks, taskID)
|
|
}
|
|
return tasks
|
|
}
|
|
|
|
// GetMaxTasks returns maximum number of concurrent tasks
|
|
func (t *TaskTracker) GetMaxTasks() int {
|
|
return t.maxTasks
|
|
}
|
|
|
|
// AddTask marks a task as active
|
|
func (t *TaskTracker) AddTask(taskID string) {
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
t.activeTasks[taskID] = true
|
|
}
|
|
|
|
// RemoveTask marks a task as completed and publishes decision if publisher available
|
|
func (t *TaskTracker) RemoveTask(taskID string) {
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
delete(t.activeTasks, taskID)
|
|
|
|
// Publish task completion decision if publisher is available
|
|
if t.decisionPublisher != nil {
|
|
go t.publishTaskCompletion(taskID, true, "Task completed successfully", nil)
|
|
}
|
|
}
|
|
|
|
// CompleteTaskWithDecision marks a task as completed and publishes detailed decision
|
|
func (t *TaskTracker) CompleteTaskWithDecision(taskID string, success bool, summary string, filesModified []string) {
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
delete(t.activeTasks, taskID)
|
|
|
|
// Publish task completion decision if publisher is available
|
|
if t.decisionPublisher != nil {
|
|
go t.publishTaskCompletion(taskID, success, summary, filesModified)
|
|
}
|
|
}
|
|
|
|
// SetDecisionPublisher sets the decision publisher for task completion tracking
|
|
func (t *TaskTracker) SetDecisionPublisher(publisher *ucxl.DecisionPublisher) {
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
t.decisionPublisher = publisher
|
|
}
|
|
|
|
// publishTaskCompletion publishes a task completion decision to DHT
|
|
func (t *TaskTracker) publishTaskCompletion(taskID string, success bool, summary string, filesModified []string) {
|
|
if t.decisionPublisher == nil {
|
|
return
|
|
}
|
|
|
|
if err := t.decisionPublisher.PublishTaskCompletion(taskID, success, summary, filesModified); err != nil {
|
|
fmt.Printf("⚠️ Failed to publish task completion for %s: %v\n", taskID, err)
|
|
} else {
|
|
fmt.Printf("📤 Published task completion decision for: %s\n", taskID)
|
|
}
|
|
}
|
|
|
|
// IsAvailable returns whether the tracker can accept new tasks
|
|
func (t *TaskTracker) IsAvailable() bool {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
return len(t.activeTasks) < t.maxTasks
|
|
}
|
|
|
|
// GetStatus returns the current status string
|
|
func (t *TaskTracker) GetStatus() string {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
currentTasks := len(t.activeTasks)
|
|
|
|
if currentTasks >= t.maxTasks {
|
|
return "busy"
|
|
} else if currentTasks > 0 {
|
|
return "working"
|
|
}
|
|
return "ready"
|
|
}
|
|
|
|
// AnnounceAvailability starts a goroutine that broadcasts current working status
|
|
func (t *TaskTracker) AnnounceAvailability() {
|
|
if t.pubsub == nil {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
t.mutex.RLock()
|
|
currentTasks := t.GetActiveTasks()
|
|
maxTasks := t.maxTasks
|
|
isAvailable := len(currentTasks) < maxTasks
|
|
status := t.GetStatus()
|
|
t.mutex.RUnlock()
|
|
|
|
availability := map[string]interface{}{
|
|
"node_id": t.nodeID,
|
|
"available_for_work": isAvailable,
|
|
"current_tasks": len(currentTasks),
|
|
"max_tasks": maxTasks,
|
|
"last_activity": time.Now().Unix(),
|
|
"status": status,
|
|
"timestamp": time.Now().Unix(),
|
|
}
|
|
|
|
if err := t.pubsub.PublishBzzzMessage(pubsub.AvailabilityBcast, availability); err != nil {
|
|
fmt.Printf("❌ Failed to announce availability: %v\n", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// CapabilityAnnouncer handles capability announcements
|
|
type CapabilityAnnouncer struct {
|
|
pubsub *pubsub.PubSub
|
|
nodeID string
|
|
logger interface{} // Using interface to avoid import cycles
|
|
}
|
|
|
|
// NewCapabilityAnnouncer creates a new capability announcer
|
|
func NewCapabilityAnnouncer(ps *pubsub.PubSub, nodeID string) *CapabilityAnnouncer {
|
|
return &CapabilityAnnouncer{
|
|
pubsub: ps,
|
|
nodeID: nodeID,
|
|
}
|
|
}
|
|
|
|
// AnnounceCapabilitiesOnChange announces capabilities only when they change
|
|
func (ca *CapabilityAnnouncer) AnnounceCapabilitiesOnChange(services *RuntimeServices) {
|
|
if ca.pubsub == nil || services == nil || services.Config == nil {
|
|
return
|
|
}
|
|
|
|
cfg := services.Config
|
|
|
|
// Get current capabilities
|
|
currentCaps := map[string]interface{}{
|
|
"node_id": ca.nodeID,
|
|
"capabilities": cfg.Agent.Capabilities,
|
|
"models": cfg.Agent.Models,
|
|
"version": "0.2.0",
|
|
"specialization": cfg.Agent.Specialization,
|
|
}
|
|
|
|
// Load stored capabilities from file
|
|
storedCaps, err := ca.loadStoredCapabilities(ca.nodeID)
|
|
if err != nil {
|
|
fmt.Printf("📄 No stored capabilities found, treating as first run\n")
|
|
storedCaps = nil
|
|
}
|
|
|
|
// Check if capabilities have changed
|
|
if ca.capabilitiesChanged(currentCaps, storedCaps) {
|
|
fmt.Printf("🔄 Capabilities changed, broadcasting update\n")
|
|
|
|
currentCaps["timestamp"] = time.Now().Unix()
|
|
currentCaps["reason"] = ca.getChangeReason(currentCaps, storedCaps)
|
|
|
|
// Broadcast the change
|
|
if err := ca.pubsub.PublishBzzzMessage(pubsub.CapabilityBcast, currentCaps); err != nil {
|
|
fmt.Printf("❌ Failed to announce capabilities: %v", err)
|
|
} else {
|
|
// Store new capabilities
|
|
if err := ca.storeCapabilities(ca.nodeID, currentCaps); err != nil {
|
|
fmt.Printf("❌ Failed to store capabilities: %v", err)
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Printf("✅ Capabilities unchanged since last run\n")
|
|
}
|
|
}
|
|
|
|
// AnnounceRoleOnStartup announces the agent's role when starting up
|
|
func (ca *CapabilityAnnouncer) AnnounceRoleOnStartup(services *RuntimeServices) {
|
|
if ca.pubsub == nil || services == nil || services.Config == nil {
|
|
return
|
|
}
|
|
|
|
cfg := services.Config
|
|
if cfg.Agent.Role == "" {
|
|
return // No role to announce
|
|
}
|
|
|
|
roleData := map[string]interface{}{
|
|
"node_id": ca.nodeID,
|
|
"role": cfg.Agent.Role,
|
|
"expertise": cfg.Agent.Expertise,
|
|
"reports_to": cfg.Agent.ReportsTo,
|
|
"deliverables": cfg.Agent.Deliverables,
|
|
"capabilities": cfg.Agent.Capabilities,
|
|
"specialization": cfg.Agent.Specialization,
|
|
"timestamp": time.Now().Unix(),
|
|
"status": "online",
|
|
}
|
|
|
|
opts := pubsub.MessageOptions{
|
|
FromRole: cfg.Agent.Role,
|
|
RequiredExpertise: cfg.Agent.Expertise,
|
|
Priority: "medium",
|
|
}
|
|
|
|
if err := ca.pubsub.PublishRoleBasedMessage(pubsub.RoleAnnouncement, roleData, opts); err != nil {
|
|
fmt.Printf("❌ Failed to announce role: %v", err)
|
|
} else {
|
|
fmt.Printf("📢 Role announced: %s\n", cfg.Agent.Role)
|
|
}
|
|
}
|
|
|
|
// Placeholder implementations for capability storage and comparison
|
|
// These would be implemented similarly to the main.go versions
|
|
|
|
func (ca *CapabilityAnnouncer) loadStoredCapabilities(nodeID string) (map[string]interface{}, error) {
|
|
// Implementation moved from main.go
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (ca *CapabilityAnnouncer) storeCapabilities(nodeID string, capabilities map[string]interface{}) error {
|
|
// Implementation moved from main.go
|
|
return fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (ca *CapabilityAnnouncer) capabilitiesChanged(current, stored map[string]interface{}) bool {
|
|
// Implementation moved from main.go
|
|
return true // Always announce for now
|
|
}
|
|
|
|
func (ca *CapabilityAnnouncer) getChangeReason(current, stored map[string]interface{}) string {
|
|
// Implementation moved from main.go
|
|
if stored == nil {
|
|
return "startup"
|
|
}
|
|
return "unknown_change"
|
|
}
|
|
|
|
// StatusReporter provides periodic status updates
|
|
type StatusReporter struct {
|
|
node interface{} // P2P node interface
|
|
logger interface{} // Logger interface
|
|
}
|
|
|
|
// NewStatusReporter creates a new status reporter
|
|
func NewStatusReporter(node interface{}) *StatusReporter {
|
|
return &StatusReporter{
|
|
node: node,
|
|
}
|
|
}
|
|
|
|
// Start begins periodic status reporting
|
|
func (sr *StatusReporter) Start() {
|
|
go func() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
// This would call the actual node's ConnectedPeers method
|
|
// peers := sr.node.ConnectedPeers()
|
|
// fmt.Printf("📊 Status: %d connected peers\n", peers)
|
|
fmt.Printf("📊 Status: periodic update\n")
|
|
}
|
|
}()
|
|
} |