Only /mcp was wrapped in BearerMiddleware; /query, /write, /ingest*, /promote, /pending and the rest on the same mux had no auth, and infra#517 found them on the internet (mitigated there by narrowing the ingress). They are still open on the tailnet and LAN NodePort 30330. mountREST puts the REST routes on their own mux, mounted at "/" behind the same bearer middleware and credential as /mcp. Routes registered on the outer mux stay explicitly public (/mcp has its own auth, /capture, the HMAC webhook, OAuth metadata, /metrics); anything else, including a route added later, is authenticated by default. The hyperguild CLI (cmd/hyperguild and internal/brain) now sends BRAIN_MCP_TOKEN, which it never did. brain-ui and brain-ingest-ntfy already send theirs. Refs #149 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.3 KiB
Go
82 lines
2.3 KiB
Go
// internal/brain/client.go
|
|
// Package brain provides a lightweight client for querying the ingestion server.
|
|
// Skill handlers call Query before spawning workers to inject relevant knowledge
|
|
// from the brain into the task prompt. Errors are suppressed — the brain is
|
|
// optional context; its absence must never block a skill invocation.
|
|
package brain
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type queryResult struct {
|
|
Path string `json:"path"`
|
|
Title string `json:"title"`
|
|
Excerpt string `json:"excerpt"`
|
|
Score int `json:"score"`
|
|
}
|
|
|
|
// Query calls the ingestion server and returns relevant knowledge as a
|
|
// formatted string ready to prepend to a worker task prompt.
|
|
// Returns empty string (no error) when baseURL or query is empty,
|
|
// when the brain is unreachable, or when no results are found.
|
|
func Query(ctx context.Context, baseURL, query string, limit int) (string, error) {
|
|
if baseURL == "" || strings.TrimSpace(query) == "" {
|
|
return "", nil
|
|
}
|
|
if limit <= 0 {
|
|
limit = 3
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]any{"query": query, "limit": limit})
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/query", bytes.NewReader(body))
|
|
if err != nil {
|
|
slog.Warn("brain: build request failed", "err", err)
|
|
return "", nil
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// The REST API requires a bearer token since hyperguild#149.
|
|
if tok := os.Getenv("BRAIN_MCP_TOKEN"); tok != "" {
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
slog.Warn("brain: ingestion server unreachable", "err", err)
|
|
return "", nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
slog.Warn("brain: ingestion server returned non-OK", "status", resp.StatusCode)
|
|
return "", nil
|
|
}
|
|
|
|
out, _ := io.ReadAll(resp.Body)
|
|
var result struct {
|
|
Results []queryResult `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(out, &result); err != nil || len(result.Results) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("## Relevant knowledge\n\n")
|
|
for _, r := range result.Results {
|
|
title := r.Title
|
|
if title == "" {
|
|
title = r.Path
|
|
}
|
|
fmt.Fprintf(&b, "### %s\n%s\n\n", title, r.Excerpt)
|
|
}
|
|
return b.String(), nil
|
|
}
|