dsh-apk-vm 0.0.0-stage → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,921 @@
1
- # Temporary Holding Version
1
+ # APK Virtual Machine — a DeepSeek Harness plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/yuxiaole-bili/dsh-apk-vm/actions/workflows/ci.yml/badge.svg)](https://github.com/yuxiaole-bili/dsh-apk-vm/actions/workflows/ci.yml)
4
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
5
+ [![GitHub](https://img.shields.io/badge/github-yuxiaole--bili%2Fdsh--apk--vm-181717)](https://github.com/yuxiaole-bili/dsh-apk-vm)
6
+
7
+ **Run a real Android APK inside a local, rooted, open-source Android VM and preview it in the DeepSeek Harness sidebar — streamed as H.264/WebCodecs video at up to 60 fps.**
8
+
9
+ This repository contains the static DSH plugin `dsh-apk-vm`. It adds a first-class sidebar
10
+ entry ("APK Virtual Machine" / "APK 虚拟机") whose main panel boots an AOSP Android Emulator,
11
+ installs an APK into the guest, streams the guest display into the panel, and forwards
12
+ tap / swipe / key / text input back into the VM. The guest is a `userdebug` image prepared for
13
+ root (`adb root` on every boot), and the plugin exposes that rooted device to DSH through a
14
+ same-origin, loopback-only debug HTTP API and an `apk_vm` Agent tool.
15
+
16
+ > **中文简介**:在 DeepSeek Harness 的侧边栏里预览 Android APK。插件会启动本地开源
17
+ > AOSP Android 虚拟机(Android 14 / API 34,`system-images;android-34;default;x86_64`),
18
+ > 安装 APK,并通过 scrcpy H.264 + WebCodecs 把画面串流到侧边栏(最高 60 fps),同时把
19
+ > 点击、滑动、按键、文本输入送回虚拟机。支持拖拽 APK 上传、环境自检卡片、root 调试
20
+ > API 与 `apk_vm` Agent 工具;多个 Agent 可各自使用独立的隔离虚拟机实例(多开)。
21
+ > 参考机上冷启动约 29.5 s、快照快速启动约 8.4–10.5 s。
22
+ > 当前 **Windows x64 为端到端已验证平台**;SDK/模拟器探测已覆盖 Windows / macOS / Linux。
23
+
24
+ **Status:** `0.3.13` · Apache-2.0 · no npm runtime dependencies · no build step.
25
+
26
+ This is an open-source DSH plugin. The package is prepared for npm as **`dsh-apk-vm`**; the
27
+ install command below uses that package name. If npm does not have the package yet, use the
28
+ source fallback below. Repository:
29
+ [github.com/yuxiaole-bili/dsh-apk-vm](https://github.com/yuxiaole-bili/dsh-apk-vm).
30
+ Maintainer: [yuxiaole-bili](https://github.com/yuxiaole-bili/).
31
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md),
32
+ [SECURITY.md](SECURITY.md), [docs/PRIVACY.md](docs/PRIVACY.md) and [docs/RELEASING.md](docs/RELEASING.md) before opening issues or
33
+ releasing.
34
+
35
+ ## One-line value
36
+
37
+ Turn DeepSeek Harness into a local Android APK workbench: boot a rooted AOSP VM, preview it at
38
+ up to 60 fps in the sidebar, and let Agents install, tap, inspect and debug APKs without leaving
39
+ the session.
40
+
41
+ ## Install
42
+
43
+ Install the published npm package (the command used by the DSH plugin center and the community
44
+ market). If the package is not yet visible in your registry, use the source fallback below.
45
+
46
+ ```bash
47
+ dsh plugin --profile web add dsh-apk-vm
48
+ ```
49
+
50
+ For the Electron-managed Desktop profile, use **Plugins → add/install local bundle** with this
51
+ directory, or install the package through the app's plugin manager.
52
+
53
+ Source fallback without npm:
54
+
55
+ ```bash
56
+ dsh plugin --profile web add github:yuxiaole-bili/dsh-apk-vm
57
+ # local clone / non-Electron profile:
58
+ dsh plugin --profile desktop add <path-to-this-clone>
59
+ ```
60
+
61
+ ## Capability category
62
+
63
+ **开发与运维 / Developer Tools** — Android VM lifecycle, APK install/preview, rooted debugging
64
+ and Agent-facing UI automation for development workflows.
65
+
66
+ ## Risks, permissions and compatibility
67
+
68
+ **Requirements**
69
+
70
+ - Android SDK with `adb`, emulator, `sdkmanager`/`avdmanager`, a matching system image and an AVD
71
+ (the Environment card checks all of this; SDK bootstrap is opt-in).
72
+ - Hardware virtualization: AEHD/WHPX on Windows, KVM on Linux, or HVF on macOS.
73
+ - Node.js ≥ 20 only for the repository's headless checks; the plugin itself has no npm
74
+ dependencies or build step.
75
+ - Each running VM costs roughly 2.5 GB RAM; `maxInstances` defaults to 3 extra VMs.
76
+
77
+ **Permissions / risk**
78
+
79
+ - The guest is intentionally `userdebug` and rooted (`adb root` on boot); do not put secrets or
80
+ personal data in the AVD.
81
+ - `apk_vm` is registered for every Agent in the profile and exposes guest root shell, app
82
+ install/uninstall, UIAutomator and input injection. Grant it the same trust as a terminal on a
83
+ rooted Android device.
84
+ - `/apk-vm/*` HTTP routes and WebSocket upgrades are same-origin and **loopback-only by
85
+ default**; `allowRemote: true` is an explicit opt-in for use behind a trusted authenticated
86
+ proxy.
87
+ - No telemetry. Network access only occurs for explicit SDK/system-image downloads through the
88
+ user's `sdkmanager`, or when the user configures a remote adb device.
89
+
90
+ **Compatibility**
91
+
92
+ - Tested with DSH Desktop `0.2.1-rc.3` (static bundle/plugin APIs).
93
+ - Windows x64 is the only platform verified end-to-end (boot, H.264, rooted debug, Agent tool).
94
+ macOS/Linux have headless setup/CI coverage but not a live emulator run in this repository.
95
+ - Optional integration with
96
+ [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar); the plugin falls back
97
+ cleanly when that service is absent.
98
+ - AOSP Android 14 / API 34, x86_64 system image; physical/remote adb devices can be attached
99
+ instead of the AVD.
100
+
101
+ ---
102
+
103
+ ## Demo / visible proof
104
+
105
+ Real screenshots from the reference host:
106
+
107
+ ![APK VM main panel](docs/screenshot-panel.png)
108
+
109
+ ![APK VM in the right sidebar](docs/screenshot-sidebar.png)
110
+
111
+ | Asset | Status |
112
+ | --- | --- |
113
+ | `docs/screenshot-panel.png` | ✅ main panel: Environment, guest H.264, controls |
114
+ | `docs/screenshot-sidebar.png` | ✅ right-sidebar layout next to a session |
115
+ | `docs/demo.gif` | optional — record a 20–30 s demo on a real host |
116
+
117
+ The [Performance](#performance) and [Verification](#verification) sections list exactly what was
118
+ measured, on which host, and how. Real `apk_vm` output from the reference run:
119
+
120
+ ```text
121
+ find_ui {"text":"Phone"} -> Phone @ 102,1121
122
+ assert_text {"text":"Phone"} -> found 1 matching UI node(s)
123
+ tap_text {"text":"Phone"} -> tapped Phone at 102,1121
124
+ key {"text":"HOME"} -> returned to launcher
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Features
130
+
131
+ - **Sidebar entry + main panel.** Registers a localized sidebar item (`sidebar.panellist`,
132
+ id `apk-vm`, order 30) with a phone glyph ([icon.svg](icon.svg)), plus a full center-column
133
+ panel (`main`, key `apk-vm`). Both are live-verified in the reference profile.
134
+ - **Environment self-check card.** A one-glance readiness report for the host — SDK, `adb`,
135
+ emulator, command-line tools (`sdkmanager`/`avdmanager`), the configured system image, the
136
+ AVD, and CPU acceleration — with the missing pieces and the available repair actions
137
+ (`GET/POST /apk-vm/setup`). The card can create the AVD when the system image is already
138
+ installed, and — when `allowSetupDownloads: true` — install missing SDK components, or
139
+ **bootstrap a completely empty host** (download Google command-line tools, extract, accept
140
+ licenses, install platform-tools/emulator/build-tools/platform).
141
+ - **Multi-agent isolation / multi-instance.** Several DSH Agents can drive separate VMs at the
142
+ same time: the first Agent gets the default VM, later Agents are assigned isolated instances
143
+ with their own AVD, emulator port, snapshot, debug state and **H.264/WebCodecs stream**
144
+ (`maxInstances`, default 3 extra; extra VMs use a 720×1280 preview display).
145
+ Extra instances expose `owner` / `lastUsedAt` / `idleMs`, and are **stopped automatically**
146
+ after `instanceIdleMs` without panel frames, stream clients or Agent commands (AVD kept for
147
+ quick reuse). The panel selector and **VM instances** card show owner/idle state and offer
148
+ Stop/Destroy. The `apk_vm` tool accepts
149
+ `instance` and exposes `list_instances` / `create_instance` / `destroy_instance`.
150
+ - **Multi-AVD switching.** The Environment card lists the AVDs visible to the emulator and can
151
+ switch (`GET /apk-vm/avds`, `POST /apk-vm/avd`); the current AVD is stopped, switched and
152
+ restarted automatically.
153
+ - **APK upload by button or drag-and-drop.** The panel has an **Upload APK** button and a
154
+ drop overlay on the screen area; the file is posted to `/apk-vm/upload`
155
+ (raw APK bytes, ≤ 500 MiB, `.apk`/ZIP magic checked), installed into the running VM and
156
+ launched. Uploaded files live in the OS temp dir and are cleaned up when the plugin is
157
+ disposed. Installing by absolute local path is still available (`/apk-vm/install`).
158
+ - **Installed-app management.** The panel's **Installed apps** card lists third-party packages
159
+ for the active instance and offers force-stop, clear-data and uninstall; it uses the same
160
+ rooted `debug/packages` + `debug/action` API as the Agent tool.
161
+ - **Optional [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar) tab.**
162
+ When that community plugin is installed, this plugin registers a real tab type
163
+ (`apk-vm:preview`) through its service — no fork, no DOM hacks, and it degrades cleanly when
164
+ the service is absent (see [compatibility](#compatibility-with-dsh-better-sidebar)).
165
+ - **VM lifecycle from the UI.** Start / stop / restart the AVD or save a snapshot; the
166
+ Environment card lists all adb devices (emulator, USB/physical, network) and can attach one at
167
+ runtime. Detach restores the managed AVD identity. Live state, serial, boot mode/time, and a
168
+ bounded diagnostic log are shown.
169
+ - **APK discovery + one-click install.** Recursively finds `*.apk` under the configured roots,
170
+ reads package name / label / launcher activity with `aapt2 dump badging`, then runs
171
+ `adb install -r` and launches the app with `am start`.
172
+ - **Static APK preview.** `GET /apk-vm/apk-info?path=…` reports version, min/target SDK,
173
+ permissions, native ABIs, features and launcher activity without starting the VM;
174
+ `GET /apk-vm/apk-icon?path=…` extracts the raster launcher icon directly from the APK ZIP.
175
+ The APK list shows icons when a raster icon is present.
176
+ - **Live H.264 stream, up to 60 fps.** Default transport is scrcpy 5.0.1 H.264 over the
177
+ same-origin WebSocket `/apk-vm/ws`, decoded with `WebCodecs VideoDecoder` onto a `<canvas>`
178
+ (configured for 60 fps, 540×960, 6 Mbps). Late clients get a `RESET_VIDEO` keyframe request.
179
+ If `WebCodecs` / `WebSocket` is unavailable, the panel falls back to `adb exec-out screencap -p`
180
+ PNG polling (target 1–10 fps).
181
+ - **Full input forwarding.** Click → tap, drag → swipe, wheel → swipe, **two-finger pinch/zoom**
182
+ via the scrcpy control socket, plus Home / Back / Recents / Power and a text field; after
183
+ clicking the screen, common hardware keys (arrows/Enter/Escape/Tab/Backspace/Delete) and
184
+ printable characters are forwarded as Android key/text input. Clipboard text can be written to
185
+ the device (scrcpy `SET_CLIPBOARD`) or read back (`GET_CLIPBOARD`). Coordinates are converted
186
+ from the rendered phone frame to the device resolution.
187
+ - **Rooted debug surface + extra device tools.** Auto `adb root` after every boot, an on-device
188
+ toolkit at `/data/local/tmp/dsh/debug.sh`, a same-origin debug HTTP API, and an `apk_vm`
189
+ Agent tool every DSH agent can call: root shell, logcat, props, packages, install, push/pull,
190
+ reboot, force-stop / clear-data / uninstall, screenshot, **rotate**, **UIAutomator UI tree**,
191
+ **5-second screen recording (pulled to the host temp dir)** and **performance snapshot**.
192
+ - **Fast start.** Hypervisor detection (AEHD / WHPX / KVM / HAXM / HVF), host GPU rendering,
193
+ quick-boot snapshots, prewarm and auto-start — measured **cold 29.5 s**, **quick boot
194
+ 8.4–10.5 s** on the reference host ([details](#fast-start-four-layers)).
195
+ - **Cross-platform SDK discovery.** `lib/setup.js` and `lib/android.js` discover Android SDK,
196
+ `adb`, the emulator and AVDs across Windows, macOS and Linux layouts (including `.exe`
197
+ suffixes and per-OS fallback locations). Windows x64 is the only platform verified end-to-end
198
+ so far.
199
+ - **Same-origin only.** Every `/apk-vm/*` HTTP route and the `/apk-vm/ws` upgrade rejects
200
+ cross-origin requests; request bodies and command output are bounded, and host processes are
201
+ spawned with argv arrays (never a shell string).
202
+ - **Themed + localized.** All chrome uses `--dsw-alias-*` theme tokens; English and 简体中文
203
+ are bundled in [locale/](locale).
204
+ - **Dependency-free backend.** The Host half uses Node built-ins only: no npm dependencies,
205
+ no build step.
206
+
207
+ ---
208
+
209
+ ## Quickstart
210
+
211
+ ### 1. Prerequisites
212
+
213
+ | Requirement | Notes |
214
+ | --- | --- |
215
+ | **DeepSeek Harness** | A profile that can install local plugin bundles (the desktop Web UI or any agent with the `plugin_manager` tool). |
216
+ | **Android SDK** | `platform-tools` (adb), `emulator`, `build-tools` (for `aapt2`), command-line tools (`sdkmanager`/`avdmanager`) and an AOSP system image. A JDK (17 works) is needed by the SDK command-line tools. |
217
+ | **AOSP system image** | `system-images;android-34;default;x86_64` (Android 14, API 34) — the image used for all measurements below. Configurable with `systemImage`. |
218
+ | **Hardware virtualization** | Required for the x86_64 image: AEHD or WHPX on Windows, KVM on Linux, HVF on macOS. See [acceleration matrix](#cpu-acceleration-matrix). |
219
+ | **Disk / RAM** | A few GB for the SDK + system image and the AVD; an emulator with a modern desktop GPU is recommended. |
220
+ | **Node.js 24+** *(optional)* | Only for running this repository's checks ([Verification](#verification)). The plugin itself has no npm dependencies. |
221
+
222
+ The plugin's **Environment** card performs this check for you (see
223
+ [First run](#5-first-run)); the commands below are the manual equivalent.
224
+
225
+ #### CPU acceleration matrix
226
+
227
+ | Host OS | Backend | How to enable |
228
+ | --- | --- | --- |
229
+ | Windows | **AEHD** (Android Emulator Hypervisor Driver) | Install via SDK Manager package `extras;google;Android_Emulator_Hypervisor_Driver`, or the standalone installer; requires administrator rights and a reboot. Recommended when Hyper-V/WHPX is **not** active. |
230
+ | Windows | **WHPX** | Enable the *Windows Hypervisor Platform* optional feature (and Hyper-V if desired), reboot. Alternative when Hyper-V is already in use. |
231
+ | Linux | **KVM** | Load `kvm_intel`/`kvm_amd`, ensure `/dev/kvm` exists and your user can access it (e.g. `kvm` group), then reboot/login again. |
232
+ | macOS | **HVF** (Hypervisor.framework) | Built into macOS; the emulator uses it automatically on Intel and Apple Silicon hosts. |
233
+
234
+ Check the host before starting the plugin:
235
+
236
+ ```bash
237
+ "$ANDROID_HOME/emulator/emulator" -accel-check
238
+ ```
239
+
240
+ The plugin also runs the SDK's `emulator-check accel` itself and reports the result in
241
+ `/apk-vm/state` (`vm.accel`), `/apk-vm/setup` (`accel`) and the panel log. If no hypervisor is
242
+ usable, it **fails fast** with an explicit error instead of hanging for 20 minutes (the x86_64
243
+ image cannot boot under pure TCG).
244
+
245
+ ### 2. Install the plugin
246
+
247
+ Copy or clone this repository anywhere local:
248
+
249
+ ```bash
250
+ git clone https://github.com/yuxiaole-bili/dsh-apk-vm.git
251
+ cd dsh-apk-vm
252
+ ```
253
+
254
+ Then pick one:
255
+
256
+ - **CLI npm install (community market / non-`desktop` profile)**:
257
+ ```bash
258
+ dsh plugin --profile web add dsh-apk-vm
259
+ ```
260
+ Source fallback: `dsh plugin --profile web add github:yuxiaole-bili/dsh-apk-vm`.
261
+ - **DSH Web UI** — open **Plugins**, choose to add/install a plugin bundle from a local
262
+ directory, and select this folder. The bundle patch is [cordis.patch.yml](cordis.patch.yml);
263
+ the `dsh.client` declaration in [package.json](package.json) loads `client.js` into the Web UI.
264
+ - **Agent tool** — ask a DSH agent to run `plugin_manager` with action `install_bundle` and the
265
+ target set to this folder path.
266
+ - **CLI local install (non-Electron profiles only)** —
267
+ `dsh plugin --profile <profile> add <path-to-this-folder>`. The Electron-managed `desktop`
268
+ profile must be managed from the app UI instead.
269
+
270
+ After installing or upgrading the plugin, **reload the DSH Web page** so the new client bundle
271
+ is loaded.
272
+
273
+ ### 3. Provide an Android SDK, system image and AVD
274
+
275
+ The plugin auto-discovers the SDK (`sdkRoot`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, then common
276
+ per-OS locations) and adb/emulator. Fastest path: open the panel, check the **Environment**
277
+ card, and use **Create AVD** once the system image is installed. If the image is missing and
278
+ `allowSetupDownloads: true` is set, the card can also run `sdkmanager --install` for it;
279
+ otherwise install it yourself:
280
+
281
+ ```bash
282
+ sdkmanager --install "platform-tools" "emulator" "build-tools;34.0.0" \
283
+ "system-images;android-34;default;x86_64"
284
+ avdmanager create avd -n dsh_apk -k "system-images;android-34;default;x86_64"
285
+ ```
286
+
287
+ The plugin can also attach to an already-running adb device instead of spawning the AVD; set
288
+ `serial` (for example `emulator-5558` or `<device-ip>:5555`) and leave `emulatorPath`/`avdName`
289
+ unset.
290
+
291
+ ### 4. Configure
292
+
293
+ The bundle ships with portable defaults (SDK auto-detection, `avdName: dsh_apk`). Review the
294
+ config for your machine — at minimum `roots` if you want extra APK scan directories. The full
295
+ list of keys is in the [configuration reference](#configuration).
296
+
297
+ ```yaml
298
+ config:
299
+ # sdkRoot: 'C:/Android/sdk' # optional; otherwise auto-detected
300
+ # emulatorPath: 'C:/Android/sdk/emulator/emulator.exe' # optional; auto-detected
301
+ # serial: 'emulator-5558' # ...or attach an existing adb serial instead of spawning
302
+ systemImage: 'system-images;android-34;default;x86_64'
303
+ avdName: dsh_apk
304
+ port: 5558
305
+ roots: ['C:/projects'] # extra APK discovery roots (workspace is automatic)
306
+ gpuMode: host
307
+ accel: auto
308
+ requireAccel: true
309
+ quickBoot: true
310
+ snapshotName: default_boot
311
+ saveSnapshotAfterBoot: true
312
+ prewarm: false # opt-in warm boot on plugin activation
313
+ prewarmDelayMs: 3000
314
+ autoStart: true
315
+ multiInstance: true # isolated VMs for concurrent agents
316
+ maxInstances: 3 # extra VMs (default VM is always available)
317
+ instanceIdleMs: 900000 # stop idle extra VMs after 15 min (AVD kept)
318
+ instanceReapIntervalMs: 60000
319
+ allowRemote: false # reject non-loopback VM API clients
320
+ allowSetupDownloads: false # gate the optional system-image download action
321
+ scrcpy: true
322
+ scrcpyMaxSize: 960
323
+ scrcpyMaxFps: 60
324
+ scrcpyBitrate: 6000000
325
+ frameFps: 2 # only the PNG fallback target (1-10)
326
+ ```
327
+
328
+ If `emulatorPath` / `adbPath` are omitted, the plugin auto-discovers the Android SDK from
329
+ `sdkRoot`, then `ANDROID_HOME` / `ANDROID_SDK_ROOT`, then common per-user/system install
330
+ locations, and finally falls back to `PATH`. Spawning an AVD still needs `avdName`; use `serial`
331
+ to attach an existing device instead.
332
+
333
+ ### 5. First run
334
+
335
+ 1. Open the DSH sidebar and click **APK Virtual Machine**. Check the **Environment** card:
336
+ green **Ready** means the SDK, image, AVD and acceleration were found; **Setup needed** lists
337
+ what is missing and offers **Create AVD** (and optionally **Download image** when
338
+ `allowSetupDownloads: true`).
339
+ 2. Press **Start VM** (or let `prewarm`/`autoStart` boot it). The **first start is a cold boot**
340
+ (measured 29.5 s on the reference host); the plugin then saves the quick-boot snapshot
341
+ `default_boot`, and later starts restore it (measured 8.4–10.5 s).
342
+ 3. Install an APK: click **Upload APK**, drag an `.apk` file onto the panel, or pick one from the
343
+ discovered list / install by absolute path. The app is installed and launched automatically.
344
+ Use the phone frame for input and the **Rooted debug** card for root shell, logcat, props,
345
+ UI tree, rotation, recording, perf and screenshot.
346
+ 4. Or delegate to an agent, for example: *"Use the `apk_vm` tool to install
347
+ `C:/work/app.apk` and take a screenshot."*
348
+
349
+ ---
350
+
351
+ ## Configuration
352
+
353
+ All keys are optional; defaults are shown in the **Default** column.
354
+
355
+ | Key | Default | Purpose |
356
+ | --- | --- | --- |
357
+ | `emulatorPath` | auto-detected | Path to the Android emulator executable. Auto-discovered from the SDK / `PATH`. |
358
+ | `avdName` | — | AVD name to spawn. Required to spawn an AVD. |
359
+ | `serial` | — | Attach this adb serial instead of spawning an emulator. |
360
+ | `port` | `5558` | Emulator console port used when spawning. |
361
+ | `sdkRoot` | `ANDROID_HOME` / `ANDROID_SDK_ROOT` | Android SDK root for `adb`, `aapt2`, `emulator-check`, `sdkmanager`/`avdmanager`. |
362
+ | `adbPath` | `<sdk>/platform-tools/adb` | Explicit adb executable. |
363
+ | `systemImage` | `system-images;android-34;default;x86_64` | System image required by the Environment card and used for AVD creation. |
364
+ | `spawnEmulator` | `true` | Set `false` to only attach (never spawn). |
365
+ | `emulatorArgs` | `[]` | Extra argv appended to the emulator command line. |
366
+ | `roots` | `[]` | Extra APK discovery roots. The DSH working directory, `process.cwd()` and the home directory are added automatically. |
367
+ | `bootTimeoutMs` | `1200000` | Max wait for the guest to report `sys.boot_completed=1`. |
368
+ | `gpuMode` | `host` | `host` uses the real GPU; `swiftshader_indirect` is the software fallback for headless hosts. |
369
+ | `accel` | `auto` | `auto` detects AEHD/WHPX/KVM/HAXM/HVF; `on`/`off` force the decision. |
370
+ | `requireAccel` | `true` | Fail fast with an explicit error when no hypervisor is found, instead of booting forever under TCG. |
371
+ | `quickBoot` | `true` | Restore the saved AVD snapshot when present; `false` adds `-no-snapshot`. |
372
+ | `snapshotName` | `default_boot` | Snapshot name to load/save. |
373
+ | `saveSnapshotAfterBoot` | `true` | Save a snapshot after the first successful cold boot. |
374
+ | `prewarm` | `false` | Boot the VM in the background shortly after plugin activation. |
375
+ | `prewarmDelayMs` | `3000` | Delay before the prewarm start. |
376
+ | `autoStart` | `true` | Start the VM on the first `/apk-vm/state` or `/apk-vm/frame` request. |
377
+ | `multiInstance` | `true` | Allow isolated extra VMs for concurrent Agents. |
378
+ | `maxInstances` | `3` | Maximum number of extra instances (in addition to the default VM). Each running instance costs guest RAM/CPU. |
379
+ | `instanceIdleMs` | `900000` | Stop an extra instance after this long without panel frames, a video client, or an Agent command (AVD is kept). |
380
+ | `instanceReapIntervalMs` | `60000` | How often the idle reaper checks extra instances. |
381
+ | `allowRemote` | `false` | Allow non-loopback clients to reach `/apk-vm/*` and `/apk-vm/ws` (only behind your own authenticated proxy). |
382
+ | `allowSetupDownloads` | `false` | Allow the Environment card's `install-image` action to run `sdkmanager --install` (large download). |
383
+ | `frameFps` | `2` | PNG fallback target frame rate, 1–10 (the H.264 path is not affected). |
384
+ | `scrcpy` | `true` | Enable the H.264/WebCodecs transport. |
385
+ | `scrcpyMaxSize` | `960` | Encoder max size; 960 yields 540×960 video. |
386
+ | `scrcpyMaxFps` | `60` | Encoder frame-rate cap, 1–60. |
387
+ | `scrcpyBitrate` | `6000000` | Video bitrate in bits/s. |
388
+ | `scrcpyPort` | `27183` | Local adb-forward port for the scrcpy server tunnel. |
389
+ | `scrcpyJarPath` | vendored JAR | Override the scrcpy server JAR path. |
390
+
391
+ The Environment report (`GET /apk-vm/setup`) returns `ok`, `platform`, `sdkRoot`, `adbPath`,
392
+ `emulatorPath`, `avdmanagerPath`, `sdkmanagerPath`, `emulatorVersion`, `systemImage` +
393
+ `systemImagePresent`, `avdName` + `avdPresent` + `avdPath`, `accel { available, backend,
394
+ detail }`, `hypervisorNotes`, `missing[]` and `actions[]` with per-action availability.
395
+
396
+ ---
397
+
398
+ ## Architecture
399
+
400
+ ```
401
+ ┌────────────────────────────── DSH Web UI ────────────────────────────────┐
402
+ │ sidebar.panellist [apk-vm, order 30] ──click──▶ main[apk-vm] panel │
403
+ │ Client half: client.js │
404
+ │ Environment card · upload/drop · APK list · phone frame (WebCodecs │
405
+ │ canvas or PNG fallback) · Rooted debug card │
406
+ └──────────────────────────────────┬───────────────────────────────────────┘
407
+ │ same-origin HTTP + WebSocket
408
+ ┌──────────────────────────────────▼───────────────────────────────────────┐
409
+ │ DSH Host: index.js (/apk-vm/* routes, ws relay, debug API, agent tool) │
410
+ │ ├─ lib/android.js AndroidVm: AVD lifecycle, adb, aapt2, input, debug │
411
+ │ ├─ lib/setup.js Environment detection, createAvd, installSystemImage │
412
+ │ ├─ lib/scrcpy.js ScrcpySession: push server, adb forward, H.264 demux │
413
+ │ │ (Annex-B → AVCC), control messages (RESET_VIDEO, …) │
414
+ │ └─ lib/ws.js dependency-free RFC 6455 WebSocket server │
415
+ └──────────────────────────────────┬───────────────────────────────────────┘
416
+ │ adb
417
+ ┌──────────────▼──────────────┐
418
+ │ AOSP Android Emulator │
419
+ │ Android 14 / API 34, x86_64 │
420
+ │ userdebug, adbd as root │
421
+ └──────────────────────────────┘
422
+ ```
423
+
424
+ | Layer | Implementation | Role |
425
+ | --- | --- | --- |
426
+ | VM | AOSP Android Emulator (`emulator` + `system-images;android-34;default;x86_64`) | Boots the open-source Android system image; driven through adb. |
427
+ | Setup | [lib/setup.js](lib/setup.js) | Dependency-free SDK / adb / emulator / AVD / acceleration detection plus one-click `create-avd` and optional `install-image`. |
428
+ | Streaming | scrcpy 5.0.1 server (`lib/vendor/scrcpy-server-v5.0.1.jar`, pushed to `/data/local/tmp/scrcpy-server.jar`) + [lib/scrcpy.js](lib/scrcpy.js) | H.264 screen capture and the scrcpy control protocol, without the desktop scrcpy client. |
429
+ | Device bridge | `adb` from Android SDK platform-tools | install / launch / screencap / input / logcat / file push-pull. |
430
+ | Plugin runtime | DSH plugin APIs (`slots`, `locale`, `webServer`, `tools`, `dsh.client`) | Sidebar entry, main panel, same-origin HTTP/WebSocket control plane, agent tool. |
431
+
432
+ No npm dependencies are used: the Host half imports only Node built-ins, `client.js` is a
433
+ pre-built browser bundle, and the plugin has no build step.
434
+
435
+ ### Fast start: four layers
436
+
437
+ | Layer | What it does |
438
+ | --- | --- |
439
+ | **Hardware CPU acceleration** | Runs `emulator-check accel`; when AEHD / WHPX / KVM / HAXM / HVF is usable the emulator gets hardware virtualization. Pure TCG is never silently attempted for the x86_64 image (`requireAccel: true` fails fast with a clear error). |
440
+ | **Host GPU** | `-gpu host` renders the VM with the real GPU instead of SwiftShader; switch `gpuMode` on headless/no-GPU hosts. |
441
+ | **Quick-boot snapshot** | The first successful cold boot saves `adb emu avd snapshot save default_boot`; later starts pass `-snapshot default_boot`. A leftover `ram.img` without `snapshot.pb` is not treated as a valid snapshot. |
442
+ | **Warm start** | `prewarm: true` boots the VM ~3 s after plugin activation; `autoStart: true` boots it on the first `/apk-vm/state`/`/apk-vm/frame` request. |
443
+
444
+ `/apk-vm/state` exposes `vm.bootKind` (`cold` / `snapshot` / attach), `vm.bootMs`,
445
+ `vm.accel`, `vm.gpuMode` and `vm.snapshotAvailable`; the panel shows `cold Ns` / `quick Ns`.
446
+
447
+ ### H.264 streaming and input
448
+
449
+ The default transport (when `scrcpy: true`) is:
450
+
451
+ ```
452
+ scrcpy-server (guest H.264) → adb forward → ScrcpyVideoDemuxer → Annex-B→AVCC
453
+ → /apk-vm/ws WebSocket → browser VideoDecoder → <canvas>
454
+ ```
455
+
456
+ - WebSocket text frame `init`: `{ codec:"h264", codecString, description (avcC, base64),
457
+ width, height, maxFps, bitrate }`.
458
+ - Binary frame: `[0]=1][1=keyframe flag][2..9]=pts (int64 LE)][10..]=AVCC NAL units]`.
459
+ - A client that connects to an already-running session triggers a scrcpy control
460
+ `RESET_VIDEO` (type 17) so it receives a fresh keyframe; clients drop delta frames until the
461
+ first keyframe so the canvas never decodes garbage.
462
+ - The session is shared by all connected clients; when the last client disconnects the scrcpy
463
+ encoder is stopped after a grace period (the VM keeps running). Stopping/restarting the VM or
464
+ unloading the plugin stops the stream.
465
+ - Input is independent of the streaming path: `/apk-vm/input` remains the only input channel
466
+ (the panel does not bypass it), so input keeps working with the PNG fallback.
467
+
468
+ The protocol notes collected while implementing this against scrcpy 5.0.1 are in
469
+ [spike/scrcpy/PROTOCOL.md](spike/scrcpy/PROTOCOL.md).
470
+
471
+ ### HTTP control plane (same origin, local only)
472
+
473
+ Every route rejects cross-origin requests; the WebSocket upgrade is checked the same way.
474
+ JSON bodies are bounded to 64 KiB; the raw APK upload is bounded to 500 MiB.
475
+
476
+ | Method | Path | Purpose |
477
+ | --- | --- | --- |
478
+ | GET | `/apk-vm/state` | VM state, serial, boot time/kind, accel/GPU, root/debug status, display size, current APK, stream counters, log tail. |
479
+ | GET | `/apk-vm/setup?force=1` | Environment report (SDK/adb/emulator/tools/image/AVD/acceleration, missing items, available actions). Cached ~15 s. |
480
+ | POST | `/apk-vm/setup` | `{"action":"refresh"\|"create-avd"\|"install-image"\|"install-tools"\|"bootstrap-sdk"}`. `install-tools`/`bootstrap-sdk` accept optional `packages`; all download actions require `allowSetupDownloads: true` (otherwise `403`) and are never automatic. |
481
+ | GET | `/apk-vm/apks` | Discover `*.apk` files under the configured roots. |
482
+ | GET | `/apk-vm/apk-info?path=…` | Static APK metadata (package/version/targetSdk/permissions/features/native code/launch activity/icon path) via `aapt2`. |
483
+ | GET/HEAD | `/apk-vm/apk-icon?path=…` | Extract the raster launcher icon from the APK ZIP (PNG/WebP/JPEG). |
484
+ | GET | `/apk-vm/devices` | List adb devices (`serial`, `state`, `kind`, `model`, `product`, transport) plus current/managed-device metadata. |
485
+ | POST | `/apk-vm/device` | `{"serial":"<adb-serial>"}` attach an online physical/network device; `{"serial":""}` detach and restore the managed AVD. |
486
+ | POST | `/apk-vm/vm` | `{"action":"start"\|"stop"\|"restart"\|"snapshot"}`. |
487
+ | POST | `/apk-vm/install` | `{"path":"C:/path/app.apk"}` → install + launch. |
488
+ | POST | `/apk-vm/upload` | Raw APK bytes (≤ 500 MiB) with the file name in the `x-apk-name` header → validate ZIP/APK magic, write to the OS temp dir, install + launch. |
489
+ | GET | `/apk-vm/frame?t=…` | Latest PNG frame (`409` while the VM is not ready). |
490
+ | POST | `/apk-vm/input` | `tap` / `swipe` / `key` / `text` payloads. |
491
+ | POST | `/apk-vm/touch` | Multi-touch scrcpy control (`{"action":"down\|move\|up\|cancel","pointerId":0,"x":…,"y":…}` in video-frame coordinates) for pinch/zoom. |
492
+ | GET/POST | `/apk-vm/clipboard` | Read the device clipboard (`GET`, scrcpy `GET_CLIPBOARD`) or write text (`POST {"text":…}`). |
493
+ | POST | `/apk-vm/stream` | `{"fps":1–10}` — set the PNG fallback target frame rate. |
494
+ | GET (upgrade) | `/apk-vm/ws` | WebSocket H.264 stream (see above). |
495
+ | GET | `/apk-vm/instances` | List default + extra VM instances, including `owner`, `lastUsedAt`, `idleMs`, `idleTimeoutMs`, `videoClients`. |
496
+ | POST | `/apk-vm/instances` | `{"name":"agent-a","start":true}` — create an isolated AVD/VM; every extra instance has its own H.264 stream. |
497
+ | DELETE | `/apk-vm/instances/:id` | Stop/dispose an extra instance (the default cannot be destroyed). |
498
+ | GET/POST | `/apk-vm/instances/:id/...` | Per-instance mirror of `state`, `frame`, `vm`, `input`, `install`, `upload`, `debug/*`. |
499
+
500
+ Uploaded APKs are written under `<os temp>/dsh-apk-vm-uploads` and that directory is removed
501
+ when the plugin is disposed (best-effort stale-file cleanup also runs before each upload).
502
+
503
+ ---
504
+
505
+ ## Rooted debug surface
506
+
507
+ The VM image is the AOSP `android-34;default;x86_64` **userdebug** build. After every boot the
508
+ Host half prepares a rooted debug environment and captures it in the quick-boot snapshot:
509
+
510
+ - **adb root by default.** `_prepareDebugSystem()` runs `adb root`, waits for adbd, and verifies
511
+ `uid=0(root)`. The image has `ro.debuggable=1` and `ro.adb.secure=0`, so there is no adb
512
+ authorization prompt. State reports `root: true` plus identity (`debugBuild`, `adbSecure`).
513
+ - **Debug-friendly settings.** adb/development settings enabled, package verifier and
514
+ adb-install verification disabled, `persist.sys.usb.config=adb`, root-access property set.
515
+ - **On-device toolkit** at `/data/local/tmp/dsh/debug.sh` (+ `README.txt`, mode 755):
516
+ `info | logs [lines] | prop <name> [value] | pkgs | install <apk> | help`.
517
+ - **Snapshot persistence.** Root state, settings and the toolkit are captured by
518
+ `adb emu avd snapshot save default_boot`; a quick boot restores them and `adb root` is
519
+ re-applied idempotently.
520
+ - The emulator accepts only `-prop qemu.*` arguments, so root/debug state is applied right after
521
+ boot (not faked via a `-prop ro.*` flag) and then snapshotted.
522
+
523
+ ### Debug HTTP API (same origin)
524
+
525
+ | Method | Path | Operation |
526
+ | --- | --- | --- |
527
+ | GET | `/apk-vm/debug/info` | serial, identity (Android/API/ABI/debug), root, toolkit, display, uptime. |
528
+ | POST | `/apk-vm/debug/shell` | `{"command":"id; getprop …"}` → rooted shell (bounded stdout/stderr/code). |
529
+ | GET | `/apk-vm/debug/logcat?lines=200&buffer=main&filter=…` | bounded logcat dump; `buffer=crash` for the crash buffer. |
530
+ | GET/POST | `/apk-vm/debug/prop` | read (`?name=`) / write (`{"name","value"}`) system properties. |
531
+ | GET | `/apk-vm/debug/packages` | third-party (or `?all=1` all) packages with APK paths. |
532
+ | POST | `/apk-vm/debug/root` | ensure adbd is root. |
533
+ | POST | `/apk-vm/debug/push` | `{"localPath","remotePath"}` (remote under `/data/local/tmp`, `/sdcard`, `/storage/emulated/0`). |
534
+ | POST | `/apk-vm/debug/pull` | `{"remotePath","localPath"}` (local confined to APK roots + temp). |
535
+ | POST | `/apk-vm/debug/reboot` | `{"mode":"system"\|"recovery"\|"bootloader"}`. |
536
+ | POST | `/apk-vm/debug/action` | `force-stop` / `clear-data` / `uninstall` by package name. |
537
+ | POST | `/apk-vm/debug/rotate` | `{"orientation":"portrait"\|"landscape"\|"left"\|"right"\|"upside-down"}` or rotation `0..3`; locks the guest rotation. |
538
+ | GET | `/apk-vm/debug/ui` | UIAutomator hierarchy XML (bounded to 500 000 chars) plus node count and guest remote path. |
539
+ | POST | `/apk-vm/debug/record` | `{"seconds":1–60}` (default 5) → `screenrecord` MP4 pulled to the host temp dir; returns `localPath` and byte size; the panel auto-downloads it. |
540
+ | GET | `/apk-vm/artifacts?name=…` | Download a screenshot/recording previously written by the plugin into the OS temp dir (basename whitelist, no arbitrary paths). |
541
+ | GET | `/apk-vm/debug/perf` | load average, `/proc/meminfo` totals/free/available and the first lines of `dumpsys cpuinfo`. |
542
+ | GET/HEAD | `/apk-vm/debug/screenshot` | PNG download (`Content-Disposition: attachment`). |
543
+
544
+ ### Agent tool `apk_vm`
545
+
546
+ Registered globally through `ctx.tools`, so every DSH agent can operate the rooted VM directly:
547
+ `info`, `shell`, `logcat`, `getprop`, `setprop`, `packages`, `install`, `push`, `pull`,
548
+ `reboot`, `root`, `force_stop`, `clear_data`, `uninstall`, `rotate`, `ui_dump`, `record`, `perf`,
549
+ `screenshot`, high-level input `tap` / `swipe` / `long_press` / `key` / `text` (device coordinates),
550
+ UI automation `find_ui` / `assert_text` / `tap_text` (UIAutomator text/resource-id/content-desc/class),
551
+ clipboard `clipboard_get` / `clipboard_set`,
552
+ static `apk_info` (metadata without booting a VM),
553
+ device management (`devices`, `attach_device`, `detach_device`),
554
+ plus `start` / `stop` and instance management (`list_instances`, `create_instance`,
555
+ `destroy_instance`). It calls the same Host methods as the HTTP routes and the panel (no
556
+ duplicated logic).
557
+
558
+ For concurrent Agents, pass `instance` to target a specific VM; when omitted, the tool resolves
559
+ the calling Agent's assigned instance (first Agent → default VM, later Agents → isolated
560
+ instances created on demand until `maxInstances`). Each extra instance has its own H.264
561
+ WebSocket (`/apk-vm/instances/<id>/ws`) and falls back to PNG if WebCodecs is unavailable.
562
+
563
+ UI automation is live-verified against a multi-instance VM: `find_ui {"text":"Phone"}` returned
564
+ `Phone @ 102,1121`, `assert_text {"text":"Phone"}` found one node, `tap_text {"text":"Phone"}`
565
+ tapped that center and opened the app, and `key HOME` returned to the launcher.
566
+
567
+ ### Panel UI
568
+
569
+ The APK VM panel has an **Environment** card (readiness, missing items, Create AVD / Download
570
+ image / Re-check) and a **Rooted debug** card: root/shell badge, Android/API/ABI line, rooted
571
+ command input (equivalent to `adb shell`), Logcat / **Live logcat** / **Crash log** / Props / Root /
572
+ **UI tree** / **Perf** /
573
+ **Record 5s** / **Rotate** / **Screenshot** buttons, and a bounded output console.
574
+
575
+ ---
576
+
577
+ ## Compatibility with dsh-better-sidebar
578
+
579
+ [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is the community sidebar
580
+ workbench (right-sidebar tabs, bottom workbench, file viewers). When it is active, this plugin
581
+ registers a real tab type through its documented service — no forks, no DOM hacks, no hard
582
+ dependency:
583
+
584
+ - The client half defers with `ctx.inject(['betterSidebar'], …)`, so the plugin keeps working
585
+ unchanged on a plain DSH and simply gains the tab when the service appears.
586
+ - `ctx.betterSidebar.registerTab({ id: 'apk-vm:preview', title, description, icon, order: 60,
587
+ single: true, component })` — the tab appears in the native right Sidebar's **+** menu and in
588
+ the bottom workbench, rendered with the same VM console adapted to a narrow pane (stacked APK
589
+ list, compact header, `visible`-gated frame polling).
590
+ - The main panel gains an **"Open in side bar"** action that calls
591
+ `ctx.betterSidebar.openTab({ type: 'apk-vm:preview', target: 'right' })` for the active
592
+ session (with a clear message when no session is open).
593
+ - Everything degrades cleanly: the service is optional, and a missing/broken service never
594
+ affects the native `main` panel or the left sidebar entry.
595
+
596
+ Live-verified in the reference profile: the `sidebar.right.pane.tab` slot lists the occupant
597
+ `dsh-better-sidebar:apk-vm:preview` (`active: true`). The sandbox test
598
+ [spike/ui-better-sidebar-smoke.cjs](spike/ui-better-sidebar-smoke.cjs) separately asserts
599
+ descriptor fields, localization, disposal, the "already present" path, and resilience when the
600
+ service disappears.
601
+
602
+ ---
603
+
604
+ ## Performance
605
+
606
+ Measured on the **reference host**: Windows x64, AEHD 2.2 hardware acceleration, `-gpu host`
607
+ (NVIDIA RTX 5070 Ti), AOSP `system-images;android-34;default;x86_64` (Android 14 / API 34).
608
+ These are observations from a single machine, not universal guarantees.
609
+
610
+ | Metric | Result |
611
+ | --- | --- |
612
+ | Cold boot (first boot; also after re-saving the snapshot for the current AVD config) | **29.5 s** |
613
+ | Quick boot (snapshot restore) | **8.4 s** measured (`Boot completed in 8371 ms`, `bootKind: snapshot`); **8.4–10.5 s** across runs — inside the 30 s target |
614
+ | Raw H.264 stream over `/apk-vm/ws` (60 Hz animation test app) | **~59.3 fps** (425–474 packets in 8 s, zero protocol errors) |
615
+ | Headless browser software WebCodecs decode | **318 frames / 6.32 s = 50.3 fps** (~57 fps excluding ~0.7 s startup) |
616
+ | Late second client (keyframe recovery) | **234 packets / 4 s (~58.5 fps)** after `RESET_VIDEO` |
617
+ | PNG `screencap` fallback | **~1.1 fps** at 720×1280 (transport limit; not a regression) |
618
+ | Rooted guest | `adb shell id` = `uid=0(root)`; `ro.debuggable=1`; `ro.adb.secure=0`; toolkit installed (755) |
619
+
620
+ FPS is **content-driven**: scrcpy only emits frames when the screen changes, so a static home
621
+ screen legitimately shows a low FPS. The 60 Hz animation test sustains ~59 fps over the wire.
622
+ The `Target FPS` selector applies only to the PNG fallback; the H.264 path is capped by
623
+ `scrcpyMaxFps`.
624
+
625
+ ---
626
+
627
+ ## Verification
628
+
629
+ ### Automated checks (CI)
630
+
631
+ [.github/workflows/ci.yml](.github/workflows/ci.yml) runs on `ubuntu-latest` with Node.js 24 on
632
+ every push and pull request (full headless suite), plus a **cross-platform headless matrix**
633
+ (`ubuntu-latest`, `windows-latest`, `macos-latest`) that runs syntax checks, setup detection,
634
+ instance lifecycle and the UI bundle smoke test:
635
+
636
+ - `node --check` on `index.js`, `client.js` and every `lib/*.js` module;
637
+ - [spike/ui-smoke.cjs](spike/ui-smoke.cjs) — 136 sandbox assertions against the real client
638
+ bundle (module wrapper, registration, i18n, rendering states, POST handlers);
639
+ - [spike/ui-better-sidebar-smoke.cjs](spike/ui-better-sidebar-smoke.cjs) — descriptor /
640
+ localization / disposal / fallback assertions for the optional integration;
641
+ - [spike/setup-smoke.mjs](spike/setup-smoke.mjs) — 30 hermetic environment-detection /
642
+ create-avd / install-image / setup-report checks (mocked OS layouts and `exec`, no SDK needed);
643
+ - [spike/android/mock-device.mjs](spike/android/mock-device.mjs) — device critical path against
644
+ a mock adb (24 checks), including real `aapt2` APK metadata parsing;
645
+ - [spike/android/launch-plan.mjs](spike/android/launch-plan.mjs) — fast-boot argv plans
646
+ (snapshot restore, fail-fast without a hypervisor, explicit TCG, `-no-snapshot`).
647
+
648
+ **No Android emulator or AVD is required in CI.** Emulator-dependent integration tests are
649
+ **Windows-local only** and are not suitable for hosted CI runners (nested virtualization is not
650
+ available on GitHub-hosted runners).
651
+
652
+ ### Reference-host verification
653
+
654
+ | Check | Result |
655
+ | --- | --- |
656
+ | Bundle install via DSH `plugin_manager install_bundle` | `application: applied`, no warnings |
657
+ | Sidebar registration (`sidebar.panellist`, id `apk-vm`, order 30) | active in the live slot tree |
658
+ | Main panel registration (`main`, key `apk-vm`) | active in the live slot tree |
659
+ | `GET /apk-vm/state` / `GET /apk-vm/apks` | `200 {"ok":true,…}`; discovered the workspace `sample.apk` and user APKs |
660
+ | `GET /apk-vm/frame` while stopped | `409` with JSON error (as specified) |
661
+ | Client bundle | `node --check` OK; 136/136 sandbox checks pass |
662
+ | dsh-better-sidebar tab | live `sidebar.right.pane.tab` occupant `dsh-better-sidebar:apk-vm:preview`; smoke test passes |
663
+ | Host/backend modules | `node --check` OK; `lib/android.js` smoke tests 11/11; mock-device 24/24 |
664
+ | Environment detection | `spike/setup-smoke.mjs` 30/30 (hermetic, runs on CI) |
665
+ | scrcpy relay | 160 assertions incl. a fake-device end-to-end protocol test; real emulator measured ~59 fps |
666
+ | H.264 / WebCodecs | raw stream 59.3 fps; headless software decode 50.3 fps; late-client keyframe recovery works |
667
+ | Fast boot | cold 29.5 s; quick boot 8.4–10.5 s (`bootKind: snapshot`) |
668
+ | Rooted guest / debug API | `root:true`, `debugPrepared:true`; `/debug/info`, `/debug/shell`, `/debug/logcat`, `/debug/prop`, `/debug/packages`, snapshot save all returned `ok` |
669
+ | Agent tool `apk_vm` | registered through `ctx.tools`; DSH reported the updated tool schema |
670
+
671
+ The APK upload, environment card, and the added device actions (rotate / UI tree / record / perf /
672
+ screenshot download) are implemented in this release and covered by the syntax and headless
673
+ suites where applicable; the measured table above only lists values that were actually observed.
674
+ No screenshots are published yet (see [Demo](#demo)).
675
+
676
+ Run the same headless suites locally (Windows or any OS) from the repository root:
677
+
678
+ ```bash
679
+ node --check index.js
680
+ node --check client.js
681
+ for f in lib/*.js; do node --check "$f"; done
682
+ node spike/ui-smoke.cjs
683
+ node spike/ui-better-sidebar-smoke.cjs
684
+ node spike/setup-smoke.mjs
685
+ node spike/android/mock-device.mjs
686
+ node spike/android/launch-plan.mjs
687
+ ```
688
+
689
+ `spike/android/mock-device.mjs` needs a real `aapt2`; install Android SDK build-tools or point
690
+ `ANDROID_HOME` / `ANDROID_SDK_ROOT` at an SDK that has them.
691
+
692
+ ---
693
+
694
+ ## Troubleshooting
695
+
696
+ ### The Environment card says "Setup needed"
697
+
698
+ The report lists each missing item: SDK path, `adb`, emulator, `sdkmanager`/`avdmanager`, the
699
+ configured system image, the AVD, or CPU acceleration.
700
+
701
+ - Missing SDK/emulator: install Android SDK packages and/or set `sdkRoot` (or `ANDROID_HOME` /
702
+ `ANDROID_SDK_ROOT`).
703
+ - Missing system image: install it with `sdkmanager`, or set `allowSetupDownloads: true` and use
704
+ **Download image** in the card (large download; never automatic).
705
+ - Missing AVD: use **Create AVD** in the card (requires the system image and `avdmanager`) or run
706
+ `avdmanager create avd -n dsh_apk -k "system-images;android-34;default;x86_64"`.
707
+ - Missing acceleration: see the next section.
708
+
709
+ ### "No usable CPU hypervisor (AEHD/WHPX) was found"
710
+
711
+ The plugin fails fast (`requireAccel: true` by default) because an x86_64 system image cannot
712
+ boot under pure software TCG. Check with `"$ANDROID_HOME/emulator/emulator" -accel-check`, then:
713
+
714
+ - **Windows (AEHD):** install the `extras;google;Android_Emulator_Hypervisor_Driver` package or
715
+ the standalone AEHD installer, then reboot. Conflicts with Hyper-V/WHPX, WSL2, memory
716
+ integrity/VBS, or vendor emulator drivers (MuMu, BlueStacks, LDPlayer, Nox) can prevent AEHD
717
+ from loading or cause an installer error such as **1450** — stop/disable the competing
718
+ driver, reboot, and retry. Do not run two hypervisor stacks at the same time.
719
+ - **Windows (WHPX):** enable the *Windows Hypervisor Platform* feature and reboot. If the
720
+ Windows component store is damaged, enabling WHPX can fail (for example DISM error **14098**);
721
+ repair Windows or use AEHD instead.
722
+ - **Linux:** enable KVM in the firmware, load the KVM module, and make sure your user can open
723
+ `/dev/kvm`.
724
+ - **macOS:** HVF is built in; make sure the machine is not running inside another VM.
725
+
726
+ Setting `requireAccel: false` / `accel: off` is only useful for diagnostics — the x86_64 guest
727
+ is not expected to finish booting that way.
728
+
729
+ ### AEHD installed but the emulator is slow or refuses to start
730
+
731
+ - Confirm the driver is running (`sc query aehd` on Windows) and that virtualization is enabled
732
+ in the BIOS/UEFI.
733
+ - A stale vendor driver (for example MuMu's `MuMuVMMDrv`) can hold AMD-V/VT-x. Stop/disable it
734
+ and reboot. (Restore it later if you still use that emulator.)
735
+ - A reboot is required after changing hypervisor-related Windows features or drivers.
736
+
737
+ ### Emulator / scrcpy port conflicts
738
+
739
+ | Port | Used for | Fix |
740
+ | --- | --- | --- |
741
+ | `5558` (`port`) | Emulator console port | Change `port`, or close the stale emulator/adb process. |
742
+ | `27183` (`scrcpyPort`) | Local adb forward for the scrcpy tunnel | Change `scrcpyPort`. |
743
+ | `5037` | adb server | `adb kill-server` if a stale or foreign adb server owns it. |
744
+
745
+ ### Quick boot fails: "different AVD configuration", black screen, or no snapshot
746
+
747
+ - The snapshot is only valid for the AVD configuration that created it. After changing the AVD
748
+ (API level, ABI, resolution, GPU mode, `-writable-system`, …) delete the old snapshot and let
749
+ the plugin create a new one: remove
750
+ `~/.android/avd/<avdName>.avd/snapshots/<snapshotName>` (on Windows,
751
+ `%USERPROFILE%\.android\avd\<avdName>.avd\snapshots\<snapshotName>`), then start cold again.
752
+ - A snapshot directory with `ram.img` but no `snapshot.pb` is not a valid snapshot; the plugin
753
+ ignores it. Delete it or set `quickBoot: false` once to force a clean cold boot.
754
+ - If a stale offline adb emulator is attached, `adb kill-server` (or kill the leftover
755
+ `emulator`/`qemu-system-*` process) and retry.
756
+
757
+ ### APK upload fails
758
+
759
+ - The upload endpoint accepts raw `.apk`/ZIP bytes up to **500 MiB**; larger files return `413`
760
+ and non-ZIP content returns `400`. Use **Install from path** for unusual cases.
761
+ - Only one install (path install or upload) can run at a time — a concurrent request returns
762
+ `409`; wait for the current install to finish.
763
+ - A file that installs on a phone can still be rejected by the emulator (ABI/split APK); the
764
+ error is shown in the panel and in `/apk-vm/state`.
765
+
766
+ ### The sidebar panel does not update after install/upgrade
767
+
768
+ - Reload the DSH Web page so the new `client.js` bundle is loaded; a long-lived tab may keep the
769
+ old bundle cached.
770
+ - If API requests return `401`/`403` after a DSH restart, sign in / refresh the session — the
771
+ DSH Web GUI requires its auth token, and the plugin routes live behind the same server.
772
+ - Check the plugin log tail in the panel (`/apk-vm/state` → `vm.logTail`) and the DSH host log.
773
+
774
+ ### Low FPS / a blank canvas
775
+
776
+ - A static screen legitimately shows low FPS — scrcpy only encodes frames when the display
777
+ changes. Measure with an animation, not the home screen.
778
+ - A blank canvas that never receives a keyframe: reload the page; the panel requests
779
+ `RESET_VIDEO` on connect. If the browser has no `WebCodecs`/`WebSocket`, the panel falls back
780
+ to PNG polling (~1 fps) automatically.
781
+ - The `Target FPS` selector changes only the PNG fallback; use `scrcpyMaxFps` for H.264.
782
+
783
+ ### APK is not listed / `aapt2` errors
784
+
785
+ - Add the project directory to `roots`; the DSH working directory, `process.cwd()` and the home
786
+ directory are searched automatically.
787
+ - Install Android SDK `build-tools` (provides `aapt2`) and make sure `ANDROID_HOME` /
788
+ `ANDROID_SDK_ROOT` points at the SDK, or set the SDK root via config.
789
+ - A malformed or non-APK file is rejected by `aapt2 dump badging`; the panel surfaces the error.
790
+
791
+ ### macOS / Linux notes
792
+
793
+ SDK/adb/emulator/AVD discovery covers the standard per-OS layouts (including Homebrew and
794
+ `/opt` style installs), but end-to-end emulator + streaming behavior has only been verified on
795
+ Windows x64 so far. If you hit a platform issue, include `/apk-vm/setup` output in the bug
796
+ report — it lists exactly which paths were detected.
797
+
798
+ ---
799
+
800
+ ## Security
801
+
802
+ This plugin intentionally exposes powerful local control over an Android emulator. Read this
803
+ before pointing the DSH Web UI at an untrusted network.
804
+
805
+ - **The plugin manages processes on your machine.** It spawns the Android emulator, `adb`,
806
+ `sdkmanager` and `avdmanager` with `child_process.spawn(exe, argv)` — never through a shell —
807
+ so user-supplied strings are not shell-interpreted. Paths are validated (APK paths must exist
808
+ and end in `.apk`; push/pull paths are restricted to `/data/local/tmp`, `/sdcard`,
809
+ `/storage/emulated/0` and allowed local roots).
810
+ - **The guest is deliberately rooted and debuggable.** `adb root`, `ro.debuggable=1` and
811
+ `ro.adb.secure=0` mean anything you install inside the VM is not sandboxed from root. Do not
812
+ put real secrets or personal data in the AVD.
813
+ - **The debug HTTP API is a local root shell for the guest.** `/apk-vm/debug/shell` runs
814
+ arbitrary commands *inside the emulator* (not on the host), and `/apk-vm/debug/action` can
815
+ uninstall packages. Requests are same-origin only and bounded, but any page/extension that can
816
+ make same-origin requests to your DSH Web server can exercise them.
817
+ - **Upload and environment setup are privileged local actions.** Upload accepts only APK/ZIP
818
+ bytes up to 500 MiB and installs them into the guest; `create-avd` writes an AVD; and
819
+ `install-image` runs `sdkmanager --install` only when the user opted in with
820
+ `allowSetupDownloads: true` (default `false`). All of these are reachable only from the same
821
+ origin.
822
+ - **The `apk_vm` Agent tool is available to every agent in the DSH profile.** Grant agent access
823
+ to this plugin with the same care as giving an agent a terminal on an Android device.
824
+ - **Loopback-only by default.** In addition to the same-origin check, every `/apk-vm/*` HTTP
825
+ route and the WebSocket upgrade reject non-loopback clients unless `allowRemote: true` is
826
+ explicitly set (for use behind your own authenticated reverse proxy). This means exposing the
827
+ DSH Web UI on the LAN does not automatically expose the root debug API.
828
+ - **Keep the DSH Web server local.** If you expose the DSH Web UI beyond localhost (reverse
829
+ proxy, LAN bind, tunnel), you also expose the plugin routes. Add authentication at the proxy
830
+ layer or keep the port local; do not disable the same-origin/loopback checks.
831
+ - **No telemetry.** The plugin makes no network requests of its own to third-party services;
832
+ it only talks to the local adb/emulator and the same-origin DSH Web server (the opt-in
833
+ `install-image` action runs the user's own `sdkmanager`, which does contact Google's SDK
834
+ repository).
835
+
836
+ To report a vulnerability, follow [SECURITY.md](SECURITY.md) — please use private disclosure,
837
+ not a public issue.
838
+
839
+ ---
840
+
841
+ ## Roadmap
842
+
843
+ The features below are **not shipped yet** — they are tracked for future releases. Everything
844
+ described above is implemented in this release.
845
+
846
+ - **Physical-device switching** — first-class selection/attachment of adb-attached physical
847
+ devices from the panel (config `serial` already supports attaching one; multi-AVD switching is
848
+ shipped via `/apk-vm/avds` + `/apk-vm/avd` and the Environment card).
849
+ - **Audio streaming** — forward guest audio alongside the video stream.
850
+ - **Cloud / remote VM execution** — run the guest on a remote host and stream it to the panel.
851
+ - **Static APK preview** *(optional)* — inspect an APK's manifest, resources and launcher UI
852
+ without booting a full VM.
853
+
854
+ QEMU + Android-x86 for hosts without hardware virtualization and `-writable-system` image
855
+ customization remain research notes in [DESIGN.md](DESIGN.md) §8.
856
+
857
+ ---
858
+
859
+ ## Known limitations
860
+
861
+ - **Windows x64 is the only end-to-end verified platform.** SDK/adb/emulator/AVD discovery is
862
+ implemented for Windows, macOS and Linux, but emulator + H.264 behavior on macOS/Linux (and
863
+ `arm64-v8a` images on Apple Silicon) is unverified.
864
+ - **Hardware virtualization is mandatory** for the x86_64 AOSP image; pure TCG fails by design.
865
+ - **Quick boot is snapshot-based.** Changing AVD configuration invalidates the snapshot; recreate
866
+ it as described in [Troubleshooting](#quick-boot-fails-different-avd-configuration-black-screen-or-no-snapshot).
867
+ - **H.264 FPS is content-driven.** Static screens show low FPS; the PNG fallback is ~1 fps by
868
+ transport limitation.
869
+ - **The rooted debug API is local-only by design.** Do not expose the DSH Web port to untrusted
870
+ networks.
871
+ - **No audio and no physical-device switching yet** (multi-AVD switching is available; see
872
+ [Roadmap](#roadmap)).
873
+
874
+ ---
875
+
876
+ ## Contributing
877
+
878
+ Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) for the development
879
+ workflow, repository layout, the checks to run before a PR, and the rules for the UI half
880
+ (theme tokens, localization, no new npm dependencies). All participants are expected to follow
881
+ [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Security issues should follow [SECURITY.md](SECURITY.md).
882
+ By contributing you agree that your contributions are licensed under the repository's Apache-2.0
883
+ terms. Release mechanics are in [docs/RELEASING.md](docs/RELEASING.md).
884
+
885
+ Quick reference for a change:
886
+
887
+ ```bash
888
+ node --check index.js && node --check client.js
889
+ for f in lib/*.js; do node --check "$f"; done
890
+ node spike/ui-smoke.cjs
891
+ node spike/ui-better-sidebar-smoke.cjs
892
+ node spike/setup-smoke.mjs
893
+ node spike/android/mock-device.mjs
894
+ node spike/android/launch-plan.mjs
895
+ node spike/instances-smoke.mjs
896
+ node scripts/privacy-scan.mjs
897
+ ```
898
+
899
+ Use [.github/ISSUE_TEMPLATE/bug_report.yml](.github/ISSUE_TEMPLATE/bug_report.yml) for bugs and
900
+ [.github/ISSUE_TEMPLATE/feature_request.yml](.github/ISSUE_TEMPLATE/feature_request.yml) for
901
+ features. Emulator integration testing is Windows-local only (see
902
+ [CONTRIBUTING.md](CONTRIBUTING.md#testing)); hosted CI runs the headless suites only.
903
+
904
+ ---
905
+
906
+ ## License
907
+
908
+ Licensed under the **Apache License, Version 2.0** — see [LICENSE](LICENSE). Third-party
909
+ components and attribution are listed in [NOTICE](NOTICE).
910
+
911
+ - **scrcpy** — © Genymobile and contributors, Apache-2.0. The server JAR
912
+ `lib/vendor/scrcpy-server-v5.0.1.jar` is redistributed unmodified under that license and is
913
+ pushed to the guest at runtime; the desktop scrcpy client is not used.
914
+ - **AOSP Android Emulator / system images / adb** — © The Android Open Source Project,
915
+ Apache-2.0. They are installed separately through the Android SDK and are not bundled in this
916
+ repository.
917
+ - **dsh-better-sidebar** — a separate community plugin (not bundled here); the optional
918
+ integration uses its published service only.
919
+
920
+ This project is not affiliated with or endorsed by Genymobile, the Android Open Source Project,
921
+ or the DeepSeek Harness project. "Android" is a trademark of Google LLC.