fix(daemon,kernel): device-selection safety — identity conflicts and ambiguity fail instead of retargeting

Merged
TH
2mo ago
+536-24

Summary

The device-selection safety cluster: three changes that make "successful response from the wrong device" impossible, rather than only unlikely. All three are admission/resolution policy — evaluated before any interaction dispatch path is selected — so no ADR 0011 guarantee cells move.

The motivating incident is concrete. Driving two attached emulators, I passed --udid emulator-5580, hit a bound-session lock, followed the error's own advice (--session-lock strip), and read three successful snapshots off the other emulator before noticing. Every step reported success.

1. A session-lock identity conflict fails instead of changing device identity. strip resolved every conflict by deleting the offending selector — including --udid/--serial/--device, after which the command ran against the bound device. It now fails under both reject and strip; strip keeps resolving what it exists for (--platform, --target, --ios-simulator-device-set, --android-device-allowlist). The error carries both identities structurally (requestedDevice, boundDevice), and the hint offers the two real recoveries — close the bound session if the requested device is intended, drop the selector if the bound device is — and never mentions strip for an identity conflict, since following that advice is what produced the wrong-device run.

2. Singular resolution refuses to guess. resolveDevice returns exactly one device, so every caller of it needs one concrete device; when the request carried no identity and several candidates were equally preferred, it returned the first by discovery order (alphabetically for Apple). This is now a refusal at the resolver boundary rather than a command-kind allowlist: correctness must not vary by state vs observability, and read-only operations are not safer — a snapshot of the wrong emulator is a wrong answer that looks right.

Established preference tiers are preserved, so the ambiguity is only what survives them equally:

  • virtual over physical, the Apple kind/target rank, then booted over offline;
  • one booted emulator beside offline candidates still resolves;
  • explicit --device/--udid/--serial still resolves, and commands inside an existing session are unambiguous by construction;
  • devices and other multi-device commands never enter singular resolution.

The refusal reuses the declared device-candidate details domain (AMBIGUOUS_MATCH + devices), so the CLI and MCP renderers already print the bounded list, with a hint naming the platform-appropriate selector.

3. Platform-specific device flags are validated before resolution. --platform android --udid emulator-5580 answered No Apple device with UDID emulator-5580 — about a platform the request had explicitly excluded. Both directions now fail as INVALID_ARGS naming the right flag. No flag unification: --udid/--serial/--device encode real platform concepts and are public API.

Validation

  • pnpm check:affected --run green on the pushed head. CI: the 30/30 green run was the previous head (51670b1); on the current head (93abb6e) iOS Smoke failed at the Checkout step and Linux Smoke was cancelled — infrastructure outcomes with no test executed, both re-run rather than claimed as passing.
  • Live, two throwaway emulators beside an unrelated third device (full transcript in the PR comments): a session bound to Pixel 7 lockA refuses a request naming Pixel 7 lockB under both reject and --session-lock strip — the latter is the command that previously returned a successful snapshot of the bound device — with requestedDevice/boundDevice in the structured details; three booted devices with no selector refuse with all three listed in the plain CLI output; --platform android --udid … fails naming --serial; and an explicit --serial still resolves normally (positive control).
  • Non-vacuity: the session-lock identity table has 13 rows crossing fresh vs existing session, matching/conflicting/absent identity, reject vs strip, binding vs inventory command, and Android serial / Android udid / Apple udid, asserting the exact structured details and hint — 7 rows are red against the previous policy. Both selector-validation assertions and the ambiguity refusal are likewise proven red against main.
  • Four existing tests pinned the old strip-an-identity behavior and one pinned the arbitrary cross-platform pick; each was rewritten to its scope-only or refusal form rather than deleted, which is the record of exactly what shipped behavior changed.

Breaking changes

Two, both CHANGELOG'd: --session-lock strip no longer discards a device identity (it fails), and ambiguous singular resolution fails instead of picking. The second can surface on machines with several booted devices or several offline AVDs and no selector — the error lists the candidates with a runnable selector, so recovery is one paste.

Docs: website/docs/docs/commands.md for both behaviors; --session-lock's CLI help now states that strip covers scope selectors only. Skills unchanged — this belongs in CLI help/metadata and runtime errors.