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,255 @@
1
+ """Per-check test history; a file that can't be read is set aside as history.json.corrupt and never stops a suite."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from statistics import median
7
+
8
+ from ..env_file import writer
9
+ from ..paths import write_text_atomic
10
+ from ..verify import perf
11
+
12
+ SCHEMA = "mobster.test-history/1"
13
+ XCTEST_FILE = "xctest:" # the file of a history entry that is an XCTest case: xctest:Target/Class/test
14
+ FILE = "history.json"
15
+ KEEP, MAX_CHECKS, MAX_TESTS = 20, 400, 2000
16
+ MIN_RUNS, FLAKY_AT = 5, 0.15
17
+ TREND_AT, TREND_MIN = 0.25, 4
18
+ KEPT = ("passed", "failed", "needs_review")
19
+ # A suite's folder in .mobster/test-results (reports.make_suite_dir): its name sorts by when it started.
20
+ SUITE_FOLDER = re.compile(r"^[0-9]{8}-[0-9]{6}-[0-9a-f]{4}$")
21
+
22
+
23
+ def path_of(root):
24
+ return Path(root) / ".mobster" / "test-results" / FILE
25
+
26
+
27
+ def key_of(check, deviceKey):
28
+ return (f"{check.get('file') or check.get('name')}|{deviceKey}"
29
+ + (f"|{check['locale']}" if check.get("locale") else ""))
30
+
31
+
32
+ def _run(item):
33
+ """A stored run, checked at the boundary; None when it isn't one."""
34
+ if not isinstance(item, dict) or item.get("status") not in KEPT:
35
+ return None
36
+ ms, attempts = item.get("ms"), item.get("attempts")
37
+ return {"suite": str(item.get("suite") or "")[:40], "at": item["at"] if isinstance(item.get("at"), int) else None,
38
+ "status": item["status"], "flaky": item.get("flaky") is True,
39
+ # A budget the run couldn't measure (a missed reading) is neither a pass nor a failure of the app.
40
+ "unmeasured": item.get("unmeasured") is True,
41
+ "ms": ms if isinstance(ms, int) and ms >= 0 else None,
42
+ "attempts": attempts if isinstance(attempts, int) and attempts > 0 else 1,
43
+ "perf": perf.clean(item.get("perf"))}
44
+
45
+
46
+ def load(root):
47
+ """(history, ok): {key: {"name", "file", "device", "runs": [...]}} (oldest run first) and whether the file was
48
+ absent or readable."""
49
+ try:
50
+ text = path_of(root).read_text(encoding="utf-8")
51
+ except FileNotFoundError:
52
+ return {}, True
53
+ except (OSError, UnicodeError):
54
+ return {}, False
55
+ try:
56
+ data = json.loads(text)
57
+ checks = data["checks"]
58
+ if data.get("schema") != SCHEMA or not isinstance(checks, dict):
59
+ raise ValueError
60
+ except (ValueError, KeyError, TypeError, AttributeError):
61
+ return {}, False
62
+ out = {}
63
+ for key, entry in checks.items():
64
+ if not isinstance(key, str) or not isinstance(entry, dict) or not isinstance(entry.get("runs"), list):
65
+ continue
66
+ runs = [run for run in map(_run, entry["runs"]) if run is not None][-KEEP:]
67
+ if runs:
68
+ out[key] = {"name": str(entry.get("name") or "")[:200], "file": entry.get("file") if isinstance(
69
+ entry.get("file"), str) else None, "device": str(entry.get("device") or "")[:200], "runs": runs}
70
+ return out, True
71
+
72
+
73
+ def _is_test(entry):
74
+ return str(entry.get("file") or "").startswith(XCTEST_FILE)
75
+
76
+
77
+ def _save(root, history):
78
+ # A project's tests are many and its checks few: each kind keeps its newest, so tests never push checks out.
79
+ for is_test, limit in ((False, MAX_CHECKS), (True, MAX_TESTS)):
80
+ keys = [key for key, entry in history.items() if _is_test(entry) is is_test]
81
+ for key in sorted(keys, key=lambda k: history[k]["runs"][-1].get("at") or 0)[:max(0, len(keys) - limit)]:
82
+ del history[key]
83
+ write_text_atomic(path_of(root), json.dumps({"schema": SCHEMA, "checks": history}, ensure_ascii=False) + "\n")
84
+
85
+
86
+ def _set_aside(root):
87
+ source = path_of(root)
88
+ try:
89
+ source.replace(source.with_name(FILE + ".corrupt"))
90
+ except OSError:
91
+ pass
92
+
93
+
94
+ def stats(runs):
95
+ """One check's numbers over its kept runs (oldest first)."""
96
+ runs = list(runs)[-KEEP:]
97
+ judged = [run for run in runs if not run.get("unmeasured")]
98
+ n = len(judged)
99
+ passed = [run["status"] == "passed" for run in judged]
100
+ flaky_runs = sum(run["flaky"] for run in judged)
101
+ changes = sum(a != b for a, b in zip(passed, passed[1:]))
102
+ score = round(max(flaky_runs / n, changes / (n - 1) if n > 1 else 0.0), 3) if n else 0.0
103
+ if n < MIN_RUNS:
104
+ state = "new"
105
+ elif not passed[-1]:
106
+ state = "failing"
107
+ elif score >= FLAKY_AT:
108
+ state = "flaky"
109
+ else:
110
+ state = "stable"
111
+ return {"runs": n, "passRate": round(sum(passed) / n, 4) if n else None, "flakyRuns": flaky_runs,
112
+ "flakeScore": score, "state": state, "lastStatus": runs[-1]["status"] if runs else None,
113
+ "lastAt": runs[-1]["at"] if runs else None,
114
+ "durationMs": [run["ms"] for run in runs], "trend": _trend([run["ms"] for run in runs]),
115
+ "outcomes": [run["status"] for run in runs], "perf": perf.trend(runs), "unmeasured": len(runs) - n,
116
+ "perfLatest": (runs[-1].get("perf") or {}) if runs else {}}
117
+
118
+
119
+ def _trend(durations):
120
+ """slower, faster or steady: the newer half's median time against the older half's; None with too few times."""
121
+ known = [ms for ms in durations if ms is not None]
122
+ if len(known) < TREND_MIN:
123
+ return None
124
+ half = len(known) // 2
125
+ before, after = median(known[:half]), median(known[-half:])
126
+ if before <= 0:
127
+ return None
128
+ change = (after - before) / before
129
+ return "slower" if change > TREND_AT else "faster" if change < -TREND_AT else "steady"
130
+
131
+
132
+ def describe(entry):
133
+ """An entry with its stats, and whether to suggest quarantine; what the CLI, the API and the report show."""
134
+ found = stats(entry["runs"])
135
+ return {"name": entry["name"], "file": entry["file"], "device": entry["device"], **found,
136
+ "suggestQuarantine": found["state"] == "flaky"}
137
+
138
+
139
+ def _items(value):
140
+ """The dicts in ``value`` when it is a list; results.json is external data, so anything else is skipped."""
141
+ items = value if isinstance(value, list) else []
142
+ return [item for item in items if isinstance(item, dict)]
143
+
144
+
145
+ def _cells(doc):
146
+ """(check, result) for every check result in ``doc``, and for every XCTest case: a case is the result of a check
147
+ named for its class and test, whose file is xctest:Target/Class/test."""
148
+ for check in _items(doc.get("checks")):
149
+ for result in _items(check.get("results")):
150
+ yield check, result
151
+ xctest = doc.get("xctest")
152
+ for target in _items(xctest.get("targets") if isinstance(xctest, dict) else None):
153
+ for case in _items(target.get("cases")):
154
+ if not all(isinstance(case.get(key), str) for key in ("classname", "name")) or not isinstance(
155
+ target.get("name"), str):
156
+ continue
157
+ yield {"name": f"{case['classname']}.{case['name']}",
158
+ "file": f"{XCTEST_FILE}{target['name']}/{case['classname']}/{case['name']}"}, case
159
+
160
+
161
+ def _add(history, doc):
162
+ """Fold the finished suite ``doc`` into ``history`` once (a finished suite is immutable); the keys it added to."""
163
+ added = set()
164
+ for check, result in _cells(doc):
165
+ reason = result.get("reason")
166
+ found = _run({"suite": doc.get("suite"), "at": doc.get("startedAt"), "status": result.get("status"),
167
+ "flaky": bool(result.get("flaky")), "ms": result.get("durationMs"),
168
+ "unmeasured": isinstance(reason, dict) and reason.get("class") == "budget_unmeasured",
169
+ "attempts": result.get("attempts"), "perf": perf.numbers(result.get("perf"))})
170
+ if found is None:
171
+ continue
172
+ key = key_of(check, result.get("deviceKey"))
173
+ entry = history.setdefault(key, {"runs": []})
174
+ entry.update(name=check.get("name"), file=check.get("file"), device=result.get("device"))
175
+ if found["suite"] and any(run["suite"] == found["suite"] for run in entry["runs"]):
176
+ continue
177
+ entry["runs"] = (entry["runs"] + [found])[-KEEP:]
178
+ added.add(key)
179
+ return added
180
+
181
+
182
+ def _unrecorded(root, history):
183
+ """The project's last KEEP suites that ``history`` has no run of (from before the file existed), oldest first."""
184
+ folder = path_of(root).parent
185
+ try:
186
+ names = sorted(item.name for item in folder.iterdir() if SUITE_FOLDER.match(item.name))[-KEEP:]
187
+ except OSError:
188
+ return []
189
+ known = {run["suite"] for entry in history.values() for run in entry["runs"]}
190
+ docs = []
191
+ for name in names:
192
+ if name in known:
193
+ continue
194
+ try:
195
+ doc = json.loads((folder / name / "results.json").read_text(encoding="utf-8"))
196
+ except (OSError, ValueError, UnicodeError):
197
+ continue
198
+ if isinstance(doc, dict) and str(doc.get("suite") or name)[:40] not in known:
199
+ docs.append({**doc, "suite": doc.get("suite") or name})
200
+ return docs
201
+
202
+
203
+ def record(root, doc):
204
+ try:
205
+ with writer(path_of(root)):
206
+ history, ok = load(root)
207
+ if not ok:
208
+ _set_aside(root)
209
+ _add(history, doc)
210
+ _save(root, history)
211
+ suggested = []
212
+ for check, result in _cells(doc):
213
+ entry = history.get(key_of(check, result.get("deviceKey")))
214
+ if entry is None or result.get("status") not in KEPT:
215
+ continue
216
+ result["history"] = stats(entry["runs"])
217
+ # A test isn't a check: the quarantine file can't name it, so its flake score is shown and not suggested.
218
+ if (result["history"]["state"] == "flaky" and not check.get("quarantined")
219
+ and not _is_test(check)):
220
+ suggested.append({"check": check.get("name"), "file": check.get("file"),
221
+ "device": result.get("device"), "flakeScore": result["history"]["flakeScore"]})
222
+ doc["history"] = {"suggestQuarantine": suggested}
223
+ except Exception:
224
+ pass
225
+ return doc
226
+
227
+
228
+ def flaky_reasons(root):
229
+ """{check file: reason} for checks whose history is flaky on any device, for --auto-quarantine."""
230
+ history, _ = load(root)
231
+ entries = [(entry, stats(entry["runs"])) for entry in history.values()]
232
+ # Quarantine applies to the whole check, so a failure on any recorded device vetoes it.
233
+ failing = {entry["file"] for entry, found in entries if found["lastStatus"] != "passed"}
234
+ return {entry["file"]: (f"flaky: passed {round(found['passRate'] * 100)}% of its last "
235
+ f"{found['runs']} runs on {entry['device']}")
236
+ for entry, found in entries if found["state"] == "flaky" and entry["file"]
237
+ and entry["file"] not in failing}
238
+
239
+
240
+ def report(root, *, only=None):
241
+ """Every check's history under ``root`` as ``describe`` shows it, flaky first, then by name; ``ok`` False when the
242
+ file couldn't be read. Suites the file has none of are folded in, by when they started (never written back)."""
243
+ history, ok = load(root)
244
+ added = set()
245
+ for doc in _unrecorded(root, history):
246
+ added |= _add(history, doc)
247
+ for key in added:
248
+ runs = sorted(history[key]["runs"], key=lambda run: run["at"] or 0)
249
+ history[key]["runs"] = runs[-KEEP:]
250
+ rows = [describe(entry) for entry in history.values() if only is None or only(entry)]
251
+ rows.sort(key=lambda row: (row["state"] != "flaky", -row["flakeScore"], str(row["name"]), row["device"]))
252
+ return {"checks": rows, "ok": ok}
253
+
254
+
255
+ __all__ = ["record", "load", "stats", "describe", "report", "flaky_reasons", "KEEP", "FILE", "SCHEMA"]
@@ -0,0 +1,321 @@
1
+ """Bounded, text-only suite results for a hosted caller.
2
+
3
+ The source document may contain app text, secrets, device IDs and paths. Only fields named in ``project`` enter the
4
+ export. The original results, reports, screenshots and logs remain on the enrolled Mac. The caller authorizes a suite
5
+ before passing its frozen artifact bytes to ``manifest`` or ``chunk``; this module never reads a file.
6
+ The caller binds expected targets; a green admitted export proves coverage only of targets declared in its document.
7
+ """
8
+
9
+ import base64
10
+ import binascii
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import re
15
+
16
+ from . import reports
17
+ from .suite import SAFETY_EXIT_STOPS
18
+
19
+ SCHEMA = "mobster.test.hosted/1"
20
+ MANIFEST_SCHEMA = "mobster.test.artifacts/1"
21
+ MAX_CHECKS = 500
22
+ MAX_TARGETS = 20
23
+ MAX_ARTIFACT = 2 * 1024 * 1024
24
+ CHUNK_BYTES = 32 * 1024
25
+ MAX_DURATION_MS = 30 * 86_400_000
26
+ MAX_TIME_MS = 10**13
27
+ KINDS = {"json": ("results.json", "application/json"), "junit": ("junit.xml", "application/xml")}
28
+ VERDICTS = frozenset({"passed", "failed", "needs_review", "couldnt_run"})
29
+ REASONS = frozenset({"assertion", "blocked", "budget", "drift", "app_not_running", "no_assertions",
30
+ "app_crashed",
31
+ "held_before_flow", "usage", "model", "environment", "simulator", "wda", "install",
32
+ "launch", "device", "stopped", "journal", "unknown_outcome", "internal",
33
+ "not_developer_build", "build", "business", "business_evidence", "business_cleanup"})
34
+ STOP_REASONS = frozenset((*SAFETY_EXIT_STOPS, "stopped"))
35
+ MESSAGES = {"failed": "Check failed.", "needs_review": "Check needs review.",
36
+ "couldnt_run": "Check could not run."}
37
+ SUITE_ID = re.compile(r"[0-9]{8}-[0-9]{6}-[0-9a-f]{4}\Z")
38
+ ARTIFACT_ID = re.compile(r"[a-f0-9]{24}\Z")
39
+ ENROLLED_ID = re.compile(r"[a-f0-9]{12}\Z")
40
+ DIGEST = re.compile(r"[a-f0-9]{64}\Z")
41
+ MODEL = re.compile(r"(?:iPhone (?:[0-9]{1,2}(?: Pro Max| Pro| Plus| mini)?|SE(?: \([1-9](?:st|nd|rd|th) "
42
+ r"generation\))?)|iPad(?: Air| mini| Pro)?(?: [0-9]{1,2}(?:-inch)?)?"
43
+ r"(?: \([A-Z][0-9]\))?)\Z")
44
+ RUNTIME = re.compile(r"iOS [0-9]{1,2}(?:\.[0-9]{1,2}){0,2}\Z")
45
+
46
+
47
+ class ExportError(ValueError):
48
+ """A canonical result cannot be safely exported."""
49
+
50
+
51
+ class InputsChanged(ExportError):
52
+ """A check ran on inputs other than the admitted suite's: it can't be reported under that identity."""
53
+
54
+
55
+ class ArtifactTooLarge(ExportError):
56
+ """A safe artifact would exceed the hosted size limit."""
57
+
58
+
59
+ class ArtifactNotFound(KeyError):
60
+ """An artifact ID does not name one of this suite's frozen exports."""
61
+
62
+
63
+ def _integer(value, maximum):
64
+ if type(value) is not int or not 0 <= value <= maximum:
65
+ raise ExportError("Invalid suite result number")
66
+ return value
67
+
68
+
69
+ def _ratio(value):
70
+ if type(value) not in (float, int) or not 0 <= value <= 1 or not math.isfinite(value):
71
+ raise ExportError("Invalid suite pass rate")
72
+ return value
73
+
74
+
75
+ def _identity(identity, count, source_checks=None):
76
+ if identity is None:
77
+ return None
78
+ if not isinstance(identity, dict) or set(identity) != {"projectId", "revision", "provenance", "cases"} or \
79
+ not isinstance(identity.get("projectId"), str) or not ENROLLED_ID.fullmatch(identity["projectId"]) or \
80
+ type(identity.get("revision")) is not int or identity["revision"] < 1 or \
81
+ not isinstance(identity.get("provenance"), str) or not DIGEST.fullmatch(identity["provenance"]) or \
82
+ not isinstance(identity.get("cases"), (list, tuple)) or not identity["cases"] or \
83
+ len(identity["cases"]) != count:
84
+ raise ExportError("Invalid admitted suite identity")
85
+ ids = set()
86
+ for number, case in enumerate(identity["cases"]):
87
+ if not isinstance(case, dict) or set(case) != {"id", "sourceFile", "checkSpecDigest", "appDigest",
88
+ "baselineDigest", "fixtureDigest"} or \
89
+ not isinstance(case["id"], str) or not ENROLLED_ID.fullmatch(case["id"]) or case["id"] in ids or \
90
+ not isinstance(case["sourceFile"], str) or not 1 <= len(case["sourceFile"]) <= 4096 or \
91
+ (source_checks is not None and (not isinstance(source_checks[number], dict) or
92
+ source_checks[number].get("file") != case["sourceFile"])) or \
93
+ not isinstance(case["checkSpecDigest"], str) or not DIGEST.fullmatch(case["checkSpecDigest"]) or \
94
+ not isinstance(case["appDigest"], str) or not DIGEST.fullmatch(case["appDigest"]) or \
95
+ (case["baselineDigest"] is not None and (not isinstance(case["baselineDigest"], str) or
96
+ not DIGEST.fullmatch(case["baselineDigest"]))) or \
97
+ (case["fixtureDigest"] is not None and (not isinstance(case["fixtureDigest"], str) or
98
+ not DIGEST.fullmatch(case["fixtureDigest"]))):
99
+ raise ExportError("Invalid admitted case identity")
100
+ ids.add(case["id"])
101
+ return identity
102
+
103
+
104
+ def _frozen(value):
105
+ """A report's "none" (the check has no such input) as the identity writes it."""
106
+ return None if value == "none" else value
107
+
108
+
109
+ def project(doc, suite_id, *, identity=None):
110
+ """A strict allowlist document suitable for both JSON and canonical JUnit generation."""
111
+ if not isinstance(doc, dict) or doc.get("schema") != reports.SCHEMA or doc.get("suite") != suite_id or \
112
+ not isinstance(suite_id, str) or not SUITE_ID.fullmatch(suite_id):
113
+ raise ExportError("Invalid suite result")
114
+ source_checks, source_devices = doc.get("checks"), doc.get("devices")
115
+ if not isinstance(source_checks, list) or not isinstance(source_devices, list) or \
116
+ len(source_checks) > MAX_CHECKS or len(source_devices) > MAX_TARGETS:
117
+ raise ExportError("Invalid suite result size")
118
+ identity = _identity(identity, len(source_checks), source_checks)
119
+ devices, device_keys, device_names = [], {}, {}
120
+ for number, source in enumerate(source_devices, 1):
121
+ if not isinstance(source, dict) or not isinstance(source.get("key"), str) or \
122
+ source["key"] in device_keys:
123
+ raise ExportError("Invalid suite target")
124
+ key = f"target-{number}"
125
+ device_keys[source["key"]] = key
126
+ device_names[key] = f"Target {number}"
127
+ devices.append({"key": key, "name": device_names[key]})
128
+ checks, cells = [], []
129
+ for number, source in enumerate(source_checks, 1):
130
+ if not isinstance(source, dict) or not isinstance(source.get("results"), list) or \
131
+ len(source["results"]) > len(source_devices) or \
132
+ type(source.get("quarantined")) is not bool:
133
+ raise ExportError("Invalid suite check")
134
+ results, seen = [], set()
135
+ for item in source["results"]:
136
+ if not isinstance(item, dict) or not isinstance(item.get("deviceKey"), str) or \
137
+ item["deviceKey"] not in device_keys or item["deviceKey"] in seen or \
138
+ not isinstance(item.get("status"), str) or \
139
+ item["status"] not in VERDICTS or type(item.get("flaky")) is not bool or \
140
+ type(item.get("quarantined")) is not bool or item["quarantined"] != source["quarantined"]:
141
+ raise ExportError("Invalid suite cell")
142
+ seen.add(item["deviceKey"])
143
+ status = item["status"]
144
+ reason = item.get("reason")
145
+ reason_class = reason.get("class") if isinstance(reason, dict) else None
146
+ safety_stop = item.get("safetyStop")
147
+ if "safetyStop" in item and (not isinstance(safety_stop, str) or
148
+ safety_stop not in SAFETY_EXIT_STOPS or
149
+ status != "couldnt_run"):
150
+ raise ExportError("Invalid suite safety stop")
151
+ safe_key = device_keys[item["deviceKey"]]
152
+ result = {"deviceKey": safe_key, "device": device_names[safe_key],
153
+ "status": status, "attempts": _integer(item.get("attempts"), 100_000),
154
+ "flaky": item["flaky"],
155
+ "quarantined": source["quarantined"],
156
+ "durationMs": _integer(item.get("durationMs"), MAX_DURATION_MS),
157
+ "reason": None if status == "passed" else {"class": reason_class if isinstance(reason_class,
158
+ str) and reason_class in REASONS
159
+ else "other", "message": MESSAGES[status]}}
160
+ environment = item.get("environment")
161
+ environment = environment if isinstance(environment, dict) else {}
162
+ kind, model, runtime = (environment.get(key) for key in ("kind", "model", "runtime"))
163
+ result["environment"] = {"kind": kind if kind in ("simulator", "iphone") else None,
164
+ "model": model if isinstance(model, str) and MODEL.fullmatch(model) else None,
165
+ "runtime": runtime if isinstance(runtime, str) and RUNTIME.fullmatch(runtime)
166
+ else None}
167
+ business = item.get("business")
168
+ assertions = business.get("assertionFields") if isinstance(business, dict) else None
169
+ failed = business.get("failedFields") if isinstance(business, dict) else None
170
+ if isinstance(business, dict) and business.get("status") in ("observed", "unavailable") and \
171
+ isinstance(assertions, list) and len(assertions) <= 100 and \
172
+ isinstance(failed, list) and len(failed) <= len(assertions) and \
173
+ all(isinstance(field, str) for field in assertions + failed) and \
174
+ len(set(assertions)) == len(assertions) and len(set(failed)) == len(failed) and \
175
+ set(failed).issubset(assertions):
176
+ result["business"] = {"status": business["status"], "assertions": len(assertions),
177
+ "failedAssertions": len(failed)}
178
+ if "passRate" in item:
179
+ result["passRate"] = _ratio(item["passRate"])
180
+ if safety_stop is not None:
181
+ result["safetyStop"] = safety_stop
182
+ results.append(result)
183
+ cells.append(result)
184
+ check = {"name": f"Check {number}", "caseId": f"check-{number}", "classname": "checks",
185
+ "quarantined": source["quarantined"], "results": results}
186
+ if identity is not None:
187
+ case = identity["cases"][number - 1]
188
+ # api.execute hashes each check again when the suite ends: a drift, or digests other than the approved
189
+ # ones, means this result came from inputs the identity doesn't name.
190
+ if source.get("provenanceDrift") or source.get("checkSpecDigest") != case["checkSpecDigest"] or \
191
+ any(_frozen(source.get(field)) != case[field] for field in ("baselineDigest", "fixtureDigest")) \
192
+ or any(item.get("appDigest") != case["appDigest"] for item in source["results"]):
193
+ raise InputsChanged("A check's inputs changed during the admitted suite")
194
+ check.update({"caseId": case["id"], "checkSpecDigest": case["checkSpecDigest"],
195
+ "appDigest": case["appDigest"], "baselineDigest": case["baselineDigest"],
196
+ "fixtureDigest": case["fixtureDigest"]})
197
+ checks.append(check)
198
+ counted = [cell for cell in cells if not cell["quarantined"]]
199
+ has_safety_stop = any(cell.get("safetyStop") for cell in cells)
200
+ summary = {"passed": sum(cell["status"] == "passed" for cell in counted),
201
+ "failed": sum(cell["status"] == "failed" for cell in counted),
202
+ "needsReview": sum(cell["status"] == "needs_review" for cell in counted),
203
+ "couldntRun": sum(cell["status"] == "couldnt_run" for cell in counted),
204
+ "flaky": sum(cell["flaky"] for cell in counted),
205
+ "quarantined": len(cells) - len(counted), "total": len(cells)}
206
+ exit_code = _integer(doc.get("exitCode"), 255)
207
+ if identity is not None and exit_code == 0 and (not devices or any(
208
+ len(check["results"]) != len(devices) for check in checks)):
209
+ raise ExportError("Successful admitted suite has a missing target result")
210
+ options = doc.get("options")
211
+ if not isinstance(options, dict) or type(options.get("strict")) is not bool or \
212
+ type(doc.get("stopped", False)) is not bool:
213
+ raise ExportError("Invalid suite options")
214
+ stop_reason = doc.get("stopReason")
215
+ if stop_reason is not None and (not isinstance(stop_reason, str) or stop_reason not in STOP_REASONS):
216
+ raise ExportError("Invalid suite stop reason")
217
+ if exit_code == 0 and (summary["failed"] or summary["needsReview"] or summary["couldntRun"] or
218
+ (options["strict"] and summary["flaky"]) or stop_reason is not None or
219
+ doc.get("stopped", False) or doc.get("error") or has_safety_stop):
220
+ raise ExportError("Inconsistent suite verdict")
221
+ safe = {"schema": SCHEMA, "suite": suite_id, "startedAt": _integer(doc.get("startedAt"), MAX_TIME_MS),
222
+ "finishedAt": _integer(doc.get("finishedAt"), MAX_TIME_MS),
223
+ "durationMs": _integer(doc.get("durationMs"), MAX_DURATION_MS), "exitCode": exit_code,
224
+ "stopped": doc.get("stopped", False), "stopReason": stop_reason,
225
+ "options": {"strict": options["strict"]},
226
+ "devices": devices, "checks": checks, "summary": summary, "mobster": {"version": "hosted/1"}}
227
+ if identity is not None:
228
+ safe["identity"] = {key: identity[key] for key in ("projectId", "revision", "provenance")}
229
+ return safe
230
+
231
+
232
+ def build(doc, suite_id, *, identity=None):
233
+ """Freeze safe JSON and JUnit bytes from one terminal canonical result document."""
234
+ safe = project(doc, suite_id, identity=identity)
235
+ data = {"json": (json.dumps(safe, ensure_ascii=False, allow_nan=False, sort_keys=True,
236
+ separators=(",", ":")) + "\n").encode("utf-8"),
237
+ "junit": reports.junit(safe, strict=safe["options"]["strict"])}
238
+ if any(len(value) > MAX_ARTIFACT for value in data.values()):
239
+ raise ArtifactTooLarge("Hosted test result exceeds 2 MiB; inspect the local report")
240
+ return data
241
+
242
+
243
+ def _checked(artifacts):
244
+ if not isinstance(artifacts, dict) or set(artifacts) != set(KINDS) or \
245
+ any(type(artifacts[key]) is not bytes or len(artifacts[key]) > MAX_ARTIFACT for key in KINDS):
246
+ raise ExportError("Invalid frozen test artifacts")
247
+ return artifacts
248
+
249
+
250
+ def _id(kind, data):
251
+ return hashlib.sha256(b"mobster.test.artifact/1\0" + kind.encode() + b"\0" + data).hexdigest()[:24]
252
+
253
+
254
+ def manifest(artifacts):
255
+ """Integrity and download metadata for already-authorized frozen artifact bytes."""
256
+ artifacts = _checked(artifacts)
257
+ return {"schema": MANIFEST_SCHEMA, "artifacts": [
258
+ {"id": _id(kind, artifacts[kind]), "name": KINDS[kind][0], "mediaType": KINDS[kind][1],
259
+ "size": len(artifacts[kind]), "sha256": hashlib.sha256(artifacts[kind]).hexdigest(),
260
+ "chunkBytes": CHUNK_BYTES}
261
+ for kind in KINDS]}
262
+
263
+
264
+ def valid_manifest(value):
265
+ """Recognize the exact metadata envelope emitted by ``manifest``."""
266
+ if not isinstance(value, dict) or set(value) != {"schema", "artifacts"} or \
267
+ value["schema"] != MANIFEST_SCHEMA or not isinstance(value["artifacts"], list) or \
268
+ len(value["artifacts"]) != len(KINDS):
269
+ return False
270
+ ids = set()
271
+ for item, (_, (name, media_type)) in zip(value["artifacts"], KINDS.items()):
272
+ if not isinstance(item, dict) or set(item) != {"id", "name", "mediaType", "size", "sha256", "chunkBytes"} or \
273
+ item["name"] != name or item["mediaType"] != media_type or \
274
+ not isinstance(item["id"], str) or not ARTIFACT_ID.fullmatch(item["id"]) or \
275
+ not isinstance(item["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", item["sha256"]) or \
276
+ type(item["size"]) is not int or not 1 <= item["size"] <= MAX_ARTIFACT or \
277
+ type(item["chunkBytes"]) is not int or item["chunkBytes"] != CHUNK_BYTES or item["id"] in ids:
278
+ return False
279
+ ids.add(item["id"])
280
+ return True
281
+
282
+
283
+ def chunk(artifacts, artifact_id, offset=0, limit=CHUNK_BYTES):
284
+ """One bounded base64 slice. Authorization and frozen-byte ownership belong to the caller."""
285
+ artifacts = _checked(artifacts)
286
+ if type(offset) is not int or type(limit) is not int or offset < 0 or not 1 <= limit <= CHUNK_BYTES:
287
+ raise ExportError("Use a nonnegative offset and a limit of 1 to 32768")
288
+ if not isinstance(artifact_id, str) or not ARTIFACT_ID.fullmatch(artifact_id):
289
+ raise ArtifactNotFound(artifact_id)
290
+ found = next(((kind, data) for kind, data in artifacts.items() if _id(kind, data) == artifact_id), None)
291
+ if found is None:
292
+ raise ArtifactNotFound(artifact_id)
293
+ _, data = found
294
+ if offset >= len(data):
295
+ raise ExportError("Artifact offset is out of range")
296
+ part = data[offset:offset + limit]
297
+ return {"id": artifact_id, "offset": offset, "size": len(data), "length": len(part),
298
+ "sha256": hashlib.sha256(data).hexdigest(), "encoding": "base64",
299
+ "data": base64.b64encode(part).decode("ascii"), "eof": offset + len(part) == len(data)}
300
+
301
+
302
+ def valid_chunk(value, *, artifact_id, offset, limit):
303
+ """Recognize a bounded chunk of the requested frozen artifact, including canonical base64."""
304
+ if not isinstance(value, dict) or set(value) != {
305
+ "id", "offset", "size", "length", "sha256", "encoding", "data", "eof"} or \
306
+ type(offset) is not int or type(limit) is not int or offset < 0 or not 1 <= limit <= CHUNK_BYTES or \
307
+ value["id"] != artifact_id or type(value["offset"]) is not int or value["offset"] != offset or \
308
+ type(value["size"]) is not int or not 1 <= value["size"] <= MAX_ARTIFACT or \
309
+ type(value["length"]) is not int or not 1 <= value["length"] <= limit or \
310
+ value["offset"] + value["length"] > value["size"] or type(value["eof"]) is not bool or \
311
+ value["eof"] != (value["offset"] + value["length"] == value["size"]) or \
312
+ not isinstance(value["id"], str) or not ARTIFACT_ID.fullmatch(value["id"]) or \
313
+ not isinstance(value["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", value["sha256"]) or \
314
+ value["encoding"] != "base64" or not isinstance(value["data"], str) or \
315
+ len(value["data"]) > ((limit + 2) // 3) * 4:
316
+ return False
317
+ try:
318
+ decoded = base64.b64decode(value["data"], validate=True)
319
+ except (ValueError, binascii.Error):
320
+ return False
321
+ return len(decoded) == value["length"] and base64.b64encode(decoded).decode("ascii") == value["data"]