mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
* 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>
302 lines
23 KiB
Markdown
302 lines
23 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
# 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
|
|
|
|
```sh
|
|
# 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_package`
|
|
admits the first valid `userId` or `appId` as `user_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 in
|
|
`numeric_issues`: the first ten issues with command, field, reason and original
|
|
`raw_line`, plus the exact `total`. Reasons are `invalid_format` or
|
|
`unsafe_integer`. Any issue makes package coverage partial; existing section
|
|
omissions can also make it partial. `version_code` remains exact source text.
|
|
- **Display nulls preserve uncertainty.** `inspect_adb_display` admits positive
|
|
safe integers. Absent or malformed optional values are null; `numeric_issues`
|
|
distinguishes malformed declarations from absence. It keeps the first ten
|
|
issues and exact total, with command, field, original line and reason
|
|
(`invalid_format`, `unsafe_integer` or `nonpositive`). 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 with `details.partial_observation`: an
|
|
`inspect_adb_display` observation from provider `adb`, reason
|
|
`physical_size_absent` or `physical_size_malformed`, and a `result` retaining
|
|
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_lines` and make directory coverage partial.
|
|
- **Inferences carry their basis.** Device `kind` reports
|
|
`kind_basis` (serial prefix or USB transport); APK `role` reports
|
|
`role_basis: file_name`; `emulator_observed` is derived from the build's
|
|
`ro.kernel.qemu` property: `true` only when its value is `"1"`, `false` for
|
|
any other reported value, and `null` when the property is absent. The raw
|
|
property value remains available in `properties`.
|
|
- **Device paths are not host paths.** `base_apk_device_path` values 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](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file),
|
|
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_package`
|
|
return `unreadable_output` with `retryable: false`, keep the `pm path` output
|
|
in `details.captured_output`, and name the refusal in `details.reason`.
|
|
`unsafe_device_path` lists refused entries in
|
|
`details.diagnostics.rejected_paths` as `{device_path, reason}`;
|
|
`duplicate_local_destination` reports the shared `file_name` and both
|
|
`device_paths`; `local_destination_unavailable` reports the `device_path`,
|
|
`file_name`, and filesystem `error_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 path` enumerates the base APK and
|
|
every installed split; pulls cover the whole set. System packages whose
|
|
APKs keep other names report the `unknown` role rather than guessing.
|
|
- **Partial results keep facts.** Failed individual pulls return successful
|
|
artifacts with `coverage: "partial"` and per-file `failures`. Listings
|
|
and package searches that contain unrecognized lines report the first ten
|
|
as `unparsed_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`, and
|
|
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
|
|
pushes check for existing device files before transfer and refuse them
|
|
unless `overwrite` is set. A failed or unexpected existence probe stops
|
|
transfer even with `overwrite`. A completed `test -e` exit 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 in `raw_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 before `adb push` writes it. When available, the device's
|
|
`sha256sum` verifies the transfer. A missing command, output without a
|
|
recognizable digest, or host capacity that prevents the digest check from
|
|
starting reports `device_digest_source: "unavailable"` with an
|
|
unverified-transfer limitation rather than a retryable error; installs
|
|
preserve the
|
|
device's `Failure [...]` 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`, or `monkey`.
|
|
The app/activity lifecycle tools use `am start -W` and `am 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_constraint`
|
|
and names the byte allowance. Deadline expiration, including the
|
|
client-version probe, reports `provider_timeout` and 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 original `adb_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](https://github.com/srmorete/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.
|