package court import ( "bytes" "context" "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" "forge-mesh/internal/config" "forge-mesh/internal/fleet" ) // Court runs the Singular Machine Court with prosecutor/defender/judge roles. type Court struct { DB *sql.DB Config config.CourtConfig Fleet *fleet.Store Client *http.Client } func New(db *sql.DB, cfg config.CourtConfig, fleetStore *fleet.Store) *Court { return &Court{ DB: db, Config: cfg, Fleet: fleetStore, Client: &http.Client{Timeout: 120 * time.Second}, } } // Session represents an active or resolved court case. type Session struct { ID string `json:"id"` HostID string `json:"host_id"` Status string `json:"status"` Transcript []TranscriptLine `json:"transcript"` Verdict string `json:"verdict,omitempty"` ClearanceRequired int `json:"clearance_required"` CreatedAt time.Time `json:"created_at"` ResolvedAt *time.Time `json:"resolved_at,omitempty"` } type TranscriptLine struct { Role string `json:"role"` Content string `json:"content"` Timestamp string `json:"timestamp"` } // OpenSession starts a court case for a stuck host. func (c *Court) OpenSession(ctx context.Context, hostID string) (*Session, error) { id := newID() _, err := c.DB.ExecContext(ctx, ` INSERT INTO court_sessions (id, host_id, status, clearance_required) VALUES (?, ?, 'open', 4)`, id, hostID) if err != nil { return nil, err } _ = c.emitSeer(ctx, hostID, "court_open", map[string]string{"session_id": id}) return c.GetSession(ctx, id) } // GetSession loads a court session by ID. func (c *Court) GetSession(ctx context.Context, sessionID string) (*Session, error) { var s Session var transcript, verdict, created, resolved sql.NullString err := c.DB.QueryRowContext(ctx, ` SELECT id, host_id, status, transcript_json, COALESCE(verdict,''), clearance_required, created_at, resolved_at FROM court_sessions WHERE id = ?`, sessionID). Scan(&s.ID, &s.HostID, &s.Status, &transcript, &verdict, &s.ClearanceRequired, &created, &resolved) if err != nil { return nil, err } if transcript.Valid { _ = json.Unmarshal([]byte(transcript.String), &s.Transcript) } s.Verdict = verdict.String if t, err := time.Parse("2006-01-02 15:04:05", created.String); err == nil { s.CreatedAt = t } if resolved.Valid && resolved.String != "" { if t, err := time.Parse("2006-01-02 15:04:05", resolved.String); err == nil { s.ResolvedAt = &t } } return &s, nil } // Deliberate runs prosecutor/defender/judge via Ollama when enabled. func (c *Court) Deliberate(ctx context.Context, sessionID string) (*Session, error) { s, err := c.GetSession(ctx, sessionID) if err != nil { return nil, err } evidence := c.gatherEvidence(ctx, s.HostID) prosecutor, err := c.prompt(ctx, "prosecutor", evidence, "Argue why this host should be remediated aggressively.") if err != nil { prosecutor = fmt.Sprintf("[ollama unavailable] host %s shows repeated tier failures", s.HostID) } defender, err := c.prompt(ctx, "defender", evidence, "Argue for conservative remediation and tier retry.") if err != nil { defender = "Recommend adaptive tier reorder before destructive action." } judgePrompt := fmt.Sprintf("Prosecutor: %s\nDefender: %s\nIssue a concise operational verdict.", prosecutor, defender) verdict, err := c.prompt(ctx, "judge", evidence, judgePrompt) if err != nil { verdict = "retry_adaptive_tiers" } now := time.Now().UTC().Format(time.RFC3339) s.Transcript = append(s.Transcript, TranscriptLine{Role: "prosecutor", Content: prosecutor, Timestamp: now}, TranscriptLine{Role: "defender", Content: defender, Timestamp: now}, TranscriptLine{Role: "judge", Content: verdict, Timestamp: now}, ) b, _ := json.Marshal(s.Transcript) _, err = c.DB.ExecContext(ctx, ` UPDATE court_sessions SET transcript_json = ?, status = 'deliberated' WHERE id = ?`, string(b), sessionID) if err != nil { return nil, err } _ = c.emitSeer(ctx, s.HostID, "court_deliberated", map[string]string{"session_id": sessionID, "verdict_draft": verdict}) return c.GetSession(ctx, sessionID) } func (c *Court) gatherEvidence(ctx context.Context, hostID string) string { var hostname, status string var hashrate float64 _ = c.DB.QueryRowContext(ctx, `SELECT hostname, status, hashrate FROM hosts WHERE id = ?`, hostID). Scan(&hostname, &status, &hashrate) var b strings.Builder fmt.Fprintf(&b, "Host: %s (%s) status=%s hashrate=%.2f\n", hostID, hostname, status, hashrate) if c.Fleet != nil { attempts, _ := c.Fleet.ListLOTL(ctx, hostID, 20) for _, a := range attempts { fmt.Fprintf(&b, "LOTL tier=%d phase=%s status=%s err=%s\n", a.Tier, a.Phase, a.Status, a.Error) } } return b.String() } type ollamaRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` Stream bool `json:"stream"` } type ollamaResponse struct { Response string `json:"response"` } func (c *Court) prompt(ctx context.Context, role, evidence, instruction string) (string, error) { if !c.Config.Enabled { return fmt.Sprintf("[%s stub] %s", role, instruction), nil } body := ollamaRequest{ Model: "llama3.2", Prompt: fmt.Sprintf("You are the %s in Singular Machine Court.\nEvidence:\n%s\n\n%s", role, evidence, instruction), Stream: false, } payload, _ := json.Marshal(body) req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.Config.OllamaURL, "/")+"/api/generate", bytes.NewReader(payload)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") resp, err := c.Client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { raw, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("ollama status %d: %s", resp.StatusCode, string(raw)) } var out ollamaResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", err } return strings.TrimSpace(out.Response), nil } // DispatchVerdict applies an L4 verdict and closes the session. func (c *Court) DispatchVerdict(ctx context.Context, sessionID, verdict string, clearance int) error { if clearance < 4 { return fmt.Errorf("L4 clearance required for verdict dispatch (got L%d)", clearance) } s, err := c.GetSession(ctx, sessionID) if err != nil { return err } _, err = c.DB.ExecContext(ctx, ` UPDATE court_sessions SET verdict = ?, status = 'resolved', resolved_at = datetime('now') WHERE id = ?`, verdict, sessionID) if err != nil { return err } _ = c.emitSeer(ctx, s.HostID, "verdict_dispatched", map[string]string{ "session_id": sessionID, "verdict": verdict, }) return c.applyVerdict(ctx, s.HostID, verdict) } func (c *Court) applyVerdict(ctx context.Context, hostID, verdict string) error { switch strings.ToLower(verdict) { case "retry_adaptive_tiers", "adaptive_retry": if c.Fleet == nil { return nil } order, err := c.Fleet.GetAdaptiveOrder(ctx, "") if err != nil { return err } _, err = RunAdaptiveRetry(ctx, c.Fleet, hostID, order) return err case "pause_host", "pause": _, err := c.DB.ExecContext(ctx, `UPDATE hosts SET status = 'paused', updated_at = datetime('now') WHERE id = ?`, hostID) return err default: return nil } } // RunAdaptiveRetry triggers tier chain with adaptive order (helper to avoid import cycle). func RunAdaptiveRetry(ctx context.Context, store *fleet.Store, hostID string, order []int) ([]*fleet.TierResult, error) { return fleet.RunTierChain(ctx, store, hostID, order) } func (c *Court) emitSeer(ctx context.Context, hostID, eventType string, payload map[string]string) error { b, _ := json.Marshal(payload) _, err := c.DB.ExecContext(ctx, ` INSERT INTO seer_events (id, host_id, event_type, payload_json) VALUES (?, ?, ?, ?)`, newID(), hostID, eventType, string(b)) return err } func newID() string { b := make([]byte, 16) _, _ = rand.Read(b) return hex.EncodeToString(b) }