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,562 @@
1
+ """App Store Connect with your own API key: TestFlight builds, TestFlight feedback and crashes, the TestFlight build
2
+ installed on your iPhone by Mobster driving the TestFlight app, and, for a paid team, the three writes that sign
3
+ Mobster's helper without Xcode.
4
+
5
+ key = load_key() # the Keychain, else MOBSTER_ASC_* in the environment
6
+ asc = Client(key)
7
+ app = asc.app("com.example.yourapp") # {"id", "name", "bundleId"}
8
+ asc.builds(app["id"], limit=10) # newest first: version, build, uploaded, state, TestFlight state
9
+ asc.feedback(app["id"]) # screenshot and crash feedback from your testers
10
+
11
+ **Reads, and three named writes.** ``request`` sends GET requests and refuses any other verb. ``download`` GETs a
12
+ link in Apple's answer (a tester's screenshot) without the token. The only writes are ``WRITES``, by name: register an
13
+ iPhone's UDID, create a development certificate from a certificate signing request, and create a development
14
+ provisioning profile. ``write`` sends one only with an ``Approval`` that names exactly that body, and only after its
15
+ ledger has made the attempt durable (team_signing/ledger.py); a write whose outcome is unknown is never sent again.
16
+ Mobster submits nothing else to Apple: no build, metadata, screenshot, review or tester change. Uploading screenshots
17
+ stays your action (`fastlane deliver`, or App Store Connect itself).
18
+
19
+ The key: an App Store Connect API key (Users and Access › Integrations), its Key ID, and the Issuer ID for a team key
20
+ (an individual key has none). `mobster asc key add` keeps it in your login Keychain, readable only by Mobster's own
21
+ programs (keychain_acl). A CI job that has no Keychain passes MOBSTER_ASC_KEY_ID, MOBSTER_ASC_ISSUER_ID and
22
+ MOBSTER_ASC_KEY (the .p8's text) or MOBSTER_ASC_KEY_PATH. Each request carries a JWT signed here (es256.py) that
23
+ lasts 20 minutes, the most Apple allows. The key never leaves this Mac and is never printed.
24
+ """
25
+
26
+ import base64
27
+ import hashlib
28
+ import json
29
+ import os
30
+ import re
31
+ import time
32
+ import urllib.error
33
+ import urllib.parse
34
+ import urllib.request
35
+ from pathlib import Path
36
+
37
+ from . import DevError, es256
38
+
39
+ API = "https://api.appstoreconnect.apple.com"
40
+ SERVICE = "Mobster App Store Connect"
41
+ ACCOUNT = "default"
42
+ # the longest a token may last for Apple
43
+ TOKEN_SECONDS = 20 * 60
44
+ KEY_ID = re.compile(r"[A-Z0-9]{8,12}")
45
+ ISSUER = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
46
+ MAX_PAGES = 20
47
+ TESTFLIGHT = "com.apple.TestFlight"
48
+ # The only writes Mobster makes to Apple, by name: the path, and the JSON:API type the body must carry. Signing the
49
+ # helper without Xcode needs these three. Nothing else is writable: `Client.request` refuses every verb but GET.
50
+ WRITES = {"register_device": ("/v1/devices", "devices"),
51
+ "create_certificate": ("/v1/certificates", "certificates"),
52
+ "create_profile": ("/v1/profiles", "profiles")}
53
+ UNKNOWN_FIX = (
54
+ "Mobster won't send it again. Check developer.apple.com › Certificates, Identifiers & Profiles, then tell "
55
+ "Mobster what you found."
56
+ )
57
+ # In an approved body, {"$ref": "<name>"} stands for an id Apple hands back earlier in the same run.
58
+ REF = "$ref"
59
+
60
+
61
+ class Key:
62
+ def __init__(self, key_id, issuer_id, p8, source):
63
+ self.key_id, self.issuer_id, self.source = key_id, issuer_id or None, source
64
+ self._d = es256.private_key_from_p8(p8)
65
+
66
+ def public(self):
67
+ return {"keyId": self.key_id, "issuerId": self.issuer_id, "kind": "team" if self.issuer_id else "individual",
68
+ "source": self.source}
69
+
70
+ def token(self, now=None):
71
+ now = int(time.time() if now is None else now)
72
+ claims = {"iat": now, "exp": now + TOKEN_SECONDS, "aud": "appstoreconnect-v1"}
73
+ claims.update({"iss": self.issuer_id} if self.issuer_id else {"sub": "user"})
74
+ return es256.jwt({"alg": "ES256", "kid": self.key_id, "typ": "JWT"}, claims, self._d)
75
+
76
+
77
+ def check_ids(key_id, issuer_id):
78
+ if not KEY_ID.fullmatch(key_id or ""):
79
+ raise DevError(f"{key_id!r} isn't a Key ID: it's 10 letters and digits, such as 2X9R4HXF34.",
80
+ "Copy it from App Store Connect › Users and Access › Integrations.")
81
+ if issuer_id and not ISSUER.fullmatch(issuer_id.lower()):
82
+ raise DevError("That isn't an Issuer ID: it's a UUID, such as 57246542-96fe-1a63-e053-0824d011072a.",
83
+ "Copy it from the top of App Store Connect › Users and Access › Integrations.")
84
+
85
+
86
+ def _refused(error, verb, kind=DevError):
87
+ """The plain-language error for an HTTP error from Apple. ``verb`` is "read" or "write"."""
88
+ detail = ""
89
+ try:
90
+ errors = json.loads(error.read().decode("utf-8")).get("errors") or []
91
+ detail = (errors[0].get("detail") or errors[0].get("title") or "") if errors else ""
92
+ except Exception:
93
+ pass
94
+ if error.code == 401:
95
+ return kind("App Store Connect refused the key (401).",
96
+ "Check the Key ID and Issuer ID, and that the key isn't revoked.")
97
+ if error.code == 403:
98
+ return kind(f"The key can't {'make that change' if verb == 'write' else 'read that'} (403). {detail}".strip(),
99
+ "Give the key the Admin or Developer role in App Store Connect." if verb == "write" else
100
+ "Give the key the App Manager or Developer role in App Store Connect.")
101
+ return kind(f"App Store Connect answered {error.code}. {detail}".strip())
102
+
103
+
104
+ def _store(store=None):
105
+ if store is not None:
106
+ return store
107
+ from ..keychain_acl import Store
108
+ return Store(SERVICE)
109
+
110
+
111
+ def account_for(project=None):
112
+ """The Keychain account of a project's key: one per project folder. None is the key every project shares."""
113
+ if project is None:
114
+ return ACCOUNT
115
+ return "project-" + hashlib.sha256(str(Path(project).resolve()).encode()).hexdigest()[:16]
116
+
117
+
118
+ def save_key(key_id, issuer_id, p8_text, *, store=None, project=None):
119
+ check_ids(key_id, issuer_id)
120
+ try:
121
+ key = Key(key_id, issuer_id, p8_text, "Keychain" if project is None else "Keychain (this project)")
122
+ key.token()
123
+ except es256.P8Error as error:
124
+ raise DevError(f"Mobster couldn't read the key: {error}", "Pass the AuthKey_<Key ID>.p8 file Apple gave you.")
125
+ _store(store).set(account_for(project),
126
+ json.dumps({"keyId": key_id, "issuerId": issuer_id or None, "p8": p8_text.strip()}))
127
+ return key
128
+
129
+
130
+ def remove_key(*, store=None, project=None):
131
+ return _store(store).delete(account_for(project))
132
+
133
+
134
+ def load_key(*, store=None, environ=None, project=None):
135
+ environ = os.environ if environ is None else environ
136
+ if environ.get("MOBSTER_ASC_KEY_ID"):
137
+ text = environ.get("MOBSTER_ASC_KEY", "")
138
+ if not text and environ.get("MOBSTER_ASC_KEY_PATH"):
139
+ try:
140
+ text = Path(environ["MOBSTER_ASC_KEY_PATH"]).expanduser().read_text()
141
+ except OSError:
142
+ raise DevError("MOBSTER_ASC_KEY_PATH names a file Mobster can't read.") from None
143
+ check_ids(environ["MOBSTER_ASC_KEY_ID"], environ.get("MOBSTER_ASC_ISSUER_ID"))
144
+ try:
145
+ return Key(environ["MOBSTER_ASC_KEY_ID"], environ.get("MOBSTER_ASC_ISSUER_ID"), text, "environment")
146
+ except es256.P8Error as error:
147
+ raise DevError(f"MOBSTER_ASC_KEY isn't a usable key: {error}") from None
148
+ try:
149
+ source = "Keychain (this project)"
150
+ stored = _store(store).get(account_for(project)) if project is not None else None
151
+ if not stored:
152
+ source, stored = "Keychain", _store(store).get(ACCOUNT)
153
+ except Exception as error:
154
+ raise DevError(f"The Keychain didn't give Mobster the App Store Connect key ({error}).",
155
+ "Unlock the Mac's login Keychain, then try again.") from None
156
+ if not stored:
157
+ raise DevError("Mobster has no App Store Connect API key.",
158
+ "Add one: mobster asc key add --key-id KEY_ID --issuer-id ISSUER_ID --p8 AuthKey_KEY_ID.p8")
159
+ data = json.loads(stored)
160
+ return Key(data["keyId"], data.get("issuerId"), data["p8"], source)
161
+
162
+
163
+ class WriteRejected(DevError):
164
+ """Apple answered a write with an error: nothing was made."""
165
+
166
+
167
+ class WriteUnknown(DevError):
168
+ """A write was sent and Mobster never saw Apple's answer: it may have gone through. It is not sent again."""
169
+
170
+
171
+ def _matches(template, body):
172
+ """Whether ``body`` is ``template``, with an id (a non-empty string) wherever the template has a {"$ref": ...}."""
173
+ if isinstance(template, dict):
174
+ if set(template) == {REF}:
175
+ return isinstance(body, str) and bool(body)
176
+ return isinstance(body, dict) and template.keys() == body.keys() and all(
177
+ _matches(template[key], body[key]) for key in template)
178
+ if isinstance(template, list):
179
+ return isinstance(body, list) and len(template) == len(body) and all(map(_matches, template, body))
180
+ return type(template) is type(body) and template == body
181
+
182
+
183
+ def canonical(value):
184
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
185
+
186
+
187
+ def digest(name, body):
188
+ """What names one write in the ledger: its name and exact body."""
189
+ return hashlib.sha256(canonical([name, body]).encode()).hexdigest()
190
+
191
+
192
+ class Approval:
193
+ """The writes a person approved on a card: each a write's name and its body, where an id Apple returns earlier in
194
+ the same run stands as {"$ref": name}. ``digest`` is what the card carries; ``Client.write`` sends a body only if it
195
+ is one of these steps, and each step once."""
196
+
197
+ def __init__(self, steps):
198
+ self.steps = [(name, body) for name, body in steps]
199
+ for name, body in self.steps:
200
+ if name not in WRITES:
201
+ raise DevError(f"{name} isn't a change Mobster makes to App Store Connect.")
202
+ self.spent = set()
203
+
204
+ @property
205
+ def digest(self):
206
+ return hashlib.sha256(canonical(self.steps).encode()).hexdigest()
207
+
208
+ def spend(self, name, body):
209
+ """Mark the step this body is as sent. DevError when it isn't an approved step, or was already sent."""
210
+ for index, (step, template) in enumerate(self.steps):
211
+ if step == name and index not in self.spent and _matches(template, body):
212
+ self.spent.add(index)
213
+ return index
214
+ raise DevError("Mobster didn't send that: it isn't a change you approved, or it was already sent.")
215
+
216
+
217
+ class Client:
218
+ """GET requests to the App Store Connect API and the https links in its answers (``download``), and the three
219
+ ``WRITES`` (``write``)."""
220
+
221
+ def __init__(self, key, *, opener=None, api=API, clock=time.time):
222
+ self.key, self.opener, self.api, self.clock = key, opener or urllib.request.urlopen, api.rstrip("/"), clock
223
+ self._token, self._expires = None, 0
224
+
225
+ def _bearer(self):
226
+ now = self.clock()
227
+ if self._token is None or now > self._expires - 60:
228
+ self._token, self._expires = self.key.token(now), now + TOKEN_SECONDS
229
+ return self._token
230
+
231
+ def request(self, method, path, params=None):
232
+ if method != "GET":
233
+ raise DevError(
234
+ "Mobster reads from App Store Connect, and `write` makes its three named changes. It sent nothing."
235
+ )
236
+ if not re.match(r"^/v[12]/[A-Za-z0-9/_\-]+$", path) and not path.startswith(self.api + "/v"):
237
+ raise DevError(f"{path} isn't an App Store Connect API path.")
238
+ url = path if path.startswith("http") else self.api + path
239
+ if params:
240
+ url += "?" + urllib.parse.urlencode(params, safe="[],")
241
+ req = urllib.request.Request(url, method="GET", headers={"Authorization": f"Bearer {self._bearer()}",
242
+ "Accept": "application/json",
243
+ "User-Agent": "mobster-asc"})
244
+ try:
245
+ with self.opener(req, timeout=30) as response:
246
+ return json.loads(response.read().decode("utf-8") or "{}")
247
+ except urllib.error.HTTPError as error:
248
+ raise _refused(error, "read") from None
249
+ except (urllib.error.URLError, OSError) as error:
250
+ raise DevError(f"Mobster couldn't reach App Store Connect ({type(error).__name__}).",
251
+ "Check the Mac's internet connection.") from None
252
+
253
+ def write(self, name, body, *, approval, ledger, summary=None):
254
+ """Send one of ``WRITES`` and return Apple's ``data`` ({id, type, attributes}). It sends only a body the
255
+ ``approval`` names, once; the ``ledger`` makes the attempt durable first (begin) and records how it ended (end).
256
+ WriteRejected when Apple answers with an error. WriteUnknown when the answer never arrived: that write is
257
+ never sent again by Mobster, and the ledger refuses it until the person says what happened."""
258
+ if name not in WRITES:
259
+ raise DevError("Mobster doesn't make that change to App Store Connect, so it sent nothing.")
260
+ path, kind = WRITES[name]
261
+ if not isinstance(body, dict) or (body.get("data") or {}).get("type") != kind:
262
+ raise DevError(f"{name} sends a {kind} resource, so Mobster sent nothing.")
263
+ approval.spend(name, body)
264
+ ident = digest(name, body)
265
+ ledger.begin(name, ident, summary or {})
266
+ req = urllib.request.Request(self.api + path, method="POST", data=canonical(body).encode(),
267
+ headers={"Authorization": f"Bearer {self._bearer()}",
268
+ "Accept": "application/json", "Content-Type": "application/json",
269
+ "User-Agent": "mobster-asc"})
270
+ try:
271
+ with self.opener(req, timeout=30) as response:
272
+ landed = getattr(response, "geturl", lambda: req.full_url)()
273
+ raw = response.read().decode("utf-8")
274
+ except urllib.error.HTTPError as error:
275
+ # A 5xx may come after Apple made the change: only a 4xx says nothing was made.
276
+ outcome = "rejected" if error.code < 500 else "unknown"
277
+ refusal = _refused(error, "write", WriteRejected if outcome == "rejected" else WriteUnknown)
278
+ ledger.end(ident, outcome, {"status": error.code})
279
+ raise refusal from None
280
+ except (urllib.error.URLError, OSError, ValueError) as error:
281
+ ledger.end(ident, "unknown", {"error": type(error).__name__})
282
+ raise WriteUnknown(f"Mobster sent {name.replace('_', ' ')} to App Store Connect and never saw its answer "
283
+ f"({type(error).__name__}), so it may have gone through.", UNKNOWN_FIX) from None
284
+ try:
285
+ data = json.loads(raw)["data"]
286
+ if landed != req.full_url or not isinstance(data, dict) or not data.get("id"):
287
+ raise ValueError
288
+ except (ValueError, KeyError, TypeError):
289
+ ledger.end(ident, "unknown", {"error": "unreadable answer"})
290
+ raise WriteUnknown(f"App Store Connect answered {name.replace('_', ' ')} with something Mobster couldn't "
291
+ "read, so it may have gone through.", UNKNOWN_FIX) from None
292
+ ledger.end(ident, "done", {"id": data["id"]})
293
+ return data
294
+
295
+ def get(self, path, params=None):
296
+ return self.request("GET", path, params)
297
+
298
+ def download(self, url, limit=20_000_000):
299
+ # no Authorization header: the key's token never goes to the CDN
300
+ if not url.startswith("https://"):
301
+ raise DevError(f"{url} isn't an https link, so Mobster didn't fetch it.")
302
+ req = urllib.request.Request(url, method="GET", headers={"User-Agent": "mobster-asc"})
303
+ try:
304
+ with self.opener(req, timeout=30) as response:
305
+ return response.read(limit)
306
+ except (urllib.error.URLError, OSError) as error:
307
+ raise DevError(f"Mobster couldn't download {url} ({type(error).__name__}).") from None
308
+
309
+ def pages(self, path, params=None, limit=None):
310
+ """Every item of a list, following links.next, with ``included`` merged into one lookup."""
311
+ items, included = [], {}
312
+ answer = self.get(path, params)
313
+ for _ in range(MAX_PAGES):
314
+ items += answer.get("data") or []
315
+ for item in answer.get("included") or ():
316
+ included[(item.get("type"), item.get("id"))] = item
317
+ following = (answer.get("links") or {}).get("next")
318
+ if not following or (limit and len(items) >= limit):
319
+ break
320
+ answer = self.get(following)
321
+ return (items[:limit] if limit else items), included
322
+
323
+ # -- what Mobster reads ------------------------------------------------------------------------------------
324
+
325
+ def app(self, bundle):
326
+ answer = self.get("/v1/apps", {"filter[bundleId]": bundle, "fields[apps]": "name,bundleId,sku"})
327
+ found = [item for item in answer.get("data") or () if (item.get("attributes") or {}).get("bundleId") == bundle]
328
+ if not found:
329
+ raise DevError(f"App Store Connect has no app {bundle} this key can see.",
330
+ "Check the bundle ID, and that the key's team owns the app.")
331
+ item = found[0]
332
+ return {"id": item["id"], "name": item["attributes"].get("name"), "bundleId": bundle}
333
+
334
+ def apps(self):
335
+ items, _ = self.pages("/v1/apps", {"fields[apps]": "name,bundleId", "limit": 200})
336
+ return [{"id": item["id"], "name": (item.get("attributes") or {}).get("name"),
337
+ "bundleId": (item.get("attributes") or {}).get("bundleId")} for item in items]
338
+
339
+ def builds(self, app_id, limit=10):
340
+ items, included = self.pages("/v1/builds", {
341
+ "filter[app]": app_id, "sort": "-uploadedDate", "limit": min(limit, 200),
342
+ "include": "preReleaseVersion,buildBetaDetail",
343
+ "fields[builds]": "version,uploadedDate,processingState,expired,minOsVersion,preReleaseVersion,"
344
+ "buildBetaDetail",
345
+ "fields[preReleaseVersions]": "version",
346
+ "fields[buildBetaDetails]": "internalBuildState,externalBuildState"}, limit=limit)
347
+ return [build_row(item, included) for item in items]
348
+
349
+ def feedback(self, app_id, *, kinds=("screenshots", "crashes"), limit=50):
350
+ out = []
351
+ for kind, path in (("screenshots", "betaFeedbackScreenshotSubmissions"),
352
+ ("crashes", "betaFeedbackCrashSubmissions")):
353
+ if kind not in kinds:
354
+ continue
355
+ items, included = self.pages(f"/v1/apps/{app_id}/{path}", {"limit": min(limit, 200),
356
+ "sort": "-createdDate", "include": "build"},
357
+ limit=limit)
358
+ out += [feedback_row(item, kind, included) for item in items]
359
+ out.sort(key=lambda row: row.get("created") or "", reverse=True)
360
+ return out[:limit]
361
+
362
+ # -- what Mobster reads to sign the helper without Xcode (team_signing) -------------------------------------
363
+
364
+ def devices(self):
365
+ """The team's iPhones: id, name, UDID, status."""
366
+ items, _ = self.pages("/v1/devices", {"filter[platform]": "IOS", "limit": 200,
367
+ "fields[devices]": "name,udid,status,deviceClass"})
368
+ return [{"id": item["id"], "name": (item.get("attributes") or {}).get("name"),
369
+ "udid": (item.get("attributes") or {}).get("udid"),
370
+ "status": (item.get("attributes") or {}).get("status"),
371
+ "class": (item.get("attributes") or {}).get("deviceClass")} for item in items]
372
+
373
+ def bundle_id(self, identifier):
374
+ """The team's registered App ID for ``identifier`` ({id, identifier, name, team}), or None. ``"*"`` is the
375
+ wildcard App ID Xcode keeps for a team; it is found by listing, since a filter can't name it."""
376
+ wildcard = identifier == "*"
377
+ params = {"limit": 200, "fields[bundleIds]": "identifier,name,platform,seedId"}
378
+ if not wildcard:
379
+ params["filter[identifier]"] = identifier
380
+ items, _ = self.pages("/v1/bundleIds", params)
381
+ for item in items:
382
+ attributes = item.get("attributes") or {}
383
+ if attributes.get("identifier") == identifier:
384
+ return {"id": item["id"], "identifier": identifier, "name": attributes.get("name"),
385
+ "team": attributes.get("seedId")}
386
+ return None
387
+
388
+ def team_id(self):
389
+ """The team's 10-character id, from the App ID prefix of any App ID it has; None when it has none."""
390
+ answer = self.get("/v1/bundleIds", {"limit": 1, "fields[bundleIds]": "seedId"})
391
+ for item in answer.get("data") or ():
392
+ seed = (item.get("attributes") or {}).get("seedId")
393
+ if isinstance(seed, str) and re.fullmatch(r"[A-Z0-9]{10}", seed):
394
+ return seed
395
+ return None
396
+
397
+ def certificate_content(self, certificate_id):
398
+ """A certificate's DER bytes (base64) from Apple, to recover one Mobster made and did not get to keep."""
399
+ data = self.get(
400
+ f"/v1/certificates/{certificate_id}", {"fields[certificates]": "certificateContent,serialNumber"}
401
+ )
402
+ attributes = (data.get("data") or {}).get("attributes") or {}
403
+ return attributes.get("certificateContent") or "", attributes.get("serialNumber")
404
+
405
+ def development_certificates(self):
406
+ """The team's development certificates: id, serial number, expiry."""
407
+ items, _ = self.pages("/v1/certificates", {
408
+ "filter[certificateType]": "DEVELOPMENT,IOS_DEVELOPMENT", "limit": 200,
409
+ "fields[certificates]": "serialNumber,certificateType,displayName,expirationDate,activated"})
410
+ return [{"id": item["id"], "serial": (item.get("attributes") or {}).get("serialNumber"),
411
+ "name": (item.get("attributes") or {}).get("displayName"),
412
+ "expires": (item.get("attributes") or {}).get("expirationDate")} for item in items]
413
+
414
+ def development_profiles(self, prefix):
415
+ """The team's active iOS development profiles whose name starts with ``prefix``, with the ids of the devices
416
+ and certificates each lists and its App ID."""
417
+ items, _ = self.pages("/v1/profiles", {"filter[profileType]": "IOS_APP_DEVELOPMENT",
418
+ "filter[profileState]": "ACTIVE", "limit": 200,
419
+ "fields[profiles]": "name,uuid,expirationDate"})
420
+ rows = []
421
+ for item in items:
422
+ attributes = item.get("attributes") or {}
423
+ if not str(attributes.get("name") or "").startswith(prefix):
424
+ continue
425
+ related = {}
426
+ for name in ("devices", "certificates"):
427
+ data, _ = self.pages(f"/v1/profiles/{item['id']}/relationships/{name}", {"limit": 200})
428
+ related[name] = sorted(entry["id"] for entry in data)
429
+ bundle = (self.get(f"/v1/profiles/{item['id']}/relationships/bundleId").get("data") or {}).get("id")
430
+ rows.append({"id": item["id"], "name": attributes.get("name"), "uuid": attributes.get("uuid"),
431
+ "expires": attributes.get("expirationDate"), "devices": related["devices"],
432
+ "certificates": related["certificates"], "bundle": bundle})
433
+ return rows
434
+
435
+ def profile_content(self, profile_id):
436
+ """A profile's .mobileprovision bytes, from Apple."""
437
+ data = self.get(f"/v1/profiles/{profile_id}", {"fields[profiles]": "profileContent"}).get("data") or {}
438
+ return base64.b64decode(((data.get("attributes") or {}).get("profileContent")) or "")
439
+
440
+ def crash_log(self, submission_id):
441
+ answer = self.get(f"/v1/betaFeedbackCrashSubmissions/{submission_id}/crashLog")
442
+ return ((answer.get("data") or {}).get("attributes") or {}).get("logText") or ""
443
+
444
+
445
+ def _related(item, name, included):
446
+ data = ((item.get("relationships") or {}).get(name) or {}).get("data")
447
+ if isinstance(data, dict):
448
+ return included.get((data.get("type"), data.get("id")))
449
+ return None
450
+
451
+
452
+ def build_row(item, included):
453
+ attributes = item.get("attributes") or {}
454
+ version = _related(item, "preReleaseVersion", included)
455
+ beta = _related(item, "buildBetaDetail", included)
456
+ beta_attributes = (beta or {}).get("attributes") or {}
457
+ return {"id": item.get("id"), "version": ((version or {}).get("attributes") or {}).get("version"),
458
+ "build": attributes.get("version"), "uploaded": attributes.get("uploadedDate"),
459
+ "processing": attributes.get("processingState"), "expired": bool(attributes.get("expired")),
460
+ "internal": beta_attributes.get("internalBuildState"),
461
+ "external": beta_attributes.get("externalBuildState")}
462
+
463
+
464
+ def feedback_row(item, kind, included):
465
+ attributes = item.get("attributes") or {}
466
+ build = _related(item, "build", included)
467
+ return {"id": item.get("id"), "kind": "crash" if kind == "crashes" else "screenshot",
468
+ "created": attributes.get("createdDate"), "comment": attributes.get("comment"),
469
+ "device": attributes.get("deviceModel"), "os": attributes.get("osVersion"),
470
+ "build": ((build or {}).get("attributes") or {}).get("version"),
471
+ "screenshots": [shot.get("url") for shot in attributes.get("screenshots") or () if isinstance(shot, dict)
472
+ and shot.get("url")]}
473
+
474
+
475
+ # -- TestFlight on the phone ---------------------------------------------------------------------------------------
476
+
477
+ INSTALL_WORDS = ("Install", "Update")
478
+ DONE_WORDS = ("Open",)
479
+ TERMS_WORDS = ("Accept", "Start Testing")
480
+
481
+
482
+ def testflight_link(apple_id):
483
+ return f"itms-beta://beta.itunes.apple.com/v1/app/{int(apple_id)}"
484
+
485
+
486
+ def _buttons(tree):
487
+ return {node.label: node for node in tree.shown() if node.role == "Button" and node.label}
488
+
489
+
490
+ class UnknownOutcome(DevError):
491
+ """Mobster tapped Install or Update and never saw TestFlight say Open: the install may still finish."""
492
+
493
+
494
+ class NeedsTap(DevError):
495
+ """TestFlight shows Install or Update and Mobster was told not to tap (an earlier tap's outcome is unknown)."""
496
+
497
+
498
+ def testflight_install(record, wda, apple_id, *, timeout=600, clock=time.monotonic, sleep=time.sleep,
499
+ progress=None, tap=True, expected=None):
500
+ from ..deeplinks import open_link
501
+ from ..phone_io import Calls
502
+ from ..verify.tree import read_tree
503
+ progress = progress or (lambda line: None)
504
+ if record.get("kind") == "simulator":
505
+ raise DevError("TestFlight runs on an iPhone, not a simulator.", "Pass --device with your iPhone's name.")
506
+ with wda(record) as request:
507
+ driver = Calls(request)
508
+ open_link(driver, testflight_link(apple_id), bundle=TESTFLIGHT)
509
+ tapped = False
510
+ deadline = clock() + timeout
511
+ while clock() < deadline:
512
+ try:
513
+ tree = read_tree(driver)
514
+ buttons = _buttons(tree)
515
+ except Exception:
516
+ buttons = {}
517
+ if any(word in buttons for word in TERMS_WORDS) and not tapped:
518
+ raise DevError("TestFlight asks you to accept its terms for this app first, and Mobster doesn't "
519
+ "accept terms for you.", "Tap Accept on the iPhone once, then run it again.")
520
+ if expected and buttons.keys() & (set(INSTALL_WORDS) | {"Open"}):
521
+ version, build = expected
522
+ label = re.compile(r"(?<![\w.])" + re.escape(str(version)) + r"\s*\(\s*" +
523
+ re.escape(str(build)) + r"\s*\)")
524
+ if not any(label.search(node.label or "") for node in tree.shown()):
525
+ error = UnknownOutcome if tapped else DevError
526
+ raise error(f"TestFlight hasn't confirmed version {version} ({build}) on this iPhone.",
527
+ "Open the app's TestFlight page and check its version and build before trying again.")
528
+ if not tapped:
529
+ word = next((word for word in INSTALL_WORDS if word in buttons), None)
530
+ if word and not tap:
531
+ raise NeedsTap(f"TestFlight shows {word} and Mobster won't tap it: an earlier tap's outcome is "
532
+ "unknown, and a second tap could start a second install.",
533
+ f"Look at the iPhone. If TestFlight is installing, wait for Open. Otherwise tap "
534
+ f"{word} yourself.")
535
+ if word:
536
+ progress(f"Tapping {word} in TestFlight")
537
+ tapped = True # from here a failure may have sent the tap
538
+ try:
539
+ _tap(request, word)
540
+ except DevError:
541
+ tapped = False # it never found the button, so nothing was sent
542
+ raise
543
+ elif "Open" in buttons:
544
+ return "already"
545
+ elif any(word in buttons for word in DONE_WORDS):
546
+ return "installed"
547
+ sleep(2)
548
+ if tapped:
549
+ raise UnknownOutcome("Mobster tapped Install in TestFlight and didn't see it finish, so the outcome is "
550
+ "unknown.", "Look at the iPhone before you try again. Mobster won't tap again until "
551
+ "TestFlight shows Open.")
552
+ raise DevError("TestFlight didn't show Install, Update or Open in time.",
553
+ "Check the iPhone's network and that you're a tester of the app, then run it again.")
554
+
555
+
556
+ def _tap(request, label):
557
+ found = request("POST", "/element", {"using": "predicate string",
558
+ "value": f"type == 'XCUIElementTypeButton' AND label == '{label}'"}, 10)
559
+ element = (found or {}).get("ELEMENT") or (found or {}).get("element-6066-11e4-a52e-4f735466cecf")
560
+ if not element:
561
+ raise DevError(f"Mobster couldn't find {label} in TestFlight.", "Tap it on the iPhone yourself.")
562
+ request("POST", f"/element/{element}/click", {}, 10)