Skip to content

Proposal: pull a real device's app data container and restore it into a simulator (app-data export/import) #2953

Description

@thymikee

Purpose

Xcode 27 Device Hub can pull a real device's app data container and restore it into a simulator. The high-value use case: a tester hits a bug that only reproduces with their data, and a developer loads that exact app state locally instead of guessing at which combination of cache rows, feature flags, locale, and settings caused it.

This issue documents research and a feasibility prototype for the equivalent in agent-device as a typed API/CLI, plus an honest cost estimate. Recommendation: park for now — no one has requested it and the shipping cost is ~2x a reasonable slice. Revisit on demand, or when an app-data/artifact command family exists for a new operation to ride alongside.

Prototype and evidence

Prototype: scripts/prototypes/app-data-transfer.mjs on branch t3code/device-data-restore-research (commit 08b52df45, https://github.com/callstack/agent-device/tree/t3code/device-data-restore-research). Throwaway, self-contained (execFile + node:fs only, per the plain-.mjs rule), not product code.

Validated on the Xcode 27.1 beta seed:

  1. Full export -> wipe -> import loop passes through the prototype's own code path against a booted simulator, with a sentinel file verified present after restore.

  2. Interchange format should be a plain directory, not .xcappdata. simctl get_app_container <sim> <bundle> data + recursive copy round-trips reliably. simctl install_app_data requires Xcode's undocumented AppDataInfo.plist schema; probing it produced The data package was not valid. (AppDataInfo.plist exists = 0, AppData exists = 1, AppData is directory = 1), then The app data package corresponds to an app that isn't currently installed once a plist was added. Public reports describe the same fragility device->sim. Standardizing on a directory sidesteps it.

  3. Device-side pull is one call each way, with the flag surface verified on this seed:

    xcrun devicectl device copy from --device <id> \
      --domain-type appDataContainer --domain-identifier <bundleId> \
      --source . --destination <dir> -r true
    

    copy to accepts the same appDataContainer / appGroupDataContainer domains, so restore onto a physical device is symmetric.

  4. The hard Apple plumbing is already vendored here, currently aimed at narrow internal jobs:

    • packages/platform-apple/src/core/physical-device-files.ts:8 — copyCoreDeviceRunnerFile already runs devicectl device copy from --domain-type appDataContainer --domain-identifier <bundleId> (reached from core/physical-device-screenshot.ts:43).
    • packages/platform-apple/src/core/app-settings.ts:204 — simctl get_app_container <sim> <bundleId> data, then host-side directory enumeration and removal, for clearing app state.
    • Physical-vs-simulator routing already resolved in core/physical-device-routing.ts.

Why no existing CLI already covers it

Tool Device -> host host -> simulator Agent/CI drivable One-shot device->sim
Xcode Devices / Device Hub 27 Download Container Upload Container / drag no (GUI) GUI only
devicectl device copy from --domain-type appDataContainer copy to targets another device, not a sim yes no
simctl n/a (sim only) get_app_container data + copy, or install_app_data yes no
iMazing / Burrow one click one click no (GUI) GUI only

The blocks exist and are Apple's. The headless command that crosses device -> simulator, and that an agent or CI job can drive, does not exist anywhere. That is the only gap worth filling — and it is the gap agent-device is chartered for.

Cost: the per-operation registration tax

The core logic is roughly 150 LOC. What costs real money is registering a dispatched runtime operation: registry completeness requires it to be declared and fact-classified in every place that admits device runtime use.

Measured by the closest comparable single operation (sendPushNotification), which appears across roughly 18 production files: contracts operations interface and defineUse plan, canonical op-name list, registry descriptor, unavailable-operation map, src/platform-runtime-managed-owner.ts, packages/platform-apple/src/runner-demand.ts, a daemon handler, the CLI facet, the published client, and an available-or-typed-unavailable fact in each of platform-apple, platform-android, platform-harmonyos, platform-web, provider-webdriver and provider-limrun.

Estimated production LOC for app-data pull + app-data restore as two operations:

Site ~LOC
Apple core, generalizing the two existing helpers 130-160
CLI facets: reader/writer/metadata/schema x2 (management/push.ts is 123 for two commands) ~120
Daemon handler (analog src/daemon/handlers/session-app-deployment.ts = 248) ~70
Contracts options/input/result/operations interface (analog 95 + 16) ~55
Registry descriptors x2 + flag definitions (packages/command-registry/src/registry.ts) ~80
Per-platform/provider op classification, 2 ops x ~8 modules ~60
Client method, client-types, facades, versioned help ~70
Production total ~585-615

Mirrored tests add roughly 400-700 more. There is no legitimate sub-300 path: shelling out directly instead of going through the runtime gateway is exactly the reach-around-the-dispatch pattern AGENTS.md forbids, and it would lose daemon, remote-transport, and typed-fact consistency. The leanest defensible slice — app-data restore only, directory format, reusing the existing pull helper — still pays registry + runtime-use + platform classification + help + client and lands ~350-450 production.

API sketch (if picked up)

Two single-device commands plus optional cross-target sugar. Both fit the one-session-one-device model; the cross-target transfer deliberately does not, so it should be a client composition rather than a session command (compare install-from-source, which is intentionally not session-scoped).

agent-device app-data pull    <app> --out ./case            # device or simulator source
agent-device app-data restore <app> --from ./case           # import target
agent-device app-data transfer <app> --from <id> --to <id>  # optional: client-side composition
client.apps.pullData({ app, output?, container?: 'data' | 'groups' });
client.apps.restoreData({ app, input });

Design commitments the prototype establishes:

  • Interchange artifact is a plain directory tree of the data container. Treat .xcappdata as an accepted input only if a consumer asks, and never as our output format.
  • Close the app before restore, require the app to be already installed under the same bundle id, and report a typed reason when the target has no data container yet.
  • Physical-device pull requires a dev-signed / debuggable build (get-task-allow) or a shared App Group with --domain-type appGroupDataContainer. Release and TestFlight builds are not extractable; Device Hub shares this limit. Fail with that explanation rather than a generic COMMAND_FAILED.
  • Non-transferrable state must be documented, not discovered: Keychain and Secure Enclave items are hardware- or device-bound, file-protection-class data may not decrypt, and apps that cached absolute container paths will not follow the move. The realistic win is Documents, Library/Preferences, Application Support/SQLite and caches.
  • Containers carry tokens, account rows, and locations. Publish exports as a session artifact path under src/daemon/session-artifact-paths.ts conventions, warn on pull and restore, default to a temp directory with cleanup, and never commit a real container.
  • Scope to Apple. adb backup is deprecated and unreliable, so report typed-unavailable on Android/HarmonyOS rather than faking parity.

Completion conditions (if implemented)

  • app-data pull from a physical device and from a simulator both produce a directory tree that a later app-data restore accepts.
  • app-data restore into a booted simulator reproduces the exported state, proven by an on-device observation (a snapshot or app-visible value), not by exit code.
  • Both paths reach the Apple runtime through a declared runtime use admitted by fact, with typed-unavailable facts elsewhere; registry completeness passes.
  • A release-build or non-installed-app source fails with the debuggable-build / install-first hint.
  • Versioned help and user docs describe the non-transferrable state and the PII handling.

Dependencies and sequencing

  • No blocker, but the cost drops materially if an app-data or session-artifact command family already exists, since a third operation on an existing surface is far cheaper than standing up a new family.
  • Live device verification requires a cabled, dev-signed test app; see docs/agents/device-verification.md.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backlogLower priority / backlogenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions