package web import ( "encoding/json" "io" "net/http" "strconv" "strings" "time" "github.com/local/glpi-neural-brain/internal/model" "github.com/local/glpi-neural-brain/internal/sourceagent" ) func (s *Server) handleGetControllerPolicy(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() p, err := s.SourceAgents.ControllerPolicy(ctx) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, p) } func (s *Server) handleSetControllerPolicy(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } var p sourceagent.ControllerPolicy if err := decode(r, &p); err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() out, err := s.SourceAgents.SetControllerPolicy(ctx, p) if err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } if s.Broker != nil { s.Broker.Publish(model.Activity{Type: "controller.policy.updated", Source: "brain", Phase: "controller-policy", Message: "Docker-Controller-Policy wurde geƤndert", Strength: .88, Metadata: map[string]any{"enabled": out.Enabled, "autonomous_enabled": out.AutonomousEnabled, "dry_run": out.DryRun, "allow_destructive": out.AllowDestructive, "max_concurrent_jobs": out.MaxConcurrentJobs}}) } writeJSON(w, 200, out) } func (s *Server) handleListControllerProfiles(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() p, err := s.SourceAgents.ListControllerProfiles(ctx) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if p == nil { p = []sourceagent.ControllerProfile{} } writeJSON(w, 200, map[string]any{"profiles": p}) } func (s *Server) handleCreateControllerProfile(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } var p sourceagent.ControllerProfile if err := decode(r, &p); err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() out, err := s.SourceAgents.UpsertControllerProfile(ctx, p) if err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusCreated, out) } func (s *Server) handleUpdateControllerProfile(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } var p sourceagent.ControllerProfile if err := decode(r, &p); err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } p.ID = r.PathValue("id") ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() out, err := s.SourceAgents.UpsertControllerProfile(ctx, p) if err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, out) } func (s *Server) handleDeleteControllerProfile(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.DeleteControllerProfile(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) handleRunControllerProfile(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } var in struct { DryRun *bool `json:"dry_run"` Overrides map[string]any `json:"overrides"` } _ = decode(r, &in) ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() profile, err := s.SourceAgents.ControllerProfile(ctx, r.PathValue("id")) if err != nil { writeJSON(w, 404, map[string]string{"error": err.Error()}) return } params := map[string]any{} for k, v := range profile.Config { params[k] = v } for k, v := range in.Overrides { params[k] = v } job := sourceagent.ControllerJob{AgentID: profile.AgentID, ProfileID: profile.ID, Kind: profile.Kind, Purpose: profile.Purpose, Parameters: params} if in.DryRun != nil { job.DryRun = *in.DryRun } job, err = s.SourceAgents.QueueControllerJob(ctx, job) if err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusAccepted, job) } func (s *Server) handleListControllerJobs(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 { limit = n } ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() jobs, err := s.SourceAgents.ListControllerJobs(ctx, limit) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if jobs == nil { jobs = []sourceagent.ControllerJob{} } writeJSON(w, 200, map[string]any{"jobs": jobs, "stats": s.SourceAgents.ControllerStats(ctx)}) } func (s *Server) handleCreateControllerJob(w http.ResponseWriter, r *http.Request) { if !s.sourceStoreAvailable(w) || !s.adminAuthorized(w, r) { return } var job sourceagent.ControllerJob if err := decode(r, &job); err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } job.Autonomous = false ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() out, err := s.SourceAgents.QueueControllerJob(ctx, job) if err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } if s.Broker != nil { s.Broker.Publish(model.Activity{Type: "controller.job.queued", Source: "brain", Phase: "controller-manual", Message: "Manueller Docker-Controller-Job wurde eingeplant", Strength: .7, Metadata: map[string]any{"job_id": out.ID, "kind": out.Kind, "agent_id": out.AgentID, "dry_run": out.DryRun}}) } writeJSON(w, http.StatusAccepted, out) } func (s *Server) handleCancelControllerJob(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.CancelControllerJob(ctx, r.PathValue("id")); err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{"ok": true}) } func agentHasCapability(a sourceagent.Agent, capability string) bool { for _, c := range a.Capabilities { if c == capability { return true } } return false } func (s *Server) handleAgentControllerClaim(w http.ResponseWriter, r *http.Request) { a, err := s.authenticateSourceAgent(r) if err != nil { writeJSON(w, 401, map[string]string{"error": "unauthorized"}) return } if !agentHasCapability(a, sourceagent.CapabilityDockerController) { writeJSON(w, 403, map[string]string{"error": "agent has not advertised docker_controller capability"}) return } ctx, cancel := contextTimeout(r, 15*time.Second) defer cancel() p, err := s.SourceAgents.ControllerPolicy(ctx) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if !p.Enabled { w.WriteHeader(http.StatusNoContent) return } job, ok, err := s.SourceAgents.ClaimControllerJob(ctx, a.ID, p.MaxJobDuration, agentHasCapability(a, sourceagent.CapabilityDockerCompose)) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if !ok { w.WriteHeader(http.StatusNoContent) return } writeJSON(w, 200, sourceagent.ControllerClaim{SchemaVersion: sourceagent.SchemaVersion, Job: job, Policy: p}) } func (s *Server) handleAgentControllerAuthorized(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, 5*time.Second) defer cancel() ok, reason, err := s.SourceAgents.ControllerJobAuthorized(ctx, a.ID, r.PathValue("id")) if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}) return } if !ok { writeJSON(w, 409, map[string]any{"authorized": false, "reason": reason}) return } writeJSON(w, 200, map[string]any{"authorized": true}) } func (s *Server) handleAgentControllerResult(w http.ResponseWriter, r *http.Request) { a, err := s.authenticateSourceAgent(r) if err != nil { writeJSON(w, 401, map[string]string{"error": "unauthorized"}) return } var result sourceagent.ControllerJobResult dec := json.NewDecoder(io.LimitReader(r.Body, 2<<20)) if err := dec.Decode(&result); err != nil { writeJSON(w, 400, map[string]string{"error": err.Error()}) return } result.JobID = strings.TrimSpace(r.PathValue("id")) if result.AgentID != "" && result.AgentID != a.ID { writeJSON(w, 403, map[string]string{"error": "agent_id mismatch"}) return } ctx, cancel := contextTimeout(r, 10*time.Second) defer cancel() job, _ := s.SourceAgents.ControllerJob(ctx, result.JobID) if err := s.SourceAgents.CompleteControllerJob(ctx, a.ID, result); err != nil { writeJSON(w, 409, map[string]string{"error": err.Error()}) return } nodeIDs := controllerJobNodeIDs(job) if job.Kind == "evidence_http_probe" && result.Status == sourceagent.ControllerJobSucceeded && result.Error == "" && s.Graph != nil { if external, ok := s.Graph.LookupExternal(strings.TrimSpace(asControllerString(job.Parameters["url"]))); ok { if external.Metadata == nil { external.Metadata = map[string]any{} } external.Metadata["controller_probe_verified_at"] = time.Now().UTC().Format(time.RFC3339Nano) external.Metadata["controller_probe_agent_id"] = a.ID external.Metadata["controller_probe_job_id"] = result.JobID for _, key := range []string{"http_status", "body_sha256", "captured_bytes", "effective_url", "content_type", "download_bytes"} { if value, exists := result.Result[key]; exists { external.Metadata["controller_probe_"+key] = value } } s.Graph.UpsertNode(external) nodeIDs = append(nodeIDs, external.ID) } } if s.Broker != nil { s.Broker.Publish(model.Activity{Type: "controller.job.completed", Source: "agent", Phase: "controller", NodeIDs: nodeIDs, Message: "Docker-Controller-Job wurde abgeschlossen", Strength: .76, Metadata: map[string]any{"job_id": result.JobID, "agent_id": a.ID, "kind": job.Kind, "status": result.Status, "duration_ms": result.DurationMS, "error": result.Error, "result": result.Result}}) } writeJSON(w, 200, map[string]any{"ok": true}) } func asControllerString(v any) string { return strings.TrimSpace(strings.TrimSpace(toControllerString(v))) } func toControllerString(v any) string { if v == nil { return "" } if s, ok := v.(string); ok { return s } b, _ := json.Marshal(v) return string(b) } func controllerJobNodeIDs(job sourceagent.ControllerJob) []string { var out []string switch raw := job.Parameters["source_node_ids"].(type) { case []string: out = append(out, raw...) case []any: for _, v := range raw { if s, ok := v.(string); ok && strings.TrimSpace(s) != "" { out = append(out, strings.TrimSpace(s)) } } } return out }