iphone-use — drive a real iPhone
Control a physical iPhone through the iphone-use
daemon: see the screen (/agent/screenshot), act on it (/agent/input), repeat.
The default backend is Direct: WebDriverAgent performs input on the phone and
the device-side screen service provides pixels. It does not need iPhone
Mirroring, Screen Recording, Accessibility, or the Mac cursor. The old
Mirroring path is an explicit compatibility backend only.
Prerequisites
A Mac on your network running the daemon, with WDA and its 8100/9100 relays
configured for the intended iPhone. Direct control needs the phone unlocked,
trusted for development, and awake. Setup blockers are reported by status.
WDA itself has no authentication. Daemon bearer auth protects /agent/*, but
does not protect the phone's own 8100/9100 listeners from another routable
host. Use Direct only on a trusted, isolated network; when practical, turn off
iPhone Wi-Fi and keep the supported Mac relays on USB loopback.
HOST="${PHONE_REMOTE_URL:-http://127.0.0.1:44321}"
AUTH="Authorization: Bearer $PHONE_REMOTE_TOKEN" # daemon password or PHONE_REMOTE_AGENT_TOKEN
MUTATION="X-Phone-Control: 1" # required on every state-changing POST
Always probe first — if this fails, stop and report (don't retry blindly;
5 consecutive auth failures lock you out for 30s):
curl -s -H "$AUTH" "$HOST/agent/status"
# {"ok":true,"backend":"direct","wda":true,"wda_actionable":true,
# "wda_locked":false,"drivable":true,"device_state":"ready",
# "screen_state":"waiting","released":false,"reconnecting":false,"hint":"",
# "setup_blocked_on":"", ...}
Status checks do not take control. For initialization, status/health checks,
or when no unfinished user-requested task needs phone access, report the state
and stop. Do not reconnect, take a hold, or poll screenshots/elements to keep
the phone ready. Idle release is intentional.
Gate phone operations and screen/UI reads on drivable:true. The current
user-requested task must need that access. phone_target is a legacy
Mirroring-window field and is not a Direct readiness signal.
device_state:"ready" + drivable:true → proceed with the requested task.
device_state:"locked" → ask the operator to unlock the iPhone and keep it
awake only if the current task needs phone access.
device_state:"released" or released:true → expected idle state; leave it
released unless the current task needs phone access. If that task cannot
proceed, check recovery_owner:"daemon" and hint / setup_blocked_on.
Once blockers are resolved and no release/reconnect is in progress, restart
Direct/WDA once. This starts on-device automation, occupies the phone, and
may require the operator to unlock it:
curl -s -H "$AUTH" -H "$MUTATION" -H 'Content-Type: application/json' \
-X POST "$HOST/agent/mode" -d '{"mode":"agent"}'
With the bundled MCP server, call phone_reconnect instead under the same
conditions. Then poll status until drivable:true; do not retry either
path in a tight loop. Neither path accepts a transient UDID.
device_state:"releasing" or releasing:true → do not reconnect or take a
hold during release. Wait for it to finish only if the current task still
needs phone access, then reassess status and the recovery conditions above.
reconnecting:true → first inspect setup_blocked_on. If it is non-empty,
follow the concrete hint; do not blindly wait or send another reconnect.
Otherwise report setup_phase / setup_message, then wait and poll. A first
build after an Xcode update can take several minutes. Never send input until
drivable:true.
device_state:"blocked" or "offline" → read hint and
setup_blocked_on (warp|proxy|usb|trust|ddi|account). For a current task
that needs phone access, resolve the blocker first; offline recovery follows
the same task, ownership, and lifecycle conditions as released recovery.
device_state:"degraded" → not a blocker; do not go looking for
setup_blocked_on, it is empty by definition here. WDA answers, but the last
read or action did not complete — usually a /source read that timed out on
a heavy page, or a stalled app. Wait the retry_after_secs from the failing
response (3s) and read again. The next health probe decides whether this
clears back to ready or drops to offline; do not restart the service for
it.
Never switch to mode=mirror as automatic recovery. Mirror is an explicit
operator-selected compatibility mode.
The API
| Call |
Purpose |
GET /agent/status |
{ok, backend, device_state, screen_state, wda, wda_actionable, wda_locked, drivable, released, hint, setup_blocked_on, setup_phase, setup_message, …} — gate on drivable |
GET /agent/elements |
Direct/WDA UI as text: {"snapshot":"…","elements":[{kind,label,identifier?,rect,depth,value?,enabled?,visible?,accessible?,focused?,placeholder?},…]} — prefer this over screenshots. Indexes and snapshot tokens are valid only for this read. Add ?since=<prior snapshot> to get {"snapshot":…,"baseline":…,"delta":{added,changed,removed,unchanged}} instead of the full tree (much cheaper on multi-step flows; unknown baseline falls back to the full tree), plus app_changed:{from,to} if the foreground app moved since that baseline. Both shapes carry a read-only ax_stats usability block — see Vision fallback below — and a sparse alert:{text,buttons} block whenever a system alert (UIAlertController) is on screen. With PHONE_REMOTE_ELEMENTS_AFFORDANCES=1 on the daemon, rows also carry sparse actions (named perform affordances), selected, and min/max |
GET /agent/screenshot |
Current phone screen as a device-side PNG; no Mirroring session required |
POST /agent/input |
One action (JSON body, below); requires X-Phone-Control: 1 |
POST /agent/actions |
One bounded, fail-closed sequence of action, wait_for, and short pause steps; Direct/WDA only; requires X-Phone-Control: 1 |
GET /agent/intents |
Curated semantic intents registry (registered Shortcuts verbs). Empty list + hint when none are set up |
POST /agent/intent |
Dispatch one registered verb ({"name":"battery","args":{}}); requires X-Phone-Control: 1. Results arrive on /agent/inbox, matched by the returned id |
If a stale caller forgets the mutation header, the 403 response names
required_header:"X-Phone-Control: 1" and includes a retry hint. Correct the
request once; do not repeat the unactionable POST.
Give your own client at least 40s on /agent/elements. The daemon keeps
rebuilding a failed source read until its own 35s deadline before answering
wda_source_failed — so a shorter client timeout (curl -m 25) cuts the
connection first and hands you an empty body, not JSON. That empty body is
your timeout, not a daemon fault and not an empty screen: re-read with a longer
one before concluding anything about the phone.
Actions — coordinates are normalized [0,1] over the phone screen
(0,0 top-left, 1,1 bottom-right), so they're resolution-independent:
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"tap","x":0.5,"y":0.3}'
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"tap","label":"新备忘录"}' # exact label must have one match; ambiguity sends nothing
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"tap","element":3,"snapshot":"<same elements response>"}'
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"scroll","x":0.5,"y":0.5,"dx":0,"dy":60}'
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"text","text":"Health"}'
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"key","name":"return"}' # return|enter|send|go|search all fire the Return key — use this to submit a chat/search; coordinate-tapping a 3rd-party keyboard's 发送/前往 key ACKs but does NOT send
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"shortcut","name":"home"}' # home|spotlight
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"longpress","x":0.4,"y":0.6,"duration_ms":700}'
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"keyboard"}' # dismiss the on-screen keyboard
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"set_value","element":5,"snapshot":"…","value":"你好"}' # write a field directly (clear-then-type; "" clears); no focus tap, no keyboard dance
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"scroll","element":7,"snapshot":"…","dy":120}' # scroll INSIDE that element's rect — never strays into a neighboring scroll view
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"alert","button":"不是 li guo?"}' # press a system-alert button by exact name (UIAlertController; use this, NOT an element tap — alert buttons ACK a coordinate/element tap without acting)
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"alert","action":"dismiss"}' # or the default accept/dismiss button
curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"perform","element":9,"snapshot":"…","action":"increment"}' # named affordance on that element: increment|decrement (wheel/stepper/slider), adjust (+"value"), toggle, menu (long-press menu), double_tap, two_finger_tap, scroll_to_visible, pinch, rotate, force_press
Halve the round trips: POST /agent/input?return=delta makes an applied
action settle briefly and return the post-action element change in the same
response — {"ok":true,"snapshot":…,"baseline":…,"delta":{…}} when the
baseline (the since query param, or the action's own snapshot field) is
still server-cached, else full elements. That replaces the separate
act-then-GET /agent/elements verify pair for routine steps.
Read the settle block before you trust the delta. The action result and
the observation are separate facts: ok:true means the action applied, and
nothing in the observation can take that back — a slow or failed read comes
back in settle.reason beside ok:true, never as an unknown outcome you have
to escalate.
settle.reason |
What it means |
What to do |
stable |
Two consecutive reads matched over a tree with something in it. |
The tree is worth reading — now check your postcondition against it. Stable is not "it worked". |
budget_exhausted |
Stability was not confirmed within the budget. The screen may still be moving, or settle_ms was too short (or 0, which does not read the tree at all), or every read was container-only. |
Do not conclude anything from the delta alone; re-read with GET /agent/elements. |
observation_failed |
The read path itself broke (plus the legacy delta_error). Running out of budget is not this — a cancelled read is not a broken one. |
The action still applied. Verify with GET /agent/elements. |
Two more flags matter: sparse:true means the tree held nothing to act on
(empty, or containers only) — two identical bare reads are evidence you cannot
see the screen, not that it settled, so never read sparse + unchanged as
"nothing happened". stale:true means the tree in the response is the LAST
good read, not a fresh one — either the refreshing read broke, or the budget
cut it off mid-sample. And settled:true only ever claims the tree
stopped changing — it is never a claim that what you wanted actually
happened; that judgment stays yours.
settle_ms, budget_ms and waited_ms all describe the TREE read only; the
alert probe is separate and hard-capped at 1.5s. Tune with settle_ms
(default 15000, max 20000): drop it to 0 for a step you are going to verify
by screenshot anyway. How long a read takes varies with the device and the
screen — on one phone with a large tree it was measured at 6–7s — so the
default leaves room for two reads rather than assuming a fast one. It is a
ceiling, not a wait: reading stops as soon as two reads match. If you set a
budget shorter than a single read on that screen, expect captures: 0;
budget_ms in the answer tells you what it had to work with.
A failed wait_for tells you whether anyone could see the screen. Its
observation block distinguishes three things you must not conflate:
read:false (no tree was ever obtained — this proves nothing, least of all
that an element is absent), read:true, stale:true (the screen was read, then
the read path failed; you are looking at the last valid observation, not the
current screen), and plain read:true (the screen was read and the condition
genuinely was not met). sparse:true means the tree was empty or
container-only; every absent locator it would have "satisfied" is listed in
absent_unproven and the step deliberately does not pass. Never conclude an
element is gone from a tree that had nothing in it — take a screenshot
instead.
app_changed means your tap landed in the wrong app. Any delta response
(?return=delta or GET /agent/elements?since=) grows an
app_changed:{from,to} block when the frontmost app is not the one the
baseline was taken in. The usual cause is a banner notification dropping in
from the top and swallowing the tap, which opens its app — the delta then
describes a screen you never asked for. Stop and re-orient when you see it:
do not keep issuing steps against the old plan, and never treat the new
screen's elements as the ones you were aiming at. Recover by sending
{"type":"home"} and re-entering the intended app.
Its absence is not a guarantee — a tree with no Application row reports
nothing rather than guessing.
After typing into a web form the keyboard covers the page's own submit/next
buttons — send {"type":"keyboard"} to dismiss it before tapping them.
shortcut:"switcher" is unsupported in Direct/WDA: iOS does not expose an
App Switcher action that WDA can synthesize. Do not send it and claim success.
{"type":"back"} is a left-edge-swipe gesture, not a universal Back button: on
a screen with no in-app back target it can carry past the edge and switch
apps (e.g. drop you onto the Home screen or another app). Prefer resolving and
tapping the on-screen back control ({"type":"tap","label":"…"} / an element
tap) when one exists; use back only when you know the current screen has an
edge-swipe-back. For unattended runs, turn on a Focus mode first — a banner
notification dropping from the top will otherwise intercept a tap and open the
notifying app (hardware-seen: a chat banner hijacked a tap and opened WeChat).
MCP alternative: the repo ships iphone-use-mcp (crates/mcp) with the
day-to-day safe subset: status, capabilities, reconnect, screenshot, elements,
coordinate and snapshot-bound element taps, strict unique-label taps, scroll,
text, named keys, Home/Spotlight, and phone_run_steps for one guarded
multi-step call.
phone_capabilities answers two separate questions without touching the
phone: what this build supports for the configured backend, and whether the
device can be driven right now (blocked_by names locked / released /
reconnecting / human_handoff / owned_by_other / offline; ok: null means no
evidence either way). It wakes nothing and takes no owner lease, so it is safe
before deciding what to attempt. Its scope is UI control and observation only.
For those seven act tools and phone_capabilities, the JSON comes back as MCP
structuredContent and the text block is only a preview (trimmed at 8 KiB), so
parse the structured field. phone_run_steps puts its complete batch result in
both, so either is safe to parse. Every other tool keeps its original shape:
phone_elements and the phone_flow_* tools return complete JSON as text
(including phone_flow_run's execution result, passed or failed), and
phone_screenshot returns an image. Errors raised before a call reaches the
phone are explanatory text instead. The rule is simply: read
structuredContent when it is there, otherwise read content according to the
tool, and call phone_status / phone_capabilities when you need a
machine-readable state. When a tool cannot confirm what happened it answers
outcome: "unknown" with retry_safe: false — the request may have reached the
phone, so check phone_elements or phone_screenshot rather than resending.
Decide by the explicit retry_safe boolean, not by outcome: a local refusal
and a daemon-reported not_sent are both valid evidence when they carry
retry_safe: true, and not_sent on one step never means a whole batch can be
replayed.
Every single-step act tool takes an optional observe. With observe: true
the daemon watches the screen settle and returns settle, snapshot and
delta alongside the result, so you do not need a separate phone_elements
round trip to see what an action produced. It is off by default because that
wait is latency the action does not otherwise pay. Read settle.reason:
stable means the screen stopped changing, budget_exhausted means the
observation window ran out and says NOTHING against the action itself, and
observation_failed means the read broke. stale: true marks a tree from the
previous successful read rather than the current screen; sparse: true marks
an empty or container-only tree, which is never reported stable.
Inside a sequence, tap_locator can act on the same strict
label/identifier/kind/value/focus/enabled/visible locator used by wait_for;
zero or multiple matches send no tap.
Maintenance and less common HTTP actions such as drag, app install/uninstall,
and target configuration are not exposed as native MCP tools.
The installed MCP binary also runs reviewed, versioned JSON without a model:
iphone-use-mcp flow validate <file> is offline, and
iphone-use-mcp flow run <file> --input key=value requires Direct plus
drivable:true before submitting the same guarded batch exactly once.
The browser's 流程 panel can record this v1 JSON without hand-writing it:
only acknowledged actions are kept, semantic labels are preferred, coordinate
gestures are marked fragile, and typed text becomes a named runtime parameter
without retaining the literal recorded value. Parameter values live only in
the current browser page or command invocation. When the
post-action element tree exposes a new unique identifier or foreground
application, the recorder adds a reviewed wait_for checkpoint instead of
relying on a fixed delay. It never copies arbitrary screen labels or values
into an automatic checkpoint because those may contain private content. A
recording that could not persist an action is an incomplete draft and cannot
run from the browser. A parameterized recording can be downloaded immediately,
but it cannot run until every required value is filled in.
The registry contract: check it, feed it, fix it
The official flow registry (leeguooooo/iphone-use-flows)
holds reviewed, deterministic per-app scripts that replay a whole task in one
call with no model and no screenshots. Every phone task you do has three
registry obligations. They are not optional politeness; they are how the next
run of the same task costs one call instead of thirty.
MCP="$HOME/Applications/iPhoneUse.app/Contents/MacOS/iphone-use-mcp"
"$MCP" flow update # once per machine; mirrors the registry
"$MCP" flow list [--category health] # id · risk · verified · inputs · name
"$MCP" flow info health/export-all # metadata + step templates
PHONE_REMOTE_TOKEN="$PHONE_REMOTE_TOKEN" "$MCP" flow run system/spotlight-search --input query=Health
"$MCP" flow publish my.json --as health/export-all-zh-cn --alias 健康 --note "iPhone 17 Pro Max, iOS 26"
"$MCP" flow report health/export-all --result @run.json --note "profile button is now '资料'"
MCP: phone_flow_list / info / run / update / publish / report.
1. Before acting — check. Call phone_flow_list (or flow list) before
driving any app step by step. phone_elements already tells you: its
registry block lists the installed flows for the app on screen and says when
the store is empty (phone_flow_update once). A matching flow beats
exploration every time. Read verified and risk: verified: no means nobody
has proved the file on a real phone yet — run it, then take one checkpoint
screenshot. risk: side_effect (send/publish/pay/delete) refuses to run without
--confirm / confirm=true; confirm only after the user approved the exact
target and inputs.
2. After succeeding — feed. When phone_run_steps completes 3+ steps, its
registry.hint reminds you: that sequence is a flow waiting to be saved. Write
it as v1 JSON (typed text → named input, page transitions guarded by
wait_for, app / category / risk / locale filled in, verified_on with
device, iOS, date), flow validate it, run it once from the file, then tell
the user you would like to publish it and, with their OK, call
phone_flow_publish(confirm=true) / flow publish. It forks if needed, adds
app.json for a new app (aliases = the app's foreground label in each
language, e.g. Health, 健康, so phone_elements can surface it later),
rebuilds index.json, and opens the PR. Unverified files open as draft PRs.
Publishing is the default outcome of a successful multi-step task, not an
extra; skip it only for one-off or private tasks.
3. On failure — fix. A flow that stops (failed_step, element_not_found,
missing_present) is a registry bug until proven otherwise. Read
phone_elements to see where the phone stopped. If the flow is wrong (label
changed, app updated, locale mismatch), tell the user and, with their OK, call
phone_flow_report(id, note, confirm=true) / flow report: the last failure of
that id is already captured (failed step, redacted daemon result, daemon
version; screen labels and typed text are stripped). If you can also fix the
locator, publish the corrected file with a bumped verified_on and mention the
issue. Do not report a phone that was merely locked, offline, or on the wrong
app. Never replay a failed flow blindly — the first tap may already have acted.
Compat is computed for you. Every flow in flow list / phone_flow_list /
the registry block carries compat, derived from the flow's verified_on
app version (the iOS version for Apple system apps) against what this phone has
installed (flow apps shows the inventory):
| compat |
what you do |
verified |
run it |
untested-newer |
the app updated since the last verification: run it, take one checkpoint screenshot, and if it worked publish the new verified_on |
incompatible / broken |
do not run (flow run refuses without --force): explore by hand, then publish the fixed flow |
needs-verification |
nightly re-verification failed and nobody fixed it yet: treat like broken, fixing it is the contribution |
draft |
no hardware record: run with a checkpoint, then publish verified_on |
unknown |
no version data (daemon has no /agent/apps and is not on loopback): behave as untested-newer |
Flow labels are locale-specific (locale): an en flow fails closed on a
Chinese phone with zero matches and no tap. Pick the variant that matches the
device language or record one (health/export-all-zh-cn). Keep your own flows
next to the official ones with flow add <file> --as <app>/<name>; they
survive flow update. Only the official source exists — there is no
sources add.
The loop: see → act → verify
- See: when
drivable:true, use GET /agent/elements first — it's text
(10× cheaper than vision) and carries exact labels. Fall back to screenshot
when you need pixels (images, maps, unlabeled UI).
- Act: when two or more consecutive actions are already understood, safe,
and verifiable, send the longest stable segment as one
phone_run_steps
call with page transitions guarded by wait_for. Use one atomic action only
while exploring an unknown screen, waiting for human confirmation, or
isolating a failed checkpoint. Prefer
phone_tap_element(element,snapshot) after choosing by
identifier/kind/label/state. phone_tap_label is safe only for an exact
unique label; zero or multiple matches send nothing. Use raw coordinates
only when the control has no semantic target. For stateful controls
(Switch / Slider / Stepper / PickerWheel), prefer perform over any tap —
see the Switches & sliders rule below; when a row advertises actions
(affordances flag on), use the listed action instead of guessing a gesture.
- Verify:
elements (or screenshot) again → confirm the expected change
before the next step. Treat a non-2xx read or an empty tree with error as a
failed checkpoint, even if an immediately preceding status said
drivable:true; current daemons revoke cached actionability on either read
path failure.
Operational rules. Hardware evidence is called out only where it exists; a
documented or unit-tested action is not automatically a current-device proof:
- System alerts (permission prompts, "以 X 的身份设置?", sign-in confirmations):
they show up as the
alert:{text,buttons} block on /agent/elements, and a
coordinate or element tap on an alert button is acknowledged but often does
nothing (same false-success class as switches). Dismiss/answer them with
{"type":"alert","button":"<exact button text>"} or
{"type":"alert","action":"accept"|"dismiss"}.
- One session per phone. Name yourself on every state-changing request
with
X-Phone-Owner: <session-name> (the MCP server does this for you, from
PHONE_REMOTE_OWNER or mcp-<pid>). Status then shows owner and
owner_lease_remaining_secs. If owner is someone else, do not drive the
phone: control calls answer 409 phone_owned with the owner's name — wait,
ask that session to POST /agent/owner {"release":true} when it is done,
and never send X-Phone-Owner-Takeover: 1 unless the user confirms the
other session is abandoned. Release your own lease when your task ends.
- Human-in-the-loop pauses within a current user-requested phone task
(waiting for the operator to type a password, approve a prompt, or fetch a
code): use a bounded HTTP
POST /agent/hold {"secs":600} before the pause
if the task needs to retain the phone. Clear it with {"secs":0} when the
pause ends or the task completes/is cancelled. Status reports
hold_remaining_secs. A hold prevents idle release; it does not start WDA
or prove readiness. Do not use or renew it for initialization, health checks,
or to keep the phone ready without a task. There is no bundled MCP hold tool.
A 503 device_release_in_progress (with Retry-After) means release already
started. Only if the current task still needs phone access, wait for release
to finish and reassess status; reconnect once under the recovery conditions
above, and take another bounded hold only if the task still needs that pause.
- Switches & sliders (hardware-verified, iOS 27): a coordinate tap on a
Switch is acknowledged but does not flip it — a silent false success.
The only reliable path is
{"type":"perform","element":N,"snapshot":"…", "action":"toggle"}, which works on both the labeled full-row switch and the
bare paired UISwitch. Sliders: perform increment/decrement steps
~10%, adjust takes a normalized "value" in 0..1. Verify the new value
from a fresh elements read (or the ?return=delta response) — never trust
the ACK alone for state changes.
- Scroll: positive
dy reveals content farther down; negative dy reveals
content above. Positive dx reveals content to the right. A scroll is an
atomic WDA swipe, not a stream of wheel events.
- Text input — focus and verify a field first, then
{"type":"text"}.
Direct/WDA sends Unicode on-device, so ASCII and CJK land without touching
the Mac clipboard or keyboard.
- Named keys — the Direct implementation supports
return/enter,
escape, space, tab,
delete/backspace, and the four arrows. Unsupported names must be treated
as errors, not as successful no-ops. Re-verify these on the target iOS/WDA
combination before relying on them in a destructive workflow.
- Shortcuts — Direct supports
home and spotlight. switcher is
unsupported. Use a supported app-launch action instead of inventing a gesture.
- WDA and iPhone Mirroring are mutually exclusive (A/B-tested on hardware):
the on-phone XCUITest runner monopolizes the device's remote session, so
while Direct is active any Mirroring window may show an interrupted state.
That is expected. Do not try to repair or open Mirroring. Reconnect Direct
only when the current task needs it and the recovery conditions above are
met, using
phone_reconnect or POST /agent/mode {"mode":"agent"}; it needs the
phone unlocked. The target is canonical: change PHONE_REMOTE_UDID, rerun
setup, and restart the daemon to switch devices. Never pass a one-off UDID
during recovery.
mode=agent stuck / wda stays false → read status.setup_blocked_on
(warp|proxy|usb|trust|ddi|account). The #1 blocker is warp: Cloudflare WARP (or any
VPN) wedges the CoreDevice tunnel xcodebuild needs when its effective Split
Tunnel exclusions omit fe80::/10 or the device RSD ULA range fd00::/8.
If WARP is only needed for selected destinations, prefer Traffic only mode
with Split Tunnels Include limited to those destination IPs/CIDRs. This
avoids the Local proxy mode request timeout that can break long Git uploads.
Local proxy mode remains route-safe for short explicit HTTP(S) traffic. If
full-tunnel WARP is still required, add both IPv6 exclusions to the Zero Trust
device profile. warp-cli disconnect is only the temporary alternative.
Run setup-wda.sh doctor to distinguish the two states.
KeepAlive retains the last concrete blocker while its next preflight pass is
checking, so an empty value means the known prerequisite checks passed.
proxy means an enabled macOS HTTP/HTTPS/SOCKS entry is malformed or points
at a loopback port with no listener; start that proxy app or disable only the
stale entry. trust = a one-time "trust the Apple Development cert" tap on
the phone.
- Reconnect only for a current task that needs phone access. Status/health
checks and initialization leave released/offline devices alone. Check
recovery_owner:"daemon", resolve hint/setup_blocked_on, and wait out any
release/reconnect already in progress. Then send mode=agent once and poll
status until drivable:true; stop recovery when the task no longer needs
the phone. Repeated bootstrap requests obscure the USB, trust, DDI, or VPN
blocker.
- Do not batch guesses. During discovery, keep one action between reads.
Once a segment is understood, default to
phone_run_steps or
POST /agent/actions and combine up to 24 steps in one call instead of
paying one model/tool round-trip per tap. Put semantic wait_for gates around
page transitions and prefer tap_locator over coordinates or repeated labels.
The daemon validates the complete batch first, holds one WDA control lock,
and stops before every later step on the first failure. Fixed
pause/after_ms values are capped at 3 seconds and are only animation
settles, not proof that the right page appeared.
- Treat
outcome_unknown as possibly executed. A 502/504 after dispatch
can mean the phone acted but its acknowledgement was lost. Read elements or
a screenshot before deciding whether to send the action again; never blindly
replay text, scroll, back, payment, send, or delete actions.
- A reliable "reset to known state":
shortcut home, then shortcut spotlight
text <app name> + key return to launch any app.
Vision fallback: when the AX tree is unusable
AX-first is the invariant; vision is a screen-scoped fallback. Two failure
modes trigger it:
Mode A — tree too sparse (games, canvas/WebGL, custom-drawn UI).
/agent/elements succeeds; judge its additive ax_stats block
({n, n_interactive, labeled_frac, coverage, container_only, max_depth}):
- unusable → go vision:
n_interactive == 0 && container_only (e.g. the
1-element tree whose only row is the Application node — coverage ≈ 1.0
there is meaningless, never read coverage before those two gates).
- degraded → hybrid:
n_interactive < 3, or labeled_frac < 0.3, or
(coverage < 0.3 and not container_only). Use AX for the rows it has,
vision for the rest.
- Otherwise usable: stay AX-only. Legitimately sparse screens exist (video
player with one "Done" button); low
n alone is not a trigger — zero usable
targets for your current intent is.
Mode B — reading the tree kills the runner (KakaoTalk, issue #44: any AX
hierarchy snapshot crashes on-phone WDA; recovery is a 1–3 min rebuild). The
signal: /agent/elements returns 502 wda_source_failed / 504
wda_source_timeout twice on the same foreground app while
GET /agent/screenshot still succeeds. Cache that verdict per app for the
session — do not re-probe and pay the rebuild again.
The AX-free loop (you are the grounding model; no new infra):
GET /agent/screenshot → reason over the pixels yourself and pick a target.
- Act with the existing coordinate actions only —
tap / longpress /
swipe/scroll with normalized [0,1] coordinates. They dispatch via W3C
/actions and never resolve the hierarchy. Unsure of the point (< ~0.5
confidence)? Send nothing (that is not_sent, retry-safe): re-screenshot,
crop the region to look closer, or scroll the target into view — then
report with a screenshot if still lost.
- Verify with a post-action screenshot (settle ~300–800 ms), never with an
element read. HARD RULE for Mode-B apps: no
?return=delta, no
element/label taps, no element wait_for gates — each one resolves the
tree and takes the device down. The loop must be hermetically AX-free while
such an app is foreground. (In Mode A return=delta is merely useless — a
game's tree doesn't change; screenshot diff is the verifier there too.)
Degradation ladder on the existing outcome grammar:
| Situation |
Daemon says |
Treat as |
| You abstained (low confidence) |
nothing sent |
not_sent, retry-safe: re-screenshot → crop → scroll → report |
| Action applied |
outcome:applied |
dispatched, not achieved — screenshot diff decides |
| 502/504 after dispatch |
outcome_unknown, retry_safe:false |
read a screenshot before any replay (same rule as ever) |
wda_pre_dispatch_failed / transition |
not_sent, retry_safe:true |
retry after status settles |
| Applied but screen unchanged |
applied |
soft failure: one adjusted retry, then stop and report |
Vision guesses are weaker evidence than AX labels: destructive targets
(send / pay / delete / 2FA) keep their explicit-verification rules regardless
of channel. Any successful AX read on a new screen flips you back to AX-first,
and every successful vision sequence should be compiled into a flow per the
next section — vision is how you discover a flow, not how you run it the
tenth time.
Semantic intents (registered Shortcuts verbs)
When the task maps to a verb in GET /agent/intents (check once per session),
prefer one semantic call over driving the UI: POST /agent/intent with
{"name":"battery","args":{}} opens the bridge shortcut's deep link on-device
and returns an id; the structured result lands on /agent/inbox (peek with
GET, consume with POST /agent/inbox/drain, match on that id). The registry
is deliberately small and human-curated — an empty list is the normal answer,
and then you use the UI channel. Caveats: the Shortcuts app foregrounds
during the run (never interleave with a mid-flight UI flow; re-orient after),
and the first run of each verb needs a one-time interactive permission blessing
on the phone — if a call dispatches but no inbox reply appears, a pending
permission dialog on the phone screen is the first suspect. At-most-once rules
apply unchanged: outcome:"not_sent" is retry-safe, outcome:"unknown"
(intent_timeout/intent_dispatch_failed) means check the inbox and observe
state before ever re-sending a side-effecting verb.
Self-improvement: vision once → script forever
The first time you do a task, you're vision-guided (screenshot + reasoning at
every step). That's expensive. Your job is to never pay that cost twice:
- While solving, log intent and evidence, not only the wire payload —
record the intended target, the successful accessibility label/role, the
precondition, the action, and the observed postcondition. A screenshot or
snapshot element index is evidence from that moment, not a durable locator.
- Compile the successful trace into a guarded flow. Prefer a fresh-resolved
accessibility identifier, then a unique role + label + state, then a
container/anchor relationship. Zero matches and multiple matches both fail
closed. Never persist WDA element IDs,
/agent/elements indexes, or snapshot
tokens: they are valid only for the source read that produced them.
- Coordinates are the final fallback, not a reliability claim. A
normalized point can drift after an app update, A/B test, keyboard change,
dynamic list reorder, orientation change, or a different phone. If a
pixel-only action is unavoidable, bind it to a known screen signature and
an immediate postcondition; refuse to run when those checks do not match.
- Wait for states, not fixed sleeps. Poll a cheap element/status
postcondition with a bounded timeout. Use a fixed delay only for a transition
that has no observable state, and keep it short and explicit.
- Keep checkpoints, drop repeated reasoning. The happy path should need no
model and no screenshot tokens. On a failed checkpoint, stop and collect the
last action, current elements, status, and one screenshot for repair. Patch
the broken locator or branch and create a new flow revision; do not silently
guess a replacement target.
- Respect at-most-once delivery. Retry automatically only when the daemon
proves
outcome:not_sent and the action is still valid. For
outcome_unknown, inspect current state before deciding. Never blindly
replay text, scroll, back, payment, send, publish, comment, like, follow, or
delete actions.
- Keep secrets and user data out of v1 flow files. Explicit string inputs
use
{"kind":"type","input":"query"} and are resolved only for the current
browser run or flow run --input query=value; the saved JSON never contains
the value. CLI values may still appear in shell history or process
inspection. Never use flow parameters for passwords, session tokens,
one-time codes, private content, payment, send, publish, comment, like,
follow, or delete actions.
The research and flow contract live in
docs/scripted-flows-research.html. phone_run_steps is the bounded in-memory
runner for stable segments; the release-matched iphone-use-mcp binary
validates and runs version-1 JSON without a model, and flow update/list/info/ run <app>/<flow> mirrors the official registry into ~/.iphone-use/flows
(see Registry first above). A compiled flow that works should end up in the
registry: add app, category, risk, locale, and verified_on, then open a
PR to leeguooooo/iphone-use-flows. There is still no branching or repai
…(truncated)
1---2name: iphone-use3description: Use when a task needs a real iPhone — operating iOS apps that have no API (Apple Health, banking, IM apps), exporting on-phone data, tapping/typing/scrolling on the phone, or taking phone screenshots. Drives the iphone-use daemon's Direct/WDA HTTP agent API.4---56# iphone-use — drive a real iPhone78Control a physical iPhone through the [iphone-use](https://github.com/leeguooooo/iphone-use)9daemon: see the screen (`/agent/screenshot`), act on it (`/agent/input`), repeat.10The default backend is Direct: WebDriverAgent performs input on the phone and11the device-side screen service provides pixels. It does not need iPhone12Mirroring, Screen Recording, Accessibility, or the Mac cursor. The old13Mirroring path is an explicit compatibility backend only.1415## Prerequisites1617A Mac on your network running the daemon, with WDA and its 8100/9100 relays18configured for the intended iPhone. Direct control needs the phone unlocked,19trusted for development, and awake. Setup blockers are reported by status.2021WDA itself has no authentication. Daemon bearer auth protects `/agent/*`, but22does not protect the phone's own `8100/9100` listeners from another routable23host. Use Direct only on a trusted, isolated network; when practical, turn off24iPhone Wi-Fi and keep the supported Mac relays on USB loopback.2526```bash27HOST="${PHONE_REMOTE_URL:-http://127.0.0.1:44321}"28AUTH="Authorization: Bearer $PHONE_REMOTE_TOKEN" # daemon password or PHONE_REMOTE_AGENT_TOKEN29MUTATION="X-Phone-Control: 1" # required on every state-changing POST30```3132**Always probe first** — if this fails, stop and report (don't retry blindly;335 consecutive auth failures lock you out for 30s):3435```bash36curl -s -H "$AUTH" "$HOST/agent/status"37# {"ok":true,"backend":"direct","wda":true,"wda_actionable":true,38# "wda_locked":false,"drivable":true,"device_state":"ready",39# "screen_state":"waiting","released":false,"reconnecting":false,"hint":"",40# "setup_blocked_on":"", ...}41```4243**Status checks do not take control.** For initialization, status/health checks,44or when no unfinished user-requested task needs phone access, report the state45and stop. Do not reconnect, take a hold, or poll screenshots/elements to keep46the phone ready. Idle release is intentional.4748**Gate phone operations and screen/UI reads on `drivable:true`.** The current49user-requested task must need that access. `phone_target` is a legacy50Mirroring-window field and is not a Direct readiness signal.5152- `device_state:"ready"` + `drivable:true` → proceed with the requested task.53- `device_state:"locked"` → ask the operator to unlock the iPhone and keep it54 awake only if the current task needs phone access.55- `device_state:"released"` or `released:true` → expected idle state; leave it56 released unless the current task needs phone access. If that task cannot57 proceed, check `recovery_owner:"daemon"` and `hint` / `setup_blocked_on`.58 Once blockers are resolved and no release/reconnect is in progress, restart59 Direct/WDA once. This starts on-device automation, occupies the phone, and60 may require the operator to unlock it:6162 ```bash63 curl -s -H "$AUTH" -H "$MUTATION" -H 'Content-Type: application/json' \64 -X POST "$HOST/agent/mode" -d '{"mode":"agent"}'65 ```6667 With the bundled MCP server, call `phone_reconnect` instead under the same68 conditions. Then poll status until `drivable:true`; do not retry either69 path in a tight loop. Neither path accepts a transient UDID.70- `device_state:"releasing"` or `releasing:true` → do not reconnect or take a71 hold during release. Wait for it to finish only if the current task still72 needs phone access, then reassess status and the recovery conditions above.73- `reconnecting:true` → first inspect `setup_blocked_on`. If it is non-empty,74 follow the concrete `hint`; do not blindly wait or send another reconnect.75 Otherwise report `setup_phase` / `setup_message`, then wait and poll. A first76 build after an Xcode update can take several minutes. Never send input until77 `drivable:true`.78- `device_state:"blocked"` or `"offline"` → read `hint` and79 `setup_blocked_on` (`warp|proxy|usb|trust|ddi|account`). For a current task80 that needs phone access, resolve the blocker first; offline recovery follows81 the same task, ownership, and lifecycle conditions as released recovery.82- `device_state:"degraded"` → **not** a blocker; do not go looking for83 `setup_blocked_on`, it is empty by definition here. WDA answers, but the last84 read or action did not complete — usually a `/source` read that timed out on85 a heavy page, or a stalled app. Wait the `retry_after_secs` from the failing86 response (3s) and read again. The next health probe decides whether this87 clears back to `ready` or drops to `offline`; do not restart the service for88 it.89- Never switch to `mode=mirror` as automatic recovery. Mirror is an explicit90 operator-selected compatibility mode.9192## The API9394| Call | Purpose |95|---|---|96| `GET /agent/status` | `{ok, backend, device_state, screen_state, wda, wda_actionable, wda_locked, drivable, released, hint, setup_blocked_on, setup_phase, setup_message, …}` — gate on **`drivable`** |97| `GET /agent/elements` | **Direct/WDA UI as text**: `{"snapshot":"…","elements":[{kind,label,identifier?,rect,depth,value?,enabled?,visible?,accessible?,focused?,placeholder?},…]}` — prefer this over screenshots. Indexes and snapshot tokens are valid only for this read. Add `?since=<prior snapshot>` to get `{"snapshot":…,"baseline":…,"delta":{added,changed,removed,unchanged}}` instead of the full tree (much cheaper on multi-step flows; unknown baseline falls back to the full tree), plus `app_changed:{from,to}` if the foreground app moved since that baseline. Both shapes carry a read-only `ax_stats` usability block — see **Vision fallback** below — and a sparse `alert:{text,buttons}` block whenever a system alert (UIAlertController) is on screen. With `PHONE_REMOTE_ELEMENTS_AFFORDANCES=1` on the daemon, rows also carry sparse `actions` (named `perform` affordances), `selected`, and `min`/`max` |98| `GET /agent/screenshot` | Current phone screen as a device-side PNG; no Mirroring session required |99| `POST /agent/input` | One action (JSON body, below); requires `X-Phone-Control: 1` |100| `POST /agent/actions` | One bounded, fail-closed sequence of `action`, `wait_for`, and short `pause` steps; Direct/WDA only; requires `X-Phone-Control: 1` |101| `GET /agent/intents` | Curated **semantic intents** registry (registered Shortcuts verbs). Empty list + hint when none are set up |102| `POST /agent/intent` | Dispatch one registered verb (`{"name":"battery","args":{}}`); requires `X-Phone-Control: 1`. Results arrive on `/agent/inbox`, matched by the returned `id` |103104If a stale caller forgets the mutation header, the 403 response names105`required_header:"X-Phone-Control: 1"` and includes a retry hint. Correct the106request once; do not repeat the unactionable POST.107108**Give your own client at least 40s on `/agent/elements`.** The daemon keeps109rebuilding a failed source read until its own 35s deadline before answering110`wda_source_failed` — so a shorter client timeout (`curl -m 25`) cuts the111connection first and hands you an **empty body**, not JSON. That empty body is112your timeout, not a daemon fault and not an empty screen: re-read with a longer113one before concluding anything about the phone.114115Actions — coordinates are **normalized [0,1]** over the phone screen116(`0,0` top-left, `1,1` bottom-right), so they're resolution-independent:117118```bash119curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"tap","x":0.5,"y":0.3}'120curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"tap","label":"新备忘录"}' # exact label must have one match; ambiguity sends nothing121curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"tap","element":3,"snapshot":"<same elements response>"}'122curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"scroll","x":0.5,"y":0.5,"dx":0,"dy":60}'123curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"text","text":"Health"}'124curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"key","name":"return"}' # return|enter|send|go|search all fire the Return key — use this to submit a chat/search; coordinate-tapping a 3rd-party keyboard's 发送/前往 key ACKs but does NOT send125curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"shortcut","name":"home"}' # home|spotlight126curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"longpress","x":0.4,"y":0.6,"duration_ms":700}'127curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"keyboard"}' # dismiss the on-screen keyboard128curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"set_value","element":5,"snapshot":"…","value":"你好"}' # write a field directly (clear-then-type; "" clears); no focus tap, no keyboard dance129curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"scroll","element":7,"snapshot":"…","dy":120}' # scroll INSIDE that element's rect — never strays into a neighboring scroll view130curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"alert","button":"不是 li guo?"}' # press a system-alert button by exact name (UIAlertController; use this, NOT an element tap — alert buttons ACK a coordinate/element tap without acting)131curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"alert","action":"dismiss"}' # or the default accept/dismiss button132curl -s -H "$AUTH" -H "$MUTATION" -X POST "$HOST/agent/input" -d '{"type":"perform","element":9,"snapshot":"…","action":"increment"}' # named affordance on that element: increment|decrement (wheel/stepper/slider), adjust (+"value"), toggle, menu (long-press menu), double_tap, two_finger_tap, scroll_to_visible, pinch, rotate, force_press133```134135**Halve the round trips**: `POST /agent/input?return=delta` makes an applied136action settle briefly and return the post-action element change in the same137response — `{"ok":true,"snapshot":…,"baseline":…,"delta":{…}}` when the138baseline (the `since` query param, or the action's own `snapshot` field) is139still server-cached, else full `elements`. That replaces the separate140act-then-`GET /agent/elements` verify pair for routine steps.141142**Read the `settle` block before you trust the delta.** The action result and143the observation are separate facts: `ok:true` means the action applied, and144nothing in the observation can take that back — a slow or failed read comes145back in `settle.reason` beside `ok:true`, never as an unknown outcome you have146to escalate.147148| `settle.reason` | What it means | What to do |149|---|---|---|150| `stable` | Two consecutive reads matched over a tree with something in it. | The tree is worth reading — now check your postcondition against it. Stable is not "it worked". |151| `budget_exhausted` | Stability was not confirmed within the budget. The screen may still be moving, or `settle_ms` was too short (or `0`, which does not read the tree at all), or every read was container-only. | Do not conclude anything from the delta alone; re-read with `GET /agent/elements`. |152| `observation_failed` | The read path itself broke (plus the legacy `delta_error`). Running out of budget is *not* this — a cancelled read is not a broken one. | The action still applied. Verify with `GET /agent/elements`. |153154Two more flags matter: `sparse:true` means the tree held nothing to act on155(empty, or containers only) — two identical bare reads are evidence you cannot156see the screen, not that it settled, so never read `sparse` + unchanged as157"nothing happened". `stale:true` means the tree in the response is the LAST158good read, not a fresh one — either the refreshing read broke, or the budget159cut it off mid-sample. And `settled:true` only ever claims *the tree160stopped changing* — it is never a claim that what you wanted actually161happened; that judgment stays yours.162163`settle_ms`, `budget_ms` and `waited_ms` all describe the TREE read only; the164`alert` probe is separate and hard-capped at 1.5s. Tune with `settle_ms`165(default 15000, max 20000): drop it to `0` for a step you are going to verify166by screenshot anyway. How long a read takes varies with the device and the167screen — on one phone with a large tree it was measured at 6–7s — so the168default leaves room for two reads rather than assuming a fast one. It is a169ceiling, not a wait: reading stops as soon as two reads match. If you set a170budget shorter than a single read on that screen, expect `captures: 0`;171`budget_ms` in the answer tells you what it had to work with.172173**A failed `wait_for` tells you whether anyone could see the screen.** Its174`observation` block distinguishes three things you must not conflate:175`read:false` (no tree was ever obtained — this proves nothing, least of all176that an element is absent), `read:true, stale:true` (the screen was read, then177the read path failed; you are looking at the last valid observation, not the178current screen), and plain `read:true` (the screen was read and the condition179genuinely was not met). `sparse:true` means the tree was empty or180container-only; every `absent` locator it would have "satisfied" is listed in181`absent_unproven` and the step deliberately does not pass. **Never conclude an182element is gone from a tree that had nothing in it** — take a screenshot183instead.184185**`app_changed` means your tap landed in the wrong app.** Any delta response186(`?return=delta` or `GET /agent/elements?since=`) grows an187`app_changed:{from,to}` block when the frontmost app is not the one the188baseline was taken in. The usual cause is a banner notification dropping in189from the top and swallowing the tap, which opens *its* app — the delta then190describes a screen you never asked for. **Stop and re-orient when you see it**:191do not keep issuing steps against the old plan, and never treat the new192screen's elements as the ones you were aiming at. Recover by sending193`{"type":"home"}` and re-entering the intended app.194Its absence is not a guarantee — a tree with no `Application` row reports195nothing rather than guessing.196197After typing into a web form the keyboard covers the page's own submit/next198buttons — send `{"type":"keyboard"}` to dismiss it before tapping them.199`shortcut:"switcher"` is unsupported in Direct/WDA: iOS does not expose an200App Switcher action that WDA can synthesize. Do not send it and claim success.201202`{"type":"back"}` is a left-edge-swipe gesture, not a universal Back button: on203a screen with no in-app back target it can carry past the edge and **switch204apps** (e.g. drop you onto the Home screen or another app). Prefer resolving and205tapping the on-screen back control (`{"type":"tap","label":"…"}` / an element206tap) when one exists; use `back` only when you know the current screen has an207edge-swipe-back. For unattended runs, turn on a **Focus mode** first — a banner208notification dropping from the top will otherwise intercept a tap and open the209notifying app (hardware-seen: a chat banner hijacked a tap and opened WeChat).210211MCP alternative: the repo ships `iphone-use-mcp` (crates/mcp) with the212day-to-day safe subset: status, capabilities, reconnect, screenshot, elements,213coordinate and snapshot-bound element taps, strict unique-label taps, scroll,214text, named keys, Home/Spotlight, and `phone_run_steps` for one guarded215multi-step call.216`phone_capabilities` answers two separate questions without touching the217phone: what this build supports for the configured backend, and whether the218device can be driven right now (`blocked_by` names locked / released /219reconnecting / human_handoff / owned_by_other / offline; `ok: null` means no220evidence either way). It wakes nothing and takes no owner lease, so it is safe221before deciding what to attempt. Its scope is UI control and observation only.222For those seven act tools and `phone_capabilities`, the JSON comes back as MCP223`structuredContent` and the text block is only a preview (trimmed at 8 KiB), so224parse the structured field. `phone_run_steps` puts its complete batch result in225both, so either is safe to parse. Every other tool keeps its original shape:226`phone_elements` and the `phone_flow_*` tools return complete JSON as text227(including `phone_flow_run`'s execution result, passed or failed), and228`phone_screenshot` returns an image. Errors raised before a call reaches the229phone are explanatory text instead. The rule is simply: read230`structuredContent` when it is there, otherwise read `content` according to the231tool, and call `phone_status` / `phone_capabilities` when you need a232machine-readable state. When a tool cannot confirm what happened it answers233`outcome: "unknown"` with `retry_safe: false` — the request may have reached the234phone, so check `phone_elements` or `phone_screenshot` rather than resending.235Decide by the explicit `retry_safe` boolean, not by `outcome`: a local refusal236and a daemon-reported `not_sent` are both valid evidence when they carry237`retry_safe: true`, and `not_sent` on one step never means a whole batch can be238replayed.239240Every single-step act tool takes an optional `observe`. With `observe: true`241the daemon watches the screen settle and returns `settle`, `snapshot` and242`delta` alongside the result, so you do not need a separate `phone_elements`243round trip to see what an action produced. It is off by default because that244wait is latency the action does not otherwise pay. Read `settle.reason`:245`stable` means the screen stopped changing, `budget_exhausted` means the246observation window ran out and says NOTHING against the action itself, and247`observation_failed` means the read broke. `stale: true` marks a tree from the248previous successful read rather than the current screen; `sparse: true` marks249an empty or container-only tree, which is never reported stable.250Inside a sequence, `tap_locator` can act on the same strict251label/identifier/kind/value/focus/enabled/visible locator used by `wait_for`;252zero or multiple matches send no tap.253Maintenance and less common HTTP actions such as drag, app install/uninstall,254and target configuration are not exposed as native MCP tools.255The installed MCP binary also runs reviewed, versioned JSON without a model:256`iphone-use-mcp flow validate <file>` is offline, and257`iphone-use-mcp flow run <file> --input key=value` requires Direct plus258`drivable:true` before submitting the same guarded batch exactly once.259The browser's **流程** panel can record this v1 JSON without hand-writing it:260only acknowledged actions are kept, semantic labels are preferred, coordinate261gestures are marked fragile, and typed text becomes a named runtime parameter262without retaining the literal recorded value. Parameter values live only in263the current browser page or command invocation. When the264post-action element tree exposes a new unique identifier or foreground265application, the recorder adds a reviewed `wait_for` checkpoint instead of266relying on a fixed delay. It never copies arbitrary screen labels or values267into an automatic checkpoint because those may contain private content. A268recording that could not persist an action is an incomplete draft and cannot269run from the browser. A parameterized recording can be downloaded immediately,270but it cannot run until every required value is filled in.271272## The registry contract: check it, feed it, fix it273274The **official flow registry** ([`leeguooooo/iphone-use-flows`](https://github.com/leeguooooo/iphone-use-flows))275holds reviewed, deterministic per-app scripts that replay a whole task in one276call with no model and no screenshots. Every phone task you do has three277registry obligations. They are not optional politeness; they are how the next278run of the same task costs one call instead of thirty.279280```bash281MCP="$HOME/Applications/iPhoneUse.app/Contents/MacOS/iphone-use-mcp"282"$MCP" flow update # once per machine; mirrors the registry283"$MCP" flow list [--category health] # id · risk · verified · inputs · name284"$MCP" flow info health/export-all # metadata + step templates285PHONE_REMOTE_TOKEN="$PHONE_REMOTE_TOKEN" "$MCP" flow run system/spotlight-search --input query=Health286"$MCP" flow publish my.json --as health/export-all-zh-cn --alias 健康 --note "iPhone 17 Pro Max, iOS 26"287"$MCP" flow report health/export-all --result @run.json --note "profile button is now '资料'"288```289290MCP: `phone_flow_list / info / run / update / publish / report`.291292**1. Before acting — check.** Call `phone_flow_list` (or `flow list`) before293driving any app step by step. `phone_elements` already tells you: its294`registry` block lists the installed flows for the app on screen and says when295the store is empty (`phone_flow_update` once). A matching flow beats296exploration every time. Read `verified` and `risk`: `verified: no` means nobody297has proved the file on a real phone yet — run it, then take one checkpoint298screenshot. `risk: side_effect` (send/publish/pay/delete) refuses to run without299`--confirm` / `confirm=true`; confirm only after the user approved the exact300target and inputs.301302**2. After succeeding — feed.** When `phone_run_steps` completes 3+ steps, its303`registry.hint` reminds you: that sequence is a flow waiting to be saved. Write304it as v1 JSON (typed text → named `input`, page transitions guarded by305`wait_for`, `app` / `category` / `risk` / `locale` filled in, `verified_on` with306device, iOS, date), `flow validate` it, run it once from the file, then **tell307the user you would like to publish it** and, with their OK, call308`phone_flow_publish(confirm=true)` / `flow publish`. It forks if needed, adds309`app.json` for a new app (`aliases` = the app's foreground label in each310language, e.g. `Health`, `健康`, so `phone_elements` can surface it later),311rebuilds `index.json`, and opens the PR. Unverified files open as draft PRs.312Publishing is the default outcome of a successful multi-step task, not an313extra; skip it only for one-off or private tasks.314315**3. On failure — fix.** A flow that stops (`failed_step`, `element_not_found`,316`missing_present`) is a registry bug until proven otherwise. Read317`phone_elements` to see where the phone stopped. If the flow is wrong (label318changed, app updated, locale mismatch), tell the user and, with their OK, call319`phone_flow_report(id, note, confirm=true)` / `flow report`: the last failure of320that id is already captured (failed step, redacted daemon result, daemon321version; screen labels and typed text are stripped). If you can also fix the322locator, publish the corrected file with a bumped `verified_on` and mention the323issue. Do not report a phone that was merely locked, offline, or on the wrong324app. Never replay a failed flow blindly — the first tap may already have acted.325326**Compat is computed for you.** Every flow in `flow list` / `phone_flow_list` /327the `registry` block carries `compat`, derived from the flow's `verified_on`328app version (the iOS version for Apple system apps) against what this phone has329installed (`flow apps` shows the inventory):330331| compat | what you do |332|---|---|333| `verified` | run it |334| `untested-newer` | the app updated since the last verification: run it, take one checkpoint screenshot, and if it worked publish the new `verified_on` |335| `incompatible` / `broken` | do not run (`flow run` refuses without `--force`): explore by hand, then publish the fixed flow |336| `needs-verification` | nightly re-verification failed and nobody fixed it yet: treat like broken, fixing it is the contribution |337| `draft` | no hardware record: run with a checkpoint, then publish `verified_on` |338| `unknown` | no version data (daemon has no `/agent/apps` and is not on loopback): behave as `untested-newer` |339340Flow labels are locale-specific (`locale`): an `en` flow fails closed on a341Chinese phone with zero matches and no tap. Pick the variant that matches the342device language or record one (`health/export-all-zh-cn`). Keep your own flows343next to the official ones with `flow add <file> --as <app>/<name>`; they344survive `flow update`. Only the official source exists — there is no345`sources add`.346347## The loop: see → act → verify3483491. **See**: when `drivable:true`, use `GET /agent/elements` first — it's text350 (10× cheaper than vision) and carries exact labels. Fall back to `screenshot`351 when you need pixels (images, maps, unlabeled UI).3522. **Act**: when two or more consecutive actions are already understood, safe,353 and verifiable, send the longest stable segment as one `phone_run_steps`354 call with page transitions guarded by `wait_for`. Use one atomic action only355 while exploring an unknown screen, waiting for human confirmation, or356 isolating a failed checkpoint. Prefer357 `phone_tap_element(element,snapshot)` after choosing by358 identifier/kind/label/state. `phone_tap_label` is safe only for an exact359 unique label; zero or multiple matches send nothing. Use raw coordinates360 only when the control has no semantic target. For stateful controls361 (Switch / Slider / Stepper / PickerWheel), prefer `perform` over any tap —362 see the Switches & sliders rule below; when a row advertises `actions`363 (affordances flag on), use the listed action instead of guessing a gesture.3643. **Verify**: `elements` (or `screenshot`) again → confirm the expected change365 before the next step. Treat a non-2xx read or an empty tree with `error` as a366 failed checkpoint, even if an immediately preceding status said367 `drivable:true`; current daemons revoke cached actionability on either read368 path failure.369370Operational rules. Hardware evidence is called out only where it exists; a371documented or unit-tested action is not automatically a current-device proof:372373- **System alerts** (permission prompts, "以 X 的身份设置?", sign-in confirmations):374 they show up as the `alert:{text,buttons}` block on `/agent/elements`, and a375 coordinate or element tap on an alert button is **acknowledged but often does376 nothing** (same false-success class as switches). Dismiss/answer them with377 `{"type":"alert","button":"<exact button text>"}` or378 `{"type":"alert","action":"accept"|"dismiss"}`.379- **One session per phone.** Name yourself on every state-changing request380 with `X-Phone-Owner: <session-name>` (the MCP server does this for you, from381 `PHONE_REMOTE_OWNER` or `mcp-<pid>`). Status then shows `owner` and382 `owner_lease_remaining_secs`. If `owner` is someone else, do not drive the383 phone: control calls answer `409 phone_owned` with the owner's name — wait,384 ask that session to `POST /agent/owner {"release":true}` when it is done,385 and never send `X-Phone-Owner-Takeover: 1` unless the user confirms the386 other session is abandoned. Release your own lease when your task ends.387- **Human-in-the-loop pauses** within a current user-requested phone task388 (waiting for the operator to type a password, approve a prompt, or fetch a389 code): use a bounded HTTP `POST /agent/hold {"secs":600}` before the pause390 if the task needs to retain the phone. Clear it with `{"secs":0}` when the391 pause ends or the task completes/is cancelled. Status reports392 `hold_remaining_secs`. A hold prevents idle release; it does not start WDA393 or prove readiness. Do not use or renew it for initialization, health checks,394 or to keep the phone ready without a task. There is no bundled MCP hold tool.395 A `503 device_release_in_progress` (with `Retry-After`) means release already396 started. Only if the current task still needs phone access, wait for release397 to finish and reassess status; reconnect once under the recovery conditions398 above, and take another bounded hold only if the task still needs that pause.399- **Switches & sliders** (hardware-verified, iOS 27): a coordinate tap on a400 Switch is **acknowledged but does not flip it** — a silent false success.401 The only reliable path is `{"type":"perform","element":N,"snapshot":"…",402 "action":"toggle"}`, which works on both the labeled full-row switch and the403 bare paired `UISwitch`. Sliders: `perform` `increment`/`decrement` steps404 ~10%, `adjust` takes a normalized `"value"` in 0..1. Verify the new `value`405 from a fresh `elements` read (or the `?return=delta` response) — never trust406 the ACK alone for state changes.407- **Scroll**: positive `dy` reveals content farther down; negative `dy` reveals408 content above. Positive `dx` reveals content to the right. A scroll is an409 atomic WDA swipe, not a stream of wheel events.410- **Text input** — focus and verify a field first, then `{"type":"text"}`.411 Direct/WDA sends Unicode on-device, so ASCII and CJK land without touching412 the Mac clipboard or keyboard.413- **Named keys** — the Direct implementation supports `return`/`enter`,414 `escape`, `space`, `tab`,415 `delete`/`backspace`, and the four arrows. Unsupported names must be treated416 as errors, not as successful no-ops. Re-verify these on the target iOS/WDA417 combination before relying on them in a destructive workflow.418- **Shortcuts** — Direct supports `home` and `spotlight`. `switcher` is419 unsupported. Use a supported app-launch action instead of inventing a gesture.420- **WDA and iPhone Mirroring are mutually exclusive** (A/B-tested on hardware):421 the on-phone XCUITest runner monopolizes the device's remote session, so422 while Direct is active any Mirroring window may show an interrupted state.423 That is expected. Do not try to repair or open Mirroring. Reconnect Direct424 only when the current task needs it and the recovery conditions above are425 met, using `phone_reconnect` or `POST /agent/mode {"mode":"agent"}`; it needs the426 phone unlocked. The target is canonical: change `PHONE_REMOTE_UDID`, rerun427 setup, and restart the daemon to switch devices. Never pass a one-off UDID428 during recovery.429- **`mode=agent` stuck / `wda` stays false → read `status.setup_blocked_on`**430 (`warp|proxy|usb|trust|ddi|account`). The #1 blocker is **`warp`**: Cloudflare WARP (or any431 VPN) wedges the CoreDevice tunnel xcodebuild needs when its effective Split432 Tunnel exclusions omit `fe80::/10` or the device RSD ULA range `fd00::/8`.433 If WARP is only needed for selected destinations, prefer **Traffic only** mode434 with Split Tunnels **Include** limited to those destination IPs/CIDRs. This435 avoids the Local proxy mode request timeout that can break long Git uploads.436 Local proxy mode remains route-safe for short explicit HTTP(S) traffic. If437 full-tunnel WARP is still required, add both IPv6 exclusions to the Zero Trust438 device profile. `warp-cli disconnect` is only the temporary alternative.439 Run `setup-wda.sh doctor` to distinguish the two states.440 KeepAlive retains the last concrete blocker while its next preflight pass is441 checking, so an empty value means the known prerequisite checks passed.442 `proxy` means an enabled macOS HTTP/HTTPS/SOCKS entry is malformed or points443 at a loopback port with no listener; start that proxy app or disable only the444 stale entry. `trust` = a one-time "trust the Apple Development cert" tap on445 the phone.446- **Reconnect only for a current task that needs phone access.** Status/health447 checks and initialization leave released/offline devices alone. Check448 `recovery_owner:"daemon"`, resolve `hint`/`setup_blocked_on`, and wait out any449 release/reconnect already in progress. Then send `mode=agent` once and poll450 status until `drivable:true`; stop recovery when the task no longer needs451 the phone. Repeated bootstrap requests obscure the USB, trust, DDI, or VPN452 blocker.453- **Do not batch guesses.** During discovery, keep one action between reads.454 Once a segment is understood, default to `phone_run_steps` or455 `POST /agent/actions` and combine up to 24 steps in one call instead of456 paying one model/tool round-trip per tap. Put semantic `wait_for` gates around457 page transitions and prefer `tap_locator` over coordinates or repeated labels.458 The daemon validates the complete batch first, holds one WDA control lock,459 and stops before every later step on the first failure. Fixed460 `pause`/`after_ms` values are capped at 3 seconds and are only animation461 settles, not proof that the right page appeared.462- **Treat `outcome_unknown` as possibly executed.** A 502/504 after dispatch463 can mean the phone acted but its acknowledgement was lost. Read elements or464 a screenshot before deciding whether to send the action again; never blindly465 replay text, scroll, back, payment, send, or delete actions.466- A reliable "reset to known state": `shortcut home`, then `shortcut spotlight`467 + `text <app name>` + `key return` to launch any app.468469## Vision fallback: when the AX tree is unusable470471AX-first is the invariant; vision is a screen-scoped fallback. Two failure472modes trigger it:473474**Mode A — tree too sparse** (games, canvas/WebGL, custom-drawn UI).475`/agent/elements` succeeds; judge its additive `ax_stats` block476(`{n, n_interactive, labeled_frac, coverage, container_only, max_depth}`):477478- **unusable → go vision**: `n_interactive == 0 && container_only` (e.g. the479 1-element tree whose only row is the `Application` node — `coverage` ≈ 1.0480 there is meaningless, never read coverage before those two gates).481- **degraded → hybrid**: `n_interactive < 3`, or `labeled_frac < 0.3`, or482 (`coverage < 0.3` and not `container_only`). Use AX for the rows it has,483 vision for the rest.484- Otherwise **usable**: stay AX-only. Legitimately sparse screens exist (video485 player with one "Done" button); low `n` alone is not a trigger — zero usable486 targets for your current intent is.487488**Mode B — reading the tree kills the runner** (KakaoTalk, issue #44: any AX489hierarchy snapshot crashes on-phone WDA; recovery is a 1–3 min rebuild). The490signal: `/agent/elements` returns 502 `wda_source_failed` / 504491`wda_source_timeout` twice on the same foreground app while492`GET /agent/screenshot` still succeeds. Cache that verdict per app for the493session — do not re-probe and pay the rebuild again.494495**The AX-free loop** (you are the grounding model; no new infra):4964971. `GET /agent/screenshot` → reason over the pixels yourself and pick a target.4982. Act with the **existing** coordinate actions only — `tap` / `longpress` /499 `swipe`/`scroll` with normalized `[0,1]` coordinates. They dispatch via W3C500 `/actions` and never resolve the hierarchy. Unsure of the point (< ~0.5501 confidence)? Send nothing (that is `not_sent`, retry-safe): re-screenshot,502 crop the region to look closer, or scroll the target into view — then503 report with a screenshot if still lost.5043. Verify with a **post-action screenshot** (settle ~300–800 ms), never with an505 element read. **HARD RULE for Mode-B apps: no `?return=delta`, no506 element/label taps, no element `wait_for` gates** — each one resolves the507 tree and takes the device down. The loop must be hermetically AX-free while508 such an app is foreground. (In Mode A `return=delta` is merely useless — a509 game's tree doesn't change; screenshot diff is the verifier there too.)510511**Degradation ladder** on the existing outcome grammar:512513| Situation | Daemon says | Treat as |514|---|---|---|515| You abstained (low confidence) | nothing sent | `not_sent`, retry-safe: re-screenshot → crop → scroll → report |516| Action applied | `outcome:applied` | *dispatched*, not *achieved* — screenshot diff decides |517| 502/504 after dispatch | `outcome_unknown`, `retry_safe:false` | read a screenshot before any replay (same rule as ever) |518| `wda_pre_dispatch_failed` / transition | `not_sent`, `retry_safe:true` | retry after status settles |519| Applied but screen unchanged | `applied` | soft failure: one adjusted retry, then stop and report |520521Vision guesses are *weaker* evidence than AX labels: destructive targets522(send / pay / delete / 2FA) keep their explicit-verification rules regardless523of channel. Any successful AX read on a new screen flips you back to AX-first,524and every successful vision sequence should be compiled into a flow per the525next section — vision is how you discover a flow, not how you run it the526tenth time.527528## Semantic intents (registered Shortcuts verbs)529530When the task maps to a verb in `GET /agent/intents` (check once per session),531prefer one semantic call over driving the UI: `POST /agent/intent` with532`{"name":"battery","args":{}}` opens the bridge shortcut's deep link on-device533and returns an `id`; the structured result lands on `/agent/inbox` (peek with534GET, consume with `POST /agent/inbox/drain`, match on that `id`). The registry535is deliberately small and human-curated — an empty list is the normal answer,536and then you use the UI channel. Caveats: the Shortcuts app **foregrounds537during the run** (never interleave with a mid-flight UI flow; re-orient after),538and the first run of each verb needs a one-time interactive permission blessing539on the phone — if a call dispatches but no inbox reply appears, a pending540permission dialog on the phone screen is the first suspect. At-most-once rules541apply unchanged: `outcome:"not_sent"` is retry-safe, `outcome:"unknown"`542(`intent_timeout`/`intent_dispatch_failed`) means check the inbox and observe543state before ever re-sending a side-effecting verb.544545## Self-improvement: vision once → script forever546547The first time you do a task, you're vision-guided (screenshot + reasoning at548every step). That's expensive. **Your job is to never pay that cost twice**:5495501. **While solving, log intent and evidence, not only the wire payload** —551 record the intended target, the successful accessibility label/role, the552 precondition, the action, and the observed postcondition. A screenshot or553 snapshot element index is evidence from that moment, not a durable locator.5542. **Compile the successful trace into a guarded flow.** Prefer a fresh-resolved555 accessibility identifier, then a unique role + label + state, then a556 container/anchor relationship. Zero matches and multiple matches both fail557 closed. Never persist WDA element IDs, `/agent/elements` indexes, or snapshot558 tokens: they are valid only for the source read that produced them.5593. **Coordinates are the final fallback, not a reliability claim.** A560 normalized point can drift after an app update, A/B test, keyboard change,561 dynamic list reorder, orientation change, or a different phone. If a562 pixel-only action is unavoidable, bind it to a known screen signature and563 an immediate postcondition; refuse to run when those checks do not match.5644. **Wait for states, not fixed sleeps.** Poll a cheap element/status565 postcondition with a bounded timeout. Use a fixed delay only for a transition566 that has no observable state, and keep it short and explicit.5675. **Keep checkpoints, drop repeated reasoning.** The happy path should need no568 model and no screenshot tokens. On a failed checkpoint, stop and collect the569 last action, current elements, status, and one screenshot for repair. Patch570 the broken locator or branch and create a new flow revision; do not silently571 guess a replacement target.5726. **Respect at-most-once delivery.** Retry automatically only when the daemon573 proves `outcome:not_sent` and the action is still valid. For574 `outcome_unknown`, inspect current state before deciding. Never blindly575 replay text, scroll, back, payment, send, publish, comment, like, follow, or576 delete actions.5777. **Keep secrets and user data out of v1 flow files.** Explicit string inputs578 use `{"kind":"type","input":"query"}` and are resolved only for the current579 browser run or `flow run --input query=value`; the saved JSON never contains580 the value. CLI values may still appear in shell history or process581 inspection. Never use flow parameters for passwords, session tokens,582 one-time codes, private content, payment, send, publish, comment, like,583 follow, or delete actions.584585The research and flow contract live in586`docs/scripted-flows-research.html`. `phone_run_steps` is the bounded in-memory587runner for stable segments; the release-matched `iphone-use-mcp` binary588validates and runs version-1 JSON without a model, and `flow update/list/info/589run <app>/<flow>` mirrors the official registry into `~/.iphone-use/flows`590(see **Registry first** above). A compiled flow that works should end up in the591registry: add `app`, `category`, `risk`, `locale`, and `verified_on`, then open a592PR to `leeguooooo/iphone-use-flows`. There is still no branching or repai593594…(truncated)