This comprehensive refactoring addresses critical architectural issues: IMPORT CYCLE RESOLUTION: • pkg/crypto ↔ pkg/slurp/roles: Created pkg/security/access_levels.go • pkg/ucxl → pkg/dht: Created pkg/storage/interfaces.go • pkg/slurp/leader → pkg/election → pkg/slurp/storage: Moved types to pkg/election/interfaces.go MODULE PATH MIGRATION: • Changed from github.com/anthonyrawlins/bzzz to chorus.services/bzzz • Updated all import statements across 115+ files • Maintains compatibility while removing personal GitHub account dependency TYPE SYSTEM IMPROVEMENTS: • Resolved duplicate type declarations in crypto package • Added missing type definitions (RoleStatus, TimeRestrictions, KeyStatus, KeyRotationResult) • Proper interface segregation to prevent future cycles ARCHITECTURAL BENEFITS: • Build now progresses past structural issues to normal dependency resolution • Cleaner separation of concerns between packages • Eliminates circular dependencies that prevented compilation • Establishes foundation for scalable codebase growth 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
423 lines
12 KiB
Go
423 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"chorus.services/bzzz/executor"
|
|
"chorus.services/bzzz/logging"
|
|
"chorus.services/bzzz/pkg/types"
|
|
"chorus.services/bzzz/sandbox"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// ChatTaskRequest represents a task request from the chat interface
|
|
type ChatTaskRequest struct {
|
|
Method string `json:"method"`
|
|
Task *types.EnhancedTask `json:"task"`
|
|
ExecutionOptions *ExecutionOptions `json:"execution_options"`
|
|
Callback *CallbackConfig `json:"callback"`
|
|
}
|
|
|
|
// ExecutionOptions defines how the task should be executed
|
|
type ExecutionOptions struct {
|
|
SandboxImage string `json:"sandbox_image"`
|
|
Timeout string `json:"timeout"`
|
|
MaxIterations int `json:"max_iterations"`
|
|
ReturnFullLog bool `json:"return_full_log"`
|
|
CleanupOnComplete bool `json:"cleanup_on_complete"`
|
|
}
|
|
|
|
// CallbackConfig defines where to send results
|
|
type CallbackConfig struct {
|
|
WebhookURL string `json:"webhook_url"`
|
|
IncludeArtifacts bool `json:"include_artifacts"`
|
|
}
|
|
|
|
// ChatTaskResponse represents the response from task execution
|
|
type ChatTaskResponse struct {
|
|
TaskID int `json:"task_id"`
|
|
Status string `json:"status"`
|
|
ExecutionTime string `json:"execution_time"`
|
|
Artifacts *ExecutionArtifacts `json:"artifacts,omitempty"`
|
|
ExecutionLog []ExecutionLogEntry `json:"execution_log,omitempty"`
|
|
Errors []ExecutionError `json:"errors,omitempty"`
|
|
GitBranch string `json:"git_branch,omitempty"`
|
|
PullRequestURL string `json:"pr_url,omitempty"`
|
|
OriginalRequest *ChatTaskRequest `json:"original_request,omitempty"`
|
|
}
|
|
|
|
// ExecutionArtifacts contains the outputs of task execution
|
|
type ExecutionArtifacts struct {
|
|
FilesCreated []FileArtifact `json:"files_created,omitempty"`
|
|
CodeGenerated string `json:"code_generated,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
TestsCreated []FileArtifact `json:"tests_created,omitempty"`
|
|
Documentation string `json:"documentation,omitempty"`
|
|
}
|
|
|
|
// FileArtifact represents a file created during execution
|
|
type FileArtifact struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
Content string `json:"content,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
}
|
|
|
|
// ExecutionLogEntry represents a single step in the execution process
|
|
type ExecutionLogEntry struct {
|
|
Step int `json:"step"`
|
|
Action string `json:"action"`
|
|
Command string `json:"command,omitempty"`
|
|
Result string `json:"result"`
|
|
Success bool `json:"success"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Duration string `json:"duration,omitempty"`
|
|
}
|
|
|
|
// ExecutionError represents an error that occurred during execution
|
|
type ExecutionError struct {
|
|
Step int `json:"step,omitempty"`
|
|
Type string `json:"type"`
|
|
Message string `json:"message"`
|
|
Command string `json:"command,omitempty"`
|
|
}
|
|
|
|
// ChatAPIHandler handles chat integration requests
|
|
type ChatAPIHandler struct {
|
|
logger *logging.HypercoreLog
|
|
}
|
|
|
|
// NewChatAPIHandler creates a new chat API handler
|
|
func NewChatAPIHandler() *ChatAPIHandler {
|
|
// Note: HypercoreLog expects a peer.ID, but for testing we use nil
|
|
// In production, this should be integrated with the actual P2P peer ID
|
|
|
|
return &ChatAPIHandler{
|
|
logger: nil, // Will be set up when P2P integration is available
|
|
}
|
|
}
|
|
|
|
// ExecuteTaskHandler handles task execution requests from N8N chat workflow
|
|
func (h *ChatAPIHandler) ExecuteTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Parse request
|
|
var req ChatTaskRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "Invalid request format", err)
|
|
return
|
|
}
|
|
|
|
// Log the incoming request
|
|
if h.logger != nil {
|
|
h.logger.Append(logging.TaskProgress, map[string]interface{}{
|
|
"task_id": req.Task.Number,
|
|
"method": req.Method,
|
|
"source": "chat_api",
|
|
"status": "received",
|
|
})
|
|
}
|
|
|
|
// Validate request
|
|
if req.Task == nil {
|
|
h.sendError(w, http.StatusBadRequest, "Task is required", nil)
|
|
return
|
|
}
|
|
|
|
// Send immediate response to N8N
|
|
response := map[string]interface{}{
|
|
"task_id": req.Task.Number,
|
|
"status": "accepted",
|
|
"message": "Task accepted for execution",
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
// Execute task asynchronously
|
|
go h.executeTaskAsync(ctx, &req)
|
|
}
|
|
|
|
// executeTaskAsync executes the task in a separate goroutine
|
|
func (h *ChatAPIHandler) executeTaskAsync(ctx context.Context, req *ChatTaskRequest) {
|
|
startTime := time.Now()
|
|
var response ChatTaskResponse
|
|
|
|
response.TaskID = req.Task.Number
|
|
response.OriginalRequest = req
|
|
|
|
// Create execution log
|
|
var executionLog []ExecutionLogEntry
|
|
var artifacts ExecutionArtifacts
|
|
var errors []ExecutionError
|
|
|
|
defer func() {
|
|
response.ExecutionTime = time.Since(startTime).String()
|
|
response.ExecutionLog = executionLog
|
|
response.Artifacts = &artifacts
|
|
response.Errors = errors
|
|
|
|
// Send callback to N8N
|
|
if req.Callback != nil && req.Callback.WebhookURL != "" {
|
|
h.sendCallback(req.Callback.WebhookURL, &response)
|
|
}
|
|
}()
|
|
|
|
// Log start of execution
|
|
executionLog = append(executionLog, ExecutionLogEntry{
|
|
Step: 1,
|
|
Action: "Starting task execution",
|
|
Result: fmt.Sprintf("Task: %s", req.Task.Title),
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Create sandbox
|
|
sb, err := sandbox.CreateSandbox(ctx, req.ExecutionOptions.SandboxImage)
|
|
if err != nil {
|
|
response.Status = "failed"
|
|
errors = append(errors, ExecutionError{
|
|
Step: 2,
|
|
Type: "sandbox_creation_failed",
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Ensure cleanup
|
|
defer func() {
|
|
if req.ExecutionOptions.CleanupOnComplete {
|
|
sb.DestroySandbox()
|
|
}
|
|
}()
|
|
|
|
executionLog = append(executionLog, ExecutionLogEntry{
|
|
Step: 2,
|
|
Action: "Created sandbox",
|
|
Result: fmt.Sprintf("Sandbox ID: %s", sb.ID[:12]),
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Clone repository if specified
|
|
if req.Task.GitURL != "" {
|
|
cloneCmd := fmt.Sprintf("git clone %s .", req.Task.GitURL)
|
|
result, err := sb.RunCommand(cloneCmd)
|
|
|
|
success := err == nil
|
|
executionLog = append(executionLog, ExecutionLogEntry{
|
|
Step: 3,
|
|
Action: "Clone repository",
|
|
Command: cloneCmd,
|
|
Result: fmt.Sprintf("Exit: %d, Output: %s", result.ExitCode, result.StdOut),
|
|
Success: success,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
if err != nil {
|
|
errors = append(errors, ExecutionError{
|
|
Step: 3,
|
|
Type: "git_clone_failed",
|
|
Message: err.Error(),
|
|
Command: cloneCmd,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Execute the task using the existing executor
|
|
result, err := executor.ExecuteTask(ctx, req.Task, h.logger)
|
|
if err != nil {
|
|
response.Status = "failed"
|
|
errors = append(errors, ExecutionError{
|
|
Type: "execution_failed",
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Collect artifacts from sandbox
|
|
h.collectArtifacts(sb, &artifacts)
|
|
|
|
// Set success status
|
|
response.Status = "success"
|
|
if result.BranchName != "" {
|
|
response.GitBranch = result.BranchName
|
|
}
|
|
|
|
executionLog = append(executionLog, ExecutionLogEntry{
|
|
Step: len(executionLog) + 1,
|
|
Action: "Task completed successfully",
|
|
Result: fmt.Sprintf("Files created: %d", len(artifacts.FilesCreated)),
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
|
|
// collectArtifacts gathers files and outputs from the sandbox
|
|
func (h *ChatAPIHandler) collectArtifacts(sb *sandbox.Sandbox, artifacts *ExecutionArtifacts) {
|
|
// List files created in workspace
|
|
result, err := sb.RunCommand("find . -type f -name '*.py' -o -name '*.js' -o -name '*.go' -o -name '*.java' -o -name '*.cpp' -o -name '*.rs' | head -20")
|
|
if err == nil && result.StdOut != "" {
|
|
files := strings.Split(strings.TrimSpace(result.StdOut), "\n")
|
|
var validFiles []string
|
|
for _, line := range files {
|
|
if strings.TrimSpace(line) != "" {
|
|
validFiles = append(validFiles, strings.TrimSpace(line))
|
|
}
|
|
}
|
|
files = validFiles
|
|
|
|
for _, file := range files {
|
|
// Get file content
|
|
content, err := sb.ReadFile(file)
|
|
if err == nil && len(content) < 10000 { // Limit content size
|
|
stat, _ := sb.RunCommand(fmt.Sprintf("stat -c '%%s' %s", file))
|
|
size := int64(0)
|
|
if stat.ExitCode == 0 {
|
|
fmt.Sscanf(stat.StdOut, "%d", &size)
|
|
}
|
|
|
|
artifact := FileArtifact{
|
|
Name: file,
|
|
Path: file,
|
|
Size: size,
|
|
Content: string(content),
|
|
Language: h.detectLanguage(file),
|
|
}
|
|
artifacts.FilesCreated = append(artifacts.FilesCreated, artifact)
|
|
|
|
// If this looks like the main generated code, set it
|
|
if artifacts.CodeGenerated == "" && size > 0 {
|
|
artifacts.CodeGenerated = string(content)
|
|
artifacts.Language = artifact.Language
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// detectLanguage detects programming language from file extension
|
|
func (h *ChatAPIHandler) detectLanguage(filename string) string {
|
|
extensions := map[string]string{
|
|
".py": "python",
|
|
".js": "javascript",
|
|
".ts": "typescript",
|
|
".go": "go",
|
|
".java": "java",
|
|
".cpp": "cpp",
|
|
".c": "c",
|
|
".rs": "rust",
|
|
".rb": "ruby",
|
|
".php": "php",
|
|
}
|
|
|
|
for ext, lang := range extensions {
|
|
if len(filename) > len(ext) && filename[len(filename)-len(ext):] == ext {
|
|
return lang
|
|
}
|
|
}
|
|
return "text"
|
|
}
|
|
|
|
// sendCallback sends the execution results back to N8N webhook
|
|
func (h *ChatAPIHandler) sendCallback(webhookURL string, response *ChatTaskResponse) {
|
|
jsonData, err := json.Marshal(response)
|
|
if err != nil {
|
|
log.Printf("Failed to marshal callback response: %v", err)
|
|
return
|
|
}
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Post(webhookURL, "application/json", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
log.Printf("Failed to send callback to %s: %v", webhookURL, err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Printf("Callback webhook returned status %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// sendError sends an error response
|
|
func (h *ChatAPIHandler) sendError(w http.ResponseWriter, statusCode int, message string, err error) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
|
|
errorResponse := map[string]interface{}{
|
|
"error": message,
|
|
"status": statusCode,
|
|
}
|
|
|
|
if err != nil {
|
|
errorResponse["details"] = err.Error()
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(errorResponse)
|
|
}
|
|
|
|
// HealthHandler provides a health check endpoint
|
|
func (h *ChatAPIHandler) HealthHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "healthy",
|
|
"service": "bzzz-chat-api",
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// StartChatAPIServer starts the HTTP server for chat integration
|
|
func StartChatAPIServer(port string) {
|
|
handler := NewChatAPIHandler()
|
|
|
|
r := mux.NewRouter()
|
|
|
|
// API routes
|
|
api := r.PathPrefix("/bzzz/api").Subrouter()
|
|
api.HandleFunc("/execute-task", handler.ExecuteTaskHandler).Methods("POST")
|
|
api.HandleFunc("/health", handler.HealthHandler).Methods("GET")
|
|
|
|
// Add CORS middleware
|
|
r.Use(func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
})
|
|
|
|
log.Printf("🚀 Starting Bzzz Chat API server on port %s", port)
|
|
log.Printf("📡 Endpoints:")
|
|
log.Printf(" POST /bzzz/api/execute-task - Execute task in sandbox")
|
|
log.Printf(" GET /bzzz/api/health - Health check")
|
|
|
|
if err := http.ListenAndServe(":"+port, r); err != nil {
|
|
log.Fatalf("Failed to start server: %v", err)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
port := "8080"
|
|
if len(os.Args) > 1 {
|
|
port = os.Args[1]
|
|
}
|
|
|
|
StartChatAPIServer(port)
|
|
} |