refactor(mobile): set permissions on the app by name, without an open first

Merged
TH
18d ago
+83-233

What and why

  • agent-device's settings permission acted only on the app its session was on. To set a permission before the first launch, the engine opened the app first just to put the session on it. That took:

    • a sessionApp field on the surface and on every slot binding;
    • a sessions.list at worker start, to check that the warm-up's session survived;
    • a foreground open before setPermission and before the permissions a launch presets;
    • a retry that matched the refusal text requires an active app in session.
  • agent-device 0.21.21 takes an explicit app (callstack/agent-device#3179, filed from this integration), and 0.21.25 resolves a display name there and refuses an app that isn't installed (callstack/agent-device#3386). This PR bumps agent-device to 0.21.25 for that: on 0.21.22 a target pinned by display name (bundleId: 'Settings', as in the quickstart) would get its permission written for a client literally named Settings. simctl privacy and pm need only the id, not a running app. All of the above is gone:

    client.settings.update({ setting: 'permission', permission, state, ...this.selection(), ...(app === undefined ? {} : { app }) })
    
  • The permission call carries the worker's device selection. Before, the open in front of it bound the session to the device; without that open, a worker with no session (after closeApp, or on a slot whose earlier worker retired) would send the change to the daemon's default device.

  • device.setPermission() acts on the pinned app, as its docs say, not on whichever app the session last opened.

  • The warm-up in prepare still opens the pinned app, so a test that starts without app.open() finds it on screen. Its other reason, that a timed-out open resets the daemon every slot shares, is gone since agent-device 0.21.21 (callstack/agent-device#3177), and the comments say so.

flowchart LR
  subgraph main
    A1[open app, foreground] --> B1[settings permission] --> C1[open --relaunch]
  end
  subgraph this branch
    B2[settings permission --app] --> C2[open --relaunch]
  end

Verified

Ran it locally: yes

  • iOS 27.1 simulator (iPhone 17 Pro), a scratch project on the built packages, target com.apple.Maps. The test runs device.closeApp(), device.setPermission('photos', 'grant'), app.open(), device.closeApp(), device.setPermission('photos', 'deny'). Commands agent-device received for the session, from its events.ndjson:
mainthis branch
boot, session_list, close, open, settings, open, close, open, settingsboot, close, settings, open, close, settings
  • The simulator's TCC.db has kTCCServicePhotos | com.apple.Maps | 0 from the branch's final deny.
  • On 0.21.25, the same simulator with the target pinned by display name (app: { bundleId: 'Maps' }): device.setPermission('photos', 'grant') before the first app.open() wrote kTCCServicePhotos | com.apple.Maps | 2.
  • Not run locally: Android, and the mobile benchmark. This PR's mobile jobs run both.

Checklist

  • Changeset added (pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only.
  • Breaking change: title carries !, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md).
  • Docs updated in the same PR: the docs/**/*.mdx page for the behavior, and skills/e2e/ if the skill describes it.
  • Contract change: emitted .d.ts reviewed, tests/types/sdk-types.ts updated; wire change edits the schema and both fixtures.
  • Engine contract change (e2e/engine): changesets for e2e, @e2e-dev/web, and @e2e-dev/mobile.

DeviceLease loses the optional sessionApp it inherited from SlotBinding. It was never documented, and the lease-to-binding path already dropped it.

🤖 Generated with Claude Code