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,754 @@
1
+ """The X-ray crawler: explores an app with no model, and every tap, replayed ones too, goes through the policy."""
2
+
3
+ import base64
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from io import BytesIO
7
+ from pathlib import Path
8
+
9
+ from ..packs.core import BACK_LABEL
10
+ from . import paywall as paywalls
11
+ from . import policy, screens
12
+
13
+ SCHEMA = "mobster.xray/1"
14
+ SETTLE_SECONDS = .5
15
+ SETTLE_TIMEOUT = 5.0
16
+ POLL_SECONDS = .1
17
+ THUMB_WIDTH = 300
18
+ BOXES_KEPT = 60
19
+ STUCK_LIMIT = 3
20
+ TOP_STEPS = 6
21
+ # A list whose detail pages look alike (fonts, products, contacts) is sampled: after this many look-alike pages, its
22
+ # other rows are left closed.
23
+ CATALOG_ALIKE = 2
24
+ # The depth the first pass stops at; screens there are explored deeper afterwards while the caps allow.
25
+ FIRST_PASS_DEPTH = 2
26
+ ALIKE = .6
27
+ # A read older than this is read again before a tap on it (WebDriverAgent refuses taps on reads over 1 s old).
28
+ FRESH_SECONDS = .7
29
+ QUIET_ROLES = frozenset({"StaticText", "Image", "Icon", "Other", "NavigationBar", "Toolbar", "TabBar", "Text",
30
+ "Window", "ScrollView", "Table", "CollectionView", "Keyboard", "Key"})
31
+ QUIET_REASONS = ("outside the content area", "under a bar", "not tappable", "no label")
32
+
33
+
34
+ class Stop(Exception):
35
+ """The crawl can't go on: the message says why, in plain words."""
36
+
37
+
38
+ class Gone(Exception):
39
+ """The control to tap is no longer on screen; nothing was tapped. ``snapshot``: the fresh read."""
40
+
41
+ def __init__(self, snapshot):
42
+ super().__init__("the control is gone")
43
+ self.snapshot = snapshot
44
+
45
+
46
+ @dataclass
47
+ class Caps:
48
+ screens: int = 60
49
+ depth: int = 6
50
+ seconds: float = 300.0
51
+ pages: int = 4 # pages of a long list it scrolls through per screen
52
+ taps: int = 800
53
+ children: int = 8 # new screens opened from one screen two or more taps in (the first two levels: all)
54
+
55
+
56
+ @dataclass
57
+ class Screen:
58
+ id: str
59
+ title: str # the screen's own title (its identity)
60
+ tokens: frozenset
61
+ union: set
62
+ lines: list
63
+ depth: int
64
+ path: list # [{"label", "kind"}] from the app's first screen
65
+ parent: str = ""
66
+ via: str = ""
67
+ seen_s: float = 0.0
68
+ thumb: str = ""
69
+ boxes: list = field(default_factory=list)
70
+ paywall: object = None
71
+ frame: bytes = None # the full capture until announce() hands it to the run
72
+ frame_id: str = None # the run's stored frame of this screen (the result's gallery)
73
+ image_only: bool = False
74
+ done: set = field(default_factory=set)
75
+ refused: dict = field(default_factory=dict)
76
+ noop: list = field(default_factory=list)
77
+ changed: list = field(default_factory=list) # taps that changed the screen in place: the rest is left alone
78
+ choice: bool = False # seen as a list of options: its rows are never tapped
79
+ kids: list = field(default_factory=list) # the identity tokens of each new screen opened from it
80
+ alike: int = 0 # how many of those look like an earlier one (a catalog's detail pages)
81
+ held_back: int = 0 # rows not opened because of the children cap or a catalog
82
+ pending: bool = False # reached at the first pass's depth with something left to open
83
+ unreachable: bool = False
84
+
85
+ @property
86
+ def name(self):
87
+ """What the report calls it: its title, or the row that opens it when the title is missing or a badge."""
88
+ if self.title and not (self.title.isupper() and len(self.title) <= 6):
89
+ return self.title
90
+ return self.via or self.title or "Untitled"
91
+
92
+ def to_json(self):
93
+ return {"id": self.id, "title": self.title, "name": self.name, "depth": self.depth, "path": self.path,
94
+ "parent": self.parent,
95
+ "via": self.via, "seen_s": round(self.seen_s, 2), "thumb": self.thumb, "frame_id": self.frame_id,
96
+ "lines": self.lines,
97
+ "tokens": sorted(self.union), "boxes": self.boxes,
98
+ "paywall": self.paywall.to_json() if self.paywall else None,
99
+ "refused": [{"label": label, "reason": reason} for label, reason in self.refused.items()],
100
+ "no_change": self.noop, "changed_in_place": self.changed, "choice_list": self.choice,
101
+ "catalog": self.alike >= CATALOG_ALIKE, "held_back": self.held_back,
102
+ "unreachable": self.unreachable}
103
+
104
+
105
+ @dataclass
106
+ class Candidate:
107
+ key: str
108
+ label: str
109
+ element: object
110
+ reason: str
111
+
112
+
113
+ def key_of(label):
114
+ """A control's identity on its screen: its label, case folded, without a trailing state (", Off")."""
115
+ return screens.STATES.sub("", policy.plain(label)).casefold()[:120]
116
+
117
+
118
+ def boxes(snapshot):
119
+ """The screen's layout for a wireframe: [x, y, w, h, kind, text] for its structural elements."""
120
+ out = []
121
+ for element in sorted(snapshot.elements, key=lambda e: (e.rect[1], e.rect[0])):
122
+ if element.role in ("Image", "Icon") or not policy.plain(element.label):
123
+ continue
124
+ kind = ("row" if element.role in ("Cell", "Button") and element.rect[2] > .5 else
125
+ "control" if element.role in ("Button", "Link", "Switch", "Toggle", "SearchField", "TextField")
126
+ else "bar" if element.role in ("NavigationBar", "TabBar", "Toolbar") else "text")
127
+ out.append([round(v, 4) for v in element.rect] + [kind, policy.plain(element.label)[:60]])
128
+ if len(out) >= BOXES_KEPT:
129
+ break
130
+ return out
131
+
132
+
133
+ CHROME = ("XCUIElementTypeToolbar", "XCUIElementTypeTabBar", "XCUIElementTypeKeyboard", "XCUIElementTypeSearchField",
134
+ "XCUIElementTypeNavigationBar")
135
+
136
+
137
+ def more_below(snapshot):
138
+ """Whether content runs past the bottom of the content area (a long list): only then is a scroll worth it."""
139
+ for element in snapshot.elements:
140
+ if element.role in policy.CONTROL_ROLES or any(part in (element.locator or "") for part in CHROME):
141
+ continue
142
+ if element.rect[1] + element.rect[3] > policy.CONTENT_BOTTOM - .02 and element.rect[1] < .995:
143
+ return True
144
+ return False
145
+
146
+
147
+ def state(snapshot):
148
+ """What the screen shows, controls' values and checkmarks included, for telling whether a tap changed it."""
149
+ return frozenset((e.role, policy.plain(e.label), e.value, e.selected) for e in snapshot.elements
150
+ if e.rect[1] >= screens.STATUS_BAR)
151
+
152
+
153
+ def is_dialog_element(element):
154
+ return element.role in ("Alert", "Sheet") or "XCUIElementTypeAlert" in (element.locator or "")
155
+
156
+
157
+ class _Timer:
158
+ def __init__(self, spent, kind, clock):
159
+ self.spent, self.kind, self.clock = spent, kind, clock
160
+
161
+ def __enter__(self):
162
+ self.started = self.clock()
163
+
164
+ def __exit__(self, *exc):
165
+ self.spent[self.kind] = self.spent.get(self.kind, 0.0) + self.clock() - self.started
166
+
167
+
168
+ class Crawler:
169
+ def __init__(self, driver, bundle, *, caps=None, out_dir=None, shots=True, clock=time.monotonic,
170
+ sleep=time.sleep, log=None, emit=None, cancelled=None):
171
+ self.driver, self.bundle = driver, bundle
172
+ self.emit, self.cancelled = emit, cancelled
173
+ self.caps = caps or Caps()
174
+ self.out = Path(out_dir) if out_dir else None
175
+ self.shots = shots and self.out is not None and callable(getattr(driver, "capture_preview", None))
176
+ self.clock, self.sleep = clock, sleep
177
+ self.log = log or (lambda message: None)
178
+ self.screens = []
179
+ self.edges = []
180
+ self.stats = {"taps": 0, "scrolls": 0, "backs": {"control": 0, "close": 0, "swipe": 0, "relaunch": 0},
181
+ "dialogs_dismissed": 0, "left_app": 0, "no_change": 0, "changed_in_place": 0,
182
+ "relaunch_failures": 0}
183
+ self.stuck = 0
184
+ self.before = None
185
+ self.limit = self.caps.depth
186
+ self.spent = {}
187
+ self.settles = {}
188
+ self.stopped = ""
189
+ self.started = 0.0
190
+
191
+ def timed(self, kind):
192
+ return _Timer(self.spent, kind, self.clock)
193
+
194
+ def note(self, event):
195
+ if self.emit is not None:
196
+ self.emit(event)
197
+
198
+ def sent(self, operation, label, send):
199
+ # Journaled before it is sent and acknowledged after; a send that raises is neither acknowledged nor retried.
200
+ self.note({"event": "action_started", "operation": operation,
201
+ "target": policy.plain(label)[:120] if label else None})
202
+ began = self.clock()
203
+ try:
204
+ result = send()
205
+ except Exception as error:
206
+ if "stale_revision" in str(error):
207
+ self.note({"event": "action_not_dispatched", "operation": operation}) # the driver sent nothing
208
+ raise
209
+ self.note({"event": "action_acknowledged", "act_ms": round((self.clock() - began) * 1000)})
210
+ return result
211
+
212
+ def elapsed(self):
213
+ return self.clock() - self.started
214
+
215
+ def capped(self):
216
+ if self.stopped:
217
+ return True
218
+ if self.cancelled is not None and self.cancelled():
219
+ self.stopped = "stopped by you"
220
+ elif len(self.screens) >= self.caps.screens:
221
+ self.stopped = f"reached {self.caps.screens} screens"
222
+ elif self.elapsed() >= self.caps.seconds:
223
+ self.stopped = f"reached the time limit ({round(self.caps.seconds)} s)"
224
+ elif self.stats["taps"] >= self.caps.taps:
225
+ self.stopped = f"reached {self.caps.taps} taps"
226
+ return bool(self.stopped)
227
+
228
+ def observe(self):
229
+ with self.timed("observe"):
230
+ return self.driver.observe(timeout=10)
231
+
232
+ def settle(self, before, kind="tap"):
233
+ started = self.clock()
234
+ with self.timed("settle"):
235
+ try:
236
+ return self._settle(before)
237
+ finally:
238
+ detail = self.settles.setdefault(kind, {"n": 0, "seconds": 0.0})
239
+ detail["n"] += 1
240
+ detail["seconds"] += self.clock() - started
241
+
242
+ def _settle(self, before):
243
+ wait = getattr(self.driver, "wait_for_change", None)
244
+ if callable(wait):
245
+ try:
246
+ # ``early``: the first changed read that agrees with the read before it, without the driver's extra
247
+ # quiet period. Two agreeing reads are still required, so a moving screen is not returned.
248
+ try:
249
+ return wait(before, timeout=SETTLE_TIMEOUT, wait_seconds=SETTLE_SECONDS, early=True)
250
+ except TypeError:
251
+ return wait(before, timeout=SETTLE_TIMEOUT, wait_seconds=SETTLE_SECONDS)
252
+ except Exception:
253
+ pass
254
+ return self._poll_settle(before)
255
+
256
+ def _poll_settle(self, before):
257
+ started = self.clock()
258
+ previous = None
259
+ while True:
260
+ read_at = self.clock()
261
+ current = self.observe()
262
+ now = self.clock()
263
+ if current.content_fingerprint == before.content_fingerprint:
264
+ previous = None
265
+ if now - started >= SETTLE_SECONDS:
266
+ return current
267
+ else:
268
+ if previous is not None and previous.content_fingerprint == current.content_fingerprint:
269
+ return current
270
+ previous = current
271
+ if now - started >= SETTLE_TIMEOUT:
272
+ return current
273
+ pause = POLL_SECONDS - (now - read_at)
274
+ if pause > 0:
275
+ self.sleep(pause)
276
+
277
+ def regrounded(self, element, snapshot):
278
+ if time.monotonic() - getattr(snapshot, "captured_at", 0) <= FRESH_SECONDS:
279
+ return element, snapshot
280
+ now = self.observe()
281
+ key, (cx, cy) = key_of(element.label), element.center
282
+ same = [e for e in now.elements if e.role == element.role and key_of(e.label) == key]
283
+ same.sort(key=lambda e: abs(e.center[1] - cy) + abs(e.center[0] - cx))
284
+ return (same[0] if same else None), now
285
+
286
+ def tap(self, element, snapshot):
287
+ """Taps ``element`` (checked by the policy here, every time) and returns the settled screen after."""
288
+ element, snapshot = self.regrounded(element, snapshot)
289
+ if element is None:
290
+ raise Gone(snapshot)
291
+ verdict = policy.decide(element, snapshot)
292
+ if not verdict.ok: # the one place every tap passes, replayed taps included
293
+ # The fresh read says no: nothing was tapped, so the control is skipped like one that has gone.
294
+ self.log(f"refused at the tap: {policy_refusal(element, verdict)}")
295
+ raise Gone(snapshot)
296
+ try:
297
+ with self.timed("tap"):
298
+ self.sent("TAP", element.label, lambda: self.driver.execute("TAP", element, snapshot, timeout=10))
299
+ except Exception as error:
300
+ # The driver refused before sending anything (the screen moved since the read): nothing happened.
301
+ if "stale_revision" in str(error):
302
+ raise Gone(self.observe()) from None
303
+ raise
304
+ self.stats["taps"] += 1
305
+ self.before = snapshot # the read the tap was made on, for telling what it changed
306
+ return self.settle(snapshot)
307
+
308
+ def tap_back(self, element, snapshot):
309
+ """A back or close control, checked on its own: its words must name no act."""
310
+ if policy.act_words(element.label) and not BACK_LABEL.match(policy.plain(element.label)):
311
+ return None
312
+ element, snapshot = self.regrounded(element, snapshot)
313
+ if element is None:
314
+ return None
315
+ try:
316
+ self.sent("TAP", element.label, lambda: self.driver.execute("TAP", element, snapshot, timeout=10))
317
+ except Exception as error:
318
+ if "stale_revision" in str(error):
319
+ return None
320
+ raise
321
+ return self.settle(snapshot, "back")
322
+
323
+ def scroll(self, snapshot, op="SWIPE_UP"):
324
+ self.sent(op, None, lambda: self.driver.execute(op, None, snapshot, timeout=10))
325
+ self.stats["scrolls"] += 1
326
+ return self.settle(snapshot, "scroll")
327
+
328
+ def edge_back(self, snapshot):
329
+ """An edge swipe from the left (iOS's back gesture), when the driver can make one; None when it can't."""
330
+ gesture = getattr(self.driver, "edge_back", None)
331
+ if callable(gesture):
332
+ swipe = lambda: gesture(snapshot) # noqa: E731
333
+ elif callable(getattr(self.driver, "call", None)) and snapshot.source == "wda":
334
+ w, h = snapshot.width, snapshot.height
335
+ swipe = lambda: self.driver.call("POST", "/actions", {"actions": [{ # noqa: E731
336
+ "type": "pointer", "id": "finger", "parameters": {"pointerType": "touch"},
337
+ "actions": [{"type": "pointerMove", "duration": 0, "x": 1, "y": round(h * .45)},
338
+ {"type": "pointerDown"},
339
+ {"type": "pointerMove", "duration": 250, "x": round(w * .8), "y": round(h * .45)},
340
+ {"type": "pointerUp"}]}]}, timeout=10)
341
+ elif hasattr(getattr(self.driver, "helper", None), "request"):
342
+ swipe = lambda: self.driver.helper.request("drag", x1=.005, y1=.45, x2=.8, y2=.45, ms=250) # noqa: E731
343
+ else:
344
+ return None
345
+ try:
346
+ self.sent("EDGE_BACK", None, swipe)
347
+ except Exception as error:
348
+ self.log(f"edge swipe failed: {error}")
349
+ return None
350
+ return self.settle(snapshot, "swipe")
351
+
352
+ def launch(self, fresh=False):
353
+ """The app's first screen. ``fresh``: closed and reopened (back to its start), when the driver can."""
354
+ self.note({"event": "app_launch_started", "app": self.bundle})
355
+ with self.timed("launch"):
356
+ snapshot = self._launch(fresh)
357
+ self.note({"event": "app_launch_acknowledged", "app": self.bundle})
358
+ return snapshot
359
+
360
+ def _launch(self, fresh):
361
+ if fresh:
362
+ relaunch = getattr(self.driver, "relaunch_app", None)
363
+ if callable(relaunch):
364
+ if relaunch(self.bundle) is False:
365
+ raise Stop("the connection stopped answering")
366
+ ready = getattr(self.driver, "observe_ready", None)
367
+ return ready(timeout=15) if callable(ready) else self.observe()
368
+ launch = getattr(self.driver, "launch", None)
369
+ if callable(launch):
370
+ result = launch(self.bundle, timeout=30)
371
+ return result[0] if isinstance(result, tuple) else result
372
+ before = self.observe()
373
+ self.driver.execute("LAUNCH_APP", self.bundle, before, timeout=20)
374
+ self.sleep(1.5)
375
+ return self.observe()
376
+
377
+ def thumbnail(self, screen):
378
+ if not self.shots:
379
+ return
380
+ with self.timed("frames"):
381
+ self._thumbnail(screen)
382
+
383
+ def _thumbnail(self, screen):
384
+ try:
385
+ url = self.driver.capture_preview(timeout=5)
386
+ from PIL import Image
387
+ data = base64.b64decode(url.split(",", 1)[1])
388
+ screen.frame = data # announce() hands it to the run, which stores it beside the run
389
+ with Image.open(BytesIO(data)) as image:
390
+ image = image.convert("RGB")
391
+ height = max(1, round(image.height * THUMB_WIDTH / image.width))
392
+ image = image.resize((THUMB_WIDTH, height), Image.LANCZOS)
393
+ frames = self.out / "frames"
394
+ frames.mkdir(parents=True, exist_ok=True)
395
+ name = f"frames/{screen.id}.jpg"
396
+ image.save(self.out / name, "JPEG", quality=78)
397
+ screen.thumb = name
398
+ except Exception as error:
399
+ self.log(f"no frame for {screen.id}: {error}")
400
+
401
+ def dialog(self, snapshot, system=False):
402
+ """Gets a dialog out of the way with Not Now, Cancel or Close; the screen after."""
403
+ inside = [e for e in snapshot.elements if is_dialog_element(e)]
404
+ if inside:
405
+ alert = inside[0]
406
+ buttons = [e for e in snapshot.elements if e.role == "Button" and (
407
+ e.locator.startswith(alert.locator + "/") if alert.locator else policy._contains(alert.rect, e.rect))]
408
+ safe = policy.safe_dismiss([e.label for e in buttons])
409
+ if safe is None:
410
+ raise Stop(f"a dialog needs you: “{policy.plain(alert.label)[:80]}”")
411
+ button = next(e for e in buttons if e.label == safe)
412
+ self.sent("TAP", button.label, lambda: self.driver.execute("TAP", button, snapshot, timeout=10))
413
+ self.stats["dialogs_dismissed"] += 1
414
+ return self.settle(snapshot, "dialog")
415
+ if system and callable(getattr(self.driver, "call", None)) and snapshot.source == "wda":
416
+ from ..wda import system_dialogs
417
+ with self.timed("dialog_checks"):
418
+ found = system_dialogs.read(self.driver, timeout=2)
419
+ if found is not None:
420
+ safe = policy.safe_dismiss(found.buttons)
421
+ if safe is None:
422
+ raise Stop(f"iOS asks something only you can answer: “{found.text[:80]}”")
423
+ self.sent("TAP", safe, lambda: self.driver.call("POST", "/alert/accept", {"name": safe}, timeout=10))
424
+ self.stats["dialogs_dismissed"] += 1
425
+ return self.settle(snapshot, "dialog")
426
+ return snapshot
427
+
428
+ def identify(self, reading):
429
+ for screen in self.screens:
430
+ if screens.same(reading.title, reading.tokens, screen.title, screen.tokens, b_union=screen.union):
431
+ return screen
432
+ return None
433
+
434
+ def is_on(self, screen, snapshot):
435
+ reading = screens.read(snapshot)
436
+ return screens.same(reading.title, reading.tokens, screen.title, screen.tokens, b_union=screen.union)
437
+
438
+ def register(self, snapshot, reading, depth, path, parent="", via=""):
439
+ screen = Screen(id=f"s{len(self.screens) + 1:03d}", title=reading.title, tokens=reading.tokens,
440
+ union=set(reading.tokens), lines=list(reading.lines), depth=depth, path=path, parent=parent,
441
+ via=via, seen_s=self.elapsed(), boxes=boxes(snapshot), image_only=reading.image_only)
442
+ screen.paywall = paywalls.detect(reading.lines)
443
+ self.screens.append(screen)
444
+ self.thumbnail(screen)
445
+ self.announce(screen)
446
+ self.log(f"{screen.id} depth {depth} “{screen.title or '(untitled)'}”"
447
+ + (f" via “{via}”" if via else "") + (" PAYWALL" if screen.paywall else ""))
448
+ return screen
449
+
450
+ def announce(self, screen):
451
+ """A ``step`` in the run for a screen just mapped, with its capture as the step's frame."""
452
+ frame, screen.frame = screen.frame, None
453
+ if self.emit is None:
454
+ return
455
+ event = {"event": "step", "n": len(self.screens), "step": len(self.screens),
456
+ "text": f"Mapped {screen.name}" + (" (a paywall)" if screen.paywall else ""),
457
+ **({"_frame": frame} if frame else {})}
458
+ self.emit(event)
459
+ screen.frame_id = event.get("frameId") # Run.emit stored the frame and put its id in the event
460
+
461
+ def learn(self, screen, snapshot):
462
+ """Adds what a scrolled read of ``screen`` shows to it."""
463
+ reading = screens.read(snapshot)
464
+ new = reading.tokens - screen.union
465
+ screen.union |= reading.tokens
466
+ for line in reading.lines:
467
+ if line not in screen.lines and len(screen.lines) < screens.LINES_KEPT * 2:
468
+ screen.lines.append(line)
469
+ return new
470
+
471
+ def saturated(self, screen):
472
+ """Whether ``screen`` has opened enough: a catalog sampled, or the children cap reached two levels in."""
473
+ return (screen.alike >= CATALOG_ALIKE
474
+ or screen.depth >= 2 and len(screen.kids) >= self.caps.children)
475
+
476
+ def candidates(self, screen, snapshot):
477
+ found, keys = [], set()
478
+ if screen.changed:
479
+ return found
480
+ if policy.choice_list(snapshot):
481
+ screen.choice = True
482
+ for element in sorted(snapshot.elements, key=lambda e: (round(e.rect[1], 3), -e.rect[2])):
483
+ verdict = policy.decide(element, snapshot)
484
+ key = key_of(element.label)
485
+ if not verdict.ok:
486
+ if (key and key not in screen.refused and verdict.reason not in QUIET_REASONS
487
+ and not (element.role in QUIET_ROLES and not policy.act_words(element.label)
488
+ and not screen.image_only)):
489
+ screen.refused[policy.plain(element.label)[:80]] = verdict.reason
490
+ continue
491
+ if not key or key in keys or key in screen.done:
492
+ continue
493
+ if verdict.reason == "tab" and screen.depth > 0:
494
+ continue # tabs are top-level: taken from a tab's first screen only
495
+ if screen.choice and verdict.reason == "cell":
496
+ screen.refused.setdefault(policy.plain(element.label)[:80], "an option in a choice list")
497
+ continue
498
+ keys.add(key)
499
+ label = policy.plain(element.label)
500
+ if element.role in ("Cell", "Button") and ", " in label:
501
+ label = label.split(", ")[0] # "Apple Account, Sign in to access..." is the row "Apple Account"
502
+ found.append(Candidate(key, label, element, verdict.reason))
503
+ return found
504
+
505
+ def back_controls(self, snapshot):
506
+ from ..frontier import back_control
507
+ control = back_control(snapshot)
508
+ if control is not None:
509
+ yield "control", control
510
+ for element in snapshot.elements:
511
+ label = policy.plain(element.label)
512
+ if (element is not control and BACK_LABEL.match(label) and element.center[1] < .2
513
+ and (element.role == "Button" or policy.image_only(snapshot) and element.center[0] < .3)):
514
+ yield ("control" if label.casefold() in ("back", "<", "‹") else "close"), element
515
+
516
+ def go_back(self, snapshot):
517
+ """One step back by a back or close control, else an edge swipe; the new snapshot,
518
+ or None when neither moved."""
519
+ for kind, control in self.back_controls(snapshot):
520
+ moved = self.tap_back(control, snapshot)
521
+ if moved is not None and moved.content_fingerprint != snapshot.content_fingerprint:
522
+ self.stats["backs"][kind] += 1
523
+ return moved
524
+ swiped = self.edge_back(snapshot)
525
+ if swiped is not None and swiped.content_fingerprint != snapshot.content_fingerprint:
526
+ self.stats["backs"]["swipe"] += 1
527
+ return swiped
528
+ return None
529
+
530
+ def to_top(self, snapshot, root=None):
531
+ """After the app opens: its first screen. Some apps (Settings on an iPhone) reopen on the last one shown."""
532
+ for _ in range(TOP_STEPS):
533
+ if root is not None and self.is_on(root, snapshot):
534
+ break
535
+ if root is None and not any(kind == "control" for kind, _ in self.back_controls(snapshot)):
536
+ break
537
+ moved = self.go_back(snapshot)
538
+ if moved is None:
539
+ break
540
+ snapshot = self.dialog(moved)
541
+ if self.bundle_left(snapshot):
542
+ break
543
+ return snapshot
544
+
545
+ def return_to(self, screen, snapshot):
546
+ """``snapshot`` on ``screen`` by Back, an edge swipe, or a relaunch and the path again; None when it can't."""
547
+ if self.is_on(screen, snapshot):
548
+ return snapshot
549
+ for _ in range(4):
550
+ moved = None
551
+ for kind, control in self.back_controls(snapshot):
552
+ moved = self.tap_back(control, snapshot)
553
+ if moved is not None:
554
+ self.stats["backs"][kind] += 1
555
+ break
556
+ if moved is None or moved.content_fingerprint == snapshot.content_fingerprint:
557
+ swiped = self.edge_back(snapshot)
558
+ if swiped is not None and swiped.content_fingerprint != snapshot.content_fingerprint:
559
+ self.stats["backs"]["swipe"] += 1
560
+ moved = swiped
561
+ else:
562
+ moved = None
563
+ if moved is None:
564
+ break
565
+ snapshot = self.dialog(moved)
566
+ if self.is_on(screen, snapshot):
567
+ return snapshot
568
+ if self.bundle_left(snapshot):
569
+ break
570
+ self.log(f"relaunching to reach {screen.id}: no back control or swipe brought it up")
571
+ return self.replay(screen)
572
+
573
+ def bundle_left(self, snapshot):
574
+ bundle = str(getattr(snapshot, "bundle_id", "") or "")
575
+ return bool(bundle) and bundle != self.bundle
576
+
577
+ def find(self, snapshot, key, pages):
578
+ """The control ``key`` names on this screen, scrolling down for it; (element, snapshot) or (None, snapshot)."""
579
+ for page in range(pages + 1):
580
+ for element in snapshot.elements:
581
+ if key_of(element.label) == key and policy.decide(element, snapshot).ok:
582
+ return element, snapshot
583
+ if page < pages:
584
+ after = self.scroll(snapshot)
585
+ if after.content_fingerprint == snapshot.content_fingerprint:
586
+ break
587
+ snapshot = after
588
+ return None, snapshot
589
+
590
+ def replay(self, screen):
591
+ """Relaunch the app and tap the path to ``screen`` again; the snapshot on it, or None."""
592
+ if self.capped():
593
+ return None
594
+ self.stats["backs"]["relaunch"] += 1
595
+ try:
596
+ snapshot = self.dialog(self.launch(fresh=True), system=True)
597
+ snapshot = self.to_top(snapshot, self.screens[0] if self.screens else None)
598
+ for step in screen.path:
599
+ element, snapshot = self.find(snapshot, step["key"], self.caps.pages)
600
+ if element is None:
601
+ raise Stop(f"“{step['label']}” is gone")
602
+ snapshot = self.dialog(self.tap(element, snapshot))
603
+ except Gone:
604
+ snapshot = None
605
+ except Stop as error:
606
+ if "needs you" in str(error) or "only you" in str(error) or "stopped answering" in str(error):
607
+ raise
608
+ self.log(f"couldn't get back to {screen.id}: {error}")
609
+ snapshot = None
610
+ if snapshot is not None and self.is_on(screen, snapshot):
611
+ self.stuck = 0
612
+ return snapshot
613
+ self.stats["relaunch_failures"] += 1
614
+ self.stuck += 1
615
+ if self.stuck >= STUCK_LIMIT:
616
+ raise Stop(f"couldn't get back to a screen {STUCK_LIMIT} times in a row")
617
+ return None
618
+
619
+ def run(self):
620
+ self.started = self.clock()
621
+ try:
622
+ refusal = policy.app_refusal(self.bundle)
623
+ if refusal:
624
+ raise Stop(refusal) # before anything is launched or tapped
625
+ snapshot = self.to_top(self.dialog(self.launch(fresh=True), system=True))
626
+ reading = screens.read(snapshot)
627
+ root = self.register(snapshot, reading, 0, [])
628
+ if root.paywall is None:
629
+ # The first levels first: a long list deep in one branch (Settings' fonts) can't use up the screens
630
+ # before the app's other sections are mapped. Then each screen left at that depth, deeper.
631
+ self.limit = min(FIRST_PASS_DEPTH, self.caps.depth)
632
+ self.explore(root, snapshot)
633
+ if self.caps.depth > self.limit:
634
+ frontier = [s for s in self.screens if s.depth == self.limit and s.pending]
635
+ self.limit = self.caps.depth
636
+ for screen in frontier:
637
+ if self.capped():
638
+ break
639
+ found = self.replay(screen)
640
+ if found is not None:
641
+ self.explore(screen, found)
642
+ if not self.stopped:
643
+ self.stopped = "explored every screen it could reach"
644
+ except Stop as error:
645
+ self.stopped = str(error)
646
+ except KeyboardInterrupt:
647
+ self.stopped = "stopped by you"
648
+ return self.map()
649
+
650
+ def explore(self, screen, snapshot):
651
+ """Explores ``screen`` (``snapshot`` shows it); the last snapshot on it, or None when it lost the screen."""
652
+ scrolls = 0
653
+ while not self.capped():
654
+ found = self.candidates(screen, snapshot)
655
+ if found and self.saturated(screen):
656
+ screen.held_back += len(found)
657
+ break
658
+ if not found:
659
+ if scrolls >= self.caps.pages - 1 or screen.changed or not more_below(snapshot):
660
+ break
661
+ after = self.scroll(snapshot)
662
+ scrolls += 1
663
+ moved = after.content_fingerprint != snapshot.content_fingerprint
664
+ self.learn(screen, after)
665
+ snapshot = after
666
+ if not moved:
667
+ break # the end of the list
668
+ continue
669
+ pick = found[0]
670
+ screen.done.add(pick.key)
671
+ snapshot = self.visit(screen, snapshot, pick)
672
+ if snapshot is None:
673
+ screen.unreachable = True
674
+ return None
675
+ return snapshot
676
+
677
+ def visit(self, screen, snapshot, pick):
678
+ """Taps ``pick`` on ``screen``, maps where it leads, explores it when new, and comes back."""
679
+ try:
680
+ after = self.tap(pick.element, snapshot)
681
+ after = self.dialog(after, system=state(after) == state(self.before))
682
+ except Gone as gone:
683
+ return gone.snapshot # it moved; the next pass reads the screen again
684
+ if self.bundle_left(after):
685
+ self.stats["left_app"] += 1
686
+ self.edges.append({"from": screen.id, "to": "", "label": pick.label, "kind": "leaves app",
687
+ "app": after.bundle_id})
688
+ return self.return_to(screen, after)
689
+ if self.is_on(screen, after):
690
+ if state(after) != state(self.before):
691
+ # The tap changed this screen in place: it may have set something, so nothing else on it is tapped.
692
+ screen.changed.append(pick.label)
693
+ self.stats["changed_in_place"] = self.stats.get("changed_in_place", 0) + 1
694
+ self.log(f"“{pick.label}” changed {screen.id} in place: leaving the rest of it alone")
695
+ else:
696
+ self.stats["no_change"] += 1
697
+ screen.noop.append(pick.label)
698
+ self.learn(screen, after)
699
+ return after
700
+ reading = screens.read(after)
701
+ target = self.identify(reading)
702
+ kind = "tab" if pick.reason == "tab" else "tap"
703
+ if target is None:
704
+ depth = 0 if kind == "tab" else screen.depth + 1
705
+ path = ([] if kind == "tab" else list(screen.path)) + [{"label": pick.label, "key": pick.key,
706
+ "kind": kind}]
707
+ target = self.register(after, reading, depth, path, parent=screen.id, via=pick.label)
708
+ shape = target.tokens - {screens.token(target.title)}
709
+ if any(screens.jaccard(shape, other) >= ALIKE for other in screen.kids):
710
+ screen.alike += 1
711
+ screen.kids.append(shape)
712
+ self.edges.append({"from": screen.id, "to": target.id, "label": pick.label, "kind": kind})
713
+ if target.paywall is None and target.depth < self.limit and not self.capped():
714
+ explored = self.explore(target, after)
715
+ after = explored if explored is not None else self.observe()
716
+ elif target.paywall is None:
717
+ target.pending = bool(self.candidates(target, after)) or more_below(after)
718
+ else:
719
+ self.edges.append({"from": screen.id, "to": target.id, "label": pick.label, "kind": kind})
720
+ if self.stopped:
721
+ return after
722
+ return self.return_to(screen, after)
723
+
724
+ def map(self):
725
+ wall = max(self.elapsed(), 1e-6)
726
+ refused = [{"screen": s.id, "label": label, "reason": reason}
727
+ for s in self.screens for label, reason in s.refused.items()]
728
+ safety = [item for item in refused if item["reason"] not in QUIET_REASONS]
729
+ paywalled = [s for s in self.screens if s.paywall]
730
+ linked = {edge["from"] for edge in self.edges}
731
+ stats = {
732
+ "screens": len(self.screens), "edges": len(self.edges),
733
+ "max_depth": max((s.depth for s in self.screens), default=0),
734
+ "wall_seconds": round(wall, 1),
735
+ "screens_per_minute": round(len(self.screens) / (wall / 60), 1),
736
+ "refused": len(safety),
737
+ "paywalls": len(paywalled),
738
+ "dead_ends": sum(1 for s in self.screens if s.id not in linked and not s.paywall),
739
+ "model_usd": 0.0,
740
+ **self.stats,
741
+ "settles": {kind: {"n": d["n"], "seconds": round(d["seconds"], 1)}
742
+ for kind, d in sorted(self.settles.items())},
743
+ "seconds_in": {kind: round(value, 1) for kind, value in sorted(self.spent.items())},
744
+ }
745
+ return {"schema": SCHEMA, "app": {"bundle": self.bundle}, "created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
746
+ "connection": "pixels" if any(s.image_only for s in self.screens) else "tree",
747
+ "caps": {"screens": self.caps.screens, "depth": self.caps.depth, "seconds": self.caps.seconds,
748
+ "pages": self.caps.pages},
749
+ "stopped": self.stopped, "stats": stats,
750
+ "screens": [s.to_json() for s in self.screens], "edges": self.edges}
751
+
752
+
753
+ def policy_refusal(element, verdict):
754
+ return Stop(f"the policy refused “{policy.plain(element.label)[:60]}”: {verdict.reason}")