merge(mcp): reconcile origin AX-6 sweep + brainclient refactor with homelab migration + features

Parent SHAs:
  origin/dev: 4420670 feat(mcp/brain): OpenBrain T1+T2 — shared client + direct/brain-seed adoption (#175 #176)
  homelab/dev: 95f8ad3 docs(security): document accepted ollama CVEs + operator runbook

Resolution strategy (29 conflicts: 26 UU + 3 AA):
- Took origin's body for every conflicting file (AX-6-clean, uses core.* helpers
  not banned stdlib; preserves the brainclient.New() refactor that landed in
  origin commit 4420670 and lifted inline HTTP code into pkg/mcp/brain/client/).
- Sed-rewrote `dappco.re/go/core/X` → `dappco.re/go/X` import paths inline so
  origin's body uses the migrated paths that homelab's go.mod declares.
- go.mod auto-merged toward homelab's NEW dep paths (dappco.re/go/{ai,api,cli,
  io,log,process,rag,webview,ws} v0.8.0-alpha.1) — correct outcome.
- Homelab's standalone-new files (cmd/openbrain-mcp/, ipc.go, tools_metrics.go,
  tools_ws_client.go, tools_webview_embed.go, etc.) preserved via git's
  non-conflicting auto-merge.

Followups (filed separately):
- Stale `replace dappco.re/go/core/process => ../go-process` directive remains
  in go.mod — pre-existing, doesn't match any current dep, will catch in
  future cleanup pass.
- Local build verification deferred: workspace-level go-proxy half-migration
  + missing go-ws entry in ~/Code/go.work block `go build ./...` from
  succeeding host-side; this merge resolved by per-file inspection.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Snider 2026-04-25 16:08:35 +01:00
commit 8b48b33622
54 changed files with 503 additions and 82 deletions

View file

@ -21,8 +21,8 @@ import (
"regexp"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
brainclient "dappco.re/go/mcp/pkg/mcp/brain/client"
)

View file

@ -1,7 +1,7 @@
package main
import (
"dappco.re/go/core/cli/pkg/cli"
"dappco.re/go/cli/pkg/cli"
mcpcmd "dappco.re/go/mcp/cmd/mcpcmd"
)

View file

@ -0,0 +1,29 @@
# openbrain-mcp
`openbrain-mcp` is a thin stdio MCP wrapper for the OpenBrain tools registered in `pkg/mcp/brain`.
Install:
```sh
go install dappco.re/go/mcp/cmd/openbrain-mcp@latest
```
Add it to Claude Code:
```sh
claude mcp add openbrain -- openbrain-mcp --brain-url=http://127.0.0.1:8000/v1/brain --api-key=$OPENBRAIN_API_KEY
```
The wrapper exposes:
- `brain_remember`
- `brain_recall`
- `brain_forget`
- `brain_list`
Flags:
- `--brain-url`: OpenBrain BrainService URL. Defaults to `http://127.0.0.1:8000/v1/brain`.
- `--api-key`: OpenBrain API key. Defaults to `OPENBRAIN_API_KEY`.
The process logs to stderr only. Stdout is reserved for MCP framing.

95
cmd/openbrain-mcp/main.go Normal file
View file

@ -0,0 +1,95 @@
// SPDX-License-Identifier: EUPL-1.2
// openbrain-mcp exposes the OpenBrain MCP tools over stdio for Claude Code.
package main
import (
"context"
"flag"
"os"
"os/signal"
"syscall"
"time"
core "dappco.re/go/core"
coreerr "dappco.re/go/log"
"dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/brain"
)
const defaultBrainURL = "http://127.0.0.1:8000/v1/brain"
var (
brainURLFlag = flag.String("brain-url", defaultBrainURL, "OpenBrain BrainService URL")
apiKeyFlag = flag.String("api-key", "", "OpenBrain API key (defaults to OPENBRAIN_API_KEY)")
)
func main() {
if err := run(); err != nil {
coreerr.Error("openbrain-mcp failed", "err", err)
os.Exit(1)
}
}
func run() error {
flag.Parse()
if err := configureBrainEnv(*brainURLFlag, *apiKeyFlag); err != nil {
return err
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
svc, err := mcp.New(mcp.Options{
Subsystems: []mcp.Subsystem{
brain.NewDirect(),
},
})
if err != nil {
return core.E("openbrain-mcp.run", "create MCP service", err)
}
defer shutdownService(svc)
if err := svc.ServeStdio(ctx); err != nil && !core.Is(err, context.Canceled) {
return core.E("openbrain-mcp.run", "serve stdio", err)
}
return nil
}
func configureBrainEnv(brainURL, apiKey string) error {
baseURL := directBrainBaseURL(brainURL)
if baseURL == "" {
baseURL = directBrainBaseURL(defaultBrainURL)
}
if err := os.Setenv("CORE_BRAIN_URL", baseURL); err != nil {
return core.E("openbrain-mcp.configure", "set CORE_BRAIN_URL", err)
}
key := core.Trim(apiKey)
if key == "" {
key = core.Trim(core.Env("OPENBRAIN_API_KEY"))
}
if key == "" {
return nil
}
if err := os.Setenv("CORE_BRAIN_KEY", key); err != nil {
return core.E("openbrain-mcp.configure", "set CORE_BRAIN_KEY", err)
}
return nil
}
func directBrainBaseURL(brainURL string) string {
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 {
coreerr.Error("openbrain-mcp shutdown failed", "err", err)
}
}

View file

@ -0,0 +1,141 @@
# Security Vulnerabilities — Accepted Findings + Operator Mitigations
This document records security findings (govulncheck, etc.) that have been
manually reviewed and **accepted with documented rationale** rather than
patched. Each entry names the CVE, what makes it not-applicable to our use
case, and any operator-side mitigations required to keep that not-applicable
status valid.
Audit history:
- Mantis #323 — 9 ollama CVEs reviewed and documented (2026-04-25)
---
## github.com/ollama/ollama (indirect via go-rag)
**Status as of 2026-04-25:** all 9 CVEs filed in Mantis #323 are **UNFIXED
upstream** per [pkg.go.dev/vuln](https://pkg.go.dev/vuln/). Pin-bumping does
not resolve any of them. We are on `v0.18.1` indirect; ollama upstream is at
`v0.21.2` (2026-04-23).
**Our usage scope:** the entire workspace imports `github.com/ollama/ollama/api`
from exactly ONE file (`go-rag/ollama.go`). The surface in use is **3 symbols
only**:
- `api.NewClient(baseURL, *http.Client)` — constructor
- `api.Client` — struct value (held as a field by `OllamaClient`)
- `api.EmbedRequest` — embedding-request DTO
**We are a CLIENT** of someone else's Ollama server. We do NOT host an Ollama
server. Most CVEs in the list are server-side code paths that govulncheck's
reachability graph flags because the package is imported, but our actual call
sites do not traverse those paths.
### CVE-by-CVE reachability assessment
| CVE | Description | Reachable from our call graph? | Action |
|---|---|---|---|
| GO-2025-3548 (CVE-2024-12886) | DoS via crafted GZIP | NO — server-side parser | Accept |
| GO-2025-3557 (CVE-2025-0315) | Resource alloc without limits | NO — server-side dispatcher | Accept |
| GO-2025-3558 | Out-of-bounds read | NO — server-side inference | Accept |
| GO-2025-3559 | Divide by zero | NO — server-side inference | Accept |
| GO-2025-3582 | Null pointer deref DoS | NO — server-side handler | Accept |
| GO-2025-3689 | Divide by zero | NO — server-side inference | Accept |
| GO-2025-3695 | Server DoS | NO — server-side handler | Accept |
| GO-2025-3824 (CVE-2025-51471) | Cross-domain token exposure | **CONDITIONAL** — see below | Watch |
| GO-2025-4251 (CVE-2025-63389) | Missing auth on model-mgmt | **OPERATOR-SIDE** — see below | Runbook |
### GO-2025-3824 — token-exposure watch flag
This CVE concerns auth tokens leaking across domain boundaries when Ollama
clients pass authentication. Currently `NewOllamaClient(cfg)` constructs over
plain HTTP/HTTPS without auth headers — the embedding client connects to a
trusted local Ollama instance per the deployment runbook below.
**If we ever add auth-token plumbing to the Ollama client** (e.g. for hosted
Ollama services), re-evaluate this CVE. The reachability flips from NO to YES
the moment we set an Authorization header on `api.NewClient`.
### GO-2025-4251 — operator-side mitigation required
This CVE is a missing authentication / authorization gap on Ollama's
model-management endpoints. The vulnerability is in the **Ollama server**,
not our client code. Our client doesn't expose model-management calls;
operators do via running an Ollama server.
**Operator mitigation (REQUIRED):** see "Ollama deployment" section below.
Operators MUST front their Ollama instance with network-level access controls
or an authentication proxy. This is also Ollama upstream's own recommendation
in the advisory.
### Watch flag
If any of the 9 CVEs gets a fixed version released, re-evaluate:
- Bump `go-rag/go.mod` require for `github.com/ollama/ollama` to the fixed version
- Re-run govulncheck and prune entries from this document accordingly
---
## Ollama deployment — operator runbook
The Ollama instance the agent connects to runs OUTSIDE of our application
boundary. Operators are responsible for these mitigations:
### 1. Network-level isolation (mandatory)
Bind the Ollama server to a private interface or front it with a reverse proxy:
```bash
# OPTION A — localhost-only binding (single-host deployments)
OLLAMA_HOST=127.0.0.1:11434 ollama serve
# OPTION B — private network only (multi-host fleet)
# Bind to the wireguard / tailscale / private-VLAN interface, not 0.0.0.0
OLLAMA_HOST=10.42.0.5:11434 ollama serve
```
**Never** expose Ollama directly to the public internet. GO-2025-4251 makes
model-management operations possible without auth.
### 2. Reverse proxy with auth (recommended for shared deployments)
If multiple agents share an Ollama server, front it with nginx/caddy/traefik
adding HTTP Basic Auth or an authentication proxy (oauth2-proxy, authentik):
```nginx
location /ollama/ {
auth_basic "Ollama API";
auth_basic_user_file /etc/nginx/ollama.htpasswd;
proxy_pass http://10.42.0.5:11434/;
}
```
Configure the agent's `OllamaConfig.Endpoint` to point at the reverse proxy
URL, and add an `Authorization` header to the http.Client passed to
`api.NewClient`. (When that change lands, re-evaluate GO-2025-3824 per
the watch-flag note above.)
### 3. CI-side govulncheck filter
Until upstream Ollama ships fixes for any of the 9 CVEs, CI should suppress
just these specific findings (not blanket-suppress all govulncheck output):
```bash
govulncheck ./... 2>&1 | grep -vE 'GO-2025-(3548|3557|3558|3559|3582|3689|3695|3824|4251)\b'
```
When a CVE gets a fix and we bump past it, drop that CVE ID from the grep
filter so future regressions surface cleanly.
---
## How to add to this document
When a new accepted finding lands:
1. Open a new H2 section named for the dependency
2. Document the reachability + rationale per CVE in a table
3. Add operator-side mitigations if any
4. Update the audit-history bullet at the top with a Mantis ticket reference
**Do NOT add findings here without a Mantis ticket.** Every accepted finding
must have a tracker entry so the rationale is auditable + reviewable.

22
go.mod
View file

@ -4,15 +4,15 @@ go 1.26.0
require (
dappco.re/go/core v0.8.0-alpha.1
dappco.re/go/core/ai v0.2.2
dappco.re/go/core/api v0.3.0
dappco.re/go/core/cli v0.5.2
dappco.re/go/core/io v0.4.1
dappco.re/go/core/log v0.1.2
dappco.re/go/core/process v0.5.0
dappco.re/go/core/rag v0.1.13
dappco.re/go/core/webview v0.2.1
dappco.re/go/core/ws v0.4.0
dappco.re/go/ai v0.8.0-alpha.1
dappco.re/go/api v0.8.0-alpha.1
dappco.re/go/cli v0.8.0-alpha.1
dappco.re/go/io v0.8.0-alpha.1
dappco.re/go/log v0.8.0-alpha.1
dappco.re/go/process v0.8.0-alpha.1
dappco.re/go/rag v0.8.0-alpha.1
dappco.re/go/webview v0.8.0-alpha.1
dappco.re/go/ws v0.8.0-alpha.1
github.com/gin-gonic/gin v1.12.0
github.com/gorilla/websocket v1.5.3
github.com/modelcontextprotocol/go-sdk v1.5.0
@ -21,8 +21,8 @@ require (
)
require (
dappco.re/go/core/i18n v0.2.3 // indirect
dappco.re/go/core/inference v0.3.0 // indirect
dappco.re/go/i18n v0.8.0-alpha.1 // indirect
dappco.re/go/inference v0.8.0-alpha.1 // indirect
github.com/99designs/gqlgen v0.17.88 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect

View file

@ -10,8 +10,8 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -9,7 +9,7 @@ import (
"net/http"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -7,7 +7,7 @@ import (
"net/http"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreio "dappco.re/go/io"
coremcp "dappco.re/go/mcp/pkg/mcp"
)

View file

@ -9,7 +9,7 @@ import (
"net/http"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -7,7 +7,7 @@ import (
"os/exec"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -10,8 +10,8 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -10,8 +10,8 @@ import (
"os/exec"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -13,8 +13,8 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
"gopkg.in/yaml.v3"

View file

@ -9,7 +9,7 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreio "dappco.re/go/io"
"gopkg.in/yaml.v3"
)

View file

@ -11,8 +11,8 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
)
func listLocalRepos(basePath string) []string {

View file

@ -9,8 +9,8 @@ import (
"syscall"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -11,7 +11,7 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreio "dappco.re/go/io"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -8,7 +8,7 @@ import (
"net/http"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -9,8 +9,8 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/io"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -7,7 +7,7 @@ import (
"time"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -6,7 +6,7 @@ import (
"os"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreio "dappco.re/go/io"
)
// os.CreateTemp, os.Remove, os.Rename are framework-boundary calls for

View file

@ -14,7 +14,7 @@ import (
"time"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -7,7 +7,7 @@ package brain
import (
"context"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/ide"
)

View file

@ -5,9 +5,9 @@ package brain
import (
"net/http"
"dappco.re/go/core/api"
"dappco.re/go/api"
"dappco.re/go/core/api/pkg/provider"
"dappco.re/go/core/ws"
"dappco.re/go/ws"
coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/ide"
"github.com/gin-gonic/gin"

View file

@ -6,7 +6,7 @@ import (
"context"
"time"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/ide"
"github.com/modelcontextprotocol/go-sdk/mcp"

View file

@ -8,7 +8,7 @@ import (
core "dappco.re/go/core"
"github.com/gin-gonic/gin"
api "dappco.re/go/core/api"
api "dappco.re/go/api"
)
// maxBodySize is the maximum request body size accepted by bridged tool endpoints.

View file

@ -17,7 +17,7 @@ import (
"dappco.re/go/mcp/pkg/mcp/agentic"
"dappco.re/go/mcp/pkg/mcp/brain"
"dappco.re/go/mcp/pkg/mcp/ide"
api "dappco.re/go/core/api"
api "dappco.re/go/api"
)
func init() {

View file

@ -9,8 +9,8 @@ import (
"sync"
"time"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/ws"
coreerr "dappco.re/go/log"
"dappco.re/go/ws"
"github.com/gorilla/websocket"
)

View file

@ -11,7 +11,7 @@ import (
"testing"
"time"
"dappco.re/go/core/ws"
"dappco.re/go/ws"
"github.com/gorilla/websocket"
)

View file

@ -9,8 +9,8 @@ import (
core "dappco.re/go/core"
coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/ws"
coreerr "dappco.re/go/log"
"dappco.re/go/ws"
)
// errBridgeNotAvailable is returned when a tool requires the Laravel bridge

View file

@ -7,7 +7,7 @@ import (
"time"
coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "dappco.re/go/core/log"
coreerr "dappco.re/go/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -9,7 +9,7 @@ import (
"time"
coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/core/ws"
"dappco.re/go/ws"
)
// --- Helpers ---

18
pkg/mcp/ipc.go Normal file
View file

@ -0,0 +1,18 @@
// SPDX-License-Identifier: EUPL-1.2
package mcp
import (
"context"
core "dappco.re/go/core"
)
func (s *Service) handleChannelPushIPC(ctx context.Context, ev ChannelPush) core.Result {
if core.Trim(ev.Channel) == "" {
return core.Result{Value: core.E("mcp.HandleIPCEvents", "channel is required", nil), OK: false}
}
s.ChannelSend(ctx, ev.Channel, ev.Data)
return core.Result{OK: true}
}

111
pkg/mcp/ipc_test.go Normal file
View file

@ -0,0 +1,111 @@
package mcp
import (
"testing"
"time"
)
func TestIPC_HandleIPCEvents_Good(t *testing.T) {
svc, err := New(Options{})
if err != nil {
t.Fatalf("New() failed: %v", err)
}
cancel, session, clientConn := connectNotificationSession(t, svc)
defer cancel()
defer session.Close()
defer clientConn.Close()
clientConn.SetDeadline(time.Now().Add(5 * time.Second))
read := readNotificationMessageUntil(t, clientConn, func(msg map[string]any) bool {
return msg["method"] == ChannelNotificationMethod
})
result := svc.HandleIPCEvents(nil, ChannelPush{
Channel: "agent.completed",
Data: map[string]any{
"repo": "core/mcp",
"ok": true,
},
})
if !result.OK {
t.Fatalf("HandleIPCEvents() returned non-OK result: %#v", result.Value)
}
res := <-read
if res.err != nil {
t.Fatalf("failed to read channel notification: %v", res.err)
}
params, ok := res.msg["params"].(map[string]any)
if !ok {
t.Fatalf("expected params object, got %T", res.msg["params"])
}
if params["channel"] != "agent.completed" {
t.Fatalf("expected channel agent.completed, got %#v", params["channel"])
}
payload, ok := params["data"].(map[string]any)
if !ok {
t.Fatalf("expected data object, got %T", params["data"])
}
if payload["repo"] != "core/mcp" || payload["ok"] != true {
t.Fatalf("unexpected payload: %#v", payload)
}
}
func TestIPC_HandleIPCEvents_Bad(t *testing.T) {
svc, err := New(Options{})
if err != nil {
t.Fatalf("New() failed: %v", err)
}
result := svc.HandleIPCEvents(nil, ChannelPush{
Channel: " \t ",
Data: map[string]any{"ok": false},
})
if result.OK {
t.Fatal("expected empty ChannelPush channel to fail")
}
if _, ok := result.Value.(error); !ok {
t.Fatalf("expected error result value, got %T", result.Value)
}
}
func TestIPC_HandleIPCEvents_Ugly(t *testing.T) {
svc, err := New(Options{})
if err != nil {
t.Fatalf("New() failed: %v", err)
}
cancel, session, clientConn := connectNotificationSession(t, svc)
defer cancel()
defer session.Close()
defer clientConn.Close()
clientConn.SetDeadline(time.Now().Add(5 * time.Second))
read := readNotificationMessageUntil(t, clientConn, func(msg map[string]any) bool {
params, ok := msg["params"].(map[string]any)
return msg["method"] == ChannelNotificationMethod && ok && params["channel"] == "agent.edge"
})
result := svc.HandleIPCEvents(nil, ChannelPush{Channel: "agent.edge"})
if !result.OK {
t.Fatalf("HandleIPCEvents() returned non-OK result: %#v", result.Value)
}
res := <-read
if res.err != nil {
t.Fatalf("failed to read edge notification: %v", res.err)
}
params, ok := res.msg["params"].(map[string]any)
if !ok {
t.Fatalf("expected params object, got %T", res.msg["params"])
}
if _, ok := params["data"]; !ok {
t.Fatalf("expected data key for nil ChannelPush data: %#v", params)
}
if params["data"] != nil {
t.Fatalf("expected nil data, got %#v", params["data"])
}
}

View file

@ -15,10 +15,10 @@ import (
"sync"
core "dappco.re/go/core"
"dappco.re/go/core/io"
"dappco.re/go/core/log"
"dappco.re/go/core/process"
"dappco.re/go/core/ws"
"dappco.re/go/io"
"dappco.re/go/log"
"dappco.re/go/process"
"dappco.re/go/ws"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -11,7 +11,7 @@ import (
"context"
"io"
"iter"
"os"
"os" // Note: required for process stdout; core Fs/Env do not expose a stdio writer.
"reflect"
"slices"
"sync"

View file

@ -7,8 +7,8 @@ import (
"time"
core "dappco.re/go/core"
"dappco.re/go/core/process"
"dappco.re/go/core/ws"
"dappco.re/go/process"
"dappco.re/go/ws"
)
// Register is the service factory for core.WithService.
@ -98,6 +98,7 @@ func (s *Service) OnStartup(ctx context.Context) core.Result {
// HandleIPCEvents implements Core's IPC handler interface.
//
// c.ACTION(mcp.ChannelPush{Channel: "agent.status", Data: statusMap})
//
// Catches ChannelPush messages from other services and pushes them to Claude Code sessions.
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result {
ctx := context.Background()
@ -109,7 +110,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result {
switch ev := msg.(type) {
case ChannelPush:
s.ChannelSend(ctx, ev.Channel, ev.Data)
return s.handleChannelPushIPC(ctx, ev)
case process.ActionProcessStarted:
startedAt := time.Now()
s.recordProcessRuntime(ev.ID, processRuntime{

View file

@ -9,8 +9,8 @@ import (
"time"
"dappco.re/go/core"
"dappco.re/go/core/process"
"dappco.re/go/core/ws"
"dappco.re/go/process"
"dappco.re/go/ws"
)
func TestRegister_Good_WiresOptionalServices(t *testing.T) {

View file

@ -7,7 +7,7 @@ import (
"errors"
"testing"
"dappco.re/go/core/process"
"dappco.re/go/process"
)
func TestToolRegistry_Good_RecordsTools(t *testing.T) {

View file

@ -8,8 +8,8 @@ import (
"time"
core "dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/log"
"dappco.re/go/ai/ai"
"dappco.re/go/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -6,8 +6,8 @@ import (
"context"
"time"
"dappco.re/go/core/log"
"dappco.re/go/core/process"
"dappco.re/go/log"
"dappco.re/go/process"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -9,7 +9,7 @@ import (
"time"
"dappco.re/go/core"
"dappco.re/go/core/process"
"dappco.re/go/process"
)
// newTestProcessService creates a real process.Service backed by a core.Core for CI tests.

View file

@ -6,8 +6,8 @@ import (
"context"
core "dappco.re/go/core"
"dappco.re/go/core/log"
"dappco.re/go/core/rag"
"dappco.re/go/log"
"dappco.re/go/rag"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -13,8 +13,8 @@ import (
"time"
core "dappco.re/go/core"
"dappco.re/go/core/log"
"dappco.re/go/core/webview"
"dappco.re/go/log"
"dappco.re/go/webview"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -8,7 +8,7 @@ import (
"time"
core "dappco.re/go/core"
"dappco.re/go/core/log"
"dappco.re/go/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -11,7 +11,7 @@ import (
"testing"
"time"
"dappco.re/go/core/webview"
"dappco.re/go/webview"
)
// skipIfShort skips webview tests in short mode (go test -short).

View file

@ -8,8 +8,8 @@ import (
"net/http"
core "dappco.re/go/core"
"dappco.re/go/core/log"
"dappco.re/go/core/ws"
"dappco.re/go/log"
"dappco.re/go/ws"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -11,7 +11,7 @@ import (
"time"
core "dappco.re/go/core"
"dappco.re/go/core/log"
"dappco.re/go/log"
"github.com/gorilla/websocket"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -3,7 +3,7 @@ package mcp
import (
"testing"
"dappco.re/go/core/ws"
"dappco.re/go/ws"
)
// TestWSToolsRegistered_Good verifies that WebSocket tools are registered when hub is available.

View file

@ -13,8 +13,8 @@ import (
"time"
core "dappco.re/go/core"
api "dappco.re/go/core/api"
coreerr "dappco.re/go/core/log"
api "dappco.re/go/api"
coreerr "dappco.re/go/log"
"github.com/gin-gonic/gin"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -6,7 +6,7 @@ import (
"context"
"os"
"dappco.re/go/core/log"
"dappco.re/go/log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -6,8 +6,8 @@ import (
"context"
"net"
"dappco.re/go/core/io"
"dappco.re/go/core/log"
"dappco.re/go/io"
"dappco.re/go/log"
)
// ServeUnix starts a Unix domain socket server for the MCP service.

View file

@ -0,0 +1,26 @@
version: "3"
tasks:
default:
deps:
- build
- vet
- test
build:
desc: Compile every package + binary in mcp.
dir: ../../..
cmds:
- GOWORK=off go build ./...
vet:
desc: Run go vet across the module.
dir: ../../..
cmds:
- GOWORK=off go vet ./...
test:
desc: Run unit tests.
dir: ../../..
cmds:
- GOWORK=off go test -count=1 ./...