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,305 @@
1
+ """Fixtures (.mobster/fixtures/<name>.yaml): what changes a device's state is for simulators, refused on a phone."""
2
+
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from contextlib import nullcontext
8
+ from dataclasses import dataclass, field, replace
9
+ from pathlib import Path
10
+
11
+ from . import dialogs as dialog_policy
12
+ from .assertions import CheckError
13
+
14
+ FOLDER = "fixtures"
15
+ SUFFIXES = (".yaml", ".yml")
16
+ NAME = re.compile(r"[a-z0-9][a-z0-9-]{0,59}")
17
+ KEYS = ("extends", "reset", "launch", "files", "keychain", "pasteboard", "steps", "dialogs")
18
+ DEPTH = 6
19
+ FILES_LIMIT = 50
20
+ BYTES_LIMIT = 20 * 1024 * 1024
21
+ PASTEBOARD_LIMIT = 10_000
22
+ PHONE_REFUSAL = "{what} changes the state of a phone, and fixtures do that on simulators only (from {name})."
23
+ PHONE_FIX = "Run the check on a simulator, or use a fixture with only launch args, env and steps on the phone."
24
+
25
+
26
+ class FixtureError(Exception):
27
+ """A fixture can't be read, found or applied: one sentence that says what to do."""
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Effective:
32
+ """A fixture and what it extends, flattened."""
33
+ names: tuple = ()
34
+ reset: str = "none"
35
+ args: tuple = ()
36
+ env: dict = field(default_factory=dict)
37
+ steps: tuple = ()
38
+ files: tuple = () # of (absolute source, destination relative to the container)
39
+ keychain: bool = False
40
+ pasteboard: str = ""
41
+ dialogs: dict = field(default_factory=dict) # how a run answers iOS's own dialogs (dialogs.py)
42
+
43
+ @property
44
+ def changes_state(self):
45
+ """What a real iPhone must not get: a reset, files, a keychain reset or a clipboard."""
46
+ return [word for word, held in (("a reset", self.reset != "none"), ("seed files", bool(self.files)),
47
+ ("a keychain reset", self.keychain), ("a pasteboard", bool(self.pasteboard)))
48
+ if held]
49
+
50
+ def public(self):
51
+ return {"names": list(self.names), "reset": self.reset, "args": list(self.args), "env": dict(self.env),
52
+ "steps": list(self.steps), "files": [{"from": str(src), "to": dest} for src, dest in self.files],
53
+ "keychain": self.keychain, "pasteboard": bool(self.pasteboard), "dialogs": dict(self.dialogs),
54
+ "destructive": bool(self.changes_state)}
55
+
56
+
57
+ def folder(root):
58
+ return Path(root) / ".mobster" / FOLDER
59
+
60
+
61
+ def path_of(root, name):
62
+ for suffix in SUFFIXES:
63
+ path = folder(root) / f"{name}{suffix}"
64
+ if path.is_file():
65
+ return path
66
+ return None
67
+
68
+
69
+ def names(root):
70
+ """Every fixture's name in the project, sorted."""
71
+ base = folder(root)
72
+ if not base.is_dir():
73
+ return []
74
+ return sorted({path.stem for path in base.iterdir() if path.suffix in SUFFIXES and NAME.fullmatch(path.stem)})
75
+
76
+
77
+ def _load(root, name):
78
+ """One fixture file as a dict of validated parts (not yet flattened)."""
79
+ from . import checks
80
+ path = path_of(root, name)
81
+ if path is None:
82
+ have = ", ".join(names(root)[:8])
83
+ raise FixtureError(f"There's no fixture called {name}." + (f" You have: {have}." if have else
84
+ " Add .mobster/fixtures/NAME.yaml."))
85
+ import yaml
86
+ try:
87
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
88
+ except (OSError, UnicodeDecodeError):
89
+ raise FixtureError(f"{path.name} can't be read. Check that it is a text file.") from None
90
+ except yaml.YAMLError:
91
+ raise FixtureError(f"{path.name} isn't valid YAML.") from None
92
+ if data is None:
93
+ data = {}
94
+ if not isinstance(data, dict) or set(data) - set(KEYS):
95
+ bad = sorted(set(data) - set(KEYS))[0] if isinstance(data, dict) else None
96
+ raise FixtureError(f"{path.name}: " + (f"`{str(bad)[:40]}` isn't a fixture key. Use {', '.join(KEYS)}."
97
+ if bad is not None else "a fixture is a mapping of keys."))
98
+ where = path.name
99
+ try:
100
+ extends = data.get("extends") or []
101
+ extends = [extends] if isinstance(extends, str) else extends
102
+ if not isinstance(extends, list) or not all(isinstance(item, str) and NAME.fullmatch(item) for item in extends):
103
+ raise CheckError("extends is a fixture name or a list of them")
104
+ reset = data.get("reset")
105
+ if reset is not None and reset not in checks.RESETS:
106
+ raise CheckError(f"reset is one of {', '.join(checks.RESETS)}, not {reset!r}")
107
+ launch = data.get("launch") or {}
108
+ if not isinstance(launch, dict) or set(launch) - {"args", "env"}:
109
+ raise CheckError("launch holds args and env")
110
+ args = checks._strings(launch.get("args"), "launch.args", checks.LIMITS["args"])
111
+ env = checks._env(launch.get("env"), "launch.env")
112
+ steps = checks._steps(data.get("steps"))
113
+ keychain = data.get("keychain")
114
+ if keychain not in (None, "reset", False):
115
+ raise CheckError("keychain is reset")
116
+ pasteboard = data.get("pasteboard") or ""
117
+ if not isinstance(pasteboard, str) or len(pasteboard) > PASTEBOARD_LIMIT:
118
+ raise CheckError(f"pasteboard is text of at most {PASTEBOARD_LIMIT:,} characters")
119
+ files = _files(data.get("files"), folder(root))
120
+ answers = dialog_policy.parse(data.get("dialogs"))
121
+ except CheckError as error:
122
+ raise FixtureError(f"{where}: {error}") from None
123
+ return {"extends": extends, "reset": reset, "args": args, "env": env, "steps": steps, "files": files,
124
+ "keychain": keychain == "reset", "pasteboard": pasteboard, "dialogs": answers}
125
+
126
+
127
+ def _files(value, base):
128
+ if value is None:
129
+ return ()
130
+ if not isinstance(value, list) or len(value) > FILES_LIMIT:
131
+ raise CheckError(f"files is a list of at most {FILES_LIMIT} entries with from and to")
132
+ out = []
133
+ for index, item in enumerate(value, 1):
134
+ if (not isinstance(item, dict) or set(item) != {"from", "to"}
135
+ or not all(isinstance(v, str) for v in item.values())):
136
+ raise CheckError(f"files[{index}] needs from and to, both text")
137
+ source = (Path(base) / item["from"])
138
+ real = Path(os.path.realpath(source))
139
+ if Path(item["from"]).is_absolute() or ".." in Path(item["from"]).parts or not real.exists() or \
140
+ not (real == Path(os.path.realpath(base)) or Path(os.path.realpath(base)) in real.parents):
141
+ raise CheckError(f"files[{index}].from {item['from']!r} must be a file or folder inside .mobster/{FOLDER}/")
142
+ dest = Path(item["to"])
143
+ if not item["to"].strip() or dest.is_absolute() or ".." in dest.parts or str(dest) in (".", ""):
144
+ raise CheckError(f"files[{index}].to {item['to']!r} must be a path inside the app's container, such as "
145
+ "Documents/cart.json")
146
+ out.append((real, dest.as_posix()))
147
+ return tuple(out)
148
+
149
+
150
+ def resolve(root, wanted):
151
+ """The flattened Effective of the fixture names in ``wanted``, in order."""
152
+ state = {"names": [], "reset": "none", "args": [], "env": {}, "steps": [], "files": {}, "keychain": False,
153
+ "pasteboard": "", "dialogs": {}}
154
+ done = []
155
+
156
+ def add(name, chain):
157
+ if name in chain:
158
+ raise FixtureError(f"Fixtures extend each other in a circle: {' → '.join([*chain, name])}.")
159
+ if len(chain) >= DEPTH:
160
+ raise FixtureError(f"Fixtures extend more than {DEPTH} deep, ending at {name}.")
161
+ if name in done:
162
+ return
163
+ part = _load(root, name)
164
+ for parent in part["extends"]:
165
+ add(parent, (*chain, name))
166
+ done.append(name)
167
+ state["names"].append(name)
168
+ if part["reset"] is not None:
169
+ state["reset"] = part["reset"]
170
+ state["args"] += part["args"]
171
+ state["env"].update(part["env"])
172
+ state["steps"] += part["steps"]
173
+ for source, dest in part["files"]:
174
+ state["files"][dest] = source
175
+ state["keychain"] = state["keychain"] or part["keychain"]
176
+ state["pasteboard"] = part["pasteboard"] or state["pasteboard"]
177
+ state["dialogs"] = dialog_policy.merge(state["dialogs"], part["dialogs"])
178
+
179
+ for name in wanted:
180
+ if not isinstance(name, str) or not NAME.fullmatch(name):
181
+ raise FixtureError(
182
+ "A fixture name is 1 to 60 lowercase letters, digits or hyphens, starting with a letter or digit.")
183
+ add(name, ())
184
+ return Effective(names=tuple(state["names"]), reset=state["reset"], args=tuple(state["args"]),
185
+ env=dict(state["env"]), steps=tuple(state["steps"]),
186
+ files=tuple((source, dest) for dest, source in state["files"].items()),
187
+ keychain=state["keychain"], pasteboard=state["pasteboard"], dialogs=dict(state["dialogs"]))
188
+
189
+
190
+ def with_fixtures(check, root):
191
+ if not check.fixture or check.fixture_origin is not None:
192
+ return check
193
+ effective = resolve(root, check.fixture)
194
+ return replace(check, fixture_origin=check,
195
+ reset=effective.reset if check.reset == "auto" and effective.reset != "none" else check.reset,
196
+ launch_args=effective.args + check.launch_args,
197
+ launch_env={**effective.env, **check.launch_env}, steps=effective.steps + check.steps,
198
+ dialogs=dialog_policy.merge(effective.dialogs, check.dialogs),
199
+ seed=Seed(effective.files, effective.keychain, effective.pasteboard, tuple(effective.changes_state)))
200
+
201
+
202
+ def for_run(check, runs_dir):
203
+ """The check with its fixtures flattened in. The project is the folder that holds ``.mobster/`` beside the runs,
204
+ else the folder the check was read from; a check that names a fixture with no project to take it from is refused
205
+ before any simulator is touched."""
206
+ if not check.fixture or check.fixture_origin is not None:
207
+ return check
208
+ from .checks import project_root
209
+ runs_dir = Path(runs_dir)
210
+ root = runs_dir.parent.parent if runs_dir.parent.name == ".mobster" else None
211
+ if root is None and check.source:
212
+ root = project_root(check.source)
213
+ if root is None:
214
+ raise FixtureError(f"This check uses the fixture {', '.join(check.fixture)}, which comes from a project's "
215
+ ".mobster/fixtures/. Run it from a project folder that has .mobster/, with --out left at "
216
+ "its default.")
217
+ return with_fixtures(check, root)
218
+
219
+
220
+ @dataclass(frozen=True)
221
+ class Seed:
222
+ files: tuple = ()
223
+ keychain: bool = False
224
+ pasteboard: str = ""
225
+ risky: tuple = () # what the fixtures change that a real iPhone must not get (Effective.changes_state)
226
+
227
+ def __bool__(self):
228
+ return bool(self.files or self.keychain or self.pasteboard)
229
+
230
+
231
+ def refuse_on_phone(check):
232
+ """FixtureError when the check's fixtures would change a real iPhone's state."""
233
+ if check.seed.risky:
234
+ raise FixtureError(PHONE_REFUSAL.format(what=check.seed.risky[0], name=", ".join(check.fixture))
235
+ + " " + PHONE_FIX)
236
+
237
+
238
+ def seed_simulator(seed, *, udid, bundle, run, pbcopy=None, actions=None):
239
+ """Put the seed on simulator ``udid``: the keychain emptied, the files copied into ``bundle``'s data container and
240
+ the pasteboard set."""
241
+ did = []
242
+ if seed.keychain:
243
+ with actions.action("FIXTURE_KEYCHAIN") if actions is not None else nullcontext():
244
+ code, out = run(["keychain", udid, "reset"])
245
+ if code != 0:
246
+ raise FixtureError(f"The simulator's keychain wouldn't reset ({out[:120] or code}).")
247
+ did.append("keychain reset")
248
+ if seed.files:
249
+ code, container = run(["get_app_container", udid, bundle, "data"])
250
+ if code != 0 or not container.strip() or container.strip() == "(null)":
251
+ raise FixtureError(f"{bundle} isn't installed on the simulator, so there is no container to put files in.")
252
+ count = _copy(seed.files, container.strip(), udid, actions=actions)
253
+ did.append(f"{count} file{'' if count == 1 else 's'} copied")
254
+ if seed.pasteboard:
255
+ with actions.action("FIXTURE_PASTEBOARD") if actions is not None else nullcontext():
256
+ (pbcopy or _pbcopy)(udid, seed.pasteboard)
257
+ did.append("pasteboard set")
258
+ return did
259
+
260
+
261
+ def _pbcopy(udid, text):
262
+ try:
263
+ done = subprocess.run(["xcrun", "simctl", "pbcopy", udid], input=text, text=True, capture_output=True,
264
+ timeout=30)
265
+ except (OSError, subprocess.TimeoutExpired):
266
+ raise FixtureError("The simulator's pasteboard wouldn't set.") from None
267
+ if done.returncode != 0:
268
+ raise FixtureError("The simulator's pasteboard wouldn't set.")
269
+
270
+
271
+ def _copy(files, container, udid, *, actions=None):
272
+ from ..sim import apps
273
+ from ..sim.api import SimError
274
+ try:
275
+ root = apps.guard_container(container, udid)
276
+ except SimError as error:
277
+ raise FixtureError(str(error)) from None
278
+ total = count = 0
279
+ plan = []
280
+ for source, dest in files:
281
+ target = (root / dest)
282
+ for path in ([source] if source.is_file() else sorted(p for p in source.rglob("*"))):
283
+ if path.is_symlink():
284
+ raise FixtureError(f"{path.name} is a symbolic link, so Mobster didn't copy it.")
285
+ if path.is_file():
286
+ total += path.stat().st_size
287
+ count += 1
288
+ plan.append((path, target if source.is_file() else target / path.relative_to(source)))
289
+ if count > FILES_LIMIT * 4 or total > BYTES_LIMIT:
290
+ raise FixtureError(f"The seed files are {count} files and {total // 1024} KB; the limit is "
291
+ f"{FILES_LIMIT * 4} files and {BYTES_LIMIT // 1024 // 1024} MB.")
292
+ for _, target in plan:
293
+ for parent in [target, *target.parents]:
294
+ if parent == root:
295
+ break
296
+ if parent.is_symlink():
297
+ raise FixtureError(f"{target.relative_to(root)} goes through a symbolic link in the container, so "
298
+ "Mobster didn't copy it.")
299
+ if root not in Path(os.path.realpath(target)).parents:
300
+ raise FixtureError(f"{target.relative_to(root)} leaves the app's container, so Mobster didn't copy it.")
301
+ for path, target in plan:
302
+ with actions.action("FIXTURE_FILE") if actions is not None else nullcontext():
303
+ target.parent.mkdir(parents=True, exist_ok=True)
304
+ shutil.copyfile(path, target)
305
+ return count
@@ -0,0 +1,213 @@
1
+ import base64
2
+ import time
3
+
4
+ from .smart import LEFT_APP, LEFT_APP_FIX, check_approver, check_guard, no_smart_key, smart_request
5
+ from .verdict import CouldntRun
6
+
7
+ NOTE_LIMIT = 500
8
+
9
+
10
+ def _safe(cancelled):
11
+ try:
12
+ return bool(cancelled())
13
+ except Exception:
14
+ return False
15
+
16
+
17
+ def _data_url_bytes(url):
18
+ if not isinstance(url, str) or not url.startswith("data:") or "," not in url:
19
+ return None
20
+ try:
21
+ return base64.b64decode(url.split(",", 1)[1])
22
+ except (ValueError, TypeError):
23
+ return None
24
+
25
+
26
+ class Flow:
27
+ def run_smart(self, cancelled=None):
28
+ from .. import costs, engines, smart_events
29
+ from ..model_clients import cost_usd
30
+ if self.mode != "smart":
31
+ raise ValueError("run_smart needs mode smart")
32
+ key = self.key or engines.smart_key()
33
+ if not key:
34
+ why, fix = no_smart_key()
35
+ raise self._fail(CouldntRun("model", f"Smart can't run: {why}.", fix))
36
+ self.key = key
37
+ self.state = "running"
38
+ spent = {"usd": 0.0}
39
+ started = time.monotonic()
40
+ deadline = started + float(self.check.max_seconds) + 60 # a backstop past the loop's own time limit
41
+ names = {self.app.bundle_id: self.app.name}
42
+
43
+ def stopping():
44
+ if self._stop.is_set() or time.monotonic() > deadline:
45
+ return True
46
+ try:
47
+ return bool(cancelled and cancelled())
48
+ except Exception:
49
+ return False
50
+
51
+ def on_inference(event):
52
+ if event.get("event") == "inference_finished" and isinstance(event.get("estimated_usd"), (int, float)):
53
+ spent["usd"] += event["estimated_usd"]
54
+ self.event(event)
55
+
56
+ def on_event(event):
57
+ kind = event.get("event")
58
+ if kind == "frontier_prompt":
59
+ data = _data_url_bytes(event.get("image"))
60
+ path = self.frame_from_bytes(data, "step") if data else None
61
+ if not data:
62
+ self._observe_screen(None)
63
+ if path is not None:
64
+ self._give_after_frame(path)
65
+ self.event({"event": kind, "step": event.get("step"), "operations": event.get("operations"),
66
+ "out": event.get("out")})
67
+ return
68
+ self.event(event)
69
+ if kind == "frontier_action":
70
+ op = event.get("operation") or ""
71
+ label = event.get("target_label") or ""
72
+ text = smart_events.step_text(op, label, names, text=event.get("text"))
73
+ typed = event.get("text") if op in smart_events.TYPING else None
74
+ step = self.record_step(op=op, text=text, target={"id": None, "label": label or None, "role": None},
75
+ typed=typed, changed=event.get("changed"))
76
+ # Its frame is the screen after it: the image Smart sees next turn, or the verdict frame.
77
+ self._awaiting_frame.append(step)
78
+ self.progress(f" {text}")
79
+
80
+ with self.lock:
81
+ client = engines.build_client(key=key)
82
+ costs.meter(client, on_inference)
83
+ guard = check_guard(self.manager, self.app.bundle_id)
84
+ driver, recording = self.driver, {}
85
+ if self.replay is not None and self.replay.mode != "off":
86
+ recorder = self.replay.recorder() # the path a later run replays, if this one passes
87
+ driver, recording = recorder.wrap(self.driver), {"step_observer": recorder.observe_step}
88
+ agent = engines.build_frontier(driver, client, apps=names, emit=on_event,
89
+ max_cost_usd=self.check.max_usd, approve=check_approver(self.manager),
90
+ cancelled=stopping, guard=guard, dialogs=True,
91
+ dialog_policy=self.dialogs, **recording)
92
+ agent.max_seconds = float(self.check.max_seconds) # the check's time limit, not the app's 15 minutes
93
+ from .. import intents
94
+ if intents.enabled() and getattr(self.target, "udid", None):
95
+ from ..intents.plan import LinkFirst
96
+ agent.link_first = LinkFirst.for_device(self.target.udid)
97
+ request = smart_request(self.app.name, self._live.steps)
98
+ self.progress(f"Smart: {request}")
99
+ outcome = agent.run(request)
100
+ self.timing["flow"] = round(time.monotonic() - started, 2)
101
+ status = outcome.get("status")
102
+ if outcome.get("code") == "unknown_outcome":
103
+ raise self._fail(CouldntRun("unknown_outcome", "A phone action's outcome is unknown.",
104
+ "Inspect the phone before another run; Mobster did not retry the action."))
105
+ if status == "stopped" and not self._stop.is_set() and not (cancelled and _safe(cancelled)):
106
+ status = "timeout" # the backstop, not the user
107
+ self._smart_status, self._smart_reason = status, str(outcome.get("reason") or "")
108
+ if guard is not None and guard.left and outcome.get("code") == "blocked_app":
109
+ self._left_app = guard.left
110
+ self._model_failed = bool(getattr(client, "model_failed", False))
111
+ loop_cost = cost_usd(getattr(client, "model", ""), outcome.get("usage") or {}) or 0.0
112
+ self.cost_usd = round(max(spent["usd"], loop_cost), 5)
113
+ note = outcome.get("answer") or outcome.get("reason") or ""
114
+ self.agent = {"status": status, "note": self._scrub(str(note))[:NOTE_LIMIT] or None,
115
+ "model": getattr(client, "model", None), "turns": outcome.get("steps"),
116
+ **_link_first(outcome)}
117
+ self.log(f"Smart ended {status} after {outcome.get('steps')} turns, ${self.cost_usd:.4f}")
118
+ self.state = "ready"
119
+
120
+ def run_flow(self, cancelled=None):
121
+ """The check's steps: replayed from the recorded path when one is cached and replay is on, else Smart."""
122
+ if (self.replay is not None and not (self.check.throughout or self.check.somewhere)
123
+ and self.replay.lookup() is not None):
124
+ return self.run_replay(cancelled) # a replay shows no screens to sweep, so `throughout` runs Smart
125
+ if self.replay is not None and self.replay.reason:
126
+ self.progress(f"Smart runs every step ({self.replay.reason}); a pass records the path to replay")
127
+ return self.run_smart(cancelled)
128
+
129
+ def run_replay(self, cancelled=None):
130
+ """Replay the recorded path with no model (replay_core); Smart heals a step that changed, for that step only,
131
+ unless replay is strict."""
132
+ from .. import engines
133
+ if self.mode != "smart" or self.replay is None:
134
+ raise ValueError("run_replay needs mode smart with replay on")
135
+ self.state = "running"
136
+ started = time.monotonic()
137
+ steps = len(self.replay.path["steps"])
138
+ if (steps and (self._a11y is not None or self._l10n is not None)
139
+ and not (self.source_observation is not None and self.source_observation.active)):
140
+ self._incomplete_screen("Replay did not read each intermediate accessibility screen")
141
+ self.progress(f"Replaying {steps} recorded step{'' if steps == 1 else 's'} (no model)")
142
+ with self.lock:
143
+ tune = getattr(self.driver, "tune", None)
144
+ if callable(tune):
145
+ try:
146
+ tune(rich=True, glide=True)
147
+ except Exception as error:
148
+ self.log(f"tune failed: {type(error).__name__}")
149
+ result = self.replay.replay(cancelled)
150
+ self.timing["flow"] = round(time.monotonic() - started, 2)
151
+ self.cost_usd = round(result.cost_usd, 5)
152
+ status = {"done": result.model_status or "done", "declined": "approval_denied", "stopped": "stopped",
153
+ "drift": "done"}.get(result.status)
154
+ if result.status == "drift":
155
+ self._replay_drift = result.reason
156
+ if result.status == "heal_failed" and result.model_status == "budget":
157
+ status = "budget"
158
+ healed = len(result.heals)
159
+ note = (f"Replayed {result.replayed} step{'' if result.replayed == 1 else 's'} with no model"
160
+ + (f", healed {healed}" if healed else "") + (f": {result.reason}" if result.reason else ""))
161
+ self.agent = {"status": status or result.status, "note": self._scrub(note)[:NOTE_LIMIT],
162
+ "model": engines.smart_model() if result.cost_usd else None, "turns": None, "replayed": True}
163
+ self.log(f"replay ended {result.status} after {result.replayed} steps, {healed} healed, "
164
+ f"${self.cost_usd:.4f}")
165
+ self.state = "ready"
166
+ if self._left_app:
167
+ raise self._fail(self._smart_failure())
168
+ if result.status == "dialog":
169
+ raise self._fail(CouldntRun("system_dialog", f"The replay stopped: {result.reason}",
170
+ "Name an answer in the check's dialogs, or answer it by hand and run the check "
171
+ "again; the recorded steps are kept."))
172
+ if status is None:
173
+ if result.model_status == "unknown_outcome":
174
+ raise self._fail(CouldntRun("unknown_outcome", f"The replay stopped: {result.reason}",
175
+ "Check the app before starting another run; the action may have happened."))
176
+ raise self._fail(CouldntRun("replay", f"The replay stopped: {result.reason}",
177
+ "Run it again: the next run records the flow afresh (or use --fresh)."))
178
+ self._smart_status, self._smart_reason = status, result.reason or ""
179
+
180
+ def _smart_failure(self):
181
+ """CouldntRun for a Smart run that ended in error or was stopped (§4 rule 4), else None."""
182
+ status = self._smart_status
183
+ if self._left_app:
184
+ return CouldntRun("usage", LEFT_APP.format(app=self.app.name, other=self._left_app), LEFT_APP_FIX)
185
+ if status == "stopped":
186
+ return CouldntRun("stopped", "The run was stopped before the flow finished.")
187
+ if status != "error":
188
+ return None
189
+ from .. import engines
190
+ reason = self._smart_reason
191
+ model = (self.agent or {}).get("model") or engines.smart_model()
192
+ found = engines.PROVIDER_ERROR.search(reason)
193
+ provider = found.group(1) if found else engines.provider_name(model)
194
+ ok, why = engines.model_failure(reason, model=model)
195
+ if ok is False:
196
+ return CouldntRun("model", why, f"Check your {provider} key and credit, then run again.")
197
+ if self._model_failed or found or "OpenAI" in reason or "Anthropic" in reason:
198
+ reach = engines.NO_ANTHROPIC_REACH if provider == "Anthropic" else engines.NO_OPENAI_REACH
199
+ return CouldntRun("model", engines.plain_error(reason, model_failed=True, model=model) or reach)
200
+ from ..frontier import CONNECTION_LOST
201
+ if any(name in reason for name in CONNECTION_LOST + ("TimeoutError", "timed out", "TransportError")):
202
+ return CouldntRun("wda", "WebDriverAgent stopped answering during the flow and didn't come back.",
203
+ "Run `mobster sim doctor --fix`, then try again.")
204
+ return CouldntRun("internal", "Smart stopped with an unexpected error. run.log has the details.")
205
+
206
+
207
+ def _link_first(outcome):
208
+ """{"linkFirst": {"taken", "landed", "modelCallsSaved"}} when the run opened a link before any model call."""
209
+ timing = outcome.get("timing") or {}
210
+ if not timing.get("link_first"):
211
+ return {}
212
+ return {"linkFirst": {"taken": timing["link_first"], "landed": timing.get("link_first_landed", 0),
213
+ "modelCallsSaved": timing.get("model_calls_saved", 0)}}
@@ -0,0 +1,141 @@
1
+ """The locale audit: what `mobster test --locale de,ja,ar` reads in the trees of a check run in a language."""
2
+
3
+ import re
4
+
5
+ from .a11y import EDGE, TEXT, _clipped, _describe
6
+ from .tree import SCROLL_CONTAINERS
7
+
8
+ NAME = "l10n"
9
+ RTL = frozenset({"ar", "he", "fa", "ur", "ps", "sd", "ug", "yi", "dv"})
10
+ # Languages whose own script isn't Latin: an ASCII sentence in one of them was not translated.
11
+ NON_LATIN = RTL | frozenset({"ja", "ko", "zh", "ru", "uk", "bg", "sr", "mk", "be", "el", "th", "hi", "bn", "ta", "te",
12
+ "ml", "kn", "gu", "pa", "mr", "ne", "si", "my", "km", "lo", "ka", "hy", "am", "mn"})
13
+ GUIDELINES = {"text-truncated": "Localisation: long strings",
14
+ "untranslated-string": "Localisation: missing translation",
15
+ "rtl-not-mirrored": "Localisation: right-to-left layout"}
16
+ ELLIPSIS = re.compile(r".*(…|\.\.\.)\s*$", re.S)
17
+ ASCII_WORDS = re.compile(r"[A-Za-z]{2,}(?:[ '\-][A-Za-z]{2,})+|[A-Za-z]{5,}")
18
+ RTL_SCRIPT = re.compile(r"[֐-ࣿיִ-﷿ﹰ-]")
19
+ WIDE = 0.6 # a container at least this share of the screen's width counts as a full-width row
20
+ NARROW = 0.7 # text narrower than this share of its row can show which edge it is flush to
21
+
22
+
23
+ def language(locale):
24
+ return (locale or "").replace("-", "_").split("_")[0].lower()
25
+
26
+
27
+ def is_rtl(locale):
28
+ return language(locale) in RTL
29
+
30
+
31
+ def _finding(rule, node, message, frame):
32
+ name, where = _describe(node)
33
+ x, y, w, h = node.rect
34
+ return {"id": rule, "audit": NAME, "guideline": GUIDELINES[rule], "level": "warn", "frame": frame,
35
+ "message": f"{name} {where}: {message}", "evidence": f"{name} {where}", "baseline": False,
36
+ "element": {"role": node.role, "label": node.label, "identifier": node.identifier, "path": node.path,
37
+ "rect": [x, y, w, h]}}
38
+
39
+
40
+ def _ancestors(path):
41
+ while "/" in path.strip("/"):
42
+ path = path.rsplit("/", 1)[0]
43
+ yield path
44
+
45
+
46
+ def audit_tree(tree, locale, *, frame=None):
47
+ """The findings of one screen's tree in ``locale``."""
48
+ shown = tree.shown()
49
+ by_path = {node.path: node for node in tree.nodes}
50
+ width = tree.size[0]
51
+ lang = language(locale)
52
+ out = []
53
+ for node in shown:
54
+ label = node.label.strip()
55
+ x, y, w, h = node.rect
56
+ if node.role in TEXT and label and w > 0 and h > 0:
57
+ if ELLIPSIS.fullmatch(label):
58
+ out.append(_finding("text-truncated", node,
59
+ "the text ends in an ellipsis, so it was cut short. Let it wrap or give it room "
60
+ "(SwiftUI: remove .lineLimit; UIKit: numberOfLines = 0).", frame))
61
+ else:
62
+ clipped = _clipped(node, by_path, width)
63
+ if clipped:
64
+ out.append(_finding("text-truncated", node,
65
+ f"in {locale} it is {clipped}. Let the text wrap or the container grow.",
66
+ frame))
67
+ if lang in NON_LATIN and node.role in TEXT and label and _ascii_sentence(label):
68
+ out.append(_finding("untranslated-string", node,
69
+ f"the text is plain English in {locale}. Translate it, or ignore this if it is a "
70
+ "brand name.", frame))
71
+ if is_rtl(locale):
72
+ out.extend(_not_mirrored(shown, by_path, width, frame))
73
+ return out
74
+
75
+
76
+ def _ascii_sentence(label):
77
+ letters = re.sub(r"[^A-Za-z]", "", label)
78
+ return bool(ASCII_WORDS.fullmatch(" ".join(label.split()))) and len(letters) >= 5 \
79
+ and len(re.sub(r"[A-Za-z '\-]", "", label)) == 0
80
+
81
+
82
+ def _not_mirrored(shown, by_path, width, frame):
83
+ out = []
84
+ for bar in (node for node in shown if node.role == "NavigationBar"):
85
+ bx, _, bw, _ = bar.rect
86
+ buttons = [node for node in shown if node.role == "Button" and node.path.startswith(bar.path + "/")
87
+ and node.rect[2] > 0]
88
+ if buttons and all(node.rect[0] + node.rect[2] / 2 < bx + bw / 2 for node in buttons) and bw >= width * WIDE:
89
+ out.append(_finding("rtl-not-mirrored", bar,
90
+ "every button sits in the left half, so the back button didn't move to the right. "
91
+ "Use leading and trailing edges, not left and right.", frame))
92
+ for node in shown:
93
+ label = node.label.strip()
94
+ x, y, w, h = node.rect
95
+ if node.role != "StaticText" or not label or not RTL_SCRIPT.search(label) or w <= 0 or h <= 0:
96
+ continue
97
+ row = next((by_path[path] for path in _ancestors(node.path) if path in by_path
98
+ and by_path[path].rect[2] >= width * WIDE and by_path[path].role not in SCROLL_CONTAINERS), None)
99
+ if row is None or w > row.rect[2] * NARROW:
100
+ continue
101
+ left, right = x - row.rect[0], (row.rect[0] + row.rect[2]) - (x + w)
102
+ if left + EDGE < right and left < row.rect[2] * 0.1:
103
+ out.append(_finding("rtl-not-mirrored", node,
104
+ "right-to-left text is flush to the left edge of its row. Align it to the leading "
105
+ "edge (SwiftUI: .multilineTextAlignment(.leading); UIKit: .natural).", frame))
106
+ return out
107
+
108
+
109
+ class Collector:
110
+ """A run's screens, audited once each (``add(tree, frame)``), like a11y.Collector."""
111
+
112
+ LIMIT = 60
113
+
114
+ def __init__(self, locale):
115
+ self.locale = locale
116
+ self._seen, self._found, self._keys = {}, [], set()
117
+
118
+ def add(self, tree, frame=None):
119
+ fingerprint = tree.audit_fingerprint()
120
+ if fingerprint in self._seen:
121
+ if frame is not None:
122
+ start, end = self._seen[fingerprint]
123
+ for finding in self._found[start:end]:
124
+ if finding["frame"] is None:
125
+ finding["frame"] = frame
126
+ return True
127
+ if len(self._seen) >= self.LIMIT:
128
+ return False
129
+ start = len(self._found)
130
+ for finding in audit_tree(tree, self.locale, frame=frame):
131
+ element = finding["element"]
132
+ key = (finding["id"], element["role"], element["label"], element["identifier"],
133
+ tuple(round(n) for n in element["rect"]))
134
+ if key not in self._keys:
135
+ self._keys.add(key)
136
+ self._found.append(finding)
137
+ self._seen[fingerprint] = (start, len(self._found))
138
+ return True
139
+
140
+ def findings(self):
141
+ return list(self._found)