Files
glpi-neuroforge-mega/services/control/main.go
jbergner a6bc71fb3a
Some checks failed
release-tag / Resolve release metadata (push) Successful in 30s
release-tag / Build knowledge (push) Failing after 4m51s
release-tag / Build control (push) Failing after 5m0s
release-tag / Build agent (push) Failing after 5m0s
release-tag / Build agent-data-init (push) Failing after 5m5s
release-tag / Build neuroforge-worker (push) Failing after 5m7s
release-tag / Build neuroforge (push) Failing after 5m9s
Init
2026-08-26 18:34:41 +02:00

198 lines
8.0 KiB
Go

package main
import (
"context"
"embed"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
//go:embed index.html
var web embed.FS
type target struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
PublicURL string `json:"public_url"`
Path string `json:"-"`
Auth string `json:"-"`
Optional bool `json:"optional,omitempty"`
}
type status struct {
Name string `json:"name"`
OK bool `json:"ok"`
Status int `json:"status"`
LatencyMS int64 `json:"latency_ms"`
Detail any `json:"detail,omitempty"`
Error string `json:"error,omitempty"`
PublicURL string `json:"public_url,omitempty"`
Optional bool `json:"optional,omitempty"`
}
type server struct {
http *http.Client
targets []target
agentURL string
agentReadToken string
neuroforgeURL string
neuroforgeKey string
codebaseMemoryURL string
codebaseMemoryPublicURL string
vectorMode string
neuroforgeSearchK string
failOpen string
controlledLearning string
outcomeLearning string
outcomeRetrieval string
outcomeSearchK string
outcomeMinSimilarity string
outcomeFailOpen string
researchEnabled string
searxngEnabled string
autonomyEnabled string
}
func env(k, d string) string {
if v := strings.TrimSpace(os.Getenv(k)); v != "" {
return v
}
return d
}
func main() {
agentURL := env("AGENT_URL", "http://agent:8080")
nfURL := env("NEUROFORGE_URL", "http://neuroforge:8080")
nfKeyRaw := strings.TrimSpace(os.Getenv("NEUROFORGE_API_KEY"))
nfAuth := ""
if nfKeyRaw != "" {
nfAuth = "Bearer " + nfKeyRaw
}
s := &server{http: &http.Client{Timeout: 6 * time.Second}, agentURL: strings.TrimRight(agentURL, "/"), agentReadToken: strings.TrimSpace(os.Getenv("CONTROL_READ_TOKEN")), neuroforgeURL: strings.TrimRight(nfURL, "/"), neuroforgeKey: nfKeyRaw, codebaseMemoryURL: strings.TrimRight(strings.TrimSpace(os.Getenv("CODEBASE_MEMORY_URL")), "/"), codebaseMemoryPublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("PUBLIC_CODEBASE_MEMORY_URL")), "/"), vectorMode: env("KNOWLEDGE_VECTOR_BACKEND", "dual"), neuroforgeSearchK: env("NEUROFORGE_SEARCH_K", "128"), failOpen: env("NEUROFORGE_FAIL_OPEN", "true"), controlledLearning: env("NEUROFORGE_CONTROLLED_LEARNING", "true"), outcomeLearning: env("OUTCOME_LEARNING_ENABLED", "true"), outcomeRetrieval: env("OUTCOME_RETRIEVAL_ENABLED", "true"), outcomeSearchK: env("OUTCOME_RETRIEVAL_SEARCH_K", "6"), outcomeMinSimilarity: env("OUTCOME_RETRIEVAL_MIN_SIMILARITY", "0.58"), outcomeFailOpen: env("OUTCOME_RETRIEVAL_FAIL_OPEN", "true"), researchEnabled: env("NEUROFORGE_RESEARCH_ENABLED", "false"), searxngEnabled: env("NEUROFORGE_SEARXNG_ENABLED", "false"), autonomyEnabled: env("NEUROFORGE_AUTONOMY_ENABLED", "false")}
s.targets = []target{
{ID: "agent", Name: "GLPI AI Agent", URL: agentURL, PublicURL: env("PUBLIC_AGENT_URL", "http://localhost:8080"), Path: "/readyz"},
{ID: "knowledge", Name: "Knowledgebase", URL: env("KNOWLEDGE_URL", "http://knowledge:8080"), PublicURL: env("PUBLIC_KNOWLEDGE_URL", "http://localhost:8081"), Path: "/api/health"},
{ID: "neuroforge", Name: "NeuroForge Brain", URL: nfURL, PublicURL: env("PUBLIC_NEUROFORGE_URL", "http://localhost:8090/admin"), Path: "/api/v1/stats", Auth: nfAuth},
}
if s.codebaseMemoryURL != "" {
s.targets = append(s.targets, target{ID: "codebase-memory", Name: "Codebase Memory MCP", URL: s.codebaseMemoryURL, PublicURL: s.codebaseMemoryPublicURL, Path: "/", Optional: true})
}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{"ok": true}) })
mux.HandleFunc("GET /api/status", s.handleStatus)
mux.HandleFunc("GET /api/config", s.handleConfig)
mux.HandleFunc("GET /api/graph/runtime", s.handleRuntimeGraph)
mux.HandleFunc("GET /api/graph/runs", s.handleGraphRuns)
mux.HandleFunc("GET /api/graph/ticket", s.handleTicketGraph)
mux.HandleFunc("GET /api/graph/learning", s.handleLearningGraph)
mux.HandleFunc("GET /api/graph/research", s.handleResearchGraph)
mux.HandleFunc("GET /api/graph/brain", s.handleBrainGraph)
mux.HandleFunc("GET /api/graph/engineering", s.handleEngineeringGraph)
mux.HandleFunc("GET /api/graph/impact", s.handleEngineeringImpact)
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
b, _ := web.ReadFile("index.html")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(b)
})
addr := env("CONTROL_ADDR", ":8070")
srv := &http.Server{Addr: addr, Handler: secure(mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second}
log.Printf("mega control listening on %s", addr)
log.Fatal(srv.ListenAndServe())
}
func secure(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'")
next.ServeHTTP(w, r)
})
}
func (s *server) handleConfig(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"vector_backend": s.vectorMode, "neuroforge_search_k": s.neuroforgeSearchK, "neuroforge_fail_open": s.failOpen, "controlled_learning": s.controlledLearning, "outcome_learning": s.outcomeLearning, "outcome_retrieval": s.outcomeRetrieval, "outcome_retrieval_search_k": s.outcomeSearchK, "outcome_retrieval_min_similarity": s.outcomeMinSimilarity, "outcome_retrieval_fail_open": s.outcomeFailOpen, "quality_replay": "available-on-agent", "research_enabled": s.researchEnabled, "searxng_enabled": s.searxngEnabled, "autonomy_enabled": s.autonomyEnabled, "control_plane": "read-only", "policy_owner": "glpi-agent", "unified_graph": true, "engineering_graph": "embedded-ast", "codebase_memory_url": s.codebaseMemoryPublicURL})
}
func (s *server) handleStatus(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
out := s.statusSnapshot(ctx)
all := true
for _, st := range out {
if !st.OK && !st.Optional {
all = false
}
}
code := 200
if !all {
code = 207
}
writeJSON(w, code, map[string]any{"ok": all, "checked_at": time.Now().UTC(), "services": out})
}
func (s *server) statusSnapshot(ctx context.Context) []status {
ch := make(chan status, len(s.targets))
for _, t := range s.targets {
go func(t target) { ch <- s.check(ctx, t) }(t)
}
out := make([]status, 0, len(s.targets))
for range s.targets {
out = append(out, <-ch)
}
return out
}
func (s *server) check(ctx context.Context, t target) status {
started := time.Now()
st := status{Name: t.Name, PublicURL: t.PublicURL, Optional: t.Optional}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(t.URL, "/")+t.Path, nil)
if err != nil {
st.Error = err.Error()
return st
}
if t.Auth != "" {
req.Header.Set("Authorization", t.Auth)
}
resp, err := s.http.Do(req)
st.LatencyMS = time.Since(started).Milliseconds()
if err != nil {
st.Error = err.Error()
return st
}
defer resp.Body.Close()
st.Status = resp.StatusCode
st.OK = resp.StatusCode >= 200 && resp.StatusCode < 300
b, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
if len(b) > 0 {
var v any
if json.Unmarshal(b, &v) == nil {
st.Detail = v
} else {
st.Detail = string(b)
}
}
if !st.OK && st.Error == "" {
st.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
return st
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}