Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
262
internal/court/court.go
Normal file
262
internal/court/court.go
Normal file
@@ -0,0 +1,262 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user