Limrun
Use Limrun to run agent-device on remote iOS simulators and Android emulators. You need a Limrun API key, or the URL and token of an instance someone else created. Limrun does not use local or physical-device selectors such as --udid, --serial, or --device.
Set credentials and connect
Export your Limrun API key, for example from CI secrets. Set LIMRUN_REGION to pick a region.
--platform android or --platform ios selects the instance type. connect checks the credentials for that platform without creating an instance.
Requests agent-device makes with your API key, including instance creation, start their User-Agent with agent-device-cli/<version> so Limrun can attribute them to agent-device for revenue sharing.
Drive an existing instance
An agent in a sandbox can drive an instance that someone else created, such as the instance behind a live preview, without your organization's API key. Create the instance where the API key lives, then give the sandbox that instance's URL and token from its status:
The lim CLI reads the same variables, so one set works for both tools. Set all of a platform's variables or none. With a partial set, connect fails, and the daemon registers without Limrun, lists it as failed in agent-device daemon status, and records provider_plugin_failed in daemon.log.
connect checks the instance credentials. agent-device never creates or deletes that instance: disconnect leaves it running, and its owner deletes it. When a platform's instance variables are set, they take precedence over LIMRUN_API_KEY for that platform.
On an attached Android instance, agent-device does not replace an existing port reverse mapping. If the owner already maps a device port, such as tcp:8081 for their Metro server, a reverse to that port fails and the owner's mapping stays in place. The error has details.reason: 'android_port_reverse_rebind_refused' when adb reverse --list shows the mapping; otherwise it is a plain ADB failure.
If your shell's account variables, or its instance variables for the platform you lease, differ from the ones the daemon started with, the first install or open refuses before it creates or attaches to an instance. See Change credentials while the daemon runs.
install, and apps before the first open, need LIMRUN_API_KEY because they use Limrun asset storage. After open, apps lists the apps installed on the instance without the key. Install your app before you hand over the instance. From the Node.js runtime, getDeviceSession(device).installRemoteApp(url) installs from a signed asset URL without the API key.
Keep idle sessions alive
Limrun ends an instance after its inactivity timeout, which an idle session can reach while a model thinks between steps. Set LIMRUN_KEEP_ALIVE=1 (or true) to ping the instance every 30 seconds while a session is open. Keep-alive is off by default. In the Node.js runtime, pass keepAlive: true.
Run a session from the CLI
A new Limrun instance does not contain your app. Run install <package-or-bundle-id> <app-path-or-url> before open. install allocates the instance when needed, so you do not need to run devices first.
On Android, agent-device connects to Limrun over an ADB tunnel. Snapshots use the Android snapshot helper, installs upload the app through Limrun asset storage, and port reverse runs over ADB, including the usual reverse setup for a local Metro server.
On iOS, agent-device supports app lifecycle commands, snapshots, screenshots, taps, text input, scrolling, the home button, clipboard read and write, app installation, and system alerts. alert get/wait/accept/dismiss read the alert from the same element tree as snapshots and dispatch the selected button; a choice is pressed only when its English label states the asked-for action (Allow, Don't Allow, Ask App Not to Track, and similar), so a localized alert is reported but never pressed. For an alert it recognizes, Limrun uses the same English button preferences as local Apple XCTest: Allow While Using App outranks Allow Once; Open, Done, and Confirm… accept; Keep Browsing, Deny, and Skip dismiss. Equal-ranked choices and unknown labels refuse. Limrun needs at least two actionable buttons to recognize an alert. App-owned dialogs are not system alerts there, so use snapshot -i and then press @ref or a label selector for the visible control. open --launch-args relaunches the app with those arguments through simctl. iOS cannot reverse a remote device port to a local host port, so for Metro use a publicly reachable HTTPS endpoint or bridge URL instead of a local-only address.
On iOS, settings supports appearance, permission, location, and clear-app-state, with the same arguments as a local simulator. clear-app-state resets the app's data container through Limrun's soft reset. That reset relaunches the app once before agent-device stops it, so the app may run once after its data is cleared. reset-keychain, wifi, airplane, mobile-data, faceid, touchid, and text-size are unsupported, and you cannot read back any iOS setting. On Android, settings mobile-data on|off runs over the ADB tunnel like on a local device.
On iOS, fill taps the target, waits for that field to take text-entry focus, and only then types. Apps that never report a globally focused element, such as Flutter forms, still fill and report textEntryReadiness: "focused-element". If the tap does not focus a field, fill fails with text_entry_focus_not_observed instead of typing into an unknown field. Use type to send text to whichever field already has focus.
To drive Limrun only through MCP, see Use a provider only through MCP. Limrun returns no provider artifacts, so artifacts has nothing to fetch.
Embed the Node.js runtime
A Node.js bridge, such as agent-device-cloud, can embed agent-device's Limrun runtime:
To drive existing instances, pass instances: { ios: { apiUrl, token } } (Android also takes adbUrl), with or without apiKey. The runtime never creates or deletes instances for a platform listed there.
A provider that reaches a harness-owned instance through the Node.js client allocates a Limrun lease in acquire and releases that same lease in release. Put the returned lease.leaseId on the client scope, and pass that same id to leases.release. Start the daemon with the instance URL and token (LIM_IOS_INSTANCE_URL and LIM_IOS_INSTANCE_TOKEN). leases.allocate attaches to that instance. leases.release returns { released, provider }. Neither call creates or deletes the instance. Allocate with retainOnClose: true because the provider releases the lease itself: without it, a session close before release ends the lease and leases.release returns released: false. A retained lease still expires after its ttlMs, so keep heartbeating it between sessions.
After allocating a lease, call runtime.getDeviceSession(device) to get the allocated device's capabilities: app inventory, foreground state where Limrun exposes it, key input, bounded log reads, recording, remote asset installation, and the interactor. On Android it also exposes agent-device's AndroidAdbProvider for helpers and reversible port forwarding. On iOS it exposes a typed simctl execution handle so your bridge can own runner lifecycle and launch policy. The runtime does not expose raw Limrun clients.
Get artifacts and troubleshoot
agent-device artifacts does not return provider artifacts for Limrun. If connect fails, check LIMRUN_API_KEY and the optional LIMRUN_REGION. For an existing instance, check its URL and token variables, and that the instance is still running.
