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,392 @@
1
+ """Wire assertions in ``expect:``: what the app sent over the network and what it stored on disk.
2
+
3
+ ``request`` and ``no_request`` are judged on the requests the run's proxy saw (netproxy.py); ``storage`` on the app's
4
+ container after the steps (wirestore.py). They run on a simulator, dark behind MOBSTER_EXPERIMENTAL_NETWORK=1. The
5
+ evidence in a result is the matched request line or the storage diff, with secrets masked and bodies cut short."""
6
+
7
+ import fnmatch
8
+ import gzip
9
+ import json
10
+ import os
11
+ import re
12
+ import zlib
13
+ from dataclasses import dataclass
14
+ from typing import Any, Optional
15
+
16
+ from .. import secret_filter
17
+ from . import netstubs, wirestore
18
+ from .assertions import AssertionResult, CheckError, Matcher, parse_matcher
19
+
20
+ FLAG = "MOBSTER_EXPERIMENTAL_NETWORK"
21
+ KINDS = ("request", "no_request", "storage")
22
+ REQUEST_KEYS = ("method", "host", "path", "status", "body", "response")
23
+ STORAGE_KEYS = ("file", "sqlite", "query", "changed", "contains", "equals", "at_least", "at_most")
24
+ STATUS = re.compile(r"[1-5]xx")
25
+ SHORTHAND = re.compile(r"(?:(?P<method>[A-Za-z]+)\s+)?(?P<host>[^/\s]+)?(?P<path>/\S*)?$")
26
+ SECRET_HEADERS = re.compile(r"authorization|cookie|token|secret|api[-_]?key|session|password|signature", re.I)
27
+ SHOWN_BODY = 200
28
+ SHOWN_SEEN = 4
29
+ OFF = (f"expect: request, no_request and storage are experimental. Set {FLAG}=1 to use them, or remove them from "
30
+ "the check.")
31
+
32
+
33
+ def enabled(environ=None):
34
+ return (os.environ if environ is None else environ).get(FLAG) == "1"
35
+
36
+
37
+ def is_wire(item):
38
+ return isinstance(item, dict) and any(kind in item for kind in KINDS)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class WireAssertion:
43
+ kind: str
44
+ method: Optional[str] = None
45
+ host: Optional[str] = None
46
+ path: Optional[Matcher] = None
47
+ status: Any = None # a number, or "2xx"
48
+ body: Optional[Matcher] = None # of the request's body
49
+ response: Optional[Matcher] = None # of the response's body
50
+ file: Optional[str] = None
51
+ sqlite: Optional[str] = None
52
+ query: Optional[str] = None
53
+ changed: bool = True
54
+ contains: Optional[Matcher] = None
55
+ equals: Optional[int] = None
56
+ at_least: Optional[int] = None
57
+ at_most: Optional[int] = None
58
+ name: Optional[str] = None
59
+
60
+ def needs_open(self):
61
+ """Whether the proxy must open the host's https to judge this: a path, status or body is inside it."""
62
+ return bool(self.host) and self.kind != "storage" and bool(self.path or self.status or self.body
63
+ or self.response)
64
+
65
+ def render(self):
66
+ if self.kind == "storage":
67
+ target = f"file {self.file}" if self.file else f"sqlite {self.sqlite}"
68
+ if self.file:
69
+ said = "changed" if self.changed else "unchanged"
70
+ return f"storage {target} {said}" + (f' containing "{self.contains.render()}"' if self.contains else "")
71
+ bound = (f"== {self.equals}" if self.equals is not None else
72
+ f">= {self.at_least}" if self.at_least is not None else f"<= {self.at_most}")
73
+ return f"storage {target} rows {bound}"
74
+ line = " ".join(part for part in (self.method, (self.host or "") + (self.path.render() if self.path else ""))
75
+ if part) or "any request"
76
+ extra = "".join((f" -> {self.status}" if self.status else "",
77
+ f" body {self.body.render()}" if self.body else "",
78
+ f" response {self.response.render()}" if self.response else ""))
79
+ return f"{self.kind} {line}{extra}"
80
+
81
+ def to_dict(self):
82
+ out = {}
83
+ if self.kind == "storage":
84
+ for key in STORAGE_KEYS:
85
+ value = getattr(self, key)
86
+ if key == "changed":
87
+ if self.file and not value:
88
+ out[key] = False
89
+ elif value is not None:
90
+ out[key] = value.raw if isinstance(value, Matcher) else value
91
+ else:
92
+ for key in REQUEST_KEYS:
93
+ value = getattr(self, key)
94
+ if value is not None:
95
+ out[key] = value.raw if isinstance(value, Matcher) else value
96
+ result = {self.kind: out}
97
+ if self.name:
98
+ result["name"] = self.name
99
+ return result
100
+
101
+
102
+ def _check_keys(body, allowed, where):
103
+ for key in body:
104
+ if key not in allowed:
105
+ raise CheckError(f"{where}.{key} isn't a setting. Use {', '.join(allowed)}.")
106
+
107
+
108
+ def _shorthand(text, where):
109
+ found = SHORTHAND.match(text.strip())
110
+ if not text.strip() or found is None or not (found["host"] or found["path"]):
111
+ raise CheckError(f"{where} needs a method, host and path such as POST api.example.com/v1/cart, or a "
112
+ "mapping.")
113
+ out = {}
114
+ method, host = found["method"], found["host"]
115
+ if method:
116
+ out["method"] = method
117
+ if host:
118
+ out["host"] = host
119
+ if found["path"]:
120
+ out["path"] = found["path"]
121
+ return out
122
+
123
+
124
+ def _parse_one(obj, where):
125
+ kinds = [kind for kind in KINDS if kind in obj]
126
+ if len(kinds) != 1:
127
+ raise CheckError(f"{where} needs exactly one of {', '.join(KINDS)}; it has {', '.join(kinds)}")
128
+ kind = kinds[0]
129
+ _check_keys(obj, KINDS + ("name",), where)
130
+ body = obj[kind]
131
+ if isinstance(body, str) and kind != "storage":
132
+ body = _shorthand(body, f"{where}.{kind}")
133
+ if not isinstance(body, dict):
134
+ raise CheckError(f"{where}.{kind} must be a mapping such as "
135
+ + ("{file: Library/Preferences/app.plist}." if kind == "storage"
136
+ else "{method: POST, host: api.example.com, path: /v1/cart}."))
137
+ name = obj.get("name")
138
+ if name is not None and (not isinstance(name, str) or not name.strip() or len(name) > 120):
139
+ raise CheckError(f"{where}.name must be text of 1 to 120 characters")
140
+ fields = {"kind": kind, "name": name.strip() if name else None}
141
+ here = f"{where}.{kind}"
142
+ if kind == "storage":
143
+ fields.update(_parse_storage(body, here))
144
+ else:
145
+ fields.update(_parse_request(body, here))
146
+ if kind == "no_request" and (fields.get("status") or fields.get("response")):
147
+ raise CheckError(f"{here} takes no status or response: a request that never left has none.")
148
+ return WireAssertion(**fields)
149
+
150
+
151
+ def _parse_request(body, where):
152
+ _check_keys(body, REQUEST_KEYS, where)
153
+ out = {}
154
+ if "method" in body:
155
+ method = body["method"]
156
+ if not isinstance(method, str) or method.upper() not in netstubs.METHODS:
157
+ raise CheckError(f"{where}.method must be one of {', '.join(netstubs.METHODS)}.")
158
+ out["method"] = method.upper()
159
+ if "host" in body:
160
+ out["host"] = netstubs._host(body["host"], f"{where}.host")
161
+ if "path" in body:
162
+ path = body["path"]
163
+ if isinstance(path, str) and not path.startswith(("/", "*")):
164
+ raise CheckError(f"{where}.path must start with /, such as /v1/cart or /v1/*, or be a /regex/.")
165
+ out["path"] = parse_matcher(path, f"{where}.path")
166
+ if "status" in body:
167
+ status = body["status"]
168
+ if isinstance(status, str) and STATUS.fullmatch(status):
169
+ out["status"] = status
170
+ elif isinstance(status, int) and not isinstance(status, bool) and 100 <= status <= 599:
171
+ out["status"] = status
172
+ else:
173
+ raise CheckError(f"{where}.status must be a number from 100 to 599 or a class such as 2xx.")
174
+ for key in ("body", "response"):
175
+ if key in body:
176
+ out[key] = parse_matcher(body[key], f"{where}.{key}")
177
+ if not out:
178
+ raise CheckError(f"{where} needs at least a method, host or path.")
179
+ return out
180
+
181
+
182
+ def _parse_storage(body, where):
183
+ _check_keys(body, STORAGE_KEYS, where)
184
+ if ("file" in body) == ("sqlite" in body):
185
+ raise CheckError(f"{where} needs one of file (a path or glob under the app's container) or sqlite.")
186
+ out = {}
187
+ for key in ("file", "sqlite"):
188
+ if key in body:
189
+ value = body[key]
190
+ if not isinstance(value, str) or not value.strip() or value.startswith("/") or ".." in value.split("/"):
191
+ raise CheckError(f"{where}.{key} must be a path inside the app's container, such as "
192
+ "Library/Preferences/app.plist, without a leading / or ..")
193
+ out[key] = value.strip()
194
+ if "file" in body:
195
+ if set(body) - {"file", "changed", "contains"}:
196
+ raise CheckError(f"{where}: a file takes changed and contains only.")
197
+ if not isinstance(body.get("changed", True), bool):
198
+ raise CheckError(f"{where}.changed must be true or false.")
199
+ out["changed"] = body.get("changed", True)
200
+ if "contains" in body:
201
+ out["contains"] = parse_matcher(body["contains"], f"{where}.contains")
202
+ else:
203
+ query = body.get("query")
204
+ if not isinstance(query, str) or not wirestore.read_only_query(query):
205
+ raise CheckError(f"{where}.query must be one SELECT or WITH statement, such as "
206
+ "SELECT 1 FROM cart WHERE sku = 'a1'.")
207
+ bounds = {key: body[key] for key in ("equals", "at_least", "at_most") if key in body}
208
+ if set(body) - {"sqlite", "query", *bounds} or len(bounds) != 1:
209
+ raise CheckError(f"{where}: a sqlite query needs exactly one of equals, at_least and at_most (rows).")
210
+ (key, number), = bounds.items()
211
+ if isinstance(number, bool) or not isinstance(number, int) or number < 0:
212
+ raise CheckError(f"{where}.{key} must be a whole number of at least 0.")
213
+ out.update(query=query.strip(), **{key: number})
214
+ return out
215
+
216
+
217
+ def parse(expect):
218
+ """The wire assertions of a check file's ``expect`` list: () when it has none. Raises CheckError."""
219
+ found = [(index, item) for index, item in enumerate(expect or ()) if is_wire(item)]
220
+ if found and not enabled():
221
+ raise CheckError(OFF)
222
+ return tuple(_parse_one(item, f"expect[{index}]") for index, item in found)
223
+
224
+
225
+ def watch_hosts(assertions):
226
+ """The hosts whose https the proxy must open, or None when no assertion reads the proxy."""
227
+ wire = [item for item in assertions if item.kind != "storage"]
228
+ return tuple(dict.fromkeys(item.host for item in wire if item.needs_open())) if wire else None
229
+
230
+
231
+ # evidence
232
+
233
+ def _mask_header(name, value):
234
+ return "••••••" if SECRET_HEADERS.search(name) else secret_filter.redact(value)
235
+
236
+
237
+ def _text(raw, limit=SHOWN_BODY):
238
+ text = secret_filter.redact(raw.decode("utf-8", "replace").strip())
239
+ return text if len(text) <= limit else text[:limit] + f"… ({len(raw)} bytes)"
240
+
241
+
242
+ def _response_body(raw):
243
+ """A captured response's body as bytes: de-chunked and inflated where it says so, else what was kept."""
244
+ head, _, body = raw.partition(b"\r\n\r\n")
245
+ lowered = head.lower()
246
+ if b"transfer-encoding: chunked" in lowered:
247
+ out, rest = b"", body
248
+ while rest:
249
+ size_line, _, rest = rest.partition(b"\r\n")
250
+ try:
251
+ size = int(size_line.split(b";")[0].strip() or b"0", 16)
252
+ except ValueError:
253
+ break
254
+ if not size:
255
+ break
256
+ out, rest = out + rest[:size], rest[size + 2:]
257
+ body = out
258
+ try:
259
+ if b"content-encoding: gzip" in lowered:
260
+ body = gzip.decompress(body)
261
+ elif b"content-encoding: deflate" in lowered:
262
+ body = zlib.decompress(body)
263
+ except (OSError, EOFError, zlib.error):
264
+ pass
265
+ return body
266
+
267
+
268
+ def request_line(item):
269
+ """``POST api.example.com/v1/cart -> 200 at 4.2 s``: one request, safe to show."""
270
+ where = secret_filter.redact(f"{item['host']}{item.get('path', '')}")
271
+ status = f" -> {item['status']}" if item.get("status") else ""
272
+ return f"{item['method']} {where}{status} at {item['atMs'] / 1000:.1f} s"
273
+
274
+
275
+ def evidence(item):
276
+ """The matched request for wire.json: its line, headers with secrets masked, and bodies cut short."""
277
+ return {"line": request_line(item),
278
+ "headers": [[name, _mask_header(name, value)] for name, value in item.get("headers", [])],
279
+ "body": _text(item["body"]) if item.get("body") else "",
280
+ "response": _text(_response_body(item["response"])) if item.get("response") else ""}
281
+
282
+
283
+ # judging
284
+
285
+ def _path_matches(matcher, path):
286
+ if matcher.is_regex:
287
+ return bool(matcher.pattern.search(path))
288
+ return fnmatch.fnmatchcase(path.split("?")[0], matcher.text) or fnmatch.fnmatchcase(path, matcher.text)
289
+
290
+
291
+ def _status_matches(want, got):
292
+ if got is None:
293
+ return False
294
+ return got // 100 == int(want[0]) if isinstance(want, str) else got == want
295
+
296
+
297
+ def request_matches(assertion, item):
298
+ if assertion.method and item["method"] != assertion.method:
299
+ return False
300
+ if assertion.host and not netstubs.host_matches(assertion.host, item["host"]):
301
+ return False
302
+ if assertion.path and not _path_matches(assertion.path, item.get("path", "")):
303
+ return False
304
+ if assertion.status and not _status_matches(assertion.status, item.get("status")):
305
+ return False
306
+ if assertion.body and not assertion.body.contains(item["body"].decode("utf-8", "replace")):
307
+ return False
308
+ return not assertion.response or assertion.response.contains(
309
+ _response_body(item["response"]).decode("utf-8", "replace"))
310
+
311
+
312
+ def _opaque(rows):
313
+ """Connections the proxy relayed or refused without opening: they name a host and nothing more."""
314
+ return [{"method": row["method"], "host": row["host"], "path": "", "atMs": row["atMs"], "headers": [],
315
+ "body": b"", "status": None, "response": b"", "opaque": True}
316
+ for row in rows if row.get("via") in ("tunnel", "refused")]
317
+
318
+
319
+ def _judge_request(assertion, captures, rows):
320
+ text = assertion.render()
321
+ hidden = _opaque(rows)
322
+ if assertion.kind == "request":
323
+ hits = [item for item in captures if request_matches(assertion, item)]
324
+ if hits:
325
+ first = hits[0]
326
+ return AssertionResult(True, text, request_line(first) + (f" and {len(hits) - 1} more"
327
+ if len(hits) > 1 else ""), (evidence(first),))
328
+ seen = ", ".join(request_line(item) for item in captures[:SHOWN_SEEN])
329
+ tunnels = sorted({item["host"] for item in hidden})
330
+ more = (f"; https to {', '.join(tunnels[:SHOWN_SEEN])} wasn't opened, so name the host to see its paths"
331
+ if tunnels and not assertion.host else "")
332
+ if not captures and not hidden:
333
+ return AssertionResult(False, text, "no request from the app reached the proxy" + more)
334
+ return AssertionResult(False, text, f"no request matched; saw {seen or 'none opened'}{more}")
335
+ hits = [item for item in (*captures, *hidden) if request_matches(assertion, item)]
336
+ if hits:
337
+ return AssertionResult(False, text, "left the app: " + request_line(hits[0]), (evidence(hits[0]),))
338
+ if not captures and not hidden:
339
+ return AssertionResult(False, text, "no request from the app reached the proxy, so Mobster can't tell "
340
+ "whether this one left")
341
+ undecided = [item for item in hidden if assertion.path or assertion.body]
342
+ if undecided:
343
+ hosts = ", ".join(sorted({item["host"] for item in undecided})[:SHOWN_SEEN])
344
+ return AssertionResult(False, text, f"https to {hosts} wasn't opened, so its paths are unknown; name the "
345
+ "host in this assertion")
346
+ return AssertionResult(True, text, f"none of {len(captures) + len(hidden)} requests matched")
347
+
348
+
349
+ class Judge:
350
+ """One run's wire assertions: ``baseline`` before the flow, ``judge`` after it. Never raises."""
351
+
352
+ def __init__(self, assertions, *, run, udid, bundle, container=wirestore.container):
353
+ self.assertions = tuple(assertions)
354
+ self._find = lambda: container(run, udid, bundle)
355
+ self._before = {}
356
+ self.evidence = []
357
+
358
+ def baseline(self):
359
+ try:
360
+ root = self._find()
361
+ except wirestore.StoreError:
362
+ return
363
+ for index, item in enumerate(self.assertions):
364
+ if item.kind == "storage":
365
+ self._before[index] = wirestore.take(root, item)
366
+
367
+ def judge(self, net):
368
+ captures, rows = net.seen() if net is not None else ([], [])
369
+ out = []
370
+ root, problem = None, None
371
+ if any(item.kind == "storage" for item in self.assertions):
372
+ try:
373
+ root = self._find()
374
+ except wirestore.StoreError as error:
375
+ problem = str(error)
376
+ for index, item in enumerate(self.assertions):
377
+ if item.kind != "storage":
378
+ out.append(_judge_request(item, captures, rows) if net is not None else AssertionResult(
379
+ False, item.render(), "the run had no proxy, so no request was seen"))
380
+ elif root is None:
381
+ out.append(AssertionResult(False, item.render(), problem))
382
+ else:
383
+ out.append(wirestore.judge(item, root, self._before.get(index)))
384
+ self.evidence = [{"assertion": item.render(), "ok": result.ok, "observed": result.observed,
385
+ "matches": [dict(m) for m in result.matches]}
386
+ for item, result in zip(self.assertions, out)]
387
+ return out
388
+
389
+ def write(self, directory):
390
+ if self.evidence:
391
+ with open(os.path.join(directory, "wire.json"), "w", encoding="utf-8") as handle:
392
+ json.dump(self.evidence, handle, indent=1, default=str)
@@ -0,0 +1,176 @@
1
+ """``expect: storage``: what the app wrote under its own container, read on the Mac after the steps.
2
+
3
+ A simulator's app container is a folder on this Mac, so nothing is pulled over a wire: ``take`` hashes the files an
4
+ assertion names before the flow, ``judge`` compares them after, and a sqlite query runs on a copy so the app's
5
+ database is never opened for writing. Evidence is a short diff or the first rows, with secrets masked."""
6
+
7
+ import difflib
8
+ import hashlib
9
+ import json
10
+ import plistlib
11
+ import re
12
+ import shutil
13
+ import sqlite3
14
+ import tempfile
15
+ import time
16
+ from pathlib import Path
17
+
18
+ from .. import secret_filter
19
+ from .assertions import AssertionResult
20
+
21
+ FILES_LIMIT = 2000
22
+ HASH_LIMIT = 16 << 20
23
+ TEXT_LIMIT = 64 << 10
24
+ DIFF_LINES = 8
25
+ ROWS_SHOWN = 3
26
+ QUERY_SECONDS = 5
27
+ READ_ONLY = re.compile(r"\s*(select|with)\b", re.I)
28
+
29
+
30
+ class StoreError(Exception):
31
+ pass
32
+
33
+
34
+ def read_only_query(sql):
35
+ """One SELECT or WITH statement (the authorizer in ``rows`` refuses writes whatever this accepts)."""
36
+ body = sql.strip().rstrip(";").strip()
37
+ return bool(READ_ONLY.match(body)) and ";" not in body and len(body) <= 2000
38
+
39
+
40
+ def container(run, udid, bundle):
41
+ """The app's data container on this Mac, from ``simctl get_app_container <udid> <bundle> data``."""
42
+ code, output = run(["get_app_container", udid, bundle, "data"])
43
+ path = Path(output.splitlines()[-1].strip()) if output.strip() else None
44
+ if code != 0 or path is None or not path.is_dir():
45
+ raise StoreError("Mobster couldn't find the app's container on the simulator"
46
+ + (f" ({output.splitlines()[-1][:120]})" if output.strip() else "") + ".")
47
+ return path
48
+
49
+
50
+ def _files(root, pattern):
51
+ """Files under ``root`` matching a glob, as {relative path: (size, sha256, text or None)}."""
52
+ found = {}
53
+ for path in sorted(root.glob(pattern)):
54
+ if len(found) >= FILES_LIMIT:
55
+ break
56
+ if not path.is_file() or path.is_symlink():
57
+ continue
58
+ size = path.stat().st_size
59
+ digest = hashlib.sha256()
60
+ with path.open("rb") as handle:
61
+ kept = handle.read(HASH_LIMIT)
62
+ digest.update(kept)
63
+ found[path.relative_to(root).as_posix()] = (size, digest.hexdigest(),
64
+ _readable(path, kept) if size <= TEXT_LIMIT else None)
65
+ return found
66
+
67
+
68
+ def _readable(path, raw):
69
+ """A file's content as text for a diff: a plist as JSON-like lines, text as is, anything else None."""
70
+ if raw[:6] == b"bplist" or raw[:5] == b"<?xml":
71
+ try:
72
+ return json.dumps(plistlib.loads(raw), indent=1, sort_keys=True, default=str)
73
+ except (plistlib.InvalidFileException, ValueError, OverflowError):
74
+ return None
75
+ try:
76
+ return raw.decode("utf-8")
77
+ except UnicodeDecodeError:
78
+ return None
79
+
80
+
81
+ def take(root, assertion):
82
+ """What the assertion looks at before the flow: its files, or its query's row count."""
83
+ if assertion.file:
84
+ return {"files": _files(root, assertion.file)}
85
+ try:
86
+ return {"rows": len(rows(root, assertion))}
87
+ except StoreError:
88
+ return {"rows": None}
89
+
90
+
91
+ def rows(root, assertion):
92
+ """The query's rows from a copy of the database (and its -wal and -shm), never the file itself."""
93
+ source = root / assertion.sqlite
94
+ if not source.is_file():
95
+ raise StoreError(f"{assertion.sqlite} isn't in the app's container.")
96
+ with tempfile.TemporaryDirectory(prefix="mobster-wire-") as scratch:
97
+ copy = Path(scratch, "db.sqlite")
98
+ shutil.copyfile(source, copy)
99
+ for suffix in ("-wal", "-shm"):
100
+ if Path(f"{source}{suffix}").is_file():
101
+ shutil.copyfile(f"{source}{suffix}", f"{copy}{suffix}")
102
+ deadline = time.monotonic() + QUERY_SECONDS
103
+ connection = sqlite3.connect(str(copy))
104
+ try:
105
+ connection.execute("PRAGMA query_only = ON")
106
+ connection.set_authorizer(lambda action, *_: sqlite3.SQLITE_OK if action in (
107
+ sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ, sqlite3.SQLITE_FUNCTION, sqlite3.SQLITE_RECURSIVE)
108
+ else sqlite3.SQLITE_DENY)
109
+ connection.set_progress_handler(lambda: int(time.monotonic() > deadline), 10000)
110
+ return connection.execute(assertion.query).fetchall()
111
+ except sqlite3.Error as error:
112
+ raise StoreError(f"the query on {assertion.sqlite} failed: {str(error)[:120]}.") from None
113
+ finally:
114
+ connection.close()
115
+
116
+
117
+ def _show(text, limit=160):
118
+ text = secret_filter.redact(str(text))
119
+ return text if len(text) <= limit else text[:limit] + "…"
120
+
121
+
122
+ def _diff(before, after):
123
+ old = (before[2] or "").splitlines() if before else []
124
+ new = (after[2] or "").splitlines() if after else []
125
+ if (before and before[2] is None) or (after and after[2] is None):
126
+ return f"{before[0] if before else 0} -> {after[0] if after else 0} bytes (not text)"
127
+ lines = [line for line in difflib.unified_diff(old, new, lineterm="", n=0) if line[:1] in "+-" and line[:3] not in
128
+ ("+++", "---")]
129
+ shown = "; ".join(_show(line, 80) for line in lines[:DIFF_LINES])
130
+ return shown + (f"; and {len(lines) - DIFF_LINES} more lines" if len(lines) > DIFF_LINES else "")
131
+
132
+
133
+ def judge(assertion, root, before):
134
+ text = assertion.render()
135
+ if assertion.sqlite:
136
+ return _judge_rows(assertion, text, root, before)
137
+ after = _files(root, assertion.file)
138
+ if before is None:
139
+ return AssertionResult(False, text, "Mobster took no baseline before the flow, so it can't tell what changed")
140
+ old = before["files"]
141
+ moved = [(name, old.get(name), after.get(name)) for name in sorted({*old, *after})
142
+ if (old.get(name) or (0, ""))[1] != (after.get(name) or (0, ""))[1]]
143
+ if assertion.contains:
144
+ holds = [name for name, new in sorted(after.items()) if new[2] and assertion.contains.contains(new[2])]
145
+ if not assertion.changed:
146
+ if moved:
147
+ name, was, now = moved[0]
148
+ return AssertionResult(False, text, f"{name} changed: {_diff(was, now)}")
149
+ return AssertionResult(True, text, f"{len(after)} matching files unchanged" if after else
150
+ "no file matches, so none changed")
151
+ if not moved:
152
+ return AssertionResult(False, text, f"none of {len(after)} matching files changed" if after
153
+ else f"no file in the container matches {assertion.file}")
154
+ name, was, now = moved[0]
155
+ kind = "added" if was is None else "deleted" if now is None else "changed"
156
+ if assertion.contains and not holds:
157
+ return AssertionResult(False, text, f"{name} {kind}, but no matching file contains "
158
+ f'"{assertion.contains.render()}"')
159
+ return AssertionResult(True, text, f"{name} {kind}: {_diff(was, now)}" + (f" and {len(moved) - 1} more files"
160
+ if len(moved) > 1 else ""),
161
+ ({"file": name, "diff": _diff(was, now)},))
162
+
163
+
164
+ def _judge_rows(assertion, text, root, before):
165
+ try:
166
+ found = rows(root, assertion)
167
+ except StoreError as error:
168
+ return AssertionResult(False, text, str(error))
169
+ count = len(found)
170
+ ok = (count == assertion.equals if assertion.equals is not None
171
+ else count >= assertion.at_least if assertion.at_least is not None else count <= assertion.at_most)
172
+ was = (before or {}).get("rows")
173
+ shown = "; ".join(_show(row) for row in found[:ROWS_SHOWN])
174
+ observed = f"{count} rows" + (f" (was {was})" if was is not None else "") + (f": {shown}" if shown else "")
175
+ shown_rows = [[_show(cell) for cell in row] for row in found[:ROWS_SHOWN]]
176
+ return AssertionResult(ok, text, observed, ({"rows": shown_rows},))
@@ -0,0 +1 @@
1
+ """Who's waiting on me: the people you left on read, with drafts you approve (Mobster for Mac)."""
@@ -0,0 +1,73 @@
1
+ """Names for the people in Messages, from this Mac's own Contacts database, read-only."""
2
+
3
+ import re
4
+ import sqlite3
5
+ from pathlib import Path
6
+ from urllib.parse import quote
7
+
8
+ ROOT = Path.home() / "Library" / "Application Support" / "AddressBook"
9
+ MATCH_DIGITS = 10
10
+
11
+
12
+ def database_paths(root=None):
13
+ base = Path(root) if root else ROOT
14
+ found = sorted(base.glob("Sources/*/AddressBook-v22.abcddb"))
15
+ top = base / "AddressBook-v22.abcddb"
16
+ return ([top] if top.is_file() else []) + found
17
+
18
+
19
+ def digits(value):
20
+ return re.sub(r"\D", "", value or "")
21
+
22
+
23
+ def phone_key(value):
24
+ """The last 10 digits of a phone number, or None when it has fewer than 7 digits."""
25
+ number = digits(value)
26
+ return number[-MATCH_DIGITS:] if len(number) >= 7 else None
27
+
28
+
29
+ def _name(first, last, nick, org):
30
+ full = " ".join(part.strip() for part in (first, last) if part and part.strip())
31
+ return full or (nick or "").strip() or (org or "").strip() or None
32
+
33
+
34
+ class ContactBook:
35
+ """Name lookup by handle. ``ContactBook.load(paths)``; an unreadable database is skipped."""
36
+
37
+ def __init__(self):
38
+ self.by_phone, self.by_email = {}, {}
39
+
40
+ @classmethod
41
+ def load(cls, paths=None):
42
+ book = cls()
43
+ for path in (database_paths() if paths is None else paths):
44
+ try:
45
+ book._read(path)
46
+ except sqlite3.Error:
47
+ continue
48
+ return book
49
+
50
+ def _read(self, path):
51
+ connection = sqlite3.connect(f"file:{quote(str(path))}?mode=ro", uri=True, timeout=3)
52
+ try:
53
+ connection.execute("PRAGMA query_only = ON")
54
+ names = {row[0]: _name(*row[1:]) for row in connection.execute(
55
+ "SELECT Z_PK, ZFIRSTNAME, ZLASTNAME, ZNICKNAME, ZORGANIZATION FROM ZABCDRECORD")}
56
+ for owner, number in connection.execute("SELECT ZOWNER, ZFULLNUMBER FROM ZABCDPHONENUMBER"):
57
+ key, name = phone_key(number), names.get(owner)
58
+ if key and name:
59
+ self.by_phone.setdefault(key, name)
60
+ for owner, address in connection.execute("SELECT ZOWNER, ZADDRESS FROM ZABCDEMAILADDRESS"):
61
+ name = names.get(owner)
62
+ if address and name:
63
+ self.by_email.setdefault(address.strip().lower(), name)
64
+ finally:
65
+ connection.close()
66
+
67
+ def name(self, handle):
68
+ """The contact's name for a Messages handle, or None."""
69
+ handle = (handle or "").strip()
70
+ if "@" in handle:
71
+ return self.by_email.get(handle.lower())
72
+ key = phone_key(handle)
73
+ return self.by_phone.get(key) if key else None