Closes #6 on gitea.d-ma.be/mathias/hyperguild. Dex is deployed at auth.d-ma.be. All three MCP servers now accept JWTs issued by Dex in addition to static bearer tokens, enabling claude.ai OAuth 2.0 integration without abandoning backward-compat CLI auth. Changes: - internal/auth/: new Validator (JWKS auto-refresh via lestrrat-go/jwx/v2), ProtectedResourceHandler (RFC 9728 /.well-known/oauth-protected-resource) - internal/mcp/Server: adds optional *auth.Validator; checkAuth tries JWT first, then static token fallback; both-nil = auth disabled (unchanged default) - cmd/supervisor, cmd/routing: construct Validator from DEX_ISSUER_URL + MCP_AUDIENCE env vars; register protected-resource handler when set - ingestion/internal/auth/: same Validator + handler (separate module) - ingestion/internal/mcp/BearerAuth: same JWT-or-static chain - ingestion/cmd/server: same wiring pattern New env vars (all optional; absent = static-token-only, same as before): DEX_ISSUER_URL — Dex issuer URL (e.g. https://auth.d-ma.be) MCP_AUDIENCE — expected aud claim (e.g. brain, supervisor) MCP_RESOURCE_URL — resource identifier for RFC 9728 metadata response Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
// ingestion/cmd/server/main.go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/auth"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/llm"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/mcp"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/watcher"
|
|
)
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envInt(key string, fallback int) int {
|
|
if v := os.Getenv(key); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func main() {
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
|
|
brainDir := envOr("INGEST_BRAIN_DIR", "../brain")
|
|
port := envOr("INGEST_PORT", "3300")
|
|
|
|
llmURL := envOr("INGEST_LLM_URL", "http://iguana:4000/v1")
|
|
llmKey := os.Getenv("INGEST_LLM_KEY")
|
|
llmModel := envOr("INGEST_LLM_MODEL", "koala/qwen35-9b-fast")
|
|
llmTimeoutMins := envInt("INGEST_LLM_TIMEOUT", 15)
|
|
chunkSize := envInt("INGEST_CHUNK_SIZE", 6000)
|
|
watchInterval := envInt("INGEST_WATCH_INTERVAL", 30)
|
|
|
|
llmClient := llm.New(llmURL, llmKey, llmModel, time.Duration(llmTimeoutMins)*time.Minute)
|
|
|
|
pipelineCfg := pipeline.Config{
|
|
Complete: llmClient.Complete,
|
|
ChunkSize: chunkSize,
|
|
}
|
|
|
|
h := api.NewHandler(brainDir, logger, pipelineCfg)
|
|
|
|
mcpSrv := mcp.NewServer(brainDir, &pipelineCfg, llmClient.Complete)
|
|
|
|
mcpToken := os.Getenv("BRAIN_MCP_TOKEN")
|
|
if mcpToken == "" {
|
|
logger.Error("BRAIN_MCP_TOKEN not set")
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if watchInterval > 0 {
|
|
watcher.Start(ctx, watcher.Config{
|
|
BrainDir: brainDir,
|
|
Interval: time.Duration(watchInterval) * time.Second,
|
|
Pipeline: pipelineCfg,
|
|
})
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /query", h.Query)
|
|
mux.HandleFunc("POST /write", h.Write)
|
|
mux.HandleFunc("POST /ingest", h.Ingest)
|
|
mux.HandleFunc("POST /ingest-path", h.IngestPath)
|
|
mux.HandleFunc("POST /ingest-raw", h.IngestRaw)
|
|
mux.HandleFunc("POST /backfill-refs", h.BackfillRefs)
|
|
mux.HandleFunc("GET /pass-rate", h.PassRate)
|
|
var jwtValidator *auth.Validator
|
|
if dexURL := os.Getenv("DEX_ISSUER_URL"); dexURL != "" {
|
|
audience := os.Getenv("MCP_AUDIENCE")
|
|
v, err := auth.NewValidator(dexURL, audience)
|
|
if err != nil {
|
|
logger.Error("build jwt validator", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
jwtValidator = v
|
|
logger.Info("jwt auth enabled", "issuer", dexURL)
|
|
}
|
|
|
|
mux.Handle("/mcp", mcp.BearerAuth(mcpToken, jwtValidator, mcpSrv))
|
|
|
|
if dexURL := os.Getenv("DEX_ISSUER_URL"); dexURL != "" {
|
|
resourceURL := os.Getenv("MCP_RESOURCE_URL")
|
|
mux.HandleFunc("GET /.well-known/oauth-protected-resource",
|
|
auth.ProtectedResourceHandler(resourceURL, os.Getenv("DEX_ISSUER_URL")))
|
|
}
|
|
|
|
addr := ":" + port
|
|
watchIntervalLog := "disabled"
|
|
if watchInterval > 0 {
|
|
watchIntervalLog = fmt.Sprintf("%ds", watchInterval)
|
|
}
|
|
logger.Info("ingestion server starting",
|
|
"addr", addr,
|
|
"brain_dir", brainDir,
|
|
"llm_url", llmURL,
|
|
"llm_model", llmModel,
|
|
"chunk_size", chunkSize,
|
|
"watch_interval", watchIntervalLog,
|
|
"mcp_enabled", true,
|
|
)
|
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
logger.Error("server stopped", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|