* fix(adb): name unexecutable adb paths in readiness refusals When REA_ADB_PATH or the PATH-resolved adb is a directory or a file the host refuses to execute, the version probe fails at spawn with EACCES or EPERM. That fell through to the generic "did not report a usable version" refusal, which named neither the path nor the cause. Classify those spawn errors as binary_not_executable (provider rejection executable_missing), name the selected path and errno, and give remediation for the environment, PATH and host-policy cases. The generic fallback now names the selected path too. Fixes #5689 * fix(process): admit synchronous spawn errors as captured failures Node throws spawn errors other than EACCES, EAGAIN, EMFILE, ENFILE and ENOENT synchronously from execFile. execFileBytes called execFile inside its Promise executor, so ENOTDIR or EPERM became a rejection without capture fields and execFileOutputFailure returned undefined. Consumers that classify by failure code, such as the adb version probe, could not see the code, which left the adb EPERM branch unreachable. Catch synchronous spawn errno errors at the capture boundary and attach the same empty capture an asynchronous spawn failure carries. Argument validation errors stay unadmitted. The adb version probe now reports an ENOTDIR selected path, such as a REA_ADB_PATH whose parent is a regular file, as a missing binary with the filesystem error code instead of the generic version refusal. * fix(process): expose spawn failures from the capture owner execFileBytes knows when a command never started, but ExecFileOutputFailure did not say so, so consumers reread raw syscall and code fields and disagreed. Mark both the synchronous throw and an asynchronous `spawn <path>` error with `spawnFailed: true`, and set `path` on the synchronous error to match the asynchronous shape. Argument validation errors stay unadmitted. Consumers now use the owner field: - inspectAdbClient never reports a spawn errno as `exit_code`. ENOENT, ENOTDIR, ELOOP and ENAMETOOLONG are `binary_missing`; EACCES and EPERM stay `binary_not_executable`; any other spawn errno, such as EINVAL, is `binary_not_executable` with a path-naming "could not be started" message. Each records `filesystem_error` and `path_source`. EACCES remediation also names parent directory search permission, and EPERM remediation no longer names host policy as the only cause. - DarwinProcessRunTokenReader tags every not-started compiler as `spawn_failed`, not only ENOENT. EACCES, EPERM and ENOTDIR were previously `failed`. - NativeDmgArtifactReader uses one not-started rule for both `processNotStarted` (previously `spawn ` only, so synchronous errors were missed) and the ENOENT unavailable/io split (previously any `spawn` prefix). Its fake hosts now reject with real execFileOutput spawn failures. * fix(process): classify spawn failures consistently across consumers Admit synchronous and asynchronous spawn errnos through one helper that requires an errno and a `spawn` or `spawn <path>` syscall. The synchronous shape now also gains `spawnargs`, so both shapes match. Use the owner's `spawnFailed` fact in the remaining consumers: - inspectAdbClient reports exhausted host capacity (EAGAIN, EMFILE, ENFILE, ENOMEM) as a host failure to retry and no longer tells the caller to change REA_ADB_PATH. Path-resolution errnos come from a predicate the filesystem owner exports. - runAdbCapture says adb could not be started instead of claiming an exit code for a process that never ran. - The Darwin compiler probe words every spawn_failed outcome as a start failure, not a compilation failure. - Asset catalog inspection treats any assetutil that never started as unavailable, not only ENOENT. * fix(process): report host spawn capacity as a retryable constraint Spawn errnos that report exhausted host capacity (EAGAIN, EMFILE, ENFILE, ENOMEM) were classified by adb readiness alone. assetutil reported them as an unavailable tool, which marked inspect_asset_catalog unavailable in the session, and adb commands reported them as protocol failures. Classify them once in the process capture owner and report a retryable capacity constraint with recovery advice from assetutil and adb commands. * docs(adb): describe host capacity failures from device operations * fix(adb): report exhausted host spawn capacity as runtime unavailability A host that cannot start adb for lack of process, memory or descriptor capacity is not a configuration problem. Give it its own failure code and map it to the runtime_unavailable provider rejection instead of not_configured. * fix(adb): report probe-time host capacity as a retryable constraint A device operation whose adb version probe could not start for lack of host capacity was refused as capability_unavailable without retryable. It now reports the same retryable capacity constraint as a later adb command. * fix(adb): keep the selected adb path in probe-time capacity errors The retryable capacity constraint raised at the version probe dropped the selected_binary and path_source diagnostics the refusal used to carry. * refactor(adb): keep the spawn-failure fact to adb consumers Restore the DMG reader, Darwin compiler probe and assetutil to main. The capture owner still marks a never-started command, including the spawn errors Node throws synchronously, and only the adb readiness probe and adb commands read that fact. * test(adb): assert spawn-failure facts instead of refusal wording The new adb tests compared complete refusal sentences and banned an old phrase. They now check the errno, selected path, path source and the owner's spawn-failure marker, so wording edits do not break them. * fix(adb): own one host-capacity remediation and cover the spawn route The host-capacity constraint and the configuration failure each carried their own remediation sentence. AdbConfiguration now owns the text and both use it, so the constraint's remediation.action equals diagnostics.remediation. The synchronous spawn-throw test now runs on Node's execFile route and on the owned spawn route adb uses (stopSignal). The provider test covers inspect_adb_client under EMFILE next to list_adb_devices, and the docs say which operations report the retryable capacity constraint and which report runtime_unavailable. * fix(process): report EMFILE and ENFILE spawn failures without stdio After an EMFILE or ENFILE spawn failure Node creates no stdio, so the owned stop-signal route threw a TypeError while reading the streams and the later spawn error went unhandled. Attach the error listener first and capture absent streams as empty, so adb reports retryable host capacity. * fix(adb): keep a completed push when host capacity stops its digest check A capacity failure starting sha256sum came back as a retryable error even though adb push had already written the file. Repeating the request with overwrite=false is then refused because the destination exists. Return the receipt with an unavailable device digest and name the capacity failure in its limitations, so the caller is told the transfer is unverified rather than advised to retry. * docs(adb): list host capacity as an unverified push case --------- Co-authored-by: t <t@t>
23 KiB
ADB device inspection and acquisition
REA inspects connected Android devices, reads device state, and transfers
files and packages through the caller's adb binary. Emulators, USB
devices, and network devices participate alike: REA neither starts nor
manages them and never installs platform tools. Every operation composes a
fixed adb argument vector — caller values occupy positional arguments only
and are quoted when adb forwards them to the device shell.
The adb client may start its local server on first device query; that is adb
client behavior, recorded in the tool result when observed.
inspect_adb_client probes only the binary and never contacts the server.
Supply tools
Use any Android platform-tools adb (verified with 34.0.5). Select it
through the environment:
# Optional: absolute path to the adb binary; without it, adb resolves from PATH.
export REA_ADB_PATH=/absolute/path/to/adb
REA never installs the Android SDK, an emulator, or a system image.
Readiness is rea inspect-adb-client: it runs adb version and reports the
resolved binary, its version, and where it was selected from. A missing
binary reports capability_unavailable with the REA_ADB_PATH remediation
rather than attempting an installation. A path that cannot resolve to a file
(ENOENT, ENOTDIR, ELOOP or ENAMETOOLONG) is reported as missing. A
selected path the host refuses to execute, such as a directory or a file
without execute permission (EACCES) or one the host does not permit
(EPERM), and any other spawn error report capability_unavailable naming
that path. Exhausted host capacity (EAGAIN, EMFILE, ENFILE or ENOMEM)
is not a configuration failure. inspect_adb_client, and any device
operation whose adb cannot start at its version probe or at a later adb
command, report a retryable capacity resource constraint rather than a
capability refusal or an adb protocol failure; the binary_session inventory
reports the provider as runtime_unavailable. Both say the host could not
start adb and ask for a retry after freeing processes or descriptors, without
blaming REA_ADB_PATH. When adb never started, the refusal diagnostics record
the spawn error as filesystem_error with path_source, never as an
exit_code.
The binary_session inventory uses the same bounded client-version probe; it
does not query devices, start the adb server, or establish that a requested
serial is reachable. Device state is checked by each device operation.
Answer an analyst question
# Device identity
rea inspect-adb-client
rea list-adb-devices
rea inspect-adb-device emulator-5554
rea inspect-adb-display emulator-5554
rea list-adb-features emulator-5554
rea list-adb-services emulator-5554
# Find and acquire an app (the easy-get flow)
rea resolve-adb-packages emulator-5554 whatsapp
rea list-adb-packages emulator-5554 --scope third_party
rea inspect-adb-package emulator-5554 com.whatsapp
rea pull-adb-package emulator-5554 com.whatsapp --output-directory /tmp/acq
rea inspect-android-package /tmp/acq/com.whatsapp/base.apk
# Device state and files
rea list-adb-processes emulator-5554
rea inspect-adb-window emulator-5554
rea read-adb-setting emulator-5554 global adb_enabled
rea read-adb-logcat emulator-5554 --count 200
rea list-adb-directory emulator-5554 /data/data/com.whatsapp
rea pull-adb-file emulator-5554 /data/local/tmp/notes.txt --output-directory /tmp/acq
rea push-adb-file emulator-5554 /tools/frida-server /data/local/tmp/frida-server
rea capture-adb-screen emulator-5554 --output-directory /tmp/shots
rea collect-adb-bugreport emulator-5554 --output-directory /tmp/bugs
# Package lifecycle and dynamic analysis (device-mutating)
rea install-adb-package emulator-5554 /tmp/app.apk --replace
rea start-adb-app emulator-5554 com.example.app
rea read-adb-logcat emulator-5554 --pid 12345
rea start-adb-activity emulator-5554 android.intent.action.VIEW --data-uri https://example.com
rea stop-adb-app emulator-5554 com.example.app
rea uninstall-adb-package emulator-5554 com.example.app
Equivalent MCP methods use the snake_case names (list_adb_devices,
pull_adb_package, resolve_adb_packages, …). Device operations take
serial exactly as adb devices reports it. The pulled APK set feeds the
existing analysis families directly, for example
rea inspect-android-package /tmp/acq/<package>/base.apk.
Operations
| Tool | Kind | What it does |
|---|---|---|
inspect_adb_client |
read | Resolved binary, version, install path; never contacts the server |
list_adb_devices |
read | Serial, state, transport, model metadata, inferred emulator/physical kind with basis |
inspect_adb_device |
read | Fixed getprop whitelist: build, SDK, patch, fingerprint, product, ABIs, debuggable, ro.kernel.qemu |
inspect_adb_display |
read | wm size and wm density as declared |
list_adb_features |
read | pm list features (hardware/software features, hex GL versions) |
list_adb_services |
read | service list binder services and interfaces |
list_adb_processes |
read | ps -A: user, pid, ppid, rss, state, name |
inspect_adb_window |
read | dumpsys window focus declarations (what is on screen) |
read_adb_setting |
read | One settings get value; device-side null is reported as such |
read_adb_logcat |
read | Bounded logcat -d -t dump for one buffer, optional pid filter |
list_adb_packages |
read | pm list packages -f with scope; base APK device paths |
resolve_adb_packages |
read | Case-insensitive package-name substring search with exact-match flag |
inspect_adb_package |
read | dumpsys package projection: versions, install times, installer, flags, and count of listed requested-permission rows (including Unicode names) |
list_adb_directory |
read | NUL-framed immediate children, including hidden entries: kinds, permissions, owner, group, sizes, symlink targets |
pull_adb_package |
read | Complete split APK set from pm path, each file digested |
pull_adb_file |
read | One absolute device path pulled and digested |
capture_adb_screen |
read | One screencap -p frame (exec-out, no device-side file) with dimensions |
collect_adb_bugreport |
read | Device-generated bugreport zip, digested |
push_adb_file |
mutates device | One local file to a device path (e.g. pushing tools into /data/local/tmp); checks for existing targets and refuses them without explicit overwrite (the check is not atomic); compares digests when device sha256sum is available |
install_adb_package |
mutates device | Local APK via adb install [-r]; preserves its Success/Failure verdict, records replace_requested, and leaves actual replacement null because prior package state is not observed |
uninstall_adb_package |
mutates device | Requests package removal for user 0 via adb shell pm uninstall --user 0, without data retention |
start_adb_app |
mutates device | Resolves the package's launcher activity (cmd package resolve-activity --brief) and starts it (am start -n) |
start_adb_activity |
mutates device | Starts an intent (am start -a with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
stop_adb_app |
mutates device | Force-stops every process of a package, ending a dynamic-analysis run |
Screen capture validates the PNG signature, IHDR framing, dimensions, and IHDR checksum before publishing. It does not validate encoding fields or decode and validate image contents; that limitation is retained in Evidence.
When push_adb_file receives an existing directory, adb places the local file
inside it under the local basename; Evidence reports that effective file path
while retaining the caller's directory path in the input parameters. The
private read-only staging copy preserves the source file's executable bits.
For inspect_adb_window, focus_contexts retains each display header, its exact
display ID when recognized, and every focus declaration with its original line.
Declarations without a display header are explicitly unscoped. A missing
declaration differs from a declaration whose value is JSON null.
source_section identifies the selected dumpsys section; observed_lines
preserves its focus declarations and display headers in source order.
focus_summary is single_context only for one context with no repeated focus
field. The singular focused_window and focused_app summarize that context;
they do not establish device-wide focus. Multiple contexts or repeated fields
produce ambiguous with null singular fields and all declarations retained.
When no focus field was declared, the status is unavailable. REA reads the
displays section only when the windows section declares neither focus field.
For install_adb_package, replace_requested records whether -r was sent.
The replaced result is null: the successful install response does not say
whether the package was installed fresh or replaced, and REA does not query the
device first. The install verdict and host APK digest remain available.
Push and install consume a private snapshot of the selected local input. The
reported size and SHA-256 identify those consumed bytes; local_path and
apk_path retain the original input path, whose contents may change later.
Snapshot creation needs temporary disk space proportional to the input size.
Failed snapshot cleanup remains owned and is retried before later operations
and during shutdown.
Interpret the result
- Package numeric declarations retain failures.
inspect_adb_packageadmits the first validuserIdorappIdasuser_id: a nonnegative safe integer, including zero. A null value means no valid declaration was found. Malformed declarations before or after a valid value remain innumeric_issues: the first ten issues with command, field, reason and originalraw_line, plus the exacttotal. Reasons areinvalid_formatorunsafe_integer. Any issue makes package coverage partial; existing section omissions can also make it partial.version_coderemains exact source text. - Display nulls preserve uncertainty.
inspect_adb_displayadmits positive safe integers. Absent or malformed optional values are null;numeric_issuesdistinguishes malformed declarations from absence. It keeps the first ten issues and exact total, with command, field, original line and reason (invalid_format,unsafe_integerornonpositive). The last recognized labeled declaration wins, even when malformed. A physical-density label suppresses bare-number fallback even when malformed; without that label, the first unsigned bare number wins, including zero or an unsafe integer that leaves the value null. Display coverage is complete only with known physical density and no numeric issues. Missing or malformed required physical size produces a protocol failure withdetails.partial_observation: aninspect_adb_displayobservation from provideradb, reasonphysical_size_absentorphysical_size_malformed, and aresultretaining client, serial, optional display facts, numeric issues, null physical size and partial coverage. - Directory names retain whitespace and newlines. Symlink targets do too,
including trailing newlines. Records containing invalid UTF-8 remain as
labelled hex in
unparsed_linesand make directory coverage partial. - Inferences carry their basis. Device
kindreportskind_basis(serial prefix or USB transport); APKrolereportsrole_basis: file_name;emulator_observedis derived from the build'sro.kernel.qemuproperty:trueonly when its value is"1",falsefor any other reported value, andnullwhen the property is absent. The raw property value remains available inproperties. - Device paths are not host paths.
base_apk_device_pathvalues live on the device filesystem. Only pulls and captures produce host-local paths, each paired with byte size and SHA-256 digest. A pulled file's host name is the device path's final POSIX segment. REA preserves the original device path and filename; a missing final segment or./..is refused before output creation. Windows hosts follow Windows filename rules, while POSIX hosts retain valid POSIX filenames such as names with spaces or colons. A package pull checks reported names against the destination filesystem before pulling, and refuses names that would share a local path. - Package path refusals are diagnosable. Refusals from
pull_adb_packagereturnunreadable_outputwithretryable: false, keep thepm pathoutput indetails.captured_output, and name the refusal indetails.reason.unsafe_device_pathlists refused entries indetails.diagnostics.rejected_pathsas{device_path, reason};duplicate_local_destinationreports the sharedfile_nameand bothdevice_paths;local_destination_unavailablereports thedevice_path,file_name, and filesystemerror_code. Diagnostics retain raw device strings, while the caller-facing message escapes invisible and bidirectional characters so they cannot hide text or reorder the path. - Splits are part of the answer.
pm pathenumerates the base APK and every installed split; pulls cover the whole set. System packages whose APKs keep other names report theunknownrole rather than guessing. - Partial results keep facts. Failed individual pulls return successful
artifacts with
coverage: "partial"and per-filefailures. Listings and package searches that contain unrecognized lines report the first ten asunparsed_lines, state the exact total in a limitation when more were seen, and mark partial coverage instead of dropping rows. - Mutations are explicit.
push_adb_file,install_adb_package, anduninstall_adb_packagedeclaremutatesTargetand never run implicitly: pushes check for existing device files before transfer and refuse them unlessoverwriteis set. A failed or unexpected existence probe stops transfer even withoverwrite. A completedtest -eexit of 1 establishes absence only with empty stdout and either empty stderr or a recognized, completed adb-server startup notice sequence. Startup notices are host observations, not remote-path diagnostics; successful push receipts retain them inraw_result.device_path_probes. Incomplete startup, extra diagnostics, truncation and cancellation do not establish absence. The check is not atomic, so a different process can create the path beforeadb pushwrites it. When available, the device'ssha256sumverifies the transfer. A missing command, output without a recognizable digest, or host capacity that prevents the digest check from starting reportsdevice_digest_source: "unavailable"with an unverified-transfer limitation rather than a retryable error; installs preserve the device'sFailure [...]reason verbatim. - Producer identity stays attached. Every result embeds the producing
adb client (
binary_path,path_source,version,installed_path).
Resource and lifecycle limits
- Caller values reach adb only as positional arguments in fixed vectors;
no tool accepts an arbitrary shell command line,
input, ormonkey. The app/activity lifecycle tools useam start -Wandam force-stop. - Per-invocation budgets: client/devices 10-20 s, shell reads 20-60 s with
64 KiB-8 MiB capture caps (logcat 4 MiB, screen 32 MiB), transfers and
bugreports 600 s. Output capture overflow reports
resource_constraintand names the byte allowance. Deadline expiration, including the client-version probe, reportsprovider_timeoutand names the effective milliseconds. This release has no runtime override for those deadlines or capture caps. Command cancellation preserves the caller's exact reason; a later abort does not replace an already completed command failure. - Pull and capture outputs are additive: the destination must not already exist. Re-running a completed pull refuses the existing directory instead of overwriting evidence.
- Cancellation during a package pull preserves caller-selected output. The error includes partial observations identifying completed artifacts and the interrupted local path; remaining APKs were not attempted.
- Screen capture checks cancellation before publishing its exclusive output. If cancellation arrives after publication, the error retains the committed file's path, digest and dimensions; the file remains available as evidence.
- Bugreports collect into isolated staging and publish with exclusive creation;
filename collisions select the next numeric suffix. If publication fails, the
completed archive remains in staging. The error's partial observation gives
its recoverable
local_path, the originaladb_reported_path, and an unknown digest status. Later operations and shutdown preserve this retained evidence. - Failed package acquisition rollback retains its directory identity and retries cleanup before later work and during shutdown. Cleanup refuses a replacement directory. Terminal shutdown stops admission and drains active operations.
- The provider holds no long-lived processes; every invocation is one bounded adb run. The adb server belongs to the caller.
Related capability and follow-up lanes
UI interaction beyond intent starts (tapping, typing, monkey fuzzing, screen
recording) remains a separate lane: candidates such as
mobile-device-mcp install
driver software on the device and pair with the Frida instrumentation lane
(#1359) behind explicit mutation-authority contracts. Network transports
beyond already-connected devices (adb connect) are not exposed yet.
Real verification
npm run verify:adb exercises observation and transfer operations against a live device. It
requires adb (or REA_ADB_PATH) and any attached emulator or phone;
--serial selects one when several are attached, --pull PACKAGE pulls a
real package and verifies the returned digests against the files on disk.
The optional --install-apk lifecycle is unavailable until the selected APK
identity and device cleanup ownership can be established safely.
Record: verified against adb 34.0.5 on Linux against an Android 14
(API 34) x86_64 emulator — all operations above, a 224-package inventory,
287 services, 92 features, a real two-APK split set pulled with byte-exact
digests, a verified push/pull roundtrip, a 1.3 MB screen capture, and the
complete package lifecycle; see docs/testing.md for the lane summary.
Intent integer extras use signed decimal text. The ADB argument encoder removes
redundant leading zeros for Android's radix-sensitive --ei parser while retained
Evidence keeps the caller's original spelling. Package inspection accepts both
userId and Android 16's appId spelling for the legacy user_id result field.
A completed push and device digest verification are separate observations. A verification timeout, cancellation, or output budget failure returns the typed failure with partial Evidence of the completed transfer. An unavailable checksum command or unrecognized checksum output leaves verification explicitly unknown.