fix: address re-review findings — nil pointer, races, curl, clone error

Important:
- Fix nil pointer dereference on resp.StatusCode when err!=nil (8 sites)
- Replace curl shell-out with net/http in monitor inbox check
- Handle clone failure in prep.go (was silently swallowed)
- Use GitHubOrg() instead of hardcoded "dAppCore"

Medium:
- Fix JSONL append race (read+write → os.OpenFile O_APPEND)
- Remove dead google/mcp/ directory

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 19:27:44 +00:00
parent 6d04c893b7
commit e66ea0512b
12 changed files with 92 additions and 334 deletions

View file

@ -1,67 +0,0 @@
# Core CLI MCP Server
This directory contains an MCP server that exposes the core CLI commands as tools for AI agents.
## Tools
### `core_go_test`
Run Go tests.
**Parameters:**
- `filter` (string, optional): Filter tests by name.
- `coverage` (boolean, optional): Enable code coverage. Defaults to `false`.
**Example:**
```json
{
"tool": "core_go_test",
"parameters": {
"filter": "TestMyFunction",
"coverage": true
}
}
```
### `core_dev_health`
Check the health of the monorepo.
**Parameters:**
None.
**Example:**
```json
{
"tool": "core_dev_health",
"parameters": {}
}
```
### `core_dev_commit`
Commit changes across repositories.
**Parameters:**
- `message` (string, required): The commit message.
- `repos` (array of strings, optional): A list of repositories to commit to.
**Example:**
```json
{
"tool": "core_dev_commit",
"parameters": {
"message": "feat: Implement new feature",
"repos": [
"core-agent",
"another-repo"
]
}
}
```

View file

@ -1,131 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os/exec"
"strings"
)
type GoTestRequest struct {
Filter string `json:"filter,omitempty"`
Coverage bool `json:"coverage,omitempty"`
}
type GoTestResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
type DevHealthResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
type DevCommitRequest struct {
Message string `json:"message"`
Repos []string `json:"repos,omitempty"`
}
type DevCommitResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
func goTestHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
var req GoTestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
args := []string{"go", "test"}
if req.Filter != "" {
args = append(args, "-run", req.Filter)
}
if req.Coverage {
args = append(args, "-cover")
}
cmd := exec.Command("core", args...)
output, err := cmd.CombinedOutput()
resp := GoTestResponse{
Output: string(output),
}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func devHealthHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
cmd := exec.Command("core", "dev", "health")
output, err := cmd.CombinedOutput()
resp := DevHealthResponse{
Output: string(output),
}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func devCommitHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
var req DevCommitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
args := []string{"dev", "commit", "-m", req.Message}
if len(req.Repos) > 0 {
args = append(args, "--repos", strings.Join(req.Repos, ","))
}
cmd := exec.Command("core", args...)
output, err := cmd.CombinedOutput()
resp := DevCommitResponse{
Output: string(output),
}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
http.HandleFunc("/core_go_test", goTestHandler)
http.HandleFunc("/core_dev_health", devHealthHandler)
http.HandleFunc("/core_dev_commit", devCommitHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "MCP Server is running")
})
log.Println("Starting MCP server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("could not start server: %s\n", err)
}
}

View file

@ -1,112 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestMain(m *testing.M) {
// Get the absolute path to the testdata directory
wd, err := os.Getwd()
if err != nil {
panic(err)
}
testdataPath := filepath.Join(wd, "testdata")
// Add the absolute path to the PATH
os.Setenv("PATH", testdataPath+":"+os.Getenv("PATH"))
m.Run()
}
func TestGoTestHandler(t *testing.T) {
reqBody := GoTestRequest{
Filter: "TestMyFunction",
Coverage: true,
}
body, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "/core_go_test", bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(goTestHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var resp GoTestResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("could not decode response: %v", err)
}
if resp.Error != "" {
t.Errorf("handler returned an unexpected error: %v", resp.Error)
}
}
func TestDevHealthHandler(t *testing.T) {
req, err := http.NewRequest("POST", "/core_dev_health", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(devHealthHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var resp DevHealthResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("could not decode response: %v", err)
}
if resp.Error != "" {
t.Errorf("handler returned an unexpected error: %v", resp.Error)
}
}
func TestDevCommitHandler(t *testing.T) {
reqBody := DevCommitRequest{
Message: "test commit",
}
body, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "/core_dev_commit", bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(devCommitHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var resp DevCommitResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("could not decode response: %v", err)
}
if resp.Error != "" {
t.Errorf("handler returned an unexpected error: %v", resp.Error)
}
}

View file

@ -1,2 +0,0 @@
#!/bin/bash
exit 0

View file

@ -196,7 +196,12 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -252,7 +257,12 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string)
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 201 { if err != nil {
return 0
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
return 0 return 0
} }
defer resp.Body.Close() defer resp.Body.Close()

View file

@ -4,10 +4,11 @@ package agentic
import ( import (
"encoding/json" "encoding/json"
"os"
"path/filepath" "path/filepath"
"time" "time"
coreio "forge.lthn.ai/core/go-io"
) )
// CompletionEvent is emitted when a dispatched agent finishes. // CompletionEvent is emitted when a dispatched agent finishes.
@ -39,6 +40,10 @@ func emitCompletionEvent(agent, workspace string) {
} }
// Append to events log // Append to events log
existing, _ := coreio.Local.Read(eventsFile) f, err := os.OpenFile(eventsFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
coreio.Local.Write(eventsFile, existing+string(data)+"\n") if err != nil {
return
}
defer f.Close()
f.Write(append(data, '\n'))
} }

View file

@ -42,3 +42,11 @@ func AgentName() string {
} }
return "charon" return "charon"
} }
// GitHubOrg returns the GitHub org for mirror operations.
func GitHubOrg() string {
if org := os.Getenv("GITHUB_ORG"); org != "" {
return org
}
return "dAppCore"
}

View file

@ -308,7 +308,12 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err) return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err)
} }
defer resp.Body.Close() defer resp.Body.Close()

View file

@ -167,7 +167,9 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
// 1. Clone repo into src/ and create feature branch // 1. Clone repo into src/ and create feature branch
srcDir := filepath.Join(wsDir, "src") srcDir := filepath.Join(wsDir, "src")
cloneCmd := exec.CommandContext(ctx, "git", "clone", repoPath, srcDir) cloneCmd := exec.CommandContext(ctx, "git", "clone", repoPath, srcDir)
cloneCmd.Run() if err := cloneCmd.Run(); err != nil {
return nil, PrepOutput{}, coreerr.E("prep", "git clone failed for "+input.Repo, err)
}
// Create feature branch // Create feature branch
taskSlug := strings.Map(func(r rune) rune { taskSlug := strings.Map(func(r rune) rune {
@ -459,7 +461,12 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return 0
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return 0 return 0
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -544,7 +551,12 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string)
req.Header.Set("Authorization", "Bearer "+s.brainKey) req.Header.Set("Authorization", "Bearer "+s.brainKey)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return 0
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return 0 return 0
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -637,7 +649,12 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return return
} }
defer resp.Body.Close() defer resp.Body.Close()

View file

@ -242,7 +242,7 @@ func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string)
} }
// Mark PR ready if draft // Mark PR ready if draft
readyCmd := exec.CommandContext(ctx, "gh", "pr", "ready", "--repo", "dAppCore/"+repo) readyCmd := exec.CommandContext(ctx, "gh", "pr", "ready", "--repo", GitHubOrg()+"/"+repo)
readyCmd.Dir = repoDir readyCmd.Dir = repoDir
readyCmd.Run() // Ignore error — might already be ready readyCmd.Run() // Ignore error — might already be ready
@ -339,8 +339,11 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string
jsonLine, _ := json.Marshal(entry) jsonLine, _ := json.Marshal(entry)
jsonlPath := filepath.Join(dataDir, "reviews.jsonl") jsonlPath := filepath.Join(dataDir, "reviews.jsonl")
existing, _ := coreio.Local.Read(jsonlPath) f, err := os.OpenFile(jsonlPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
coreio.Local.Write(jsonlPath, existing+string(jsonLine)+"\n") if err == nil {
defer f.Close()
f.Write(append(jsonLine, '\n'))
}
} }
// saveRateLimitState persists rate limit info for cross-run awareness. // saveRateLimitState persists rate limit info for cross-run awareness.

View file

@ -105,7 +105,12 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string,
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err) return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -129,7 +134,12 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 { if err != nil {
return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err) return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
} }
defer resp.Body.Close() defer resp.Body.Close()

View file

@ -14,7 +14,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "net/http"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@ -238,14 +238,26 @@ func (m *Subsystem) checkInbox() string {
} }
// Call the API to check inbox // Call the API to check inbox
cmd := exec.Command("curl", "-sf", apiURL := os.Getenv("CORE_API_URL")
"-H", "Authorization: Bearer "+strings.TrimSpace(apiKeyStr), if apiURL == "" {
"https://api.lthn.sh/v1/messages/inbox?agent="+agentic.AgentName(), apiURL = "https://api.lthn.sh"
) }
out, err := cmd.Output() req, err := http.NewRequest("GET", apiURL+"/v1/messages/inbox?agent="+agentic.AgentName(), nil)
if err != nil { if err != nil {
return "" return ""
} }
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(apiKeyStr))
client := &http.Client{Timeout: 10 * time.Second}
httpResp, err := client.Do(req)
if err != nil {
return ""
}
defer httpResp.Body.Close()
if httpResp.StatusCode != 200 {
return ""
}
var resp struct { var resp struct {
Data []struct { Data []struct {
@ -254,7 +266,7 @@ func (m *Subsystem) checkInbox() string {
Subject string `json:"subject"` Subject string `json:"subject"`
} `json:"data"` } `json:"data"`
} }
if json.Unmarshal(out, &resp) != nil { if json.NewDecoder(httpResp.Body).Decode(&resp) != nil {
return "" return ""
} }