diff --git a/pkg/mcp/agentic/dispatch.go b/pkg/mcp/agentic/dispatch.go index 0daac51..b57cc67 100644 --- a/pkg/mcp/agentic/dispatch.go +++ b/pkg/mcp/agentic/dispatch.go @@ -59,7 +59,7 @@ func agentCommand(agent, prompt string) (string, []string, error) { switch base { case "gemini": - args := []string{"-p", prompt, "--yolo"} + args := []string{"-p", prompt, "--yolo", "--sandbox"} if model != "" { args = append(args, "-m", "gemini-2.5-"+model) } @@ -209,6 +209,9 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, writeStatus(wsDir, st) } + // Ingest scan findings as issues + s.ingestFindings(wsDir) + // Drain queue: pop next queued workspace and spawn it s.drainQueue() }() diff --git a/pkg/mcp/agentic/ingest.go b/pkg/mcp/agentic/ingest.go new file mode 100644 index 0000000..aafef73 --- /dev/null +++ b/pkg/mcp/agentic/ingest.go @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" +) + +// ingestFindings reads the agent output log and creates issues via the API +// for scan/audit results. Only runs for conventions and security templates. +func (s *PrepSubsystem) ingestFindings(wsDir string) { + st, err := readStatus(wsDir) + if err != nil || st.Status != "completed" { + return + } + + // Read the log file + logFiles, _ := filepath.Glob(filepath.Join(wsDir, "agent-*.log")) + if len(logFiles) == 0 { + return + } + + content, err := os.ReadFile(logFiles[0]) + if err != nil || len(content) < 100 { + return + } + + body := string(content) + + // Skip quota errors + if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") { + return + } + + // Only ingest if there are actual findings (file:line references) + findings := countFileRefs(body) + if findings < 2 { + return // No meaningful findings + } + + // Determine issue type from the template used + issueType := "task" + priority := "normal" + if strings.Contains(body, "security") || strings.Contains(body, "Security") { + issueType = "bug" + priority = "high" + } + + // Create a single issue per repo with all findings in the body + title := fmt.Sprintf("Scan findings for %s (%d items)", st.Repo, findings) + + // Truncate body to reasonable size for issue description + description := body + if len(description) > 10000 { + description = description[:10000] + "\n\n... (truncated, see full log in workspace)" + } + + s.createIssueViaAPI(st.Repo, title, description, issueType, priority, "scan") +} + +// countFileRefs counts file:line references in the output (indicates real findings) +func countFileRefs(body string) int { + count := 0 + for i := 0; i < len(body)-5; i++ { + if body[i] == '`' { + // Look for pattern: `file.go:123` + j := i + 1 + for j < len(body) && body[j] != '`' && j-i < 100 { + j++ + } + if j < len(body) && body[j] == '`' { + ref := body[i+1 : j] + if strings.Contains(ref, ".go:") || strings.Contains(ref, ".php:") { + count++ + } + } + } + } + return count +} + +// createIssueViaAPI posts an issue to the lthn.sh API +func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, priority, source string) { + if s.brainKey == "" { + return + } + + // Read the agent API key from file + home, _ := os.UserHomeDir() + apiKeyData, err := os.ReadFile(filepath.Join(home, ".claude", "agent-api.key")) + if err != nil { + return + } + apiKey := strings.TrimSpace(string(apiKeyData)) + + payload, _ := json.Marshal(map[string]string{ + "title": title, + "description": description, + "type": issueType, + "priority": priority, + "reporter": "cladius", + }) + + req, _ := http.NewRequest("POST", s.brainURL+"/v1/issues", bytes.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + resp, err := s.client.Do(req) + if err != nil { + return + } + resp.Body.Close() +} diff --git a/pkg/mcp/agentic/queue.go b/pkg/mcp/agentic/queue.go index 76d7868..b0dba4b 100644 --- a/pkg/mcp/agentic/queue.go +++ b/pkg/mcp/agentic/queue.go @@ -240,6 +240,9 @@ func (s *PrepSubsystem) drainQueue() { writeStatus(wsDir, st2) } + // Ingest scan findings as issues + s.ingestFindings(wsDir) + s.drainQueue() }()