Files
glpi-neural-brain/internal/web/source_agents.go

309 lines
9.7 KiB
Go

package web
import (
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/local/glpi-neural-brain/internal/sourceagent"
)
func (s *Server) sourceStoreAvailable(w http.ResponseWriter) bool {
if s.SourceAgents != nil {
return true
}
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "source agent service is not initialized"})
return false
}
func (s *Server) adminAuthorized(w http.ResponseWriter, r *http.Request) bool {
if s.authorized(r) {
return true
}
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return false
}
func (s *Server) handleListSourceAgents(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
agents, err := s.SourceAgents.ListAgents(ctx)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
tasks, _ := s.SourceAgents.ListTasks(ctx, "")
stats, _ := s.SourceAgents.Stats(ctx)
writeJSON(w, 200, map[string]any{"agents": agents, "tasks": tasks, "inbox": stats})
}
func (s *Server) handleCreateSourceAgent(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
var in struct {
ID string `json:"id"`
Name string `json:"name"`
}
if err := decode(r, &in); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
a, token, err := s.SourceAgents.CreateAgent(ctx, in.ID, in.Name)
if err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, map[string]any{"agent": a, "token": token, "token_notice": "Der Token wird nur in dieser Antwort im Klartext ausgegeben."})
}
func (s *Server) handlePatchSourceAgent(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
var in struct {
Enabled *bool `json:"enabled"`
}
if err := decode(r, &in); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
if in.Enabled == nil {
writeJSON(w, 400, map[string]string{"error": "enabled is required"})
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
if err := s.SourceAgents.SetAgentEnabled(ctx, r.PathValue("id"), *in.Enabled); err != nil {
writeJSON(w, 404, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleDeleteSourceAgent(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
if err := s.SourceAgents.DeleteAgent(ctx, r.PathValue("id")); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleRotateSourceAgentToken(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
token, err := s.SourceAgents.RotateToken(ctx, r.PathValue("id"))
if err != nil {
writeJSON(w, 404, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"token": token, "token_notice": "Der neue Token wird nur in dieser Antwort im Klartext ausgegeben."})
}
func (s *Server) handleListSourceTasks(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
tasks, err := s.SourceAgents.ListTasks(ctx, r.PathValue("id"))
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"tasks": tasks})
}
func (s *Server) handleCreateSourceTask(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
var t sourceagent.Task
if err := decode(r, &t); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
t.AgentID = r.PathValue("id")
if strings.TrimSpace(t.ID) == "" {
t.ID = "task-" + strconv.FormatInt(time.Now().UnixNano(), 36)
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
out, err := s.SourceAgents.UpsertTask(ctx, t)
if err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, out)
}
func (s *Server) handleUpdateSourceTask(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
var t sourceagent.Task
if err := decode(r, &t); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
t.ID = r.PathValue("id")
if t.AgentID == "" {
writeJSON(w, 400, map[string]string{"error": "agent_id required"})
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
out, err := s.SourceAgents.UpsertTask(ctx, t)
if err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, out)
}
func (s *Server) handleDeleteSourceTask(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
if err := s.SourceAgents.DeleteTask(ctx, r.PathValue("id")); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
func (s *Server) handleSourceInbox(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
limit := 100
if n, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && n > 0 && n <= 1000 {
limit = n
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
docs, err := s.SourceAgents.ListInbox(ctx, strings.TrimSpace(r.URL.Query().Get("status")), limit)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"documents": docs})
}
func (s *Server) handleSourceInboxStatus(w http.ResponseWriter, r *http.Request) {
if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) {
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
st, err := s.SourceAgents.Stats(ctx)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, st)
}
func bearerToken(r *http.Request) string {
v := strings.TrimSpace(r.Header.Get("Authorization"))
if strings.HasPrefix(v, "Bearer ") {
return strings.TrimSpace(strings.TrimPrefix(v, "Bearer "))
}
return ""
}
func (s *Server) authenticateSourceAgent(r *http.Request) (sourceagent.Agent, error) {
if s.SourceAgents == nil {
return sourceagent.Agent{}, errors.New("source agent service unavailable")
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
a, err := s.SourceAgents.Authenticate(ctx, bearerToken(r))
if err != nil {
return a, err
}
if want := strings.TrimSpace(r.Header.Get("X-Brain-Agent-ID")); want != "" && want != a.ID {
return sourceagent.Agent{}, errors.New("agent id does not match token")
}
return a, nil
}
func (s *Server) handleAgentConfig(w http.ResponseWriter, r *http.Request) {
a, err := s.authenticateSourceAgent(r)
if err != nil {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
cfg, err := s.SourceAgents.RemoteConfig(ctx, a)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, cfg)
}
func (s *Server) handleAgentHeartbeat(w http.ResponseWriter, r *http.Request) {
a, err := s.authenticateSourceAgent(r)
if err != nil {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
var h sourceagent.Heartbeat
if err := decode(r, &h); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
if h.AgentID != "" && h.AgentID != a.ID {
writeJSON(w, 403, map[string]string{"error": "agent_id mismatch"})
return
}
ctx, cancel := contextTimeout(r, 10*time.Second)
defer cancel()
if err := s.SourceAgents.Heartbeat(ctx, a.ID, h); err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
writeJSON(w, 200, map[string]any{"ok": true, "server_time": time.Now().UTC()})
}
func (s *Server) handleAgentIngest(w http.ResponseWriter, r *http.Request) {
a, err := s.authenticateSourceAgent(r)
if err != nil {
writeJSON(w, 401, map[string]string{"error": "unauthorized"})
return
}
var batch sourceagent.IngestBatch
dec := json.NewDecoder(io.LimitReader(r.Body, 32<<20))
if err := dec.Decode(&batch); err != nil {
writeJSON(w, 400, map[string]string{"error": err.Error()})
return
}
if batch.AgentID != "" && batch.AgentID != a.ID {
writeJSON(w, 403, map[string]string{"error": "agent_id mismatch"})
return
}
if batch.SchemaVersion != 0 && batch.SchemaVersion != sourceagent.SchemaVersion {
writeJSON(w, 400, map[string]string{"error": "unsupported schema_version"})
return
}
if strings.TrimSpace(batch.TaskID) == "" || len(batch.Documents) == 0 || len(batch.Documents) > 500 {
writeJSON(w, 400, map[string]string{"error": "task_id and 1..500 documents are required"})
return
}
ctx, cancel := contextTimeout(r, 60*time.Second)
defer cancel()
result, err := s.SourceAgents.Ingest(ctx, a.ID, batch.TaskID, batch.Documents)
if err != nil {
writeJSON(w, 500, map[string]string{"error": err.Error()})
return
}
_ = s.SourceAgents.Heartbeat(ctx, a.ID, sourceagent.Heartbeat{AgentID: a.ID, Status: "ingest", Documents: result.Accepted})
writeJSON(w, http.StatusAccepted, result)
}