diff --git a/agent.go b/agent.go index 3d41e98..1c9a64d 100644 --- a/agent.go +++ b/agent.go @@ -55,10 +55,11 @@ type LinuxDiagnosticAgent struct { // NewLinuxDiagnosticAgent creates a new diagnostic agent func NewLinuxDiagnosticAgent() *LinuxDiagnosticAgent { - endpoint := os.Getenv("NANNYAPI_ENDPOINT") - if endpoint == "" { - // Default endpoint - OpenAI SDK will append /chat/completions automatically - endpoint = "http://tensorzero.netcup.internal:3000/openai/v1" + // Get Supabase project URL for TensorZero proxy + supabaseURL := os.Getenv("SUPABASE_PROJECT_URL") + if supabaseURL == "" { + fmt.Printf("Warning: SUPABASE_PROJECT_URL not set, TensorZero integration will not work\n") + supabaseURL = "https://gpqzsricripnvbrpsyws.supabase.co" // fallback } model := os.Getenv("NANNYAPI_MODEL") @@ -67,14 +68,9 @@ func NewLinuxDiagnosticAgent() *LinuxDiagnosticAgent { fmt.Printf("Warning: Using default model '%s'. Set NANNYAPI_MODEL environment variable for your specific function.\n", model) } - // Create OpenAI client with custom base URL - // Note: The OpenAI SDK automatically appends "/chat/completions" to the base URL - config := openai.DefaultConfig("") - config.BaseURL = endpoint - client := openai.NewClientWithConfig(config) - + // Note: We don't use the OpenAI client anymore, we use direct HTTP to Supabase proxy agent := &LinuxDiagnosticAgent{ - client: client, + client: nil, // Not used anymore model: model, executor: NewCommandExecutor(10 * time.Second), // 10 second timeout for commands } @@ -195,7 +191,7 @@ type TensorZeroResponse struct { EpisodeID string `json:"episode_id"` } -// sendRequest sends a request to the TensorZero API with tensorzero::episode_id support +// sendRequest sends a request to the TensorZero API via Supabase proxy with JWT authentication func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessage) (*openai.ChatCompletionResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -223,17 +219,14 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa return nil, fmt.Errorf("failed to marshal request: %w", err) } - // Create HTTP request - endpoint := os.Getenv("NANNYAPI_ENDPOINT") - if endpoint == "" { - endpoint = "http://tensorzero.netcup.internal:3000/openai/v1" + // Get Supabase project URL and build TensorZero proxy endpoint + supabaseURL := os.Getenv("SUPABASE_PROJECT_URL") + if supabaseURL == "" { + supabaseURL = "https://gpqzsricripnvbrpsyws.supabase.co" } - // Ensure the endpoint ends with /chat/completions - if endpoint[len(endpoint)-1] != '/' { - endpoint += "/" - } - endpoint += "chat/completions" + // Build Supabase function URL with OpenAI v1 compatible path + endpoint := supabaseURL + "/functions/v1/tensorzero-proxy/openai/v1/chat/completions" req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(requestBody)) if err != nil { @@ -242,6 +235,14 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa req.Header.Set("Content-Type", "application/json") + // Add JWT authentication header + accessToken, err := a.getAccessToken() + if err != nil { + return nil, fmt.Errorf("failed to get access token: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + // Make the request client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) @@ -257,7 +258,7 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("TensorZero API request failed with status %d: %s", resp.StatusCode, string(body)) } // Parse TensorZero response @@ -274,3 +275,31 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa return &tzResponse.ChatCompletionResponse, nil } + +// getAccessToken retrieves the current access token for authentication +func (a *LinuxDiagnosticAgent) getAccessToken() (string, error) { + // Read token from the standard token file location + tokenPath := os.Getenv("TOKEN_PATH") + if tokenPath == "" { + tokenPath = "/var/lib/nannyagent/token.json" + } + + tokenData, err := os.ReadFile(tokenPath) + if err != nil { + return "", fmt.Errorf("failed to read token file: %w", err) + } + + var tokenInfo struct { + AccessToken string `json:"access_token"` + } + + if err := json.Unmarshal(tokenData, &tokenInfo); err != nil { + return "", fmt.Errorf("failed to parse token file: %w", err) + } + + if tokenInfo.AccessToken == "" { + return "", fmt.Errorf("access token is empty") + } + + return tokenInfo.AccessToken, nil +} diff --git a/main.go b/main.go index fc92087..47e8b64 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,14 @@ package main import ( + "bufio" "fmt" "log" + "os" + "os/exec" + "strconv" + "strings" + "syscall" "time" "nannyagentv2/internal/auth" @@ -13,9 +19,135 @@ import ( const Version = "v2.0.0" +// checkRootPrivileges ensures the program is running as root +func checkRootPrivileges() { + if os.Geteuid() != 0 { + fmt.Fprintf(os.Stderr, "❌ ERROR: This program must be run as root for eBPF functionality.\n") + fmt.Fprintf(os.Stderr, "Please run with: sudo %s\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Reason: eBPF programs require root privileges to:\n") + fmt.Fprintf(os.Stderr, " - Load programs into the kernel\n") + fmt.Fprintf(os.Stderr, " - Attach to kernel functions and tracepoints\n") + fmt.Fprintf(os.Stderr, " - Access kernel memory maps\n") + os.Exit(1) + } +} + +// checkKernelVersionCompatibility ensures kernel version is 4.4 or higher +func checkKernelVersionCompatibility() { + output, err := exec.Command("uname", "-r").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot determine kernel version: %v\n", err) + os.Exit(1) + } + + kernelVersion := strings.TrimSpace(string(output)) + + // Parse version (e.g., "5.15.0-56-generic" -> major=5, minor=15) + parts := strings.Split(kernelVersion, ".") + if len(parts) < 2 { + fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot parse kernel version: %s\n", kernelVersion) + os.Exit(1) + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot parse major kernel version: %s\n", parts[0]) + os.Exit(1) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot parse minor kernel version: %s\n", parts[1]) + os.Exit(1) + } + + // Check if kernel is 4.4 or higher + if major < 4 || (major == 4 && minor < 4) { + fmt.Fprintf(os.Stderr, "❌ ERROR: Kernel version %s is too old for eBPF.\n", kernelVersion) + fmt.Fprintf(os.Stderr, "Required: Linux kernel 4.4 or higher\n") + fmt.Fprintf(os.Stderr, "Current: %s\n", kernelVersion) + fmt.Fprintf(os.Stderr, "Reason: eBPF requires kernel features introduced in 4.4+:\n") + fmt.Fprintf(os.Stderr, " - BPF system call support\n") + fmt.Fprintf(os.Stderr, " - eBPF program types (kprobe, tracepoint)\n") + fmt.Fprintf(os.Stderr, " - BPF maps and helper functions\n") + os.Exit(1) + } + + fmt.Printf("✅ Kernel version %s is compatible with eBPF\n", kernelVersion) +} + +// checkEBPFSupport validates eBPF subsystem availability +func checkEBPFSupport() { + // Check if /sys/kernel/debug/tracing exists (debugfs mounted) + if _, err := os.Stat("/sys/kernel/debug/tracing"); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "⚠️ WARNING: debugfs not mounted. Some eBPF features may not work.\n") + fmt.Fprintf(os.Stderr, "To fix: sudo mount -t debugfs debugfs /sys/kernel/debug\n") + } + + // Check if we can access BPF syscall + fd, _, errno := syscall.Syscall(321, 0, 0, 0) // BPF syscall number on x86_64 + if errno != 0 && errno != syscall.EINVAL { + fmt.Fprintf(os.Stderr, "❌ ERROR: BPF syscall not available (errno: %v)\n", errno) + fmt.Fprintf(os.Stderr, "This may indicate:\n") + fmt.Fprintf(os.Stderr, " - Kernel compiled without BPF support\n") + fmt.Fprintf(os.Stderr, " - BPF syscall disabled in kernel config\n") + os.Exit(1) + } + if fd > 0 { + syscall.Close(int(fd)) + } + + fmt.Printf("✅ eBPF syscall is available\n") +} + +// runInteractiveDiagnostics starts the interactive diagnostic session +func runInteractiveDiagnostics(agent *LinuxDiagnosticAgent) { + fmt.Println("") + fmt.Println("🔍 Linux eBPF-Enhanced Diagnostic Agent") + fmt.Println("=======================================") + fmt.Println("Linux Diagnostic Agent Started") + fmt.Println("Enter a system issue description (or 'quit' to exit):") + + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Print("> ") + if !scanner.Scan() { + break + } + + input := strings.TrimSpace(scanner.Text()) + if input == "quit" || input == "exit" { + break + } + + if input == "" { + continue + } + + // Process the issue with AI capabilities via TensorZero + if err := agent.DiagnoseIssue(input); err != nil { + fmt.Printf("Error: %v\n", err) + } + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + fmt.Println("Goodbye!") +} + func main() { fmt.Printf("🚀 NannyAgent v%s starting...\n", Version) + // Perform system compatibility checks first + fmt.Println("Performing system compatibility checks...") + checkRootPrivileges() + checkKernelVersionCompatibility() + checkEBPFSupport() + fmt.Println("✅ All system checks passed") + fmt.Println("") + // Load configuration cfg, err := config.LoadConfig() if err != nil { @@ -36,21 +168,23 @@ func main() { fmt.Println("✅ Authentication successful!") - // Start metrics collection and heartbeat loop - fmt.Println("❤️ Starting metrics collection and heartbeat...") + // Initialize the diagnostic agent + agent := NewLinuxDiagnosticAgent() - ticker := time.NewTicker(time.Duration(cfg.MetricsInterval) * time.Second) - defer ticker.Stop() + // Start background metrics collection in a goroutine + go func() { + fmt.Println("❤️ Starting background metrics collection and heartbeat...") - // Send initial heartbeat - if err := sendHeartbeat(cfg, token, metricsCollector); err != nil { - log.Printf("⚠️ Initial heartbeat failed: %v", err) - } + ticker := time.NewTicker(time.Duration(cfg.MetricsInterval) * time.Second) + defer ticker.Stop() - // Main heartbeat loop - for { - select { - case <-ticker.C: + // Send initial heartbeat + if err := sendHeartbeat(cfg, token, metricsCollector); err != nil { + log.Printf("⚠️ Initial heartbeat failed: %v", err) + } + + // Main heartbeat loop + for range ticker.C { // Check if token needs refresh if authManager.IsTokenExpired(token) { fmt.Println("🔄 Token expiring soon, refreshing...") @@ -85,7 +219,10 @@ func main() { } // No logging for successful heartbeats - they should be silent } - } + }() + + // Start the interactive diagnostic session (blocking) + runInteractiveDiagnostics(agent) } // sendHeartbeat collects metrics and sends heartbeat to the server