Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ jobs:
with:
version: latest
working-directory: go
args: --timeout=5m --tests=false
# Tests are linted, and everything is reported. Policy now lives in
# go/.golangci.yml — see the comment there for what --tests=false and
# the default issue caps were each hiding.
args: --timeout=5m

sonarcloud:
name: SonarCloud
Expand Down
32 changes: 32 additions & 0 deletions go/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# golangci-lint configuration.
#
# This repository had none, so policy lived in a CI argument where it was both
# wrong and invisible. Two instruments were lying about the debt at once:
#
# * The defaults cap output at max-issues-per-linter: 50 and
# max-same-issues: 3. This repo's lint gate reported "71 issues" for as long
# as anyone looked. Uncapped, the same code reports 248. A capped number is
# not a measurement — fixing findings can move the total by zero as
# suppressed ones surface to replace them, which is how the same fiction was
# caught in dappcore/agent.
#
# * --tests=false, passed in CI, skipped test files entirely. That reports
# production symbols as dead when their only callers are test-injection
# seams, and simultaneously hides dead scaffolding inside the test files.
# Both directions wrong, from one flag.
#
# So: everything is reported, and tests are linted. errcheck is excluded for
# them instead, which is the narrower cut — an unchecked Close() in test setup
# is noise, while an unchecked write-close in production loses data.
version: "2"

linters:
exclusions:
rules:
- path: '_test\.go'
linters:
- errcheck

issues:
max-issues-per-linter: 0
max-same-issues: 0
9 changes: 6 additions & 3 deletions go/cmd/brain-seed/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ import (
const seedDivider = "======================================================="

var (
apiURL = flag.String("api", brainclient.DefaultURL, "OpenBrain API base URL")
apiKey = flag.String("api-key", core.Env("CORE_BRAIN_KEY"), "OpenBrain API key (Bearer token)")
server = flag.String("server", "hosthub-agent", "Legacy MCP server ID flag; accepted for compatibility")
apiURL = flag.String("api", brainclient.DefaultURL, "OpenBrain API base URL")
apiKey = flag.String("api-key", core.Env("CORE_BRAIN_KEY"), "OpenBrain API key (Bearer token)")
// Registered but deliberately never read: this keeps `-server=x` parsing
// for callers that still pass it. Assigning to _ keeps the side effect
// while saying the value is unwanted.
_ = flag.String("server", "hosthub-agent", "Legacy MCP server ID flag; accepted for compatibility")
org = flag.String("org", core.Env("CORE_BRAIN_ORG"), "OpenBrain org for seeded memories")
agent = flag.String("agent", "charon", "Agent ID for attribution")
dryRun = flag.Bool("dry-run", false, "Preview without storing")
Expand Down
14 changes: 12 additions & 2 deletions go/cmd/mcpcmd/cmd_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,18 @@ var unrestrictedFlag bool
// AddMCPCommands registers the `mcp` command tree on the Core instance.
//
// cli.Main(cli.WithCommands("mcp", mcpcmd.AddMCPCommands))
//
// register reports a failed command registration instead of dropping it: an
// unregistered command is not a missing feature at startup, it is a "command
// not found" much later, a long way from the cause.
func register(c *core.Core, path string, cmd core.Command) {
if r := c.Command(path, cmd); !r.OK {
core.Warn("mcpcmd: registering command failed", "path", path, "reason", r.Value)
}
}

func AddMCPCommands(c *core.Core) {
c.Command("mcp", core.Command{
register(c, "mcp", core.Command{
Description: "Model Context Protocol server (stdio, TCP, Unix socket, HTTP).",
Action: runServeAction,
Flags: core.NewOptions(
Expand All @@ -53,7 +63,7 @@ func AddMCPCommands(c *core.Core) {
),
})

c.Command("mcp/serve", core.Command{
register(c, "mcp/serve", core.Command{
Description: "Start the MCP server with auto-selected transport (stdio, TCP, Unix, or HTTP).",
Action: runServeAction,
Flags: core.NewOptions(
Expand Down
25 changes: 12 additions & 13 deletions go/cmd/mcpcmd/cmd_mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"context"
"testing"

. "dappco.re/go"
core "dappco.re/go"
"dappco.re/go/mcp/pkg/mcp"
)
Expand Down Expand Up @@ -228,27 +227,27 @@ func stubMCPService(t *testing.T) func() {
}

// moved AX-7 triplet TestCmdMcp_AddMCPCommands_Good
func TestCmdMcp_AddMCPCommands_Good(t *T) {
c := New()
func TestCmdMcp_AddMCPCommands_Good(t *core.T) {
c := core.New()
AddMCPCommands(c)
commands := c.Commands()
AssertContains(t, commands, "mcp")
AssertContains(t, commands, "mcp/serve")
core.AssertContains(t, commands, "mcp")
core.AssertContains(t, commands, "mcp/serve")
}

// moved AX-7 triplet TestCmdMcp_AddMCPCommands_Bad
func TestCmdMcp_AddMCPCommands_Bad(t *T) {
var c *Core
AssertPanics(t, func() { AddMCPCommands(c) })
AssertNil(t, c)
func TestCmdMcp_AddMCPCommands_Bad(t *core.T) {
var c *core.Core
core.AssertPanics(t, func() { AddMCPCommands(c) })
core.AssertNil(t, c)
}

// moved AX-7 triplet TestCmdMcp_AddMCPCommands_Ugly
func TestCmdMcp_AddMCPCommands_Ugly(t *T) {
c := New()
func TestCmdMcp_AddMCPCommands_Ugly(t *core.T) {
c := core.New()
AddMCPCommands(c)
AddMCPCommands(c)
commands := c.Commands()
AssertContains(t, commands, "mcp")
AssertContains(t, commands, "mcp/serve")
core.AssertContains(t, commands, "mcp")
core.AssertContains(t, commands, "mcp/serve")
}
34 changes: 17 additions & 17 deletions go/cmd/openbrain-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"flag"
"time"

. "dappco.re/go"
core "dappco.re/go"
"dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/brain"
)
Expand All @@ -22,8 +22,8 @@ var (

func main() {
if err := run(); err != nil {
Error("openbrain-mcp failed", "err", err)
Exit(1)
core.Error("openbrain-mcp failed", "err", err)
core.Exit(1)
}
}

Expand All @@ -45,12 +45,12 @@ func run() (
},
})
if err != nil {
return E("openbrain-mcp.run", "create MCP service", err)
return core.E("openbrain-mcp.run", "create MCP service", err)
}
defer shutdownService(svc)

if err := svc.ServeStdio(ctx); err != nil && !Is(err, context.Canceled) {
return E("openbrain-mcp.run", "serve stdio", err)
if err := svc.ServeStdio(ctx); err != nil && !core.Is(err, context.Canceled) {
return core.E("openbrain-mcp.run", "serve stdio", err)
}
return nil
}
Expand All @@ -65,36 +65,36 @@ func configureBrainEnv(
if baseURL == "" {
baseURL = directBrainBaseURL(defaultBrainURL)
}
if r := Setenv("CORE_BRAIN_URL", baseURL); !r.OK {
if r := core.Setenv("CORE_BRAIN_URL", baseURL); !r.OK {
err, _ := r.Value.(error)
return E("openbrain-mcp.configure", "set CORE_BRAIN_URL", err)
return core.E("openbrain-mcp.configure", "set CORE_BRAIN_URL", err)
}

key := Trim(apiKey)
key := core.Trim(apiKey)
if key == "" {
key = Trim(Env("OPENBRAIN_API_KEY"))
key = core.Trim(core.Env("OPENBRAIN_API_KEY"))
}
if key == "" {
return nil
}
if r := Setenv("CORE_BRAIN_KEY", key); !r.OK {
if r := core.Setenv("CORE_BRAIN_KEY", key); !r.OK {
err, _ := r.Value.(error)
return E("openbrain-mcp.configure", "set CORE_BRAIN_KEY", err)
return core.E("openbrain-mcp.configure", "set CORE_BRAIN_KEY", err)
}
return nil
}

func directBrainBaseURL(brainURL string) string {
baseURL := Trim(brainURL)
baseURL = TrimSuffix(baseURL, "/")
baseURL = TrimSuffix(baseURL, "/v1/brain")
return TrimSuffix(baseURL, "/")
baseURL := core.Trim(brainURL)
baseURL = core.TrimSuffix(baseURL, "/")
baseURL = core.TrimSuffix(baseURL, "/v1/brain")
return core.TrimSuffix(baseURL, "/")
}

func shutdownService(svc *mcp.Service) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := svc.Shutdown(ctx); err != nil {
Error("openbrain-mcp shutdown failed", "err", err)
core.Error("openbrain-mcp shutdown failed", "err", err)
}
}
19 changes: 14 additions & 5 deletions go/pkg/mcp/agentic/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,11 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
// - NO_COLOR=1 disables colour output
devNullResult := core.Open("/dev/null")
if !devNullResult.OK {
outFile.Close()
_ = outFile.Close()
return nil, DispatchOutput{}, core.E("dispatch", "failed to open /dev/null", resultError(devNullResult))
}
devNull := devNullResult.Value.(*core.OSFile)
defer devNull.Close()
defer func() { _ = devNull.Close() }()

cmd := shellCommand(context.Background(), srcDir, command, args...)
cmd.Stdin = devNull
Expand All @@ -225,7 +225,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

if err := cmd.Start(); err != nil {
outFile.Close()
_ = outFile.Close()
// Revert status so the slot is freed
s.saveStatus(wsDir, &WorkspaceStatus{
Status: "failed",
Expand Down Expand Up @@ -258,8 +258,17 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
// Background goroutine: close file handle when process exits,
// update status, then drain queue if a slot opened up.
go func() {
cmd.Wait()
outFile.Close()
// The exit status is the only signal that the agent crashed rather than
// finished. Status is still reported as "completed" below — see the
// dedicated issue; this at least stops the crash being silent.
if err := cmd.Wait(); err != nil {
core.Warn("agentic: agent process exited non-zero", "workspace", wsDir, "error", err)
}
// The agent's whole transcript is in this file; a failed close can mean a
// truncated log, and the log is the only record of what the agent did.
if err := outFile.Close(); err != nil {
core.Warn("agentic: closing agent log failed", "workspace", wsDir, "error", err)
}

postCtx := context.WithoutCancel(ctx)
status := "completed"
Expand Down
24 changes: 18 additions & 6 deletions go/pkg/mcp/agentic/epic.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
if err != nil {
return ChildRef{}, core.E("createIssue", "request failed", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 201 {
return ChildRef{}, core.E("createIssue", core.Sprintf("returned %d", resp.StatusCode), nil)
Expand All @@ -180,7 +180,9 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
Number int `json:"number"`
HTMLURL string `json:"html_url"`
}
json.NewDecoder(resp.Body).Decode(&result)
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return ChildRef{}, core.E("createIssue", "decoding created issue failed", err)
}

return ChildRef{
Number: result.Number,
Expand All @@ -204,7 +206,7 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n
if err != nil {
return nil
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
return nil
}
Expand All @@ -213,7 +215,12 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n
ID int64 `json:"id"`
Name string `json:"name"`
}
json.NewDecoder(resp.Body).Decode(&existing)
if err := json.NewDecoder(resp.Body).Decode(&existing); err != nil {
// Carrying on would silently treat every existing label as absent and
// re-create them all.
core.Warn("agentic: decoding label list failed", "repo", repo, "error", err)
return nil
}

nameToID := make(map[string]int64)
for _, l := range existing {
Expand Down Expand Up @@ -267,15 +274,20 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string)
if err != nil {
return 0
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 201 {
return 0
}

var result struct {
ID int64 `json:"id"`
}
json.NewDecoder(resp.Body).Decode(&result)
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// No error return here; a zero ID is indistinguishable from "label
// created with id 0", so say so rather than hand back a silent zero.
core.Warn("agentic: decoding created label failed", "repo", repo, "name", name, "error", err)
return 0
}
return result.ID
}

Expand Down
2 changes: 1 addition & 1 deletion go/pkg/mcp/agentic/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p
if err != nil {
return false
}
resp.Body.Close()
_ = resp.Body.Close()
return resp.StatusCode < 400
}

Expand Down
6 changes: 3 additions & 3 deletions go/pkg/mcp/agentic/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue
if err != nil {
return core.E("unlockIssue", "failed to update issue", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= http.StatusBadRequest {
return core.E("unlockIssue", core.Sprintf("issue unlock returned %d", resp.StatusCode), nil)
}
Expand All @@ -188,7 +188,7 @@ func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue
if err != nil {
return nil, core.E("fetchIssue", "failed to fetch issue", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, core.E("fetchIssue", core.Sprintf("issue %d not found in %s/%s", issue, org, repo), nil)
}
Expand Down Expand Up @@ -230,7 +230,7 @@ func (s *PrepSubsystem) lockIssue(
if err != nil {
return core.E("lockIssue", "failed to update issue", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= http.StatusBadRequest {
return core.E("lockIssue", core.Sprintf("issue update returned %d", resp.StatusCode), nil)
}
Expand Down
4 changes: 3 additions & 1 deletion go/pkg/mcp/agentic/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ func (s *PrepSubsystem) mirror(ctx context.Context, _ *mcp.CallToolRequest, inpu
}

basePath := repoRootFromCodePath(s.codePath)
repos := []string{}
// Declared, not initialised to an empty slice: both branches below assign
// it unconditionally, so the literal was written and immediately discarded.
var repos []string
if input.Repo != "" {
repos = []string{input.Repo}
} else {
Expand Down
Loading
Loading