mobster-cli 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,793 @@
1
+ """Device adapters: the driver contract and WebDriverAgent (WDA). Extensions may register more."""
2
+
3
+ import abc
4
+ import json
5
+ import os
6
+ import time
7
+ from urllib.parse import quote
8
+
9
+ from .state import ADJUSTABLE_ROLES, HOST_KEY_OPERATIONS, WDA_HOST_KEY_NAMES, validate_bundle_id
10
+ from .transport import HTTP, Deadline, TransportError, error_name
11
+ from .wda import timeouts as wda_timeouts
12
+ from .wda import watchdog as wda_watchdog
13
+ from .wda.gestures import (
14
+ WDA_GLIDE_SPAN,
15
+ WDA_SWIPE_SECONDS,
16
+ WDAGestures,
17
+ _swipe_points,
18
+ clear_stroke_start,
19
+ switch_point,
20
+ )
21
+ from .wda.guards import finite_positive, grounded_target
22
+ from .wda.observe import WDA_STATE_SECONDS, WDAObserve
23
+ from .wda.text import WDAText
24
+
25
+ # Snapshot depth. 25 was chosen when a full-depth widget home screen took
26
+ # 40.5 s with WDA's `visible` attribute; without it the home screen reads in
27
+ # ~270 ms at depth 80. Depth 25 then blinded apps with deep view trees
28
+ # (Spotify: text recall 0.23 at 25 vs 0.77 at 50, +70 ms). 60 stays within the
29
+ # parser's 64-level nesting limit.
30
+ WDA_SNAPSHOT_MAX_DEPTH = 60
31
+ # WDA's default idle wait held every tap for ~1.2 s; with both at 0 a W3C tap
32
+ # returns in ~470 ms. Mid-animation reads are then possible, so settling
33
+ # requires consecutive identical reads instead (WDA.wait_for_change).
34
+ # A read that cannot finish fails in seconds instead of hanging: WDA serves one request
35
+ # at a time, and on 2026-09-23 stuck TikTok reads (default 15 s snapshot timeouts,
36
+ # stacked by retries) froze it for 21 minutes. Normal reads take 0.13-0.21 s.
37
+ WDA_SNAPSHOT_TIMEOUT = 6
38
+ WDA_SESSION_SETTINGS = {"snapshotMaxDepth": WDA_SNAPSHOT_MAX_DEPTH,
39
+ "waitForIdleTimeout": 0, "animationCoolOffTimeout": 0,
40
+ "snapshotTimeout": WDA_SNAPSHOT_TIMEOUT, "customSnapshotTimeout": WDA_SNAPSHOT_TIMEOUT}
41
+ # Requests after which the page shown may be another one.
42
+ # (Switching to an app and back keeps its tab: only a fresh launch or a URL opens another.)
43
+ PAGE_SWITCHING_PATHS = frozenset({"/wda/apps/launch", "/url"})
44
+ # /window/size resolves the active application, which can hang on a system overlay.
45
+ WDA_WINDOW_SIZE_SECONDS = 3
46
+ # Until the exact screen center is known: a point inside every iPhone screen
47
+ # (the smallest is 320x568 pt), below the banner area and away from the edges.
48
+ WDA_FALLBACK_DETECTION_POINT = "160,320"
49
+ # snapshotMaxChildren (WDA's default is unbounded): a bound on one node's children in a snapshot.
50
+ # iOSWorld mem-046's runner died on CityRide's map screens in 2 of 4 runs (ConnectionReset, then refused).
51
+ WDA_MAX_CHILDREN = 300
52
+ # maxTypingFrequency on a simulator (WDA's default 60 types ~57 characters a second). Measured on an
53
+ # iOSWorld simulator (F2, 26 Sep; 20 bodies of 500 characters typed into a Notes body at each rate, read
54
+ # back whole): 60, 120, 180, 240, 360, 480, 720 and 960 all dropped no character; 500 characters took
55
+ # 8.8 / 4.6 / 3.3 / 2.6 / 1.9 / 1.6 / 1.2 / 1.1 s. The phone keeps WDA's default until measured there.
56
+ WDA_SIMULATOR_TYPING_FREQUENCY = 960
57
+ # Text at least this long goes in through the pasteboard and the edit menu's Paste, not keystrokes, at
58
+ # WDA's typing rate (~57 characters a second: 1,000 take ~18 s, a paste ~2.4 s). A faster rate moves the
59
+ # line (WDA.tune): at 960 a simulator typed 1,000 characters in 1.5 s, faster than the paste (F2, 26 Sep).
60
+ WDA_PASTE_MIN_CHARS = 200
61
+ # Characters typed per second per unit of maxTypingFrequency at high rates (450 a second at 960), and a
62
+ # paste's usual seconds: typing is kept while it is expected to be quicker.
63
+ WDA_TYPED_PER_RATE, WDA_PASTE_SECONDS = .47, 2.5
64
+ # Recovery: how long a restarted or re-supervised runner has to answer /status again.
65
+ WDA_RECOVER_SECONDS = 90
66
+ # Opening or closing a USB iPhone's screen-capture device (WATCH hears and sees through it) resets the phone's USB
67
+ # network link: a blip of 0.4-1.1 s at once, then xcodebuild, which hosts the runner over that link, exits 6-16 s
68
+ # later and the runner with it, so the session is gone (iPhone 15 Pro, iOS 26.0.1, 14 Oct 2026: 2 of 2 opens and
69
+ # 2 of 2 closes; finding the device without opening it changed nothing in 60 s). after_capture waits out the blip,
70
+ # then restarts the runner at once (runner_restart) instead of waiting for the old one to die.
71
+ WDA_LINK_BLIP_SECONDS = 2.0
72
+ # Without runner_restart (a runner Mobster doesn't manage): how long the doomed runner may keep answering.
73
+ WDA_LINK_DOOMED_SECONDS = 25.0
74
+
75
+
76
+ def _env_int(name):
77
+ """A positive integer from the environment, else None."""
78
+ try:
79
+ value = int(os.environ.get(name, "") or 0)
80
+ except ValueError:
81
+ return None
82
+ return value if value > 0 else None
83
+
84
+
85
+ class _Declared(abc.ABCMeta):
86
+ """Driver's metaclass: a driver is checked against its declared capabilities once its ``__init__`` has run."""
87
+
88
+ def __call__(cls, *args, **kwargs):
89
+ driver = super().__call__(*args, **kwargs)
90
+ check_capabilities(driver)
91
+ return driver
92
+
93
+
94
+ class Driver(abc.ABC, metaclass=_Declared):
95
+ """Required device contract. Instantiation fails when a method is missing.
96
+
97
+ Optional capabilities are probed with ``getattr`` at the call site, because each has a load-bearing fallback.
98
+ ``OPTIONAL`` lists them with what their absence means, and ``capabilities`` names the ones a driver has:
99
+ instantiation fails when it names one the new driver lacks or leaves one out, and ``operations_for`` derives
100
+ what a loop may offer the driver from it. tests/test_driver_contract.py
101
+ fails when code probes a driver for a name that isn't listed.
102
+ """
103
+
104
+ OPTIONAL = {
105
+ # Reading
106
+ "observe_ready": "a settled read; without it, observe",
107
+ "wait_for_change": "a native wait for the screen to change; without it, observe with the whole budget",
108
+ "capture_preview": "an operator preview frame; without it, another capture route (no default)",
109
+ "active_app": "the bundle id in front; without it, the snapshot's bundle_id",
110
+ "slow_front": "the app in front when its tree is too slow to wait for, confirmed without the tree; without it, "
111
+ "the front app comes from active_app",
112
+ "pixel_snapshot": "the screen in front as a tree read from a screenshot, for an app whose tree is too slow; "
113
+ "without it, the evidence read waits for the tree",
114
+ "frame_clock": "a FrameClock on the phone's video (frame_clock.attach_frame_clock sets it)",
115
+ "settled_by": "how the last settle was proven (one_read, agreeing_reads...); without it, unproven",
116
+ "predicted_settle": "the last settle's prediction and outcome (MOBSTER_EXPERIMENTAL_SETTLE); without it, none",
117
+ "settles_early": "the last settle ended before its quiet period, on a frame-clock proof",
118
+ "settles_on_one_read": "a settle may end on one read (wait_for_change's single_read: True, or \"rest\" for one "
119
+ "read only when the pixels already rest)",
120
+ "ax_only_steps": "steps left that read the tree with no screenshot",
121
+ "next_step": "called at the top of each turn; returns the steps left of ax_only_steps",
122
+ "source_reads": "how many screen reads this driver made (the run's timing)",
123
+ "source_seconds": "the seconds those reads took",
124
+ "rich_rows": "reads disabled and selected controls (WDA.tune(rich=True))",
125
+ "tune": "set the driver up for the agent (rich rows, glides, typing rate); without it, as it is",
126
+ "evidence_source": "the screen's tree as WebDriverAgent-shaped XML, read by the connection itself (Android); "
127
+ "without it, verify reads WebDriverAgent's /source through call",
128
+ # Acting beyond execute
129
+ "tap_point": "tap a screen point (TAP_XY, DISMISS), with informational purpose; without it, those actions fail",
130
+ "dismiss_at": "where DISMISS taps; without it, tap_point",
131
+ "long_press": "LONG_PRESS; without it, the action fails",
132
+ "set_value": "set an adjustable control (a picker wheel); without it, SET_TEXT on one fails",
133
+ "write_text": "type with a read-back and one repair; without it, execute TYPE, which types at the end",
134
+ "replace_text": "clear and type through one element reference; without it, clear_text then TYPE",
135
+ "clear_text": "empty a field; without it, SET_TEXT to nothing fails",
136
+ "relaunch_app": "quit and reopen an app (a fresh start); without it, LAUNCH_APP",
137
+ "launch": "open an app and return its first screen (App X-ray); without it, LAUNCH_APP, a pause and a read",
138
+ "edge_back": "the left-edge swipe that goes back (App X-ray); without it, a raw drag on "
139
+ "WebDriverAgent, else none",
140
+ "open_url": "open a link on the phone; without it, WebDriverAgent's /url through call",
141
+ "drag_next": "a slow short drag that reveals a list row's buttons",
142
+ "last_swipe_glided": "the last swipe was a glide, so it can't have flung past rows",
143
+ "last_relaunch": "when the driver last relaunched the app",
144
+ "recover": "restart a stuck connection; without it, the run ends on the error",
145
+ "after_capture": "bring the connection back after a USB iPhone's capture device closed, which resets its link "
146
+ "(WATCH, MCP listen); without it, the next step's own recovery does",
147
+ # WebDriverAgent only: the skills that read Notification Center and codes send raw requests
148
+ "call": "a raw WebDriverAgent request (method, path, body); without it, those skills aren't offered",
149
+ "call_trace": "the WebDriverAgent requests of the last step, for timing reports",
150
+ "url": "the WebDriverAgent address",
151
+ "prefix": "the WebDriverAgent session path",
152
+ "http": "the WebDriverAgent HTTP client (ios.system reads the iOS version from /status); without it, unknown",
153
+ # Image-only connections
154
+ "helper": "the native helper that captures and acts (iPhone Mirroring, Cable + Bluetooth)",
155
+ # iOS's own moves (ios.system), on any connection
156
+ "recent_dialogs": "system_dialogs.Recent owned by this driver; absence starts with no "
157
+ "clipboard or link evidence",
158
+ "opens_by_bundle": "False: apps can't open by bundle id, so open_app goes by name (Spotlight, App Library)",
159
+ "reads_front_app": "False: active_app is a guess, so an app counts as opened when iOS's own screens left",
160
+ "system_shortcut": "send the connection's key for home, spotlight or app_switcher; without it, Home and taps",
161
+ "type_focused": "type into the field that has the keyboard; without it, TYPE on the field element",
162
+ "clear_focused": "empty the field that has the keyboard; without it, its Clear text button when shown",
163
+ "ios_version": "the phone's iOS version when the connection knows it; without it, WebDriverAgent's /status",
164
+ "effects": "the methods that change the device, on a driver with no WebDriverAgent transport, so a verify run "
165
+ "journals each call; without it, the run journals WebDriverAgent's HTTP",
166
+ # VerifyRun's private wrappers: absence means the driver is not owned by that run layer yet.
167
+ "_verify_action_driver_journal": "the action journal owning recovery callbacks; without it, this run may "
168
+ "guard them",
169
+ "_verify_source_owner": "the run observing accessibility source reads; without it, this run may attach",
170
+ }
171
+
172
+ capabilities: frozenset[str] = frozenset()
173
+ can_type = False
174
+ last_image = None
175
+
176
+ @abc.abstractmethod
177
+ def observe(self, timeout=10):
178
+ """Read the current screen. Never mutates the device."""
179
+
180
+ @abc.abstractmethod
181
+ def execute(self, operation, target, snapshot, text=None, timeout=10):
182
+ """Dispatch one grounded action. At most once; never retried here."""
183
+
184
+ @abc.abstractmethod
185
+ def close(self):
186
+ """Release driver resources. Never terminates the caller's session."""
187
+
188
+
189
+ def check_capabilities(driver):
190
+ """Raise TypeError unless ``driver.capabilities`` names exactly the Driver.OPTIONAL attributes ``driver`` has."""
191
+ declared = frozenset(driver.capabilities)
192
+ present = frozenset(name for name in Driver.OPTIONAL if hasattr(driver, name))
193
+ if declared != present:
194
+ raise TypeError(f"{type(driver).__name__}.capabilities declares {sorted(declared - present)} it doesn't have "
195
+ f"and leaves out {sorted(present - declared)} it has. Set capabilities to the Driver.OPTIONAL "
196
+ "names the driver has.")
197
+
198
+
199
+ # What an operation needs beyond execute: any one of these Driver.OPTIONAL names. An operation not listed needs
200
+ # nothing more (READ_LIST and SCROLL_TO swipe and read through execute and observe).
201
+ OPERATION_NEEDS = {"LONG_PRESS": frozenset({"long_press"}), "TAP_XY": frozenset({"tap_point"}),
202
+ "DISMISS": frozenset({"dismiss_at", "tap_point"})}
203
+
204
+
205
+ def operations_for(capabilities, base_operations, role=None):
206
+ """The operations of ``base_operations``, in order, that a loop may offer a driver with ``capabilities``: each
207
+ one whose OPERATION_NEEDS the driver meets. With ``role``, those it may offer on an element of that role:
208
+ SET_TEXT on an adjustable control (a picker wheel) needs set_value, while on a field it clears and types
209
+ through execute."""
210
+ def offered(op):
211
+ needs = frozenset({"set_value"}) if op == "SET_TEXT" and role in ADJUSTABLE_ROLES else OPERATION_NEEDS.get(op)
212
+ return needs is None or not needs.isdisjoint(capabilities)
213
+ return tuple(op for op in base_operations if offered(op))
214
+
215
+
216
+ def call_kind(method, path):
217
+ """A WDA request's kind for timing reports: "GET source", "POST element/*/value"."""
218
+ parts = path.split("?", 1)[0].strip("/").split("/")
219
+ if len(parts) > 1 and parts[0] == "element" and parts[1] != "active":
220
+ parts[1] = "*"
221
+ return method + " " + "/".join(parts)
222
+
223
+
224
+ def resolve_wda_session(url, preferred=None, timeout=5, create=True):
225
+ """The WDA session to drive: WDA serves one at a time, so its own report wins.
226
+
227
+ A configured id goes stale whenever the runner restarts (measured: every
228
+ restart issued a new id and the server failed until restarted by hand).
229
+ Preference: the session WDA reports as current, then ``preferred`` only if
230
+ WDA reports none and it is still accepted, else a newly created session.
231
+ Returns None when WDA is unreachable or has no session and ``create`` is off.
232
+ """
233
+ client = HTTP(url)
234
+ try:
235
+ status = client.request("GET", "/status", timeout=timeout)
236
+ current = status.get("sessionId") if isinstance(status, dict) else None
237
+ if isinstance(current, str) and current:
238
+ return _settled_session(client, url, current, timeout)
239
+ if preferred:
240
+ try:
241
+ # The session itself, not /window/size: that waits for the app to go idle,
242
+ # which an autoplaying feed (TikTok) never does.
243
+ client.request("GET", "/session/" + quote(preferred, safe=""), timeout=timeout)
244
+ return _settled_session(client, url, preferred, timeout)
245
+ except Exception:
246
+ pass
247
+ if not create:
248
+ return None
249
+ created = client.request("POST", "/session", {"capabilities": {"alwaysMatch": {
250
+ "platformName": "iOS", "shouldWaitForQuiescence": False, "waitForIdleTimeout": 0}}},
251
+ timeout=max(timeout, 30))
252
+ session = created.get("sessionId") or (created.get("value") or {}).get("sessionId")
253
+ if not isinstance(session, str) or not session:
254
+ raise TransportError("WDA did not create a session")
255
+ return _settled_session(client, url, session, timeout)
256
+ finally:
257
+ client.close()
258
+
259
+
260
+ # Sessions already given the no-idle-wait settings, per WDA URL. WDA by default waits
261
+ # for the app to go idle before every read and action; an app playing video never
262
+ # does, so reads hung until timeout (measured on TikTok, 2026-09-23) and each stuck
263
+ # call blocked WDA's single request queue for everything after it.
264
+ _settled_sessions = {}
265
+
266
+
267
+ def _settled_session(client, url, session, timeout):
268
+ if _settled_sessions.get(url) != session:
269
+ try:
270
+ client.request("POST", "/session/" + quote(session, safe="") + "/appium/settings",
271
+ {"settings": dict(WDA_SESSION_SETTINGS)}, timeout=timeout)
272
+ _settled_sessions[url] = session
273
+ except Exception:
274
+ pass # Retried on the next resolve; the driver's configure() applies them too.
275
+ return session
276
+
277
+
278
+
279
+ def wait_out_capture(status_ok, runner_restart=None, timeout=WDA_RECOVER_SECONDS, *, sleep=time.sleep,
280
+ clock=time.monotonic):
281
+ """After a USB iPhone's screen-capture device closed (WDA_LINK_BLIP_SECONDS), wait until the runner that serves
282
+ next is the one to attach to: wait out the link's blip, then restart the runner now (``runner_restart``, which
283
+ returns a check that the new one serves) and wait for it to say so, or, without one, wait for the old runner to
284
+ stop answering (``status_ok(timeout)``), since its host is about to exit. The old runner may answer a while longer:
285
+ measured 4 s after a restart, through the restarted relay, and attaching to it would lose the session again.
286
+ Returns whether it restarted the runner. The caller then waits for /status and attaches."""
287
+ started = clock()
288
+ sleep(WDA_LINK_BLIP_SECONDS)
289
+ fresh = None
290
+ if callable(runner_restart):
291
+ try:
292
+ fresh = runner_restart()
293
+ except Exception: # noqa: BLE001 -- not this phone's managed runner any more: wait for its own end
294
+ fresh = None
295
+ if callable(fresh):
296
+ while clock() - started < timeout and not fresh():
297
+ sleep(.5)
298
+ return True
299
+ doomed = clock() + WDA_LINK_DOOMED_SECONDS
300
+ while clock() < doomed and status_ok(1.5):
301
+ sleep(.5)
302
+ return False
303
+
304
+ class WDA(WDAObserve, WDAGestures, WDAText, Driver):
305
+ capabilities = frozenset({
306
+ "observe_ready", "wait_for_change", "capture_preview", "active_app", "slow_front", "pixel_snapshot",
307
+ "frame_clock", "settled_by", "predicted_settle", "settles_early", "settles_on_one_read", "ax_only_steps",
308
+ "next_step",
309
+ "source_reads", "source_seconds", "rich_rows", "tune", "tap_point", "long_press", "set_value", "write_text",
310
+ "replace_text", "clear_text", "relaunch_app", "launch", "drag_next", "last_swipe_glided", "last_relaunch",
311
+ "recover", "after_capture", "call", "call_trace", "url", "prefix", "http"})
312
+ can_type = True
313
+ # wait_for_change(on_settling=...) reports a likely-final screen before it is proven.
314
+ reports_settling = True
315
+
316
+ # Screen size in points per WDA URL: fixed for a phone, read once per process.
317
+ _screen_sizes = {}
318
+ # Apps whose reads needed the foreground hint, per WDA URL, for this process:
319
+ # every run builds a new driver, and each failed read costs ~8 s.
320
+ _overlay_apps = {}
321
+ # Optional JSON file that keeps _overlay_apps across processes (the app's
322
+ # state directory). Measured 2026-09-23: with the invisible Siri overlay up,
323
+ # the first read of every new process took 8.3 s (a failed read, then the
324
+ # hint) and 0.2 s afterwards; a remembered app is hinted at activation.
325
+ overlay_hint_path = None
326
+ # A list to collect every WDA request and read as (kind, started monotonic, ms): the bench's
327
+ # per-action cost report. None (the default) records nothing.
328
+ call_trace = None
329
+ # WDA.tune (the frontier): rich rows (Other leaves, disabled and selected controls), glides in
330
+ # every app (FrameClock demotes one that keeps moving), and the settings below.
331
+ rich_rows = False
332
+ glide_everywhere = False
333
+ tuned_settings = None
334
+ paste_min_chars = WDA_PASTE_MIN_CHARS
335
+ last_swipe_glided = False
336
+ # A callable that restarts this WDA runner (a simulator's xcodebuild).
337
+ # None: recover() waits for a supervisor to bring it back (the USB runner is supervised).
338
+ restarter = None
339
+ # runner_restart() -> None: restarts this phone's runner now (the device manager's start_runner), for
340
+ # after_capture; None for a runner Mobster doesn't manage.
341
+ runner_restart = None
342
+ # killer(bundle_id, timeout) -> bool: closes an app without WDA (app_killer.py), when WDA's terminate fails.
343
+ killer = None
344
+ last_relaunch = None
345
+ last_recovery = None
346
+ # Simulators by WDA URL (None: not asked yet).
347
+ _simulators = {}
348
+
349
+ def __init__(self, url, session):
350
+ if not session:
351
+ raise ValueError("An existing WDA session ID is required")
352
+ self.url = url
353
+ self.http = HTTP(url)
354
+ # Operator screenshots must not contend for the action client's in-flight lock.
355
+ self.preview_http = HTTP(url)
356
+ self.prefix = "/session/" + quote(session, safe="")
357
+ # pressButton is a core WDA endpoint; a WDA too old to serve it
358
+ # rejects the dispatch, which surfaces as a TransportError.
359
+ self.host_operations = frozenset(HOST_KEY_OPERATIONS)
360
+ self.last_image = None
361
+ self._stable = None
362
+ self._settled = {}
363
+ # When the last action was dispatched: a layer visibility answer from before it is asked again.
364
+ self._dispatched_at = float("-inf")
365
+ self._swiped_at = float("-inf")
366
+ self._detection_point_pending = False
367
+ self._pages_stale = False
368
+ self._page_groups = ()
369
+ self._stalls_seen = 0
370
+ self._drag_once = False
371
+ self._write_rank = None
372
+ self._foreground_hint = None
373
+ self._pane_visibility = {}
374
+ self._last_bundle = ""
375
+ self._hinted = None
376
+ self._frame_watch = None
377
+ self._frame_action = None
378
+ self._frame_followup = None
379
+
380
+ @classmethod
381
+ def load_overlay_hints(cls, path):
382
+ """Remember apps that needed the foreground hint across processes."""
383
+ cls.overlay_hint_path = path
384
+ try:
385
+ with open(path, encoding="utf-8") as handle:
386
+ saved = json.load(handle)
387
+ except (OSError, ValueError):
388
+ return
389
+ if isinstance(saved, dict):
390
+ for url, bundles in saved.items():
391
+ if isinstance(url, str) and isinstance(bundles, list):
392
+ for bundle in bundles[:64]:
393
+ try:
394
+ cls._overlay_apps.setdefault(url, set()).add(validate_bundle_id(bundle))
395
+ except ValueError:
396
+ pass
397
+
398
+ def _save_overlay_hints(self):
399
+ path = WDA.overlay_hint_path
400
+ if not path:
401
+ return
402
+ try:
403
+ data = {url: sorted(bundles) for url, bundles in WDA._overlay_apps.items()}
404
+ temporary = f"{path}.tmp"
405
+ with open(temporary, "w", encoding="utf-8") as handle:
406
+ json.dump(data, handle)
407
+ os.replace(temporary, path)
408
+ except OSError:
409
+ pass
410
+
411
+ def call(self, method, path, body=None, timeout=None):
412
+ # None: the bound for this class of call (wda_timeouts.WDA_CALL_TIMEOUTS).
413
+ # Any value is capped at MAX_WDA_TIMEOUT.
414
+ timeout = wda_timeouts.bounded(timeout, method, path)
415
+ if method == "POST" and path in PAGE_SWITCHING_PATHS:
416
+ self._pages_stale = True # another app or screen: which tab page shows is asked again
417
+ started = time.monotonic()
418
+ try:
419
+ # An action is recorded while it is in flight;
420
+ # one that ends unanswered is an unknown outcome, never re-sent.
421
+ result = wda_watchdog.send(self.url, method, path,
422
+ lambda: self.http.request(method, self.prefix + path, body, timeout))
423
+ finally:
424
+ if self.call_trace is not None:
425
+ self.call_trace.append((call_kind(method, path), started, (time.monotonic() - started) * 1000))
426
+ if path in ("/wda/apps/activate", "/url") and isinstance(body, dict) and body.get("bundleId"):
427
+ # The app a caller brought forward is WDA's active application from now on.
428
+ # Measured 2026-09-24 (iOS 26.0.1): with detection on "auto", every Safari read
429
+ # failed after 8 s (HTTP 404, the Siri overlay as active app), including right
430
+ # after a /url open; naming Safari read the page in 0.5-2.8 s. One settings
431
+ # call (~50 ms) is far cheaper than a single failed read.
432
+ self._foreground_hint = body.get("bundleId")
433
+ self._hint_app(self._foreground_hint, timeout)
434
+ return self.response_value(result)
435
+
436
+ def relaunch_app(self, bundle, timeout=40):
437
+ """Close and reopen ``bundle`` when WDA answers and the app does not; False when WDA doesn't (recovery's job).
438
+ A stuck accessibility server blocks every read until its app closes; a WDA restart doesn't clear it (10 Oct)."""
439
+ deadline = Deadline(timeout)
440
+ try:
441
+ self.http.request("GET", "/status", None, min(5, deadline.remaining()))
442
+ except Exception:
443
+ return False
444
+ self.last_relaunch = "wda"
445
+ try:
446
+ self.call("POST", "/wda/apps/terminate", {"bundleId": bundle}, min(15, deadline.remaining()))
447
+ except Exception:
448
+ # A stuck app can block WDA's own terminate too (ab1, 11 Oct): close it out of band when this device has
449
+ # a way to (app_killer.py), else the failure stands.
450
+ if not callable(self.killer) or not self.killer(bundle, max(1.0, min(15, deadline.remaining()))):
451
+ raise
452
+ self.last_relaunch = "out_of_band"
453
+ self.call("POST", "/wda/apps/activate", {"bundleId": bundle}, min(20, deadline.remaining()))
454
+ self._pages_stale = True # the app comes back on its default tab: which page shows is asked again
455
+ return True
456
+
457
+ def _hint_app(self, bundle, timeout):
458
+ self.call("POST", "/appium/settings", {"settings": {"defaultActiveApplication": bundle}}, timeout)
459
+ self._hinted = bundle
460
+
461
+ @staticmethod
462
+ def response_value(result):
463
+ # WDA answers every command with a value, null included. With none, nothing says the command ran or didn't.
464
+ if "value" not in result:
465
+ raise TransportError("WDA answered with no value; request outcome unknown; not retried")
466
+ value = result.get("value")
467
+ if (isinstance(value, dict) and value.get("error")) or result.get("status", 0) != 0:
468
+ raise TransportError("WDA rejected command; not retried", status=200, wda_error=error_name(value))
469
+ return value
470
+
471
+ def configure(self, timeout=10):
472
+ """Apply measured session settings to the caller's session.
473
+
474
+ The settings go first, before anything that WDA resolves through the
475
+ "active application": a fresh session otherwise pays WDA's default idle
476
+ wait on that first call. A rejection fails the run here rather than
477
+ silently observing at 40-second snapshots.
478
+
479
+ WDA decides the foreground app by hit-testing one point, default
480
+ (64, 64): under a notification banner. On a real phone an incoming
481
+ message then made SpringBoard "active" mid-task and put the banner's
482
+ text in the observation. The screen center is where the app is.
483
+ Measured 2026-09-23: /window/size on a fresh session hung 8-10 s and
484
+ failed ("stale element ... com.apple.siri.IntelligentLight",
485
+ kAXErrorServerNotFound) while a system overlay was the active app. It
486
+ is asked briefly; if it cannot answer, the first observation's
487
+ Application frame supplies the center instead.
488
+ """
489
+ deadline = Deadline(timeout)
490
+ # The screen size is fixed per phone: after the first run the centre goes
491
+ # out with the other settings in one request (the desktop app lost ~3 s per
492
+ # run to a /window/size timeout here).
493
+ known = WDA._screen_sizes.get(self.url)
494
+ if known:
495
+ self._set_detection_point(*known, deadline.remaining())
496
+ return
497
+ # Never leave WDA's (64, 64) default in force, even briefly: it sits
498
+ # under notification banners and the iOS 26 edge-glow overlay.
499
+ settings = {**WDA_SESSION_SETTINGS, "activeAppDetectionPoint": WDA_FALLBACK_DETECTION_POINT}
500
+ self.call("POST", "/appium/settings", {"settings": settings}, deadline.remaining())
501
+ self._detection_point_pending = True
502
+ size = None
503
+ try:
504
+ # /wda/screen measures through SpringBoard, never the "active" app that
505
+ # a system overlay can hijack; /window/size is the fallback.
506
+ screen = self.response_value(self.http.request("GET", "/wda/screen", None,
507
+ min(WDA_WINDOW_SIZE_SECONDS, deadline.remaining())))
508
+ size = screen.get("screenSize") if isinstance(screen, dict) else None
509
+ except (TransportError, TimeoutError, ValueError):
510
+ size = None
511
+ if not (isinstance(size, dict) and all(finite_positive(size.get(k)) for k in ("width", "height"))):
512
+ try:
513
+ size = self.call("GET", "/window/size", timeout=min(WDA_WINDOW_SIZE_SECONDS, deadline.remaining()))
514
+ except (TransportError, TimeoutError, ValueError):
515
+ return
516
+ if isinstance(size, dict) and all(finite_positive(size.get(k)) for k in ("width", "height")):
517
+ WDA._screen_sizes[self.url] = (size["width"], size["height"])
518
+ self._set_detection_point(size["width"], size["height"], deadline.remaining())
519
+
520
+ def _set_detection_point(self, width, height, timeout):
521
+ settings = {**WDA_SESSION_SETTINGS, "activeAppDetectionPoint": f"{width / 2:.0f},{height / 2:.0f}"}
522
+ self.call("POST", "/appium/settings", {"settings": settings}, timeout)
523
+ self._detection_point_pending = False
524
+
525
+ def launch(self, bundle_id, timeout=20):
526
+ """Bring an app forward and return its first settled observation.
527
+
528
+ Activation applies the remembered foreground hint (overlay_hint_path),
529
+ which is what makes the first read fast (measured: 9.0 s -> 0.16 s in a
530
+ fresh process under the Siri overlay). The read then settles with
531
+ ``observe_ready`` (the FrameClock rule in "on" mode). Waiting for the
532
+ pixels before reading was measured and rejected: iOS's zoom-in
533
+ animation outlasts the app's AX tree, so warm launches got slower
534
+ (Settings 0.70 -> 1.49 s, Safari 1.5-1.8 -> 3.0 s). Returns
535
+ ``(snapshot, {"activate_ms", "ready_ms"})``.
536
+ """
537
+ bundle = validate_bundle_id(bundle_id)
538
+ deadline = Deadline(timeout)
539
+ started = time.monotonic()
540
+ self._dispatching()
541
+ self.call("POST", "/wda/apps/activate", {"bundleId": bundle}, min(10, deadline.remaining()))
542
+ info = {"activate_ms": round((time.monotonic() - started) * 1000, 1)}
543
+ snapshot = self.observe_ready(timeout=deadline.remaining())
544
+ info["ready_ms"] = round((time.monotonic() - started) * 1000, 1)
545
+ if self.frame_clock is not None:
546
+ self.frame_clock.record({"kind": "launch", "bundle": bundle, **info})
547
+ return snapshot, info
548
+
549
+ def active_app(self, timeout=5):
550
+ """The foreground bundle, read device-wide within the probe bound: a TransportError means it is unknown.
551
+ A slow-tree app is confirmed by slow_front: activeAppInfo there took 17-50 s, then WDA died (14 Oct, TikTok)."""
552
+ timeout = min(float(timeout), wda_timeouts.WDA_CALL_TIMEOUTS["probe"])
553
+ started = time.monotonic()
554
+ slow = self.slow_front(min(WDA_STATE_SECONDS, timeout))
555
+ if slow:
556
+ return slow
557
+ timeout = max(.25, timeout - (time.monotonic() - started))
558
+ value = self.response_value(self.http.request("GET", "/wda/activeAppInfo", timeout=timeout))
559
+ bundle = value.get("bundleId") if isinstance(value, dict) else None
560
+ if not isinstance(bundle, str) or not bundle:
561
+ raise TransportError("WDA did not report a foreground app")
562
+ return bundle
563
+
564
+ def capture_preview(self, timeout=3):
565
+ try:
566
+ result = self.preview_http.request("GET", self.prefix + "/screenshot", timeout=timeout)
567
+ except (TransportError, TimeoutError) as error:
568
+ if isinstance(error, TimeoutError) or "timeout" in str(error).casefold():
569
+ self.note_screenshot_timeout()
570
+ raise
571
+ value = result.get("value") if isinstance(result, dict) else None
572
+ if isinstance(value, dict) and "screenshot" in str(value.get("message") or "").casefold() \
573
+ and "timeout" in str(value.get("message") or "").casefold():
574
+ self.note_screenshot_timeout()
575
+ image = self.response_value(result)
576
+ self.screenshot_timeouts = 0
577
+ if not isinstance(image, str) or not image:
578
+ raise TransportError("WDA screenshot unavailable")
579
+ return "data:image/png;base64," + image
580
+
581
+ def execute(self, operation, target, snapshot, text=None, timeout=10):
582
+ deadline = Deadline(timeout)
583
+ if operation in HOST_KEY_OPERATIONS:
584
+ # WDA is device-level, so no bundle scoping applies: HOME leaves
585
+ # the app and later observations simply describe the new screen.
586
+ # pressButton (not the sessionless /wda/homescreen) carries all
587
+ # three keys; verified live against upstream WDA on iOS 26.
588
+ self._dispatching()
589
+ self._frame_mark(operation)
590
+ self.call("POST", "/wda/pressButton",
591
+ {"name": WDA_HOST_KEY_NAMES[operation]}, deadline.remaining())
592
+ self._frame_dispatched()
593
+ return {"dispatch_attempted": True}
594
+ if operation == "LAUNCH_APP":
595
+ # The switch is trusted like any dispatched action; the next
596
+ # snapshot reports the foreground bundle from its Application node.
597
+ bundle = validate_bundle_id(target)
598
+ self._dispatching()
599
+ self._frame_mark(operation)
600
+ self.call("POST", "/wda/apps/activate", {"bundleId": bundle}, deadline.remaining())
601
+ self._frame_dispatched()
602
+ return {}
603
+ if operation == "SUBMIT":
604
+ grounded_target(operation, target, snapshot)
605
+ if "SUBMIT" not in target.actions:
606
+ raise ValueError("SUBMIT needs a keyboard and an editable field")
607
+ self._check_current(snapshot, deadline)
608
+ self._dispatching()
609
+ self._frame_mark(operation)
610
+ # Return on the focused field: Go in Safari, Search in search bars.
611
+ self.call("POST", "/wda/keys", {"value": ["\n"]}, deadline.remaining())
612
+ self._frame_dispatched()
613
+ elif operation == "TAP":
614
+ grounded_target(operation, target, snapshot)
615
+ self._check_current(snapshot, deadline)
616
+ self._note_tap(target.locator)
617
+ # A coordinate tap on the observed, occlusion-filtered frame. No
618
+ # XPath resolution means no index-based locator that can silently
619
+ # resolve to a replacement cell after a reload.
620
+ x, y = switch_point(target, snapshot.width)
621
+ self._frame_mark(operation)
622
+ self._tap(x * snapshot.width, y * snapshot.height, deadline)
623
+ self._frame_dispatched()
624
+ elif operation in {"TYPE", "TYPE_SUBMIT"}:
625
+ grounded_target(operation, target, snapshot)
626
+ return self._type_text(operation, target, snapshot, text, deadline)
627
+ elif operation in {"SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT"}:
628
+ aimed, reveal = None, False
629
+ if target is not None and operation in {"SWIPE_LEFT", "SWIPE_RIGHT"}:
630
+ grounded = any(target is element or target == element for element in snapshot.elements)
631
+ if not grounded:
632
+ raise ValueError("Action target is not part of the supplied observation")
633
+ aimed, reveal = target.rect, target.role == "Cell"
634
+ x1, y1, x2, y2 = _swipe_points(operation, snapshot.width, snapshot.height, aimed, reveal)
635
+ self._dispatching()
636
+ glide = self._glide_ok(operation, snapshot)
637
+ if glide and operation in WDA_GLIDE_SPAN:
638
+ # A glide's time is its fixed segments (~480 ms), not its distance: the
639
+ # longer span moves ~0.68 screens per swipe instead of ~0.47 for the same
640
+ # cost, and still overlaps the previous screen by about a third.
641
+ start, end = WDA_GLIDE_SPAN[operation]
642
+ y1, y2 = snapshot.height * start, snapshot.height * end
643
+ y1 = clear_stroke_start(snapshot, operation, x1, y1, snapshot.width, snapshot.height,
644
+ fixed=glide and self.glide_everywhere)
645
+ self._frame_mark(operation, glide=glide)
646
+ self._swiped_at = time.monotonic()
647
+ self.last_swipe_glided = bool(glide)
648
+ if reveal:
649
+ self._reveal(x1, y1, x2, y2, deadline)
650
+ elif glide:
651
+ self._glide(x1, y1, x2, y2, deadline)
652
+ elif operation in {"SWIPE_LEFT", "SWIPE_RIGHT"}:
653
+ # A flick: a paging view turns on speed, and XCTest's slow half-screen drag snapped
654
+ # back (iOSWorld Weather's city pages: 3-7 SWIPE_LEFTs with no effect a run, 25 Sep;
655
+ # live, the flick turned each page in 0.7 s against no turn in 1.4 s). Same distance
656
+ # as before: a Mail row's Delete and Archive were revealed, not triggered.
657
+ self._flick(x1, y1, x2, y2, deadline)
658
+ else:
659
+ self.call("POST", "/wda/dragfromtoforduration", {
660
+ "fromX": x1, "fromY": y1,
661
+ "toX": x2, "toY": y2, "duration": WDA_SWIPE_SECONDS}, deadline.remaining())
662
+ self._frame_dispatched()
663
+ else:
664
+ raise ValueError("Unsupported WDA operation")
665
+
666
+ # The frontier's options: Jev and the plain driver never call these.
667
+
668
+ def is_simulator(self, timeout=3):
669
+ """True when this WDA runs on a simulator, asked once per URL; no answer is False and is asked again."""
670
+ known = WDA._simulators.get(self.url)
671
+ if known is None:
672
+ try:
673
+ status = self.http.request("GET", "/status", timeout=timeout)
674
+ ios = (status.get("value") or {}).get("ios") if isinstance(status, dict) else None
675
+ known = bool(isinstance(ios, dict) and ios.get("simulatorVersion"))
676
+ except (TransportError, TimeoutError, ValueError, AttributeError):
677
+ return False
678
+ WDA._simulators[self.url] = known
679
+ return known
680
+
681
+ def tune(self, rich=True, glide=True, max_children=WDA_MAX_CHILDREN, typing_frequency=None, timeout=5):
682
+ """The frontier policy's driver: rich rows (state.from_wda_root), glides in every app, and WDA
683
+ settings: a bound on a snapshot's children and, on a simulator, the measured typing rate
684
+ (MOBSTER_TYPING_FREQUENCY sets one on any device). Returns the settings applied; they are
685
+ applied again after a recovery."""
686
+ self.rich_rows, self.glide_everywhere = bool(rich), bool(glide)
687
+ settings = {}
688
+ if max_children:
689
+ settings["snapshotMaxChildren"] = int(max_children)
690
+ rate = typing_frequency or _env_int("MOBSTER_TYPING_FREQUENCY")
691
+ if not rate and WDA_SIMULATOR_TYPING_FREQUENCY and self.is_simulator():
692
+ rate = WDA_SIMULATOR_TYPING_FREQUENCY
693
+ if rate:
694
+ settings["maxTypingFrequency"] = int(rate)
695
+ self.paste_min_chars = max(WDA_PASTE_MIN_CHARS, round(WDA_PASTE_SECONDS * WDA_TYPED_PER_RATE * (rate or 0)))
696
+ self.tuned_settings = settings
697
+ if settings:
698
+ self.call("POST", "/appium/settings", {"settings": settings}, timeout)
699
+ return settings
700
+
701
+ def _status_ok(self, timeout=3):
702
+ try:
703
+ status = self.http.request("GET", "/status", timeout=timeout)
704
+ except (TransportError, TimeoutError, ValueError):
705
+ self.http.close()
706
+ return False
707
+ value = status.get("value") if isinstance(status, dict) else None
708
+ return isinstance(value, dict) and value.get("ready", True) is not False
709
+
710
+ def recover(self, timeout=WDA_RECOVER_SECONDS):
711
+ """Bring back a runner that reset or refused the connection and attach; TransportError past ``timeout``."""
712
+ started = time.monotonic()
713
+ deadline = Deadline(timeout)
714
+ phases = self.last_recovery = {"restarted": False}
715
+ if not self._status_ok(min(3, deadline.remaining())) and callable(self.restarter):
716
+ self.restarter(deadline.remaining())
717
+ phases["restarted"] = True
718
+ while not self._status_ok(min(3, deadline.remaining())):
719
+ if deadline.end - time.monotonic() < 1.5:
720
+ raise TransportError("WDA did not come back")
721
+ time.sleep(.5)
722
+ phases["ready_ms"] = round((time.monotonic() - started) * 1000)
723
+ self.reattach(timeout=min(30, deadline.remaining()))
724
+ phases["total_ms"] = round((time.monotonic() - started) * 1000)
725
+ return time.monotonic() - started
726
+
727
+ def after_capture(self, timeout=WDA_RECOVER_SECONDS, sleep=time.sleep, clock=time.monotonic):
728
+ """Bring the runner back after the phone's screen-capture device closed (see WDA_LINK_BLIP_SECONDS).
729
+ Nothing is sent to the phone meanwhile, so no action is in doubt; TransportError past ``timeout``."""
730
+ started = clock()
731
+ restarted = wait_out_capture(self._status_ok, self.runner_restart, timeout, sleep=sleep, clock=clock)
732
+ self.recover(timeout=max(1.0, timeout - (clock() - started)))
733
+ self.last_recovery = {**(self.last_recovery or {}), "after_capture": True, "restarted": restarted}
734
+ return clock() - started
735
+
736
+ def reattach(self, timeout=30):
737
+ """Drive the runner's current session (a new one when it has none), set up as configure() does."""
738
+ for client in (self.http, self.preview_http):
739
+ client.close()
740
+ _settled_sessions.pop(self.url, None)
741
+ factory = getattr(self, "new_session", None)
742
+ session = factory() if callable(factory) else resolve_wda_session(self.url, timeout=min(10, timeout))
743
+ if not session:
744
+ raise TransportError("WDA has no session")
745
+ self.prefix = "/session/" + quote(session, safe="")
746
+ self._stable, self._settled, self._pane_visibility = None, {}, {}
747
+ self._dispatched_at = float("-inf")
748
+ self._hinted, self._frame_watch, self._frame_action, self._frame_followup = None, None, None, None
749
+ self._pages_stale = True
750
+ self.configure(timeout=min(10, timeout))
751
+ if self.tuned_settings:
752
+ self.call("POST", "/appium/settings", {"settings": dict(self.tuned_settings)}, min(5, timeout))
753
+ return session
754
+
755
+ def close(self):
756
+ # The session belongs to the caller; do not terminate its app.
757
+ clock, self.frame_clock = self.frame_clock, None
758
+ try:
759
+ if clock is not None:
760
+ try:
761
+ clock.close() # Releases the relay subscription (or its own stream).
762
+ except Exception:
763
+ pass
764
+ self.http.close()
765
+ finally:
766
+ self.preview_http.close()
767
+
768
+
769
+ # Driver kinds by name. The core registers WDA; mirroring.py registers "mirroring" (experimental) when imported, and an
770
+ # optional private package (harness_api.hooks) may register more before ``build_driver`` looks one up.
771
+ DRIVERS = {"wda": WDA}
772
+
773
+
774
+ def register_driver(kind, cls):
775
+ """Add a driver kind. The class must subclass ``Driver``."""
776
+ if not isinstance(kind, str) or not kind or not (isinstance(cls, type) and issubclass(cls, Driver)):
777
+ raise TypeError("register_driver needs a name and a Driver subclass")
778
+ DRIVERS[kind] = cls
779
+
780
+
781
+ def build_driver(kind, **kwargs):
782
+ """One validated construction site: an unknown kind is a ValueError, a class missing the contract a TypeError."""
783
+ if isinstance(kind, str) and kind not in DRIVERS:
784
+ from . import harness_api
785
+ harness_api.hooks()
786
+ try:
787
+ cls = DRIVERS[kind]
788
+ except (KeyError, TypeError):
789
+ raise ValueError(f"Unknown driver {kind!r}; expected one of {sorted(DRIVERS)}") from None
790
+ driver = cls(**kwargs)
791
+ if not isinstance(driver, Driver):
792
+ raise TypeError(f"Driver {kind!r} does not implement the device contract")
793
+ return driver