orion-phone
Give your agent its own phone.
orion-phone runs Android phones (emulators) that AI agents drive through an MCP server or a CLI: Claude Code, Codex, or any MCP client. An agent leases a phone, reads its screen, taps, types and opens apps, and every action leaves an audit record with a screenshot. A phone in the locked tier does nothing until you unlock it yourself.
Why
Agents that share one desktop browser collide when two of them want it at once, and they cannot use apps that exist only on phones. Handing an agent your own phone is worse: everything on it is then within reach. orion-phone gives agents phones of their own, signed in to the accounts you choose, with three rules built in:
- One agent per phone. An agent leases a phone before it acts. A second agent gets
BUSYwith the holder's name and expiry instead of stepping on the first. - A locked tier that waits for unlock. A phone in the
lockedtier never starts on its own and cold-boots for every session. It permits holder actions only after an unlock, and it uses a boot only after it has seen the lock screen showing over a screen lock and then found the phone unlocked. It shuts down when the session is released, when its lease lapses, and at a hard session cap. orion-phone does not handle the credential or attest who supplied it. Use it on a trusted host: local adb and host access remain outside this cooperative tool boundary. Independent verification of credential-encrypted storage remains open as Q2 in the spec. - A record of everything. Each holder action writes an audit line, and each successful one a screenshot, to a private state directory outside any repository.
Requirements
- Linux with systemd user services (version 250 or later) and KVM. Windows works through WSL2 with nested virtualization.
- Python 3.11 or later
- A Java runtime, version 17 or later, for Google's Android command-line tools
- Google's Android command-line tools, version 23 or later ("Command line tools only" at
https://developer.android.com/studio), unzipped so that
~/Android/Sdk/cmdline-tools/latest/bin/sdkmanagerexists (~/Android/Sdkis the defaultsdk_rootin the config) - A Google Play system image for any locked phone (
system_imagein the config; the default is one, andsetupdownloads it). orion-phone refuses a config that has a locked phone on any other tag, and refuses to use a locked phone whose booted image does not report file-based encryption (ro.crypto.typeisfile) and a build that is not debuggable (ro.debuggableis0)
Install and set up
uv tool install orion-phone # or: pipx install orion-phone; from a checkout: uv tool install .
orion-phone init # writes ~/.config/orion-phone/config.toml: phone a open, phone b locked
# Before setup, read and accept the Android SDK License yourself: https://developer.android.com/studio/terms
# (current SDK tools show the terms once and never ask again; orion-phone cannot check acceptance)
orion-phone setup # SDK packages, phones, systemd user units; starts open phones
orion-phone status a # exits 0 once the first boot finishes
Edit the config between init and setup to add, remove or rename phones. Run setup from a
desktop shell so the phone windows appear, and sign in to accounts by hand on each phone; to see a
phone over Remote Desktop or WSL2, mirror it with scrcpy (see "Viewing a phone" in
docs/runbooks/phone-services.md). A locked phone has no window
until it is leased: follow "First boot of a locked phone" in the same runbook, which sets its
screen-lock PIN before anything else.
Phone data (each phone's disk images and snapshots) lives in the AVD home, found the way the
Android tools find it: ANDROID_AVD_HOME, else the avd directory inside ANDROID_USER_HOME,
else ~/.android/avd. Choose it before setup, as an absolute path outside any git repository:
orion-phone refuses a home inside a repository. Besides one unit per phone, setup writes
orion-phone-adb-server.service, which runs the debug bridge's server on loopback port 5037; the
runbook explains why it runs its own and how to upgrade from a version without it.
Connect your agent
The MCP server is the orion-phone-mcp command, over stdio. Each server process is one lease
holder, and its tools mirror the CLI.
claude mcp add --scope user orion-phone -- orion-phone-mcp # Claude Code
# Codex: ~/.codex/config.toml
[mcp_servers.orion-phone]
command = "orion-phone-mcp"
{"mcpServers": {"orion-phone": {"command": "orion-phone-mcp"}}}
Do not launch the server as python -m orion_phone.mcp_server from inside a project directory,
because a package named orion_phone there would shadow this one; the orion-phone-mcp command
is unaffected.
Use it from a shell
H=--holder=my-agent-session # one name per agent session; never shared
orion-phone lease a $H
orion-phone ui a $H # elements with tap centers
orion-phone tap a 540 260 $H
orion-phone type a $H -- "hello world"
orion-phone key a BACK $H
orion-phone open a https://example.com $H
orion-phone shot a $H
orion-phone release a $H
Add --json before the command for one machine-readable object per call. A holder name
identifies one agent session: never put ORION_PHONE_HOLDER in a harness's global
environment, or every session becomes the same holder.
| Exit | Code | Meaning |
|---|---|---|
| 0 | OK | Done. |
| 1 | INTERNAL | An unexpected error inside orion-phone (traceback on stderr). |
| 2 | USAGE | Bad arguments or config. |
| 3 | BUSY | Another agent holds the phone. |
| 4 | NOT_HOLDER | You do not hold the lease (never held, released, or expired); on a locked phone, also when its session ended: lease again. |
| 5 | NEEDS_OPERATOR | A locked phone is starting, still booting, has no screen lock, has a boot that cannot be used (found unlocked without ever being seen at its lock screen, or seen with no screen lock and given one since), is showing its lock screen, or has a lock state or screen lock that cannot be read. The result's state says which: locked asks for the unlock and no-screen-lock for a PIN to be set, both on the phone's own screen (starting and booting mean lease again shortly, and do not unlock yet, restart-needed means lease again, which restarts the phone, and unknown, a lock state or screen lock that cannot be read, also means lease again). |
| 6 | PHONE_DOWN | The phone is not running. |
| 7 | UNHEALTHY | Running, but failed the health proof (not booted, or a blank screen). |
Installing from a checkout
For a local development or release-check environment, bootstrap the packaging tools separately from runtime dependencies:
python3.11 -m venv .venv
.venv/bin/python -m pip install --require-hashes --force-reinstall -r bootstrap-requirements.txt
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pytest -q
This changes only the selected virtual environment. Release preparation and exact-artifact
approval are described in docs/runbooks/release-artifacts.md.
What a task costs
Six runs on 2026-10-07, three per task, each a fresh headless Claude Code session (claude -p)
on Claude Opus 5.5, driving one open-tier phone through the MCP tools. Every run is listed.
| Task | Succeeded | Cost per run | Phone tool calls per run |
|---|---|---|---|
| Read the Android version and the storage used, in Settings | 3 of 3 | $0.38, $0.36, $0.20 | 19, 22, 10 |
| Create a labeled 6:45 AM alarm in Clock, see it listed, delete it, see it gone | 3 of 3 | $0.46, $0.33, $0.33 | 37, 22, 21 |
- Cost is the whole session as Claude Code reports it, its own system prompt and tool definitions included. The runs went back to back, so its prompt cache was warm; with a cold cache, a run costs about $0.18 more.
- Settings' About page shows a running uptime counter, so
uifailed there in two runs (it waits for the screen to settle), about 25 seconds per failed call, and the agent read it withshot. - Two of the three Settings runs read the storage figure from the summary on the main Settings list instead of opening the Storage page.
- The costliest alarm run spent 15 of its 37 calls on typing the time, which did not take, before it set the time on the clock dial.
Security
Leases coordinate agents; they are not a sandbox. Everything a phone shows is untrusted input to an agent, and an agent that holds an open phone can do there whatever its accounts allow. Read SECURITY.md before signing a phone in to accounts that matter.
Roadmap (help wanted)
orion-phone runs Android emulators on Linux and WSL2 today. Planned, and open to contributors:
- Real Android devices over adb (USB, Wi-Fi, or a Tailscale network). The open tier already works through plain adb; it needs a way to name a device by its serial.
- iPhone, which needs a Mac and Apple's developer tools.
- macOS and native Windows hosts, in place of systemd.
- An SMS bridge: real text messages delivered to a phone's emulated SIM.
Known limits
typeaccepts printable ASCII only, and a literal%sbecomes a space (Android'sinput textbehavior).uilists at most 300 elements and stops adding them at 32,000 characters; a final# truncated ...line says when it cut the list short.- A UI dump over 4 MiB is refused (UNHEALTHY), and neither saved nor read past the limit.
uican fail on a screen that changes every second, such as one showing a running clock or an uptime counter: the dump waits for the screen to settle.shotworks there.- Under WSL2, the emulator's clipboard sharing can crash WSLg's compositor, and every phone window
with it (microsoft/wslg#1407). Turn it off with
no phone running: set
clipboardSharing=falsein~/.config/Android Open Source Project/Emulator.conf. - A locked phone needs a display so its operator can unlock it. It needs an X11 display (
DISPLAY; Wayland desktops provide one through XWayland), and orion-phone refuses to start a locked phone without one. - Phones run only while your user's systemd services run.
- Leases use the wall clock. A forward jump (a host suspend, for instance) ends leases early: safe, because a lease taken over from a holder still mid-action waits for that action to finish, and a locked phone whose lease ends early shuts itself down. A backward jump can briefly revive an expired one.
License
MIT
Metadata
Release files for orion-phone 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| orion_phone-0.2.0.tar.gz | 316.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| orion_phone-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 405.5 kB
Release files / orion_phone-0.2.0.tar.gz
| Download URL | orion_phone-0.2.0.tar.gz |
|---|---|
| Size | 316.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f8eb3a011aa59e116cbf66b6c23b04362737b5f888f3fbe62769b566ca2c5fcb
|
|
BLAKE2b-256 checksum How to use checksums |
26740a96e9a59eba73c62162b7068b8c087cef968d2e4e68245b47fe4e73c483
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / orion_phone-0.2.0-py3-none-any.whl
| Download URL | orion_phone-0.2.0-py3-none-any.whl |
|---|---|
| Size | 89.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
98c581241d425bb557ac8a8b1eb564128e91ae4d22393a07e75d1b6eeeb82ddd
|
|
BLAKE2b-256 checksum How to use checksums |
75d4f74792bcb940194119560f99948220ae3201a86691586be9b3d356291cbb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log