Mobile Harness

skills.sh Mobilerun website Follow Mobilerun on X Mobilerun Discord community Mobilerun Telegram support

> Portable operating instructions for AI agents controlling Android and iOS devices—locally or in the cloud. Mobile Harness is a compact Markdown harness, not an agent runtime. Its primary control path is Python's `mobilerun_core`, with optional client apps where needed. ## Benchmarks We evaluated mobile-harness on AndroidWorld and iOSWorld using a standard coding agent with **GPT-6 Astra low**, app cards, and persistent memory.

AndroidWorld: 116 tasks, 100% mean score, 100% binary pass rate, 116/116 full-score tasks. iOSWorld: 133 tasks, 98.24% mean score, 93.23% binary pass rate, 124/133 full-score tasks.

[AndroidWorld tasks](https://mobilerun.ai/benchmark/?platform=android#tasks) · [iOSWorld tasks](https://mobilerun.ai/benchmark/?platform=ios#tasks) **Mean score** averages the task scores. **Binary pass rate** counts only tasks that received a full score of **1.0**. Explore the [results and task replays](https://mobilerun.ai/benchmark/) for screenshots and recorded interactions, or read the [methodology](https://mobilerun.ai/benchmark/method/) for evaluation details. ## Agent Setup Prompt Copy paste it into your agent: ```text Set up https://github.com/droidrun/mobile-harness for me. Read `install.md` and follow the steps to install `mobile-harness`. ``` ## Scope - Android through `mobilerun-core` using local ADB with optional Portal, Portal HTTP-only, or cloud. - iOS through `mobilerun-core` using `mobilerun-ios` local HTTP, cloud, or the `ios-portal` app on Simulator. ## Skill Install via npx (skills.sh) Install the harness as an agent skill with the [skills CLI](https://skills.sh): ```bash npx skills add droidrun/mobile-harness ``` When you want to update an installed copy, run `npx skills update` from the same project. First use still requires the Python setup in `install.md`. ## Claude Code Plugin ```text /plugin marketplace add droidrun/mobile-harness /plugin install mobile-harness@mobile-harness ``` The plugin keeps `.venv/` and `local/` in its data directory, which plugin updates leave in place. First use still requires the Python setup in `install.md`. ## Manual Install Install the full public control API: ```bash cd /path/to/mobile-harness "$(command -v python3.13 || command -v python3.12 || command -v python3.11)" -m venv .venv .venv/bin/python -m pip install "mobilerun-core[local]" .venv/bin/python -c "from mobilerun_core import Mobilerun; from importlib.metadata import version; print('mobilerun-core', version('mobilerun-core'), '/ mobilerun-core-local', version('mobilerun-core-local'))" ``` Needs Python 3.11-3.13; if none is on `PATH`, `uv python find '>=3.11,<3.14'` prints one. Tell agents which Python runtime to use: ```text Use /path/to/mobile-harness/.venv/bin/python for mobile-harness. ``` Normal sessions check the installed dependencies and use the existing versions. Updates happen only when requested or needed to fix a confirmed compatibility problem. See `UPDATE.md`; pinned versions stay fixed unless you approve a change. Base `mobilerun-core` includes cloud support through `mobilerun-sdk`. The `local` extra installs `mobilerun-core-local`, which `mobilerun-core` uses internally for local Android and iOS backends. ## Primary API ```python from mobilerun_core import Mobilerun m = Mobilerun() device = m.connect("", backend="cloud") device = m.connect("R5CT123456", backend="local-android-adb") device = m.connect(backend="local-ios-http", url="http://127.0.0.1:8080") device = m.connect( backend="local-android-http", url="http://127.0.0.1:18080", token="...", ) device.ui() device.screenshot() device.start_app("com.android.settings") ``` After connecting, agents should use `device.supports(...)` before optional operations. `device.execute_script("")` runs JavaScript in the device's foreground Chrome tab and returns its JSON result. Cloud devices only; local backends raise `UnsupportedOperation`. Gate it with `device.supports("execute_script")`; the platform guides describe this key's network-probe behavior and its two server-side errors. ## Common Device Helpers Use these helpers through the `device` returned by `Mobilerun.connect(...)`: - `device.find_nodes(...)` searches the accessibility tree. `any_contains=` matches case-insensitive substrings across text, content description, resource id, and accessibility identifier. Nodes may carry `offscreen: True` (outside the viewport; scroll to reach it) and `hidden: True` (reported not visible; scrolling alone may not reveal it). A missing flag is not proof of visibility. - `device.tap_node(node)` taps the center of an accessibility node and raises if the node has no usable bounds. Before any bounds check, it raises a distinct error for a node flagged hidden unless the node is also offscreen. - `device.tap_text("label")` taps the first on-screen, non-hidden match across text, description, resource id, and accessibility identifier. It raises a distinct error when matches exist but none are tappable on-screen. - `device.scroll(direction, distance=0.5, ms=..., verify=False)` scrolls content-relative; `verify=True` returns whether the viewport actually moved. - `device.scroll_until(text_contains=..., direction="down", max_swipes=10)` scrolls until a match is on-screen, returning the node or `None`. It stops early with `None` when the viewport stops moving; do not re-call it blindly. - `device.type("text", clear=True)` clears the focused field before typing when the backend supports text input. `device.clear_input()` is available on cloud and every local backend. - `device.list_apps()` excludes system apps by default. Pass `include_system_apps=True` when a full inventory is needed and supported. - `device.open_deep_link(...)`, `device.get_clipboard()`, `device.set_clipboard(...)`, `device.grant_permission(...)` and `device.stop_app(..., clear_data=True)` depend on the backend; check `device.supports(...)` first. - `device.ui(filter=False)` returns the unfiltered tree, including off-screen nodes. ## Cloud Mode Cloud devices use the same `Mobilerun` facade: ```bash export MOBILERUN_CLOUD_API_KEY="..." export MOBILERUN_API_BASE_URL="https://api.mobilerun.ai/v1" ``` ```python from mobilerun_core import Mobilerun m = Mobilerun() device = m.connect("", backend="cloud") device.ui() device.screenshot() device.start_app("com.android.settings") ``` ## Loading Model Skill-based runtimes can load `SKILL.md`; all runtimes should start with `AGENTS.md`. It routes agents to the smallest needed file: - `platforms/android/GUIDE.md` for Android work. - `platforms/ios/GUIDE.md` for iOS work. - `platforms//recovery/GUIDE.md` only when control fails. - the credentials guide under `core/credentials` only when a credential or human-gated screen appears. - `core/memory/GUIDE.md` only when reading or writing local agent-owned memory. - `apps/android//CARD.md` or `apps/ios//CARD.md` only for the foreground app. - the same path under `local/apps/` after the shipped card, and prefer it where the two disagree. - `UPDATE.md` when an update is requested or a confirmed version incompatibility blocks the task. ## Local Android Modes | ADB | Android Portal HTTP | Mode | | --- | --- | --- | | yes | yes | `backend="local-android-adb"`: core uses ADB and automatically uses Portal features when available. | | yes | no | `backend="local-android-adb"`: core uses ADB-native control, UI, text input, screenshots, and app lifecycle. | | no | yes | `backend="local-android-http"` with the user-provided Portal base URL and bearer token. | | no | no | Blocked: ask the user to enable ADB or provide reachable Portal HTTP access. | Android Portal HTTP-only means the agent already has both: - a base URL such as `http://127.0.0.1:18080` - a bearer token for `Authorization: Bearer ` Without ADB, the harness cannot install, enable, port-forward, or fetch a token for Portal. Android Mobilerun Portal: https://github.com/droidrun/mobilerun-portal ## Local iOS Modes | Target | Server | Mode | | --- | --- | --- | | iPhone or iPad over USB | `mobilerun-ios --local ` | `backend="local-ios-http"`, default `http://127.0.0.1:8080`. | | iOS Simulator | `ios-portal` app | `backend="local-ios-http"`, default `http://127.0.0.1:6643`. | `backend="local-ios-http"` detects which server answers. `stop_app`, `list_apps`, `install_app`, and the `back`, `enter`, `delete`, and `app_switch` keys need `mobilerun-ios`. `mobilerun-ios` cannot drive Simulators. - mobilerun-ios local mode: https://docs.mobilerun.ai/framework/guides/device-setup (iOS tab) - iPhone preparation and WDA signing: https://docs.mobilerun.ai/guides/connect-iphone - ios-portal: https://github.com/droidrun/ios-portal ## Local State Everything the user or the agent owns lives under one git-ignored root, `local/`: `local/memory/` for agent-written memory, `local/credentials/` for optional credential notes, and `local/apps/` for your own app cards or overrides of shipped ones. The repository tracks only their rules and templates, keeping your own content separate from `git pull` and plugin updates. In a Claude Code plugin install, `local/` lives in the plugin's data directory instead. See `local/README.md`. Agents may write operational memory after reading `core/memory/GUIDE.md`.