Files
callstack__agent-device/CHANGELOG.md
T
Michał Pierzchała 315a0ac679 fix(daemon): fail closed when the auth hook is silent about tenant (#2104)
An auth hook that ran but returned no tenantId opted the deployment into
tenant attestation; falling back to the client's own claim (RPC body
meta.tenantId, aux-route x-agent-device-tenant header) let a holder of one
valid shared token impersonate any tenant on /rpc and on the diagnostics/
upload/download routes. resolveTrustedTenant() in the new
src/daemon/server/tenant-trust.ts is now the single seam both surfaces go
through and the only place that computes the resulting identity: hook
attests -> use it; no hook configured -> keep today's client-declared
behavior (loopback/dev unchanged); hook configured but silent with a
client-declared tenant -> refuse (401) instead of trusting the claim, and
no raw client-declared metadata survives into the dispatched request in
that case either.

Fixes #2095
2026-08-28 11:50:02 +02:00

33 KiB

Changelog

Unreleased

  • Security (daemon, remote/proxy HTTP only): when AGENT_DEVICE_HTTP_AUTH_HOOK is configured and a request's hook result does not attest a tenantId, the request is now refused (401) outright — the daemon no longer runs it as whichever tenant the client declared (RPC body meta.tenantId or flags.tenant, or the x-agent-device-tenant header on the upload/artifact-download/diagnostics routes) and no longer admits it unscoped when the client declares nothing either. This closes both a shared-token impersonation path and an unscoped-access path to tenant-owned sessions/artifacts in multi-tenant deployments. Deployments with no hook configured (the local loopback CLI) are unaffected. A hook must attest tenantId on every request it wants the daemon to admit.
  • Breaking (0.21): removed aggregate performance compatibility (perf, perf sample, perf metrics, the metrics alias, optionless client.observability.perf(), and SDK area: 'metrics'). Use perf frames, perf memory sample, perf cpu profile start|stop|report, or perf trace start|stop; removed CLI and raw daemon forms fail with this migration guidance.
  • Breaking (0.21): removed legacy batch JSON steps with positionals/flags. Use {"command":"...","input":{...}}; rejected steps now include a concrete structured example.
  • Breaking (0.21): removed the deprecated Node client command.rotate wrapper and its RotateCommandOptions / RotateCommandResult exports. Use command.orientation; the already-removed CLI rotate form keeps its targeted migration error.
  • Security (MCP/AI-SDK tool surface): the operator-owned endpoint and path inputs — daemonBaseUrl, the Metro proxyBaseUrl, stateDir, cwd, iosSimulatorDeviceSet, iosXctestrunFile, iosXctestDerivedDataPath, iosXctestEnvDir — follow the credential inputs off the model-writable tool surface: no longer advertised, refused as explicit input with guidance, and resolved from env/config only (a model-writable daemonBaseUrl/proxyBaseUrl would redirect the env-resolved token to an arbitrary server). Dropping these plus the credential fields shrinks tools/list by roughly half. CLI flags and the SDK client options are unchanged.
  • MCP tool descriptions now declare their enforced client timeout envelope (90s default, 180s install, 300s+ lease allocation, unbounded only for the streaming test runner), sourced from the descriptor registry's timeout policy so the declared number cannot drift from the enforced one.
  • Security (MCP/AI-SDK tool surface): the shared command-tool executor now enforces the advertised tool schema as an admission boundary — every raw tools/call argument must appear in the tool's advertised (additionalProperties: false) schema, or it is refused before config/env resolution. Hiding a key from tools/list alone was insufficient: the router forwards raw arguments verbatim and the MCP config resolver read config/remoteConfig as CLI flags, so a model-supplied config file could load daemonBaseUrl/daemonAuthToken and redirect the operator's token to an arbitrary endpoint. Deny-by-default closes that, the operator keys, and any unknown key at once; operator env/config defaults still resolve (they never arrive as tool input). Retired keys are still admitted so their migration guidance answers.
  • Security (MCP/AI-SDK tool surface): daemonAuthToken and the Metro bearerToken are no longer advertised as tool input properties, and an explicit value is refused with guidance instead of being forwarded. Credentials are operator-owned: set AGENT_DEVICE_DAEMON_AUTH_TOKEN (or daemonAuthToken in ~/.agent-device/config.json) and AGENT_DEVICE_METRO_BEARER_TOKEN on the process serving the tools. The model both reads untrusted app UI text and picks tool arguments, so a model-writable credential parameter was a prompt-injection exfiltration path. CLI flags (--daemon-auth-token, --bearer-token) and env/config resolution are unchanged.
  • Release hygiene: after npm publish, release:mark-dev moves main to the next patch with a -dev prerelease marker so the version on main never equals a published version (registry scanners diff the tool surface per version string, and a moving surface under a released number reads as a republish). release:prepare refuses to publish while the -dev marker is in place.
  • Windows --platform web works again. agent-device web setup no longer fails with npm not found in PATH, and every web command — including web doctor — no longer fails with spawn EINVAL. The managed agent-browser backend is now launched as node <js-entry> on every platform instead of through its node_modules/.bin console shim, which is a .cmd file on Windows that child_process.spawn refuses without a shell (CVE-2024-27980 hardening); shell: true would only trade that for argument-quoting hazards and a DEP0190 warning on every command. Setup spawns npm from PATH unchanged on macOS and Linux, and runs npm's own npm-cli.js under the current Node only on Windows, where a bare npm is not spawnable. A managed install now counts as present only when the backend package itself is, and web setup --json / web doctor --json gain entryScript and packageDir; the published binaryPath is unchanged and still names npm's console shim, now informational rather than the spawned command (#2022).
  • Parameterized fill --record-as protection is now recording-session-scoped instead of fill-step-scoped (ADR 0017 amendment): a later, unrelated recorded action (wait, is, get) can no longer re-serialize an app-rendered echo of an already-parameterized value into its own result or target-v1 identity evidence. An echoing wait landmark no longer qualifies as an ADR 0016 destination guard, so session save-script refuses it and directs the author to a stable landmark instead of silently publishing the secret. The protection uses one small, explicit, ephemeral, never-serialized per-session map populated only from values the author already opted to parameterize; ordinary non-parameterized recordings are unaffected (#1398).
  • Android covered-state publication now has one owner: same-window surfaces remain visible for diagnosis, while the daemon marks exactly ordered covered controls non-actionable with interactionBlocked: "covered". Helper-only drawing-order stays private rather than entering the snapshot contract, sparse floating overlays remain usable, and API 23 fails conservative with the existing androidSnapshot.occlusionScanUnavailable disclosure (#1832).
  • iOS regular snapshot --depth now measures depth after structural accessibility wrappers collapse. The recursive-tree backend follows a bounded presented-depth frontier, so controls that fit the requested regular depth are no longer lost behind raw wrappers; raw --depth remains a traversal-depth limit. Flat query recovery is limited to one presented level, and private AX does not claim deeper regular-depth completeness until it has a hierarchy-aware frontier (#1797).
  • iOS regular snapshot nodes now publish presentation-owned effective geometry through the existing rect field: backend-reported frames remain available to acquisition, while regular output uses the viewport and declared scroll-clip intersection. Raw snapshots and direct element reads retain reported geometry (#1797).
  • Android recording-session commands now warn when blocking-dialog readiness inspection fails open. The requested command still runs, but the successful response discloses that readiness could not be inspected and preserves the inspection error's actionable hint, including pnpm build:android when the snapshot helper is unavailable (#1895).
  • Breaking (device selection): when a command needs one concrete device, carries no --device/--udid/--serial, and more than one candidate is equally preferred, agent-device now refuses with AMBIGUOUS_MATCH and the candidate list instead of quietly picking one. Established preferences are unchanged — virtual over physical, booted over offline, and the Apple kind/target ranking — so a single booted emulator beside offline ones still resolves, an existing session binding still resolves (its identity is already fixed), and explicit selectors still resolve. What no longer happens is choosing between two equally booted devices by discovery order or alphabetically: that produced a successful response describing a device the caller never selected, and reads are no safer than writes there. devices and other genuinely multi-device commands never enter singular resolution and are unaffected. The error carries the bounded candidate list in the declared devices details domain, so CLI and MCP print it, with a hint naming the right selector for the platform (--serial for Android/HarmonyOS, --udid for Apple, or --device "<name>").
  • --udid with --platform android (and --serial with an Apple platform) now fails as the flag mistake it is — INVALID_ARGS naming the right flag — instead of reaching device resolution and answering No Apple device with UDID emulator-5580 for an explicitly Android request. --udid addresses Apple devices, --serial addresses Android and HarmonyOS; matching pairs and requests that name no platform are unchanged.
  • Breaking (--session-lock strip): a device selector that names a different device than the bound session is no longer silently discarded. strip exists to drop redundant platform/scope selectors; when it also dropped --udid/--serial/--device, the command kept running against the bound device instead of the one the caller named — a wrong-device action that looks like a success. Such a request now fails with INVALID_ARGS under both reject and strip, and the error carries the two identities structurally (requestedDevice, boundDevice) plus a hint offering the two real recoveries: close the bound session if the requested device is intended, or remove the selector if the bound device is. The hint no longer suggests --session-lock strip for an identity conflict, since following that advice is what produced the wrong-device run. Scope-only stripping (--platform, --target, --ios-simulator-device-set, --android-device-allowlist) is unchanged.
  • iOS regular snapshots now apply one backend-neutral eligibility rule after every capture backend: a node survives when its accessibility type is interactive or it carries a non-empty label, identifier, or value. This removes the tree backend's extra "hittable non-Other" membership path and drops unlabeled decorative nodes consistently; labeled images, identifier-only nodes, and value-only nodes still survive. Raw snapshot membership is unchanged.
  • iOS regular snapshots now run one shared clip fold inside presentation for every capture backend (#1797). Backends serialize reported facts -- every traversed node, at raw traversal depth -- and presentation alone decides what the viewport and scroll clips hide, books the scroll hints, and collapses depth; no backend carries its own copy of that interpretation anymore (the copies are what produced the scroll-overflow leak class, #1784). Three intentional edge deltas ride along, all in the direction of one backend-neutral rule: sub-pixel content-free decorations are now dropped by every backend (previously private-AX only); labeled offscreen Application/Window carriers now survive on every backend (previously tree only), still never hittable; and a query-sweep recovery snapshot without -i no longer lists offscreen elements. Nothing outside its clip, and nothing without geometry, is ever hittable in a regular snapshot, whatever the backend reported.
  • iOS snapshot --raw is now the acquired accessibility tree on every backend that can serve it (#1797). A raw request that recovered onto the private-AX backend — the route an app whose XCTest tree capture fails takes — returned the regular projection's viewport-pruned nodes labeled raw: everything scrolled out of the viewport, and every sub-pixel decoration, was missing from the one view whose purpose is showing what the pruned view hid. Raw now keeps every node the backend serialized, at traversal depth, and --depth still narrows it (for raw, presented depth is traversal depth). Two structural rules replace the hand-synchronized ones: the raw capture plan is derived from each backend's declared ability to serve raw, so the interactive query sweep — which has no hierarchy to return — cannot be planned for a raw request; and presentation refuses an acquisition captured for the other projection instead of relabeling it, dropping that tier with a structured failure. Breaking in the same direction: snapshot --raw -i now returns the acquired tree instead of an interactive-filtered one — -i narrows the regular projection, and the pair used to produce a third membership rule that differed per backend. Regular and -i output is unchanged. Backends now read one derived capture hint rather than the request itself, so what a capture is allowed to skip is stated once, next to the proof that skipping it keeps the projection complete.
  • Android snapshot --raw is now the acquired accessibility tree (#1832 C3): the regular-projection classifiers for nodes Android marks invisible and stale application windows no longer run at parse time, so --raw keeps everything the helper serialized (normalization only). Covered same-window surfaces are publication annotations rather than membership pruning. Also: Android blocking-dialog recovery now reads the same daemon presentation an agent's snapshot sees instead of a hand-rolled subset, and acts on its occlusion result — a stale "App isn't responding" surface left under the foreground one no longer triggers recovery, and a covered "Close app" is never tapped ahead of the visible one; the Android freshness route signature no longer keys on role/selected, fields the Android backend never carries; and the Android helper's declared fidelity residues (no checked/checkable/selected/long-clickable, 5000-node cap before scoping, API-level cache-reset divergence) are recorded in CONTEXT.md.
  • agent-device mcp now carries its own usage guidance, so MCP-only clients (Codex CLI, Cursor, custom agents) no longer depend on a separately installed skill (#1833). The handshake instructions — returned by both server/discover and, newly, the legacy initialize — is a compact (< 2 KB, the Claude Code truncation limit) workflow card: start with open {app, foreground: true} instead of probing, act with settle: true and continue from the diff, verify with wait/is/get/find, copy @refs byte-for-byte, recover from sparse/AX-unavailable, follow error hints, close. A new MCP-only help tool serves the full guides on demand: no topic returns the CLI's decision card; topic returns agent-device help <topic|command> verbatim (workflow, gestures, scripting, tv, macos, web, remote, debugging, …, or any tool name for its complete flag reference), prefixed with the one-line CLI→tool-property mapping. help is router-owned rather than a command descriptor, so it appears in tools/list only — not in the CLI, Node client, or batch — and its description tells the model it is not a startup step. Legacy initialize gains the optional instructions field; no other legacy field changes.
  • Android snapshot --scope (and every selector command's --scope, e.g. press "Save" --scope Panel) now resolves scope exactly once, inside the Android projection, under the shared scope specification: the scope root is the first node in document order whose label, value, or identifier contains the scope text (case-insensitive) and whose subtree still has content in the projection you asked for, the result is that subtree re-rooted at depth 0, and no match returns an empty snapshot (#1832). That second clause is what makes snapshot -i --scope panel return the button inside a structural container -i drops, and stops a decorative heading that happens to match from emptying the snapshot. Before, Android ran two passes with contradictory rules — a breadth-first platform match that fell back to the full tree on a miss, then the daemon's document-order pass — so a shallower later container could win over an earlier match, and an interaction capture whose scope reached only the daemon layer was silently unscoped. --depth under --scope counts from the scope root, filtering the depths the response prints (a node shown at depth 0 is never hidden by --depth 0), and ancestor context above the scope root (a clickable row, a list) still shapes -i membership inside it. The rule is pinned by contracts/fixtures/snapshot-scope-policy.json, the same golden table the iOS runner consumes (#1797).
  • New hover <x y|@ref|selector> command for --platform web (#1783). It moves the pointer over the target without pressing, so hover-gated UI — a message row's ... toolbar, a menu that opens on pointer enter — becomes reachable through agent-device the way it already was through the underlying agent-browser backend (mouse move). It is a member of the targeted-touch family: same @ref/selector/coordinate targeting, occlusion and off-screen guards, and --settle (the settled diff carries the revealed controls with fresh refs, e.g. + @e4 [button] "Delete"), but no --verify, since hover reveals rather than activates. hover @ref publishes as a portable selector line in recorded scripts, and the Node client exposes interactions.hover. Hover is a pointer state that touch platforms do not have, so capabilities advertises it on web only and iOS/Android/Linux reject it during admission with UNSUPPORTED_OPERATION and a hint naming --platform web; longpress remains the mobile hold-gesture verb.
  • Internal: session recording is now derived from the script-publication lifecycle instead of being stored beside it. SessionState.recordSession is removed; isRecordingPublication answers the question from the aggregate (ordinary authoring records only while ARMED, a repair transaction records for its whole lifetime). The stored flag was a second source of truth that handler surfaces set directly, which is how #1533's aborted-recording drift arose — no surface can now arm recording without moving the lifecycle that authorizes it, and the script writer's publication gate is answered entirely by the aggregate. Behavior-preserving: the derivation reproduces what the flag held at every transition.
  • Fixed: a script recording aborted by a second open is no longer published by a later bare close (#1533). open <app> --save-script followed by a second successful open terminates the recording and warns "Script publication was aborted…", and close --save-script correctly refuses it with "Retry with plain close; it will tear down the session without writing." But when that second open itself carried --save-script, the flag re-armed recording behind the terminal status, and a bare close then wrote the full session log to disk — publishing a recording the caller had been told was aborted, and breaking the promise the refusal makes. An aborted authoring lifecycle is now terminal by construction: --save-script arms nothing on any surface that handles it — the re-open builder, the close finalizer, and the recorded-action ingress — and the script writer refuses the lifecycle from every path that reaches it (bare close, teardown, idle-reap, active publication). This also stops an aborted session from paying recording-time costs it can never publish: a re-opened aborted recording no longer keeps the direct iOS selector fast paths for click and get disabled. Armed recordings, published recordings, and every repair transaction are unaffected.
  • agent-device mcp now serves the stateless MCP 2026-07-28 revision alongside the handshake-based revisions it already spoke, as the spec's "dual-era server". Modern clients probe server/discover, which advertises the supported revisions, the tools capability, and server identity; their requests declare a protocol version in _meta, and their results carry resultType: "complete" plus _meta["io.modelcontextprotocol/serverInfo"]. tools/list and server/discover now return the ttlMs/cacheScope cache hints, so a client can cache the 55-tool, ~223KB tool list for an hour instead of re-fetching it on every start; the list was already emitted in a deterministic (sorted) order, which is the other half of what makes it cacheable. Each revision is answered on its own wire contract: a request declaring 2025-11-25 or 2025-06-18 through modern framing still gets the legacy result shape, and initialize never agrees to 2026-07-28, which has no handshake to establish. A declared revision this server does not implement is rejected with UnsupportedProtocolVersionError (-32022) naming the ones it does, rather than being served under a version the client did not ask for, and modern framing that omits its required protocolVersion/clientCapabilities metadata — or supplies a clientInfo that is not a valid Implementation — is rejected as invalid params. initialize and ping were removed in 2026-07-28, so a modern-framed call to either is answered -32601 rather than served inside a resultType: "complete" envelope. Responses to legacy clients are unchanged byte-for-byte — initialize and ping are still served, and no cache, resultType, or _meta field is added to their results. Nothing here affects the CLI, Node, or daemon surfaces: the stdio transport, the tool set, and every tool's input/output schema are untouched.
  • Fixed: the MCP initialize handshake now answers with the protocol revision the client requested when it is one this server implements, instead of always answering 2025-11-25. A client pinned to 2025-06-18 was told to speak a revision it had not asked for, which the lifecycle contract answers by disconnecting.
  • agent-device help workflow is now a compact ~8KB card instead of a ~41KB dump; the same depth still exists, split into help scripting (save-script, secret-safe fills, batch JSON, replay divergence/repair, recording) and help gestures (multi-touch shapes and platform quirks), plus a few paragraphs folded into the topics that already owned the subject (help debugging, help physical-device, help validate). Every help <topic> first line is now agent-device <version> — <topic> so an agent can read the installed version from its mandatory first help read instead of a separate agent-device --version call.
  • Cloud iOS (BrowserStack, AWS Device Farm): snapshot and diff no longer fail with SESSION_NOT_FOUND on a live provider session (#1658). The app-session guard they ran belongs to the local XCUITest runner, which must attach to a target app; a cloud capture reads the provider's own driver session and needs no app identity, so the guard now applies to local Apple targets only. Relatedly, a cloud iOS open com.example.app now records that bundle id on the session — the provider path skips local app resolution (no simctl/devicectl reaches a hosted device), and used to drop an explicitly spelled bundle id along with it, leaving the session with no app identity at all. Opening a second bundle id replaces the first, matching the local path, where an explicitly spelled target always wins over the session's current app; deep links, display names, and bare open still keep the app already tracked.
  • Cloud fill (BrowserStack, AWS Device Farm) now witnesses that the field it tapped actually holds text-entry focus before sending its keys, instead of dispatching tap and keys in back-to-back requests (#1658). A WebView input — an OAuth/SSO page in a Safari view controller, for example — takes first responder asynchronously, so the keys used to land with nothing focused while fill still answered "Filled N chars"; tapping and filling as two separate commands worked only because the round trip between them gave the field time to focus. The witness is the focused element's own geometry: fill polls the active element and proceeds only once it contains the point it tapped, which is the one signal that identifies which field took focus. Keyboard visibility cannot — it reads the same before and after a second fill into an already-open form, so it could not tell a focused password field from the email field the previous fill left focused. The response discloses textEntryReadiness: focused-element, or keyboard-shown when the driver has no active-element route but the keyboard rose from hidden after the tap. Both describe a fill that witnessed focus before typing; there is deliberately no value for typing without evidence, because nothing renders this field and such a value would reach a caller as an ordinary success. Breaking: when focus cannot be witnessed, cloud fill now FAILS with COMMAND_FAILED / text_entry_focus_not_observed and sends no keys, instead of typing into whatever holds first responder and answering "Filled N chars" — a fill with no witness must not read as a filled field. That covers a tap that focused nothing, a keyboard already up on a driver that cannot name the focused field, and a driver that reports neither (text_entry_focus_unobservable, which points at press + type as the deliberate way to enter text unwitnessed). Only a positively classified unimplemented route counts as unsupported, so a dead session, an auth rejection, or a grid outage surfaces instead of degrading into a blind text entry.
  • Changed mutating selector ambiguity semantics (press/click/fill/longpress): duplicate accessibility wrappers collapse only when every match forms one ancestor-descendant chain resolving to the same actionable node. Matches in distinct subtrees now fail fast with AMBIGUOUS_MATCH and a bounded, immediately reusable candidate-ref frame; visible/depth/area geometry no longer silently picks a mutation target. The direct iOS XCTest path now counts raw exact matches before hittability and delegates ambiguity to the same runtime rule. AppControlBench provenance: element-14 ran on 0.20.5; this change is intended for 0.20.7+, and comparative benchmark reports should note that it can replace a wrong-success recovery loop with one candidate-pick turn while occasionally adding that turn for genuinely distinct duplicates.
  • scroll and back now accept --settle (with --settle-quiet and --timeout), collapsing scroll-then-observe and back-then-observe into one call (#1638). The response carries the same settled payload the touch commands return — verdict, changed-lines diff with fresh refs on added lines, the unchanged-interactive tail, and refsGeneration when the settled tree was stored — and is best-effort: it never fails the action. One difference is deliberate: scroll/back resolve no element, so the diff baseline is the session's stored pre-action tree ("the last tree you observed") rather than a freshly resolved pre-action capture. Both commands now also preserve the daemon on timeout, like the other settle-capable commands.
  • Security: repository ./agent-device.json now accepts only project-safe automation defaults. It rejects daemon endpoint/auth/transport/server settings, tenant/run/lease selectors, provider/cloud and Metro connection fields, headers, executable reporter modules, local write destinations, and other operator-controlled values before local module loading or any daemon health/RPC request. Put remote endpoint and token together in protected CI environment variables, user config, an explicit --config file, or the existing connect/--remote-config workflow. Daemon auth tokens no longer travel in serialized command flags.
  • viewport is now rejected during capability admission on Apple targets instead of reaching the device and failing inside dispatch. No Apple backend can resize a screen — simulator and device geometry is fixed by the selected device type — so viewport on iOS/iPadOS/tvOS/macOS now fails with UNSUPPORTED_OPERATION, viewport is not supported on this device, and a hint pointing at --platform web and at picking a different simulator. capabilities no longer advertises viewport on Apple targets. Web viewport resizing (agent-device viewport 1280 900 --platform web) is unchanged, and Android was already denied.
  • --save-script is now accepted only by the commands that declare it — open, close, and replay. A hand-built daemon request (or a batch step) that set saveScript on any other command, such as record or trace, used to arm script publication and could write a .ad artifact; it is now rejected with INVALID_ARGS before the request reaches admission, the device, or any handler. CLI, Node, and MCP usage of --save-script on its documented commands is unchanged.
  • diff screenshot no longer runs the retired best-effort OCR and non-text analyzers. Their optional ocr and nonTextDeltas fields remain in the result type for source compatibility but are no longer emitted; use the baseline/current images and diff artifact with vision for qualitative interpretation.
  • Breaking: removed the deprecated --session-locked and --session-lock-conflicts flags. Use --session-lock reject|strip instead; passing either old flag now fails with Unknown flag: ... Use --session-lock reject|strip instead.
  • Breaking: removed the replay export --format flag. replay export always writes Maestro YAML.
  • Breaking: removed the unused LeaseAllocatePayload, LeaseHeartbeatPayload, and LeaseReleasePayload type exports from agent-device/contracts. Lease request metadata is fully described by DaemonRequestMeta.
  • Maestro compat: assertVisible and assertNotVisible now accept childOf for ancestor scoping, matching tapOn (#1294).
  • Breaking: removed deprecated gesture duration and rotate velocity inputs (#1218).
    • swipe x1 y1 x2 y2 no longer accepts a trailing durationMs positional; use gesture pan x1 y1 (x2-x1) (y2-y1) durationMs for deliberate timed drags.
    • Maestro swipe operations with a duration continue to normalize to gesture pan with the endpoint-hold execution profile, preserving the Maestro-compatible fast-swipe-then-hold behavior on iOS.
    • gesture fling direction x y no longer accepts a trailing durationMs positional; use gesture pan for timed movement.
    • gesture swipe preset no longer accepts a trailing durationMs positional; use gesture pan for timed movement.
    • gesture rotate degrees [x] [y] no longer accepts a trailing velocity positional; rotation pacing is derived from degrees.
    • MCP/Node schemas no longer advertise velocity or durationMs on swipe/fling/gesture swipe; durationMs remains on gesture pan and gesture transform.
    • A .ad script that still carries a removed positional now fails when the script is parsed, before the replay executes any device action, naming the line and its rewrite (for example swipe accepts 4 arguments: x1 y1 x2 y2 (line 6). The trailing durationMs positional was removed: use "gesture pan 197 650 0 -350 300" ...). Previously the script ran up to that step and then failed as a replay divergence.
    • Published the gesture migration guide covering CLI, Node.js, MCP, and saved .ad recordings, plus the deprecation policy the next such removal follows (#1216).
    • replay export now writes an explicit duration: 100 for swipe — the canonical fling duration — instead of omitting it and letting Maestro apply its own 400ms default. Maestro flows replayed by agent-device are unaffected; a timed Maestro swipe still normalizes to gesture pan with the endpoint-hold profile.
  • Breaking: the deprecated rotate CLI command alias has been removed. Use orientation instead; invoking rotate now fails with rotate was renamed to orientation; for the two-finger gesture use: gesture rotate.
  • Breaking (ADR 0014, session ref-frame lifetime): a mutation through an @ref now expires the session's ref frame, so a later ref mutation without a fresh observation fails closed with a typed details.reason (ref_frame_expired, ref_generation_mismatch, plain_ref_requires_complete_frame, or ref_not_issued) instead of acting on a possibly-navigated screen. A ref-oriented sequence that performs several mutations must re-snapshot between them, consume an honestly issued --settle ref in pinned @eN~s<gen> form, or use selectors. Enforcement applies on every platform, not just iOS. Legacy hand-written .ad scripts that reuse several bare refs from one snapshot must capture between mutations or use selectors.
  • Ref reads resolve against the authorized ref frame's source tree, so an internal read-only capture (including Android freshness) can no longer retarget an admitted @ref by positional coincidence. Read-only ref consumers keep the structured staleness warning while the frame retains the ref's evidence.

0.15.0

  • Breaking: apps discovery and public app-list helpers now default to user-installed apps. Use --all or filter: 'all' to include system/OEM apps.
  • Breaking: removed the agent-device/android-apps public subpath. Use the Android app helpers from agent-device/android-adb.
  • Breaking: removed the agent-device/daemon public subpath. Use agent-device/contracts for daemon request/response types.
  • Breaking: removed public local ADB bypass/selection helpers such as spawnAndroidAdbBySerial and resolveAndroidAdbProvider; use createLocalAndroidAdbProvider(device) or pass providers directly to the helpers from agent-device/android-adb.
  • Added Android ADB provider helpers for exec, stream, clipboard, keyboard, app lifecycle, logcat, and port reverse workflows.