# SSH Android capability proof matrix — stable implementation, review r2

Scope: takeover of [#17239](https://git.cafe/pingdotgg/t3code/pulls/17239), replacement single PR on origin; this artifact does not create or modify a PR. Derived from the entire three-file diff and SshDeviceHost bootstrap/callers. Planning only: no runtime captures, tests, or SSH connections performed by this audit.

Immutable comparison:
- Before/main: [909e4c9482](https://git.cafe/pingdotgg/t3code/commit/909e4c948278afc2b3e1e665075fb1755a13f894).
- Final implementation: [59fdabb266](https://git.cafe/pingdotgg/t3code/commit/59fdabb266c155c56c63866c8fb0cc52f710f9b9).
- Original contribution for comparison: [390ff71aea](https://github.com/Quicksaver/t3code/commit/390ff71aeaf4dfb827ad1e5bffb173ba8dc06cbc).
- Superseded audit head: [045c81e686](https://git.cafe/pingdotgg/t3code/commit/045c81e686e3ef5d54f125e45e349baab14b3e0a). The regular-file fix is now committed in the final head above; no pending implementation head remains for this matrix.

## Affected rows and expected observations

All availability entries refer to the Android platforms entry, not the SSH command exit code: a successful probe can correctly return available:false. Reasons must identify the failed component and selected SDK path where applicable. Before means main, not the original contributor branch. Every SDK tool is an executable regular file unless the row changes that condition. A working adb prints a marker and exits zero; broken adb exits one. The same seeded state must be used for before and after.

| Row | Inputs / isolated seed | Before/main expected | Final-head expected | Observation / minimum evidence |
| --- | --- | --- | --- | --- |
| A: PATH-only adb | No SDK variables or default SDK directories; working adb on fixture PATH (VM adb can be linked here). | available:true from adb version. | available:false, SDK not found. | Actual generated probe over localhost SSH, both revisions; retain JSON and sanitized seed manifest. Required runtime capture. |
| B: adb-only SDK | ANDROID_HOME points to SDK containing only platform-tools/adb; no emulator or avdmanager. | available:true. | available:false, Android Emulator missing, selected SDK named. | Actual SSH probes at both revisions. Required runtime capture. Existing focused regression covers this row. |
| C: complete SDK and selection consistency | ANDROID_HOME unset; ANDROID_SDK_ROOT points to custom complete SDK; both HOME/Library/Android/sdk and HOME/Android/Sdk exist but are empty; fixture PATH also has working fallback adb. | available:true through fallback adb despite selecting empty mac-style default; later ANDROID_HOME is that default and emulator cannot resolve from the selected SDK. | available:true; ANDROID_HOME equals custom SDK; later adb and emulator resolve to that SDK. | Actual probes plus a SECOND independent SSH command with the same environment initialization at each revision. Log selected root, command -v adb/emulator and execute both marker stubs. Required runtime capture. Existing focused selection test checks same-shell emulator resolution; second SSH command closes its operational evidence gap. |
| D: failed selected adb | ANDROID_HOME points to complete SDK; selected platform-tools/adb executable exits one; working fallback adb remains on fixture PATH. | available:false because SDK PATH entry shadows fallback adb. | available:false with selected adb failed-to-run reason. | Actual SSH probes at both revisions; marker log proves selected adb ran and fallback adb did not. Required runtime capture. Existing focused regression covers rejection. |
| E: directory masquerading as tool | Complete SDK except emulator/emulator is a searchable directory; repeat independently for latest/bin/avdmanager directory. | available:true as adb works. | available:false, appropriate missing component. | Focused generated-script tests. Committed regression sets both directories and sees first failure (emulator); avdmanager-only variant is additional branch coverage, not a separate runtime capture requirement. |
| F: missing/non-executable components | Independently remove adb, emulator, avdmanager; independently chmod each regular tool to 0644. PATH fallback adb is present. | Missing/non-executable SDK adb may fall through to PATH adb and report true; other missing tools do not affect true. | available:false, first failing component reason. | Focused generated-script diagnostic cases if further branch evidence is needed; no six-way SSH capture expansion required. Run as non-root for meaningful execute-access denial. |
| G: explicit HOME precedence | ANDROID_HOME=A and ANDROID_SDK_ROOT=B; both complete, distinct markers. Also A incomplete and B complete. | A chosen; working A adb yields true even if other A tools absent. | A chosen; true only when A complete; incomplete A rejected rather than silently choosing B. | Focused shell/probe check; use root/path markers. This is preserved explicit-HOME precedence, not a new fallback search requirement. |
| H: default selection | Variables unset; only mac-style default complete, then only Linux-style default complete. When both exist, mac-style directory wins even if incomplete. | adb-based availability from chosen default. | same deterministic root selection; completeness gates capability. | Focused shell/probe check; no need to execute on macOS to exercise HOME path selection. Actual iOS capability cannot be proved on Linux. |
| I: independent iOS / prerequisites | Linux probe reports iOS unavailable before/after. On macOS with working xcrun, Android incomplete must not remove iOS support. Node >=22 and npm available in noninteractive shell for all captures. | Existing iOS and Node/npm behavior. | unchanged iOS calculation and prerequisite gates. | Source inspection plus focused existing tests; Linux transcript records iOS false only. Do not claim macOS runtime coverage. |
| J: SSH setup regression | Isolated Android Studio JBR path; shell metacharacters/newlines in arguments. | Existing Java discovery and quoting work. | unchanged. | Existing script tests; do not restore lifecycle tests/code removed from current main. |
| K: user guidance | docs/user/devices.md now permits ANDROID_SDK_ROOT and explains adb alone is insufficient. | Older requirements prose. | Matches A–H selection/completeness behavior. | Read prose against implementation. No screenshot, recording, or independent docs runtime capture required. |

## Minimum runtime capture set

One plain-text transcript containing A, B, C, D at before and final revisions: eight actual generated probe invocations over SSH. C additionally needs a separate later SSH session at each revision showing the generated shell setup chooses the same SDK and resolves/executes the chosen adb and emulator. This is a server operational transcript, not a time/interaction video. It does not require starting device hubs, installing npm packages, starting emulators, or connecting to real remote hosts. Stubbed emulator and avdmanager validate capability detection, selection and executable checks, not real emulator boot or an actual avdmanager invocation. State that limitation explicitly.

Focused test evidence must accompany the runtime transcript. Dispatcher reports six script tests and ten total focused tests at the final head; this audit has not rerun or certified that result. Record exact commands, full tested SHA, counts, exit status, and the focused suite names when capturing. At minimum retain adb-only rejection, failed-adb rejection, directory-tool rejection and SDK-selection success results. Tests and actual SSH evidence are complementary; local sh-only execution is not the requested SSH transcript.

## Seeding and capture instructions (not executed)

1. Allocate a fresh directory outside the worktree, e.g. mktemp -d /tmp/t3-17239-proof. Keep drivers, fixture roots, generated scripts, transcripts and seed manifests there. Do not read, copy or write ~/.t3/userdata. Do not run the application server or its database seeding.
2. For each row create a fresh isolated HOME. Launch the remote command through env -i with HOME, controlled PATH, explicitly required SDK variables and a known Node >=22 location. Remote HOME must be selected inside the SSH command, not merely in the SSH client's environment. Include a deterministic npm --version stub (or installed npm) so prerequisite failure cannot mask SDK results. Keep host-wide tool locations out of the fixture wherever possible; log any unavoidable tool resolution.
3. Create adb-only/bin/adb as a link to the VM adb or a marker shell stub; use it as PATH fallback. Complete SDK layout: platform-tools/adb, emulator/emulator, cmdline-tools/latest/bin/avdmanager. Each is a regular chmod 0755 shell script. Use selected-root-specific output/log markers; adb version must exit zero except D. Create both empty default directories for C. Keep PATH fallback adb working in C and D so the before result is deterministic and failed selected adb cannot be masked.
4. Load each revision's actual exported remoteDeviceEnvironment, quoteRemoteArg and remoteDeviceScript(owner, "probe") from revision source, not a hand-written imitation. Obtain committed source with git show into the temporary staging tree, including needed imported device modules/dependencies; do not checkout, edit or rebase the repository. Resolve imports with existing package tooling or a temporary bundler entry; preserve the full generated script and SHA-256 digest in the evidence directory.
5. Match SshDeviceHost bootstrap: SSH remote sh -c with quoteRemoteArg(remoteDeviceEnvironment + Node lookup/exec), feed the actual generated probe on stdin. Place fixture env setup before generated environment initialization. Decode JSON only after preserving raw stdout/stderr and exit code. For C, issue another SSH command applying the same revision's remoteDeviceEnvironment and same fixture inputs; print ANDROID_HOME, command -v adb/emulator and execute markers. Do not append a command after exec node and assume it will run.
6. Use only localhost SSH. Existing localhost SSH must use a temporary UserKnownHostsFile and strict verification; do not alter global SSH config or known_hosts. If unavailable, start an isolated sshd on loopback with temp host/client keys, authorized_keys and known_hosts. Use ssh -F /dev/null, IdentitiesOnly=yes, BatchMode=yes, explicit port/identity and isolated host verification. Never print/upload private keys; a sanitized command may show their temporary filenames. sudo -n is available if daemon startup requires it. Capture only processes you start and retain owned PIDs for cleanup; never kill by pattern. Do not touch real remote hosts.
7. Transcript headers: revision/full SHA, row identifier, sanitized seed/environment manifest, generated-script digest, SSH command with secret material omitted, raw probe output, exit status, parsed expectations and PASS/FAIL. C must show both SSH sessions in sequence. Do not dump ambient env or authentication settings; auth config is transport setup, not a changed surface in this diff.
8. Stop every owned process using captured PIDs, remove temporary private keys and fixtures after preserving sanitized evidence. Upload only sanitized transcript/matrix with ghdrop upload -m from outside the worktree. Never commit scratch, seed drivers, keys or PR evidence. Review artifact contents before public upload.

## Surface decision

No changed web/desktop/mobile render paths, contracts, provider adapters or UI entry points. Shared SSH server behavior changes what existing clients receive as host platform availability; no browser, screenshot, viewport, theme or diff-image matrix applies. No UI interaction exists, so no timing or interaction recording requirement. Remote/relay/tunnel clients use the same server-side SSH probe; the evidence isolates its host boundary. No capture requirement for docs prose; runtime server behavior still requires the transcript override above.

## Audit status / friction

Evidence plan ready against final 59fdabb266c155c56c63866c8fb0cc52f710f9b9. No captures taken. No repo modifications, pushes, reviews/comments, watchers or remote-host operations performed by this audit.

FRICTION: none

Provenance update by lead: main merged by merge commit 59fdabb266; the three takeover file blobs are identical to reviewed d991fdc8c3. Matrix rows and requirements unchanged; only before/after refs updated to the final integration.
