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,614 @@
1
+ """A check (docs/checks.md). Every unknown key is an error, so a typo never silently weakens a check."""
2
+
3
+ import difflib
4
+ import json
5
+ import math
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from .. import links
13
+ from ..state import validate_bundle_id
14
+ from . import conditions, dialogs, netstubs, perf, wire
15
+ from .assertions import CheckError, expand_assertion, reject_unknown_keys
16
+ from .snapshots import Snapshot, parse_snapshot
17
+ from .system_surfaces import spec as surface_spec
18
+
19
+ __all__ = ["CheckError", "Check", "check_from_dict", "load_check", "dump_check", "project_root",
20
+ "RESETS", "LIMITS"]
21
+
22
+ RESETS = ("none", "data", "reinstall")
23
+ LIMITS = {"name": 120, "steps": 20, "step": 500, "expect": 50, "args": 20, "env": 20, "url": 2000,
24
+ "max_usd": 1.00, "max_seconds": 600, "tags": 10}
25
+ DEFAULT_MAX_SECONDS = 180
26
+ DEFAULT_MAX_USD = 0.25
27
+ FILE_LIMIT = 1_000_000
28
+ MAX_NESTING = 100 # lists and objects in a check file; one nests a few deep
29
+ ENV_KEY = re.compile(r"[A-Z_][A-Z0-9_]*")
30
+ URL_SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.\-]*:")
31
+ TAG = re.compile(r"[a-z0-9][a-z0-9_-]{0,39}")
32
+ LAUNCH_ONLY_NAME = "Launch check"
33
+
34
+ FILE_KEYS = ("version", "name", "app", "device", "runtime", "reset", "tags", "launch", "steps", "expect",
35
+ "throughout", "somewhere", "audit", "a11y", "fixture", "business", "conditions", "dialogs", "stubs",
36
+ "budget", "schedule")
37
+ TOOL_KEYS = ("name", "app_path", "bundle_id", "device", "runtime", "reset", "launch_args", "launch_env", "open_url",
38
+ "steps", "expect", "max_usd", "max_seconds")
39
+ TOOL_ONLY = ("image",)
40
+ FILE_SHAPE_KEYS = ("version", "app", "launch", "budget")
41
+ TOOL_SHAPE_KEYS = ("app_path", "bundle_id", "launch_args", "launch_env", "open_url", "max_usd", "max_seconds")
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Check:
46
+ name: str
47
+ steps: tuple # of str
48
+ expect: tuple # of assertions.Assertion
49
+ app_path: Optional[str] # absolute
50
+ bundle_id: Optional[str]
51
+ device: Optional[str]
52
+ runtime: Optional[str]
53
+ reset: str # "auto" | "none" | "data" | "reinstall"
54
+ launch_args: tuple
55
+ launch_env: dict = field(default_factory=dict)
56
+ open_url: Optional[str] = None
57
+ max_seconds: float = DEFAULT_MAX_SECONDS
58
+ max_usd: float = DEFAULT_MAX_USD
59
+ source: Optional[str] = None # the file it came from
60
+ tags: tuple = () # of str: `mobster test --tag` picks checks by them
61
+ # "every 1h" or a five-field cron: Mobster runs this check by itself (testkit/health.py)
62
+ schedule: Optional[str] = None
63
+ throughout: tuple = () # of assertions.Assertion (no_text, absent): must hold on every screen the run reads
64
+ somewhere: tuple = () # of assertions.Assertion: each must hold on at least one screen the run reads
65
+ audit: tuple = () # of str: static audits of the built app (audit.AUDITS)
66
+ fixture: tuple = () # of str: names in .mobster/fixtures/ the check starts from (fixtures.py)
67
+ business: object = None # business.BusinessSpec: locally registered app-state fixture and comparisons
68
+ seed: object = field(default_factory=lambda: _no_seed()) # fixtures.Seed, once the fixtures are flattened in
69
+ fixture_origin: object = None # the check as written, once the fixtures are flattened into it
70
+ sound: tuple = () # of str: sounds (.mobster/sounds/) the iPhone must play during the run (sound.py)
71
+ a11y: Optional[str] = None # "warn" | "strict": the accessibility audit (a11y.py); `audit: a11y` is strict
72
+ a11y_scope: str = "" # the check's file as results.json names it: a baseline entry's first part
73
+ a11y_baseline: frozenset = frozenset() # baseline keys (testkit/a11y_baseline.py) whose findings never fail
74
+ a11y_refresh: bool = False # internal --update-a11y-baseline run; never part of a saved check
75
+ draft_trial: bool = False # internal `mobster test minimize` trial; never part of a saved check
76
+ conditions: dict = field(default_factory=dict) # conditions.parse's block: the device's state for the run
77
+ dialogs: dict = field(default_factory=dict) # dialogs.parse's block: how the run answers iOS's own dialogs
78
+ perf_budget: dict = field(default_factory=dict) # budget.launch_ms / memory_mb / hangs (perf.judge)
79
+ surfaces: tuple = () # of system_surfaces.Surface: widgets, Live Activities and the like (experimental)
80
+ stubs: tuple = () # of dict: netstubs.parse's entries, answered by the run's proxy (simulator only)
81
+ wire: tuple = () # of wire.WireAssertion: expect request, no_request and storage (dark, simulator only)
82
+ l10n: bool = False # `mobster test --locale`: audit the screens for truncation, untranslated text, RTL
83
+ # the `snapshot:` in expect: the final screen against its baseline (snapshots.py)
84
+ snapshot: Optional[Snapshot] = None
85
+ snapshot_update: bool = False # `mobster test --update-snapshots`: write the baseline from this run
86
+ snapshot_record: bool = True # False in an approved CI suite: a missing baseline fails, and none is recorded
87
+
88
+ def reset_level(self):
89
+ """The reset this run uses: the check's own, else reinstall with a .app and data without. A system app
90
+ (com.apple.*) is never reset."""
91
+ if (self.bundle_id or "").startswith("com.apple."):
92
+ return "none"
93
+ if self.reset != "auto":
94
+ return self.reset
95
+ return "reinstall" if self.app_path else "data"
96
+
97
+
98
+ def _text(value, where, limit, *, required=False):
99
+ if value is None:
100
+ if required:
101
+ raise CheckError(f"{where} is required")
102
+ return None
103
+ if isinstance(value, bool) or not isinstance(value, str):
104
+ raise CheckError(f"{where} must be text; quote it if it looks like a number or yes/no")
105
+ value = value.strip()
106
+ if not value:
107
+ if required:
108
+ raise CheckError(f"{where} is empty")
109
+ return None
110
+ if len(value) > limit:
111
+ raise CheckError(f"{where} is longer than {limit:,} characters")
112
+ return value
113
+
114
+
115
+ def _number(value, where, low, high, default):
116
+ if value is None:
117
+ return default
118
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
119
+ raise CheckError(f"{where} must be a number")
120
+ if not low < value <= high:
121
+ raise CheckError(f"{where} must be more than {low:g} and at most {high:g}")
122
+ return float(value)
123
+
124
+
125
+ def _strings(value, where, limit, item_limit=1000):
126
+ if value is None:
127
+ return ()
128
+ if not isinstance(value, list):
129
+ raise CheckError(f"{where} must be a list")
130
+ if len(value) > limit:
131
+ raise CheckError(f"{where} has more than {limit} items")
132
+ out = []
133
+ for index, item in enumerate(value):
134
+ if isinstance(item, bool) or not isinstance(item, str):
135
+ raise CheckError(f"{where}[{index}] must be text; quote it, as in \"YES\" or \"3\"")
136
+ if len(item) > item_limit:
137
+ raise CheckError(f"{where}[{index}] is longer than {item_limit:,} characters")
138
+ out.append(item)
139
+ return tuple(out)
140
+
141
+
142
+ def _stubs(data, base_dir):
143
+ stubs = netstubs.parse(data.get("stubs"), base_dir)
144
+ if stubs and (data.get("conditions") or {}).get("network") == "offline":
145
+ raise CheckError("stubs answer requests, and conditions.network offline refuses them all. Use slow-3g, lte "
146
+ "or flaky with stubs, or remove the stubs.")
147
+ return stubs
148
+
149
+
150
+ def _env(value, where):
151
+ if value is None:
152
+ return {}
153
+ if not isinstance(value, dict):
154
+ raise CheckError(f"{where} must be an object of KEY: value")
155
+ if len(value) > LIMITS["env"]:
156
+ raise CheckError(f"{where} has more than {LIMITS['env']} entries")
157
+ out = {}
158
+ for key, item in value.items():
159
+ if not isinstance(key, str) or not ENV_KEY.fullmatch(key):
160
+ raise CheckError(f"{where}: {key!r} is not a variable name (A-Z, 0-9 and _, not starting with a digit)")
161
+ if isinstance(item, bool) or not isinstance(item, str):
162
+ raise CheckError(f"{where}.{key} must be text; quote it, as in \"3\" or \"YES\"")
163
+ if len(item) > 1000:
164
+ raise CheckError(f"{where}.{key} is longer than 1,000 characters")
165
+ out[key] = item
166
+ return out
167
+
168
+
169
+ def _perf_budget(budget):
170
+ """``budget.launch_ms``, ``memory_mb`` and ``hangs``: the limits a run's measurements must stay inside."""
171
+ out = {}
172
+ for key, high in (("launch_ms", 600_000), ("memory_mb", 65_536)):
173
+ if budget.get(key) is not None:
174
+ out[key] = _plain_number(_number(budget[key], f"budget.{key}", 0, high, None))
175
+ hangs = budget.get("hangs")
176
+ if hangs is not None:
177
+ if isinstance(hangs, bool) or not isinstance(hangs, int) or not 0 <= hangs <= 1000:
178
+ raise CheckError("budget.hangs must be a whole number from 0 to 1000: the hangs of 250 ms or longer "
179
+ "a run may have")
180
+ out["hangs"] = hangs
181
+ return out
182
+
183
+
184
+ def _steps(value):
185
+ if value is None:
186
+ return ()
187
+ if isinstance(value, str):
188
+ value = [value]
189
+ if not isinstance(value, list):
190
+ raise CheckError("steps must be a list of plain-English steps")
191
+ if len(value) > LIMITS["steps"]:
192
+ raise CheckError(f"steps has more than {LIMITS['steps']} steps")
193
+ return tuple(_text(step, f"steps[{index}]", LIMITS["step"], required=True) for index, step in enumerate(value))
194
+
195
+
196
+ def _expect_list(value, key, snapshots=None):
197
+ """The assertions of a list; a ``snapshot`` entry goes to ``snapshots`` (only ``expect`` takes one)."""
198
+ if value is None:
199
+ return ()
200
+ if not isinstance(value, list):
201
+ raise CheckError(f"{key} must be a list of assertions, such as [{{text: Continue}}]")
202
+ out = []
203
+ for index, item in enumerate(value):
204
+ if isinstance(item, dict) and "snapshot" in item:
205
+ if snapshots is None:
206
+ raise CheckError(f"{key}[{index}] is a snapshot, which goes in expect")
207
+ if snapshots:
208
+ raise CheckError(f"{key}[{index}] is a second snapshot. A check ends on one screen and has one "
209
+ "snapshot; write another check for another screen.")
210
+ snapshots.append(parse_snapshot(item, f"{key}[{index}]"))
211
+ continue
212
+ if key == "expect" and wire.is_wire(item):
213
+ continue # wire.parse reads these
214
+ out.extend(expand_assertion(item, f"{key}[{index}]"))
215
+ if len(out) > LIMITS["expect"]:
216
+ raise CheckError(f"{key} has more than {LIMITS['expect']} assertions")
217
+ return tuple(out)
218
+
219
+
220
+ def _expect(value):
221
+ """(the assertions, the snapshot or None) of a check's ``expect``."""
222
+ snapshots = []
223
+ return _expect_list(value, "expect", snapshots), (snapshots[0] if snapshots else None)
224
+
225
+
226
+ def _split_expect(value):
227
+ """(the tree assertions, the sound names) of ``expect``: ``{sound: sent}`` is heard (sound.py), not read."""
228
+ from .sound import NAME
229
+ if not isinstance(value, list):
230
+ return value, ()
231
+ tree, sounds = [], []
232
+ for index, item in enumerate(value):
233
+ if not (isinstance(item, dict) and "sound" in item):
234
+ tree.append(item)
235
+ elif set(item) != {"sound"} or not isinstance(item["sound"], str) or not NAME.fullmatch(item["sound"]):
236
+ raise CheckError(f"expect[{index}] sound is a name such as {{sound: sent}}: lowercase letters, digits, "
237
+ "- and _")
238
+ else:
239
+ sounds.append(item["sound"])
240
+ return tree, tuple(dict.fromkeys(sounds))
241
+
242
+
243
+ THROUGHOUT_KINDS = ("no_text", "absent")
244
+
245
+
246
+ def _throughout(value):
247
+ """``throughout``: assertions that must hold on every screen the run reads, so only the ones about what must never
248
+ show (no_text, absent)."""
249
+ out = _expect_list(value, "throughout")
250
+ for index, item in enumerate(out):
251
+ if item.kind not in THROUGHOUT_KINDS:
252
+ raise CheckError(f"throughout[{index}] is a {item.kind}; use no_text or absent, which can hold on "
253
+ "every screen")
254
+ return out
255
+
256
+
257
+ def _audit(value):
258
+ from .a11y import NAME as A11Y
259
+ from .audit import AUDITS
260
+ from .device_audit import NAME as AX_DEVICE
261
+ names = (*AUDITS, A11Y, AX_DEVICE)
262
+ if value is None:
263
+ return ()
264
+ if isinstance(value, str):
265
+ value = [value]
266
+ if not isinstance(value, list):
267
+ raise CheckError("audit must be a list of names, such as [usage-descriptions]")
268
+ for index, name in enumerate(value):
269
+ if name not in names:
270
+ close = difflib.get_close_matches(str(name), list(names), n=1)
271
+ raise CheckError(f"audit[{index}] {name!r} is not an audit; "
272
+ + (f"did you mean {close[0]}?" if close else f"use {', '.join(names)}"))
273
+ return tuple(dict.fromkeys(value))
274
+
275
+
276
+ def _no_seed():
277
+ from .fixtures import Seed
278
+ return Seed()
279
+
280
+
281
+ def _fixture(value, base_dir):
282
+ """A check file's ``fixture``: a name or a list of names. With a project folder, each must exist."""
283
+ from . import fixtures
284
+ if value is None:
285
+ return ()
286
+ value = [value] if isinstance(value, str) else value
287
+ if not isinstance(value, list) or not value or len(value) > 10 or not all(
288
+ isinstance(item, str) and fixtures.NAME.fullmatch(item) for item in value):
289
+ raise CheckError("fixture is the name of a fixture in .mobster/fixtures/, or a list of names")
290
+ if base_dir is not None and (Path(base_dir) / ".mobster").is_dir():
291
+ for name in value:
292
+ if fixtures.path_of(base_dir, name) is None:
293
+ have = ", ".join(fixtures.names(base_dir)[:8])
294
+ raise CheckError(f"fixture {name!r} isn't in .mobster/fixtures/"
295
+ + (f"; you have {have}" if have else ""))
296
+ return tuple(dict.fromkeys(value))
297
+
298
+
299
+ def _a11y(value):
300
+ if value is None or value is False:
301
+ return None
302
+ if value is True:
303
+ return "warn"
304
+ if value not in ("warn", "strict"):
305
+ raise CheckError("a11y is warn, strict or false: warn reports findings and passes, strict fails the check")
306
+ return value
307
+
308
+
309
+ def _url(value, where):
310
+ url = _text(value, where, LIMITS["url"])
311
+ if url is not None and not URL_SCHEME.match(url):
312
+ raise CheckError(f"{where} must be a link with a scheme, such as yourapp://lesson")
313
+ return url
314
+
315
+
316
+ def _tags(value):
317
+ """A check file's ``tags``: a list of short lowercase words (a-z, 0-9, - and _), or one such word."""
318
+ if value is None:
319
+ return ()
320
+ if isinstance(value, str):
321
+ value = [value]
322
+ if not isinstance(value, list):
323
+ raise CheckError("tags must be a list of words, such as [smoke, paywall]")
324
+ if len(value) > LIMITS["tags"]:
325
+ raise CheckError(f"tags has more than {LIMITS['tags']} tags")
326
+ out = []
327
+ for index, item in enumerate(value):
328
+ if isinstance(item, bool) or not isinstance(item, str) or not TAG.fullmatch(item.strip()):
329
+ raise CheckError(f"tags[{index}] must be a word of 1 to 40 of a-z, 0-9, - and _, such as smoke")
330
+ if item.strip() not in out:
331
+ out.append(item.strip())
332
+ return tuple(out)
333
+
334
+
335
+ def _resolve(path, base_dir):
336
+ path = Path(os.path.expanduser(path))
337
+ if not path.is_absolute():
338
+ path = Path(base_dir or os.getcwd()) / path
339
+ return str(path.resolve())
340
+
341
+
342
+ def _business(value):
343
+ from .business import parse_business
344
+ return parse_business(value)
345
+
346
+
347
+ def check_from_dict(data, *, base_dir=None):
348
+ """A Check from the file shape (§2.1) or the MCP tool arguments."""
349
+ if not isinstance(data, dict):
350
+ raise CheckError("a check must be an object (a YAML mapping)")
351
+ surfaces = data.get("surfaces")
352
+ data = {key: value for key, value in data.items() if key not in (*TOOL_ONLY, "surfaces")}
353
+ file_only = [key for key in FILE_SHAPE_KEYS if key in data]
354
+ tool_only = [key for key in TOOL_SHAPE_KEYS if key in data]
355
+ if file_only and tool_only:
356
+ raise CheckError(f"the check mixes the file format ({', '.join(file_only)}) and tool arguments "
357
+ f"({', '.join(tool_only)})")
358
+ if "tags" in data and tool_only:
359
+ raise CheckError("tags goes in a check file, not in tool arguments")
360
+ if "schedule" in data and tool_only:
361
+ raise CheckError("schedule goes in a check file, not in tool arguments")
362
+ if surfaces is not None and tool_only:
363
+ raise CheckError("surfaces goes in a check file, not in tool arguments")
364
+ if not file_only and not tool_only:
365
+ reject_unknown_keys(data, tuple(dict.fromkeys(FILE_KEYS + TOOL_KEYS)), "the check")
366
+ raise CheckError("the check needs an app: app.bundle or app.path in a check file, bundle_id or app_path "
367
+ "in tool arguments")
368
+ perf_limits = {}
369
+ if tool_only:
370
+ reject_unknown_keys(data, TOOL_KEYS, "the check")
371
+ fields = {"app_path": data.get("app_path"), "bundle": data.get("bundle_id"),
372
+ "args": data.get("launch_args"), "env": data.get("launch_env"), "url": data.get("open_url"),
373
+ "max_usd": data.get("max_usd"), "max_seconds": data.get("max_seconds"),
374
+ "where": {"path": "app_path", "bundle": "bundle_id", "args": "launch_args",
375
+ "env": "launch_env", "url": "open_url", "max_usd": "max_usd",
376
+ "max_seconds": "max_seconds"}}
377
+ else:
378
+ reject_unknown_keys(data, FILE_KEYS, "the check")
379
+ version = data.get("version", 1)
380
+ if version != 1 or isinstance(version, bool):
381
+ raise CheckError(f"version {version!r} is not supported; 1 is the only version")
382
+ app = {} if data.get("app") is None else data["app"]
383
+ if not isinstance(app, dict):
384
+ raise CheckError("app must be an object with bundle and/or path")
385
+ reject_unknown_keys(app, ("bundle", "path"), "app")
386
+ launch = {} if data.get("launch") is None else data["launch"]
387
+ if not isinstance(launch, dict):
388
+ raise CheckError("launch must be an object with args, env and/or url")
389
+ reject_unknown_keys(launch, ("args", "env", "url"), "launch")
390
+ budget = {} if data.get("budget") is None else data["budget"]
391
+ perf_limits = budget
392
+ if not isinstance(budget, dict):
393
+ raise CheckError("budget must be an object with max_seconds, max_usd, launch_ms, memory_mb and/or hangs")
394
+ reject_unknown_keys(budget, ("max_seconds", "max_usd", *perf.BUDGET_KEYS), "budget")
395
+ fields = {"app_path": app.get("path"), "bundle": app.get("bundle"), "args": launch.get("args"),
396
+ "env": launch.get("env"), "url": launch.get("url"), "max_usd": budget.get("max_usd"),
397
+ "max_seconds": budget.get("max_seconds"),
398
+ "where": {"path": "app.path", "bundle": "app.bundle", "args": "launch.args", "env": "launch.env",
399
+ "url": "launch.url", "max_usd": "budget.max_usd", "max_seconds": "budget.max_seconds"}}
400
+ where = fields["where"]
401
+ app_path = _text(fields["app_path"], where["path"], 4096)
402
+ if app_path is not None:
403
+ app_path = _resolve(app_path, base_dir)
404
+ bundle = _text(fields["bundle"], where["bundle"], 255)
405
+ if bundle is not None:
406
+ try:
407
+ bundle = validate_bundle_id(bundle)
408
+ except ValueError:
409
+ raise CheckError(f"{where['bundle']} {bundle!r} is not a bundle ID such as com.example.app") from None
410
+ if app_path is None and bundle is None:
411
+ raise CheckError(f"the check needs {where['bundle']} or {where['path']}: the app to check")
412
+ reset = data.get("reset")
413
+ if reset is not None and reset not in RESETS:
414
+ raise CheckError(f"reset must be one of {', '.join(RESETS)}, not {reset!r}")
415
+ steps = _steps(data.get("steps"))
416
+ name = _text(data.get("name"), "name", LIMITS["name"])
417
+ if name is None:
418
+ name = (steps[0][:LIMITS["name"] - 1] + "…" if len(steps[0]) > LIMITS["name"] else steps[0]) if steps \
419
+ else LAUNCH_ONLY_NAME
420
+ tree_expect, sounds = _split_expect(data.get("expect"))
421
+ expect, snapshot = _expect(tree_expect)
422
+ return Check(
423
+ name=name, steps=steps, expect=expect, snapshot=snapshot, sound=sounds, app_path=app_path, bundle_id=bundle,
424
+ device=_text(data.get("device"), "device", 100), runtime=_text(data.get("runtime"), "runtime", 100),
425
+ reset=reset or "auto", launch_args=_strings(fields["args"], where["args"], LIMITS["args"]),
426
+ launch_env=_env(fields["env"], where["env"]), open_url=_url(fields["url"], where["url"]),
427
+ max_seconds=_number(fields["max_seconds"], where["max_seconds"], 0, LIMITS["max_seconds"],
428
+ DEFAULT_MAX_SECONDS),
429
+ max_usd=_number(fields["max_usd"], where["max_usd"], 0, LIMITS["max_usd"], DEFAULT_MAX_USD),
430
+ tags=_tags(data.get("tags")), schedule=_text(data.get("schedule"), "schedule", 128),
431
+ throughout=_throughout(data.get("throughout")),
432
+ somewhere=_expect_list(data.get("somewhere"), "somewhere"),
433
+ audit=_audit(data.get("audit")), a11y=_a11y(data.get("a11y")), fixture=_fixture(data.get("fixture"), base_dir),
434
+ business=_business(data.get("business")),
435
+ conditions=conditions.parse(data.get("conditions"), base_dir), dialogs=dialogs.parse(data.get("dialogs")),
436
+ stubs=_stubs(data, base_dir), wire=wire.parse(data.get("expect")),
437
+ surfaces=surface_spec.parse(surfaces, base_dir), perf_budget=_perf_budget(perf_limits))
438
+
439
+
440
+ def project_root(path=None):
441
+ """The directory holding ``.mobster/``, walking up from ``path`` (a file or folder; default the working directory);
442
+ else ``path``'s own folder."""
443
+ path = Path(path or Path.cwd()).resolve()
444
+ start = path if path.is_dir() else path.parent
445
+ for folder in (start, *start.parents):
446
+ if (folder / ".mobster").is_dir():
447
+ return folder
448
+ return start
449
+
450
+
451
+ def load_check(path):
452
+ """A check file (.yaml, .yml or .json). Raises CheckError with the file and line on a problem."""
453
+ path = Path(path).expanduser()
454
+ try:
455
+ size = path.stat().st_size
456
+ except FileNotFoundError:
457
+ raise CheckError(f"{path} does not exist") from None
458
+ except OSError as error:
459
+ raise CheckError(f"{path} can't be read: {error.strerror or error}") from None
460
+ if not path.is_file():
461
+ raise CheckError(f"{path} is not a file")
462
+ if size > FILE_LIMIT:
463
+ raise CheckError(f"{path} is larger than 1 MB")
464
+ try:
465
+ text = path.read_text(encoding="utf-8")
466
+ except (OSError, UnicodeDecodeError) as error:
467
+ raise CheckError(f"{path} can't be read as UTF-8 text ({type(error).__name__})") from None
468
+ if path.suffix.lower() == ".json":
469
+ try:
470
+ data = json.loads(text)
471
+ except json.JSONDecodeError as error:
472
+ raise CheckError(f"{path} line {error.lineno}: {error.msg}") from None
473
+ except (RecursionError, ValueError):
474
+ raise CheckError(f"{path}: nested too deeply") from None
475
+ if _nests_too_deep(data):
476
+ raise CheckError(f"{path}: nested too deeply")
477
+ else:
478
+ import yaml
479
+ try:
480
+ data = yaml.safe_load(text)
481
+ except yaml.YAMLError as error:
482
+ mark = getattr(error, "problem_mark", None)
483
+ line = f" line {mark.line + 1}" if mark is not None else ""
484
+ problem = getattr(error, "problem", None) or "not valid YAML"
485
+ raise CheckError(f"{path}{line}: {problem}") from None
486
+ except (RecursionError, ValueError):
487
+ raise CheckError(f"{path}: nested too deeply") from None
488
+ if data is None:
489
+ raise CheckError(f"{path} is empty")
490
+ try:
491
+ check = check_from_dict(data, base_dir=project_root(path))
492
+ except CheckError as error:
493
+ raise CheckError(f"{path}: {error}") from None
494
+ return _with_source(check, str(path.resolve()))
495
+
496
+
497
+ def _nests_too_deep(value):
498
+ """True when ``value``'s lists and objects nest more than MAX_NESTING deep. From Python 3.14 json.loads parses
499
+ nesting that 3.13 and earlier refused with RecursionError; YAML's parser still refuses it that way."""
500
+ pending = [(value, 0)]
501
+ while pending:
502
+ node, depth = pending.pop()
503
+ if isinstance(node, (dict, list)):
504
+ if depth == MAX_NESTING:
505
+ return True
506
+ pending.extend((child, depth + 1) for child in (node.values() if isinstance(node, dict) else node))
507
+ return False
508
+
509
+
510
+ def _with_source(check, source):
511
+ from dataclasses import replace
512
+ return replace(check, source=source)
513
+
514
+
515
+ def check_to_dict(check, *, project_root=None):
516
+ """The file shape of ``check``: ``app.path`` relative to ``project_root`` when it sits inside it."""
517
+ check = check.fixture_origin or check # a check with its fixtures flattened in is saved as it was written
518
+ out = {"version": 1, "name": check.name}
519
+ app = {}
520
+ if check.bundle_id:
521
+ app["bundle"] = check.bundle_id
522
+ if check.app_path:
523
+ path = Path(check.app_path)
524
+ if project_root is not None:
525
+ try:
526
+ path = path.resolve().relative_to(Path(project_root).resolve())
527
+ except ValueError:
528
+ pass
529
+ app["path"] = str(path)
530
+ out["app"] = app
531
+ if check.device:
532
+ out["device"] = check.device
533
+ if check.runtime:
534
+ out["runtime"] = check.runtime
535
+ if check.reset != "auto":
536
+ out["reset"] = check.reset
537
+ if check.tags:
538
+ out["tags"] = list(check.tags)
539
+ if check.schedule:
540
+ out["schedule"] = check.schedule
541
+ launch = {}
542
+ if check.launch_args:
543
+ launch["args"] = list(check.launch_args)
544
+ if check.launch_env:
545
+ launch["env"] = dict(check.launch_env)
546
+ if check.open_url:
547
+ launch["url"] = check.open_url
548
+ if launch:
549
+ out["launch"] = launch
550
+ if check.steps:
551
+ out["steps"] = list(check.steps)
552
+ out["expect"] = [assertion.to_dict() for assertion in (*check.expect, *check.wire)] + [
553
+ {"sound": name} for name in check.sound]
554
+ if check.snapshot:
555
+ out["expect"].append(check.snapshot.to_dict())
556
+ if check.throughout:
557
+ out["throughout"] = [assertion.to_dict() for assertion in check.throughout]
558
+ if check.somewhere:
559
+ out["somewhere"] = [assertion.to_dict() for assertion in check.somewhere]
560
+ if check.audit:
561
+ out["audit"] = list(check.audit)
562
+ if check.a11y:
563
+ out["a11y"] = check.a11y
564
+ if check.fixture:
565
+ out["fixture"] = list(check.fixture)
566
+ if check.business is not None:
567
+ out["business"] = check.business.to_dict()
568
+ if check.conditions:
569
+ out["conditions"] = conditions.to_dict(check.conditions, project_root)
570
+ if check.dialogs:
571
+ out["dialogs"] = dict(check.dialogs)
572
+ if check.stubs:
573
+ out["stubs"] = netstubs.to_dict(check.stubs, project_root)
574
+ if check.surfaces:
575
+ out["surfaces"] = surface_spec.to_dict(check.surfaces)
576
+ budget = {}
577
+ if check.max_seconds != DEFAULT_MAX_SECONDS:
578
+ budget["max_seconds"] = _plain_number(check.max_seconds)
579
+ if check.max_usd != DEFAULT_MAX_USD:
580
+ budget["max_usd"] = _plain_number(check.max_usd)
581
+ budget.update(check.perf_budget)
582
+ if budget:
583
+ out["budget"] = budget
584
+ return out
585
+
586
+
587
+ def _plain_number(value):
588
+ return int(value) if float(value).is_integer() else value
589
+
590
+
591
+ def dump_check(check, *, project_root=None):
592
+ """``check`` as a YAML check file that ``load_check`` reads back to the same check."""
593
+ import yaml
594
+ header = (f"# yaml-language-server: $schema={links.CHECK_SCHEMA}\n"
595
+ "# A Mobster check. Run it with: mobster verify --check <this file>\n")
596
+ return header + yaml.safe_dump(check_to_dict(check, project_root=project_root), sort_keys=False,
597
+ allow_unicode=True, default_flow_style=False, width=100)
598
+
599
+
600
+ SAVE_NAME = re.compile(r"[a-z0-9][a-z0-9-]{0,59}")
601
+
602
+
603
+ def save_check_file(check, name, checks_dir):
604
+ """Write ``check`` as ``<checks_dir>/<name>.yaml``, with paths relative to that project. Returns the path."""
605
+ if not isinstance(name, str) or not SAVE_NAME.fullmatch(name):
606
+ raise CheckError("a check's name is 1 to 60 of a-z, 0-9 and -, starting with a letter or digit")
607
+ checks_dir = Path(checks_dir)
608
+ checks_dir.mkdir(parents=True, exist_ok=True)
609
+ root = checks_dir.parent.parent if checks_dir.parent.name == ".mobster" else None
610
+ path = checks_dir / f"{name}.yaml"
611
+ temporary = path.with_suffix(".yaml.tmp")
612
+ temporary.write_text(dump_check(check, project_root=root), encoding="utf-8")
613
+ os.replace(temporary, path)
614
+ return path