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,93 @@
1
+ """Network X-ray's routes, for the Mac app's session only: no agent or MCP tool, so no hostname reaches a model."""
2
+
3
+ from ..api_errors import APIError
4
+ from ..api_routes import Response, Route
5
+ from ..api_routes import register as add_route
6
+ from ..harness_api import STAY_ON_MAC
7
+ from . import job, target
8
+
9
+ APP_ONLY = "Only the Mobster app can record your iPhone's network traffic. Open Mobster and ask there."
10
+
11
+
12
+ def _app_only(request):
13
+ # Fail closed: the capture is of everything the phone does, so only the Mac app's own session starts or reads it.
14
+ if not request.app_session:
15
+ raise APIError(APP_ONLY, 403, "app_only")
16
+
17
+
18
+ def _board(request):
19
+ board = getattr(request.runtime, "netxray_board", None)
20
+ if board is None:
21
+ raise APIError("Network X-ray isn't running", 503, "xray_unavailable")
22
+ return board
23
+
24
+
25
+ def parse(body):
26
+ """(device, seconds, wifi, key) from a start request; ValueError says what's wrong."""
27
+ body = body if isinstance(body, dict) else {}
28
+ device, seconds, wifi, key = (body.get("device"), body.get("seconds", job.DEFAULT_SECONDS),
29
+ body.get("wifi", False), body.get("key"))
30
+ if not isinstance(device, str) or not 1 <= len(device) <= 128:
31
+ raise ValueError("device is the phone's id, UDID or name")
32
+ if isinstance(seconds, bool) or not isinstance(seconds, int) or not job.MIN_SECONDS <= seconds <= job.MAX_SECONDS:
33
+ raise ValueError(f"seconds is {job.MIN_SECONDS} to {job.MAX_SECONDS}")
34
+ if not isinstance(wifi, bool):
35
+ raise ValueError("wifi is true or false")
36
+ if key is not None and (not isinstance(key, str) or not 1 <= len(key) <= 100):
37
+ raise ValueError("key is 1 to 100 characters")
38
+ return device, seconds, wifi, key
39
+
40
+
41
+ def start(request):
42
+ _app_only(request)
43
+ device, seconds, wifi, key = parse(request.body)
44
+ board = _board(request)
45
+ try:
46
+ phone = target.pick(device, wifi=wifi)
47
+ except target.TargetError as error:
48
+ raise APIError(str(error), 400, "xray_device") from None
49
+ gate = getattr(request.runtime, "plan_gate", None)
50
+ if gate is not None:
51
+ gate.require()
52
+ started = board.start(phone, seconds, key=f"{phone.udid}-{key}" if key else None)
53
+ return Response.json({"jobId": started.id}, 202)
54
+
55
+
56
+ def status(request):
57
+ _app_only(request)
58
+ found = _board(request).get(request.params["id"])
59
+ if found is None:
60
+ raise APIError("There is no such X-ray", 404, "xray_not_found")
61
+ return Response.json(found.view())
62
+
63
+
64
+ def stop(request):
65
+ _app_only(request)
66
+ return Response.json(_board(request).stop(request.params["id"]).view())
67
+
68
+
69
+ def open_board(runtime):
70
+ runtime.netxray_board = job.Board(runtime)
71
+
72
+
73
+ def close_board(runtime):
74
+ board = getattr(runtime, "netxray_board", None)
75
+ if board is not None:
76
+ board.close()
77
+ runtime.netxray_board = None
78
+
79
+
80
+ def stay_on_mac(value, runtime):
81
+ if value is not True:
82
+ raise ValueError(f"{STAY_ON_MAC} is true or left out")
83
+ return True
84
+
85
+
86
+ def register(api):
87
+ """``api`` is mobile_agent.harness_api."""
88
+ # The finished report's run stays on this Mac: nothing shares, sends or reads it but the app.
89
+ api.register_run_field(STAY_ON_MAC, stay_on_mac)
90
+ api.register_service(open_board, close_board)
91
+ add_route(Route("netxray", "POST", "/api/xray/network", start))
92
+ add_route(Route("netxray", "GET", "/api/xray/network/{id}", status, body="none"))
93
+ add_route(Route("netxray", "POST", "/api/xray/network/{id}/stop", stop, body="none"))
@@ -0,0 +1,214 @@
1
+ """netxray.json (``mobster.netxray/1``), report.html and report.md; names from the traffic are data, always escaped."""
2
+
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from .. import links
8
+ from ..verify.report import BASE_STYLE, CSP, _e, brand, icon
9
+ from .analyze import headline
10
+ from .trackers import CATEGORY_LABELS, TRACKING
11
+
12
+ CATEGORY_TONE = {"advertising": "fail", "analytics": "warn", "attribution": "review", "engagement": "warn",
13
+ "crash": "none", "social": "none", "platform": "none", "cdn": "none"}
14
+
15
+ STYLE = BASE_STYLE + """
16
+ .stats { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1px; margin: 0 0 12px;
17
+ border-radius: var(--radius-card); overflow: hidden; }
18
+ .stat { padding: 14px 16px; background: var(--card); min-width: 0; }
19
+ .stat dt { font-size: 12px; line-height: 18px; color: var(--ink-3); }
20
+ .stat dd { margin: 2px 0 0; font-size: 20px; line-height: 28px; font-weight: var(--weight-medium);
21
+ font-variant-numeric: tabular-nums; }
22
+ .stat.alert dd { color: var(--coral); }
23
+ table.hosts { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 20px; }
24
+ table.hosts th { text-align: left; font-weight: var(--weight-medium); color: var(--ink-3); font-size: 12px;
25
+ padding: 10px 12px 6px; white-space: nowrap; }
26
+ table.hosts td { padding: 8px 12px; border-top: 1px solid var(--line); vertical-align: middle; }
27
+ table.hosts td.num, table.hosts th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
28
+ table.hosts .host { font-family: var(--font-mono); font-size: 12.5px; overflow-wrap: anywhere; }
29
+ table.hosts .company { color: var(--ink-2); white-space: nowrap; }
30
+ table.hosts tr.tracking .host { color: var(--ink); }
31
+ .bar { display: block; height: 3px; margin-top: 4px; border-radius: 2px; background: var(--tone, var(--ink-4));
32
+ opacity: .55; min-width: 2px; }
33
+ .hosts-card { padding: 4px 4px 8px; overflow-x: auto; }
34
+ .tag.tone.none::before { background: var(--ink-4); }
35
+ .unnamed { margin: 8px 16px 4px; font-size: 13px; color: var(--ink-3); }
36
+ .privacy { margin: 16px 0 0; max-width: 720px; font-size: 13px; line-height: 20px; color: var(--ink-2); }
37
+ .synthetic { --tone: var(--amber); }
38
+ @media (max-width: 760px) {
39
+ .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
40
+ table.hosts .hide-narrow { display: none; }
41
+ table.hosts td:first-child { min-width: 150px; }
42
+ table.hosts td, table.hosts th { padding-left: 8px; padding-right: 8px; }
43
+ }
44
+ """
45
+
46
+
47
+ def size(count):
48
+ """Bytes as people read them: 812 B, 14.2 KB, 3.1 MB."""
49
+ if count < 1000:
50
+ return f"{count} B"
51
+ if count < 1_000_000:
52
+ return f"{count / 1000:.1f} KB"
53
+ return f"{count / 1_000_000:.1f} MB"
54
+
55
+
56
+ def category_tag(category):
57
+ if not category:
58
+ return '<span class="quiet">Unknown</span>'
59
+ label = CATEGORY_LABELS.get(category, category)
60
+ return f'<span class="tag tone {CATEGORY_TONE.get(category, "none")}">{_e(label)}</span>'
61
+
62
+
63
+ def _app_section(app, seconds, index, *, lede=True):
64
+ domains = app["domains"]
65
+ biggest = max([d["up"] + d["down"] for d in domains] or [1]) or 1
66
+ parts = [f'<section aria-labelledby="app-{index}"><h2 id="app-{index}">{_e(app["name"])}'
67
+ f'<span class="count">{len(domains)} domain{"s" * (len(domains) != 1)}</span></h2>']
68
+ if lede:
69
+ parts.append(f'<p class="lede" style="margin-bottom:16px">{_e(headline(app, seconds))}</p>')
70
+ via = ", ".join(app["via"])
71
+ stats = [("Domains", app["domain_count"], False), ("Companies", len(app["companies"]), False),
72
+ ("Ads, analytics, attribution", len(app["tracking"]), bool(app["tracking"])),
73
+ ("Sent", size(app["up"]), False), ("Received", size(app["down"]), False)]
74
+ parts.append('<dl class="stats">' + "".join(
75
+ f'<div class="stat{" alert" * alert}"><dt>{_e(label)}</dt><dd>{_e(value)}</dd></div>'
76
+ for label, value, alert in stats) + "</dl>")
77
+ if domains:
78
+ rows = []
79
+ for domain in domains:
80
+ total = domain["up"] + domain["down"]
81
+ tone = CATEGORY_TONE.get(domain["category"] or "", "none")
82
+ tracking = " tracking" if domain["category"] in TRACKING else ""
83
+ rows.append(
84
+ f'<tr class="{tone}{tracking}"><td><span class="host">{_e(domain["host"])}</span>'
85
+ f'<span class="bar" style="width:{max(1, round(100 * total / biggest))}%"></span></td>'
86
+ f'<td class="company hide-narrow">{_e(domain["company"])}</td>'
87
+ f'<td>{category_tag(domain["category"])}</td>'
88
+ f'<td class="num">{_e(size(domain["up"]))}</td><td class="num">{_e(size(domain["down"]))}</td>'
89
+ f'<td class="num hide-narrow">{domain["connections"]}</td>'
90
+ f'<td class="num hide-narrow">{domain["first"]:.1f} s</td></tr>')
91
+ parts.append('<div class="card hosts-card"><table class="hosts"><thead><tr><th>Host</th>'
92
+ '<th class="hide-narrow">Company</th>'
93
+ '<th>Category</th><th class="num">Sent</th><th class="num">Received</th>'
94
+ '<th class="num hide-narrow">Connections</th><th class="num hide-narrow">First seen</th>'
95
+ '</tr></thead><tbody>' + "".join(rows) + "</tbody></table>")
96
+ unnamed = app["unnamed"]
97
+ if unnamed["connections"]:
98
+ parts.append(f'<p class="unnamed">{unnamed["connections"]} more connection'
99
+ f'{"s" * (unnamed["connections"] != 1)} with no hostname in the capture '
100
+ f'({_e(size(unnamed["up"] + unnamed["down"]))}). Their addresses aren’t shown.</p>')
101
+ parts.append("</div>")
102
+ if app["kind"] == "ios" and app["processes"]:
103
+ parts.append(f'<p class="privacy">These are parts of iOS itself: {_e(", ".join(app["processes"]))}.</p>')
104
+ if app.get("note"):
105
+ parts.append(f'<p class="privacy">{_e(app["note"])}</p>')
106
+ if app["kind"] == "process" and app["processes"]:
107
+ parts.append(f'<p class="privacy">Process: {_e(", ".join(app["processes"]))}.</p>')
108
+ if via:
109
+ parts.append(f'<p class="privacy">Some of this traffic went through {_e(via)} on the app’s behalf.</p>')
110
+ parts.append("</section>")
111
+ return "".join(parts)
112
+
113
+
114
+ def render_html(result, *, title="Network X-ray", synthetic=False):
115
+ capture = result["capture"]
116
+ apps = result["apps"]
117
+ seconds = capture["seconds"]
118
+ started = time.strftime("%d %b %Y %H:%M", time.localtime(capture["started"])) if capture["started"] else "-"
119
+ focus = apps[0] if len(apps) == 1 else None
120
+ heading = headline(focus, seconds) if focus else (
121
+ f"{len(apps)} process{'es' * (len(apps) != 1)} talked to "
122
+ f"{len({d['host'] for a in apps for d in a['domains']})} domains")
123
+ chips = ['<span class="tag">Metadata only</span>']
124
+ if synthetic:
125
+ chips.append('<span class="tag tone synthetic">Synthetic capture</span>')
126
+ if capture["truncated"]:
127
+ chips.append('<span class="tag tone warn">The capture ends mid-packet</span>')
128
+ if not capture["with_process"]:
129
+ chips.append('<span class="tag tone warn">No process names in this capture</span>')
130
+ meta = [("clock", f"{seconds:.0f} s captured"), ("calendar", started),
131
+ ("phone", f"{capture['packets']:,} packets")]
132
+ parts = [
133
+ "<!doctype html>", '<html lang="en"><head><meta charset="utf-8">',
134
+ f'<meta http-equiv="Content-Security-Policy" content="{CSP}">',
135
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
136
+ '<meta name="color-scheme" content="light dark">',
137
+ f"<title>{_e(title)}</title><style>{STYLE}</style></head><body><main>",
138
+ brand("network x-ray"),
139
+ '<header class="head"><div class="chips">', "".join(chips), "</div>",
140
+ f"<h1>{_e(heading)}</h1>",
141
+ '<p class="lede">Hostnames, sizes and times, read from the phone’s traffic. Nothing was decrypted '
142
+ "and no payload was kept.</p>",
143
+ '<ul class="meta">' + "".join(f"<li>{icon(name)}{_e(text)}</li>" for name, text in meta) + "</ul></header>",
144
+ ]
145
+ for index, app in enumerate(apps):
146
+ if app["domains"] or app["unnamed"]["connections"]:
147
+ parts.append(_app_section(app, seconds, index, lede=focus is None))
148
+ quiet = [app["name"] for app in apps if not app["domains"] and not app["unnamed"]["connections"]]
149
+ local = result["local"]
150
+ notes = []
151
+ if local["packets"]:
152
+ notes.append(f"{local['packets']:,} packets ({size(local['bytes'])}) stayed on the local network. "
153
+ "Local names and addresses aren’t shown.")
154
+ if quiet:
155
+ notes.append("Looked up names but opened no connection: " + ", ".join(quiet) + ".")
156
+ if notes:
157
+ parts.append('<section><h2>Also in the capture</h2>' + "".join(f'<p class="privacy">{_e(n)}</p>'
158
+ for n in notes) + "</section>")
159
+ parts.append(f'<footer class="foot"><span><a href="{links.MADE_WITH_XRAY}">Mobster</a> Network X-ray</span>'
160
+ '<span>Hostnames come from DNS '
161
+ 'answers, TLS and QUIC server names and HTTP Host headers</span>'
162
+ '<span>Companies and categories come from Mobster’s own list</span></footer>')
163
+ parts.append("</main></body></html>")
164
+ return "".join(parts)
165
+
166
+
167
+ def _md(text):
168
+ """Text from the capture, safe inside a Markdown table cell or line."""
169
+ text = str(text)
170
+ for char in "\\`*_[]<>|#":
171
+ text = text.replace(char, "\\" + char)
172
+ return text.replace("\n", " ")
173
+
174
+
175
+ def render_markdown(result, *, synthetic=False):
176
+ capture = result["capture"]
177
+ seconds = capture["seconds"]
178
+ lines = ["# Network X-ray", ""]
179
+ if synthetic:
180
+ lines += ["Synthetic capture.", ""]
181
+ lines += [f"{seconds:.0f} s captured, {capture['packets']:,} packets. Hostnames, sizes and times only: nothing was "
182
+ "decrypted and no payload was kept.", ""]
183
+ for app in result["apps"]:
184
+ if not app["domains"] and not app["unnamed"]["connections"]:
185
+ continue
186
+ lines += [f"## {_md(app['name'])}", "", _md(headline(app, seconds)), ""]
187
+ if app["domains"]:
188
+ lines += ["| Host | Company | Category | Sent | Received | Connections |", "|---|---|---|---:|---:|---:|"]
189
+ for domain in app["domains"]:
190
+ category = CATEGORY_LABELS.get(domain["category"], "") if domain["category"] else "Unknown"
191
+ lines.append(f"| `{domain['host']}` | {_md(domain['company'])} | {category} | {size(domain['up'])} | "
192
+ f"{size(domain['down'])} | {domain['connections']} |")
193
+ if app["unnamed"]["connections"]:
194
+ lines += ["", f"{app['unnamed']['connections']} {'more ' * bool(app['domains'])}connections with no "
195
+ f"hostname in the capture ({size(app['unnamed']['up'] + app['unnamed']['down'])})."]
196
+ if app.get("note"):
197
+ lines += ["", _md(app["note"])]
198
+ if app["kind"] in ("ios", "process") and app["processes"]:
199
+ lines += ["", _md(("Parts of iOS: " if app["kind"] == "ios" else "Process: ")
200
+ + ", ".join(app["processes"]) + ".")]
201
+ lines.append("")
202
+ return "\n".join(lines)
203
+
204
+
205
+ def write(result, out_dir, *, synthetic=False):
206
+ """Write netxray.json, report.html and report.md into ``out_dir``; their paths."""
207
+ out = Path(out_dir)
208
+ out.mkdir(parents=True, exist_ok=True)
209
+ out.chmod(0o700)
210
+ paths = {"json": out / "netxray.json", "html": out / "report.html", "markdown": out / "report.md"}
211
+ paths["json"].write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
212
+ paths["html"].write_text(render_html(result, synthetic=synthetic), encoding="utf-8")
213
+ paths["markdown"].write_text(render_markdown(result, synthetic=synthetic), encoding="utf-8")
214
+ return paths
@@ -0,0 +1,48 @@
1
+ """Where a Network X-ray lives: a private folder (0700) on this Mac. The capture stays only while read, or when kept."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from .. import paths
7
+
8
+
9
+ def root():
10
+ return Path(paths.user_data_dir()) / "xray" / "network"
11
+
12
+
13
+ def new_folder(now=None, base=None):
14
+ """A new private folder named for the time. Never reuses one: a second in the same second gets -2, -3."""
15
+ base = Path(base) if base else root()
16
+ stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(now if now is not None else time.time()))
17
+ for number in range(1, 100):
18
+ folder = base / (stamp if number == 1 else f"{stamp}-{number}")
19
+ try:
20
+ base.mkdir(parents=True, exist_ok=True)
21
+ folder.mkdir(mode=0o700)
22
+ except FileExistsError:
23
+ continue
24
+ folder.chmod(0o700)
25
+ return folder
26
+ raise FileExistsError("Too many X-rays in one second")
27
+
28
+
29
+ def capture_path(folder):
30
+ return Path(folder) / "capture.pcapng"
31
+
32
+
33
+ def discard_capture(folder):
34
+ """Delete the capture (and nothing else) in ``folder``."""
35
+ capture_path(folder).unlink(missing_ok=True)
36
+
37
+
38
+ def conclude(folder, names, *, keep=False, only=None, source=None):
39
+ # The folder's capture goes whatever happens, so a failed analysis leaves no packet metadata behind.
40
+ from . import analyze, report, summary
41
+ try:
42
+ result = analyze.analyze_file(source or capture_path(folder), names=names)
43
+ if only:
44
+ result = summary.only(result, only)
45
+ return result, report.write(result, folder)
46
+ finally:
47
+ if not keep:
48
+ discard_capture(folder)
@@ -0,0 +1,157 @@
1
+ """What Network X-ray says about an analysis. Every string has already passed the secret filter."""
2
+
3
+ from .. import result_blocks
4
+ from . import attribution
5
+ from .analyze import headline
6
+ from .trackers import CATEGORY_LABELS, TRACKING
7
+
8
+ # What a company that does several things is shown as: the one a person would want to know first.
9
+ CATEGORY_ORDER = ("advertising", "attribution", "analytics", "engagement", "crash", "social", "platform", "cdn")
10
+ NOT_SEEN = ("Names sent through encrypted DNS appear only when the connection itself names the server. iCloud Private "
11
+ "Relay and Encrypted Client Hello hide the server's name, so those connections are counted without one.")
12
+
13
+
14
+ def _plural(count, one, many):
15
+ return f"{count} {one if count == 1 else many}"
16
+
17
+
18
+ def named(result):
19
+ """The apps with something to say: a domain, or a connection with no hostname."""
20
+ return [app for app in result["apps"] if app["domains"] or app["unnamed"]["connections"]]
21
+
22
+
23
+ def only(result, name):
24
+ """``result`` with only the app called ``name`` (any case), or the process that sent it.
25
+ The local total goes too."""
26
+ wanted = name.strip().casefold()
27
+ kept = [app for app in result["apps"]
28
+ if app["name"].casefold() == wanted or any(process.casefold() == wanted for process in app["processes"])]
29
+ return {**result, "apps": kept, "local": {"packets": 0, "bytes": 0}}
30
+
31
+
32
+ def lines(result):
33
+ """One sentence per app that contacted a domain, then one for traffic nobody could be blamed for."""
34
+ seconds = result["capture"]["seconds"]
35
+ out = [headline(app, seconds) for app in result["apps"] if app["domain_count"]
36
+ and app["kind"] != attribution.UNATTRIBUTED_KIND]
37
+ out += [headline(app, seconds) for app in result["apps"]
38
+ if app["kind"] == attribution.UNATTRIBUTED_KIND and app["connections"]]
39
+ return out
40
+
41
+
42
+ def companies(result):
43
+ """[{"name", "category", "categoryLabel", "apps", "domains", "bytes"}] across all apps, biggest first."""
44
+ found = {}
45
+ for app in result["apps"]:
46
+ for domain in app["domains"]:
47
+ entry = found.setdefault(domain["company"], {"name": domain["company"], "categories": set(), "apps": [],
48
+ "domains": set(), "bytes": 0})
49
+ if domain["category"]:
50
+ entry["categories"].add(domain["category"])
51
+ if app["name"] not in entry["apps"]:
52
+ entry["apps"].append(app["name"])
53
+ entry["domains"].add(domain["host"])
54
+ entry["bytes"] += domain["up"] + domain["down"]
55
+ out = []
56
+ for entry in found.values():
57
+ category = next((name for name in CATEGORY_ORDER if name in entry["categories"]), None)
58
+ out.append({"name": entry["name"], "category": category,
59
+ "categoryLabel": CATEGORY_LABELS.get(category, "Other"),
60
+ "apps": sorted(entry["apps"]), "domains": len(entry["domains"]), "bytes": entry["bytes"]})
61
+ out.sort(key=lambda item: (-item["bytes"], item["name"].lower()))
62
+ return out
63
+
64
+
65
+ def live(result):
66
+ """The numbers the Mac app shows while a capture runs: {"apps", "companies", "totals"}."""
67
+ apps = [{"name": app["name"], "kind": app["kind"], "process": ", ".join(app["processes"]),
68
+ "domains": app["domain_count"], "companies": len(app["companies"]), "tracking": len(app["tracking"]),
69
+ "bytes": app["up"] + app["down"]} for app in named(result)]
70
+ shared = companies(result)
71
+ unattributed = next((app for app in result["apps"] if app["kind"] == attribution.UNATTRIBUTED_KIND), None)
72
+ hosts = {domain["host"] for app in result["apps"] for domain in app["domains"]}
73
+ tracking = {domain["host"] for app in result["apps"] for domain in app["domains"]
74
+ if domain["category"] in TRACKING}
75
+ return {"apps": apps, "companies": shared,
76
+ "totals": {"apps": len([app for app in apps if app["kind"] != attribution.UNATTRIBUTED_KIND]),
77
+ "domains": len(hosts), "companies": len(shared), "tracking": len(tracking),
78
+ "bytes": sum(app["up"] + app["down"] for app in result["apps"]),
79
+ "unattributed": {"bytes": unattributed["up"] + unattributed["down"] if unattributed else 0,
80
+ "connections": unattributed["connections"] if unattributed else 0},
81
+ "packets": result["capture"]["packets"]}}
82
+
83
+
84
+ def answer(result):
85
+ """The task's one-line answer: counts across all apps, then the traffic that couldn't be blamed."""
86
+ seconds = max(1, round(result["capture"]["seconds"]))
87
+ total = live(result)["totals"]
88
+ shown = [app for app in named(result) if app["kind"] != attribution.UNATTRIBUTED_KIND and app["domain_count"]]
89
+ if not shown and not total["unattributed"]["bytes"]:
90
+ return f"In {seconds} s your iPhone contacted no domain Mobster could name."
91
+ text = (f"In {seconds} s {_plural(len(shown), 'app', 'apps')} contacted "
92
+ f"{_plural(total['domains'], 'domain', 'domains')} "
93
+ f"at {_plural(total['companies'], 'company', 'companies')}")
94
+ if total["tracking"]:
95
+ text += (f". {total['tracking']} of those domains {'is' if total['tracking'] == 1 else 'are'} ads, "
96
+ "analytics or attribution")
97
+ text += "."
98
+ if total["unattributed"]["bytes"]:
99
+ text += " Mobster couldn't tell which app sent some of the traffic, so it is listed as Unattributed."
100
+ return text
101
+
102
+
103
+ def _kb(count):
104
+ return round(count / 1000, 1)
105
+
106
+
107
+ def blocks(result, stamp=None, kept=False):
108
+ """The finished task's blocks: who each app talked to, every domain, and what was and wasn't seen."""
109
+ people = []
110
+ for app in named(result):
111
+ grouped = {}
112
+ for domain in app["domains"]:
113
+ entry = grouped.setdefault(domain["company"], {"categories": set(), "domains": 0, "up": 0, "down": 0})
114
+ if domain["category"]:
115
+ entry["categories"].add(domain["category"])
116
+ entry["domains"] += 1
117
+ entry["up"] += domain["up"]
118
+ entry["down"] += domain["down"]
119
+ for company, entry in grouped.items():
120
+ what = ", ".join(CATEGORY_LABELS[name] for name in CATEGORY_ORDER if name in entry["categories"])
121
+ people.append([app["name"], company, what or "Unknown", entry["domains"], _kb(entry["up"]),
122
+ _kb(entry["down"])])
123
+ people.sort(key=lambda row: (-(row[4] + row[5]), row[0].lower(), row[1].lower()))
124
+ hosts = [[app["name"], domain["host"], domain["company"],
125
+ CATEGORY_LABELS.get(domain["category"], "Unknown") if domain["category"] else "Unknown",
126
+ domain["connections"], _kb(domain["up"] + domain["down"])]
127
+ for app in result["apps"] for domain in app["domains"]]
128
+ hosts.sort(key=lambda row: (-row[5], row[0].lower(), row[1]))
129
+ out = []
130
+ if people:
131
+ out.append(result_blocks.table(
132
+ [{"key": "app", "label": "App"}, {"key": "company", "label": "Company", "type": "brand"},
133
+ {"key": "does", "label": "What it does"}, {"key": "domains", "label": "Domains", "type": "number"},
134
+ {"key": "sent", "label": "Sent (KB)", "type": "number"},
135
+ {"key": "received", "label": "Received (KB)", "type": "number"}],
136
+ people, title="Who your apps talked to", id="companies"))
137
+ out.append(result_blocks.table(
138
+ [{"key": "app", "label": "App"}, {"key": "domain", "label": "Domain"},
139
+ {"key": "company", "label": "Company", "type": "brand"}, {"key": "category", "label": "Category"},
140
+ {"key": "connections", "label": "Connections", "type": "number"},
141
+ {"key": "bytes", "label": "Bytes (KB)", "type": "number"}],
142
+ hosts, title="Every domain", id="domains"))
143
+ notes = ["Mobster read hostnames, sizes and times from your iPhone's network traffic. It decrypted nothing and "
144
+ "kept no page content, URL path or address."]
145
+ for app in result["apps"]:
146
+ if app.get("note") and (app["up"] or app["down"] or app["connections"]):
147
+ notes.append(f"**{app['name']}:** {app['note']} ({_kb(app['up'] + app['down'])} KB in "
148
+ f"{_plural(app['connections'], 'connection', 'connections')}.)")
149
+ if result["local"]["packets"]:
150
+ notes.append(f"{result['local']['packets']:,} packets stayed on your local network. Their names and addresses "
151
+ "aren't shown.")
152
+ notes.append("**What Mobster couldn't see.** " + NOT_SEEN)
153
+ if stamp:
154
+ notes.append(f"Saved on this Mac, in Mobster's data folder under `xray/network/{stamp}`. " + (
155
+ "The capture file is kept there too." if kept else "The capture file was deleted after the analysis."))
156
+ out.append(result_blocks.markdown("\n\n".join(notes), title="What was read", id="notes"))
157
+ return out
@@ -0,0 +1,67 @@
1
+ """Which phone to capture, and which apps it has. Simulators have no pcapd service, so they are refused."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from . import attribution
6
+
7
+
8
+ class TargetError(Exception):
9
+ """The phone can't be captured; the message says why and what to do."""
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Phone:
14
+ id: str
15
+ name: str
16
+ udid: str
17
+ network: bool # capture over Wi-Fi (mobster-pcap --network), not the cable
18
+
19
+
20
+ def on_cable(record):
21
+ """Whether ``record`` (devices.discover) is on a cable now."""
22
+ if "transport" in record:
23
+ return record["transport"] == "usb"
24
+ return record.get("state") != "disconnected"
25
+
26
+
27
+ def pick(query=None, *, wifi=False, records=None):
28
+ """The Phone to capture."""
29
+ from .. import devices
30
+ records = devices.discover(probe=False) if records is None else records
31
+ if query:
32
+ try:
33
+ record = devices.resolve(records, query)
34
+ except LookupError as error:
35
+ raise TargetError(str(error)) from None
36
+ if record.get("kind") == "simulator":
37
+ raise TargetError(f"“{record['name']}” is a simulator. Network X-ray reads a real iPhone's traffic: "
38
+ "name an iPhone.")
39
+ if record.get("kind") != "usb" or not record.get("udid"):
40
+ raise TargetError(f"“{record['name']}” isn't an iPhone Mobster set up. Network X-ray needs the phone's "
41
+ "UDID: plug it in once with a cable.")
42
+ else:
43
+ phones = [item for item in records
44
+ if item.get("kind") == "usb" and item.get("udid") and (wifi or on_cable(item))]
45
+ if not phones:
46
+ raise TargetError("Mobster sees no iPhone. Plug one in, or name one with --device.")
47
+ if len(phones) > 1:
48
+ raise TargetError(f"{len(phones)} iPhones are connected, so Mobster won't guess which one to capture. "
49
+ "Name one with --device: " + ", ".join(f"{item['name']} ({item['id']})"
50
+ for item in phones[:8]) + ".")
51
+ record = phones[0]
52
+ return Phone(record["id"], record.get("name") or record["id"], record["udid"], bool(wifi or not on_cable(record)))
53
+
54
+
55
+ def phone_names(phone, *, tool=None, runner=None):
56
+ """attribution.Names for ``phone``, from the apps it has installed (user and system, with their executables)."""
57
+ from .. import installed_apps
58
+ from ..mac_tools import tool as find_tool
59
+ kwargs = {"runner": runner} if runner else {}
60
+ binary = tool or find_tool("ideviceinstaller")
61
+ rows = []
62
+ for kind in ("user", "system"):
63
+ try:
64
+ rows += installed_apps.list_apps(binary, phone.udid, kind, network=phone.network, **kwargs)
65
+ except installed_apps.ListingError:
66
+ pass
67
+ return attribution.Names(attribution.executables_from(rows))
@@ -0,0 +1,66 @@
1
+ """Find (or build) ``mobster-pcap``, Mobster's own pcapd client."""
2
+
3
+ import hashlib
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ NAME = "mobster-pcap"
10
+ SOURCE = Path(__file__).resolve().with_name(f"{NAME}.c")
11
+ LIBRARIES = ("libimobiledevice-1.0", "libplist-2.0")
12
+
13
+
14
+ def _cached():
15
+ if not SOURCE.is_file():
16
+ return None
17
+ from ..paths import user_data_dir
18
+ digest = hashlib.sha256(SOURCE.read_bytes()).hexdigest()[:8]
19
+ return Path(user_data_dir()) / "helpers" / f"{NAME}-{digest}"
20
+
21
+
22
+ def find():
23
+ """The app's bundled tool or a previously built copy; None when neither exists."""
24
+ from ..mac_tools import bundled_tools_dir, executable
25
+ folder = bundled_tools_dir()
26
+ if folder is not None and (path := executable(folder, NAME)):
27
+ return str(path)
28
+ cached = _cached()
29
+ if cached is not None and cached.is_file() and os.access(cached, os.X_OK):
30
+ return str(cached)
31
+ return None
32
+
33
+
34
+ def build():
35
+ """Compile the packaged tool source against Homebrew's libimobiledevice."""
36
+ target = _cached()
37
+ if target is None:
38
+ raise LookupError("This Mobster has no mobster-pcap source to build")
39
+ if target.is_file():
40
+ return str(target)
41
+ pkg_config = shutil.which("pkg-config") or next(
42
+ (path for path in ("/opt/homebrew/bin/pkg-config", "/usr/local/bin/pkg-config") if os.access(path, os.X_OK)),
43
+ None)
44
+ if not pkg_config:
45
+ raise LookupError("pkg-config isn't installed. Run `brew install pkg-config libimobiledevice`.")
46
+ try:
47
+ flags = subprocess.run([pkg_config, "--cflags", "--libs", *LIBRARIES], capture_output=True, text=True,
48
+ timeout=30, check=True).stdout.split()
49
+ except (OSError, subprocess.SubprocessError):
50
+ raise LookupError("libimobiledevice isn't installed. Run `brew install libimobiledevice`.") from None
51
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
52
+ partial = target.with_suffix(".partial")
53
+ try:
54
+ subprocess.run(["xcrun", "clang", "-O2", str(SOURCE), "-o", str(partial), *flags], capture_output=True,
55
+ timeout=120, check=True)
56
+ except (OSError, subprocess.SubprocessError):
57
+ partial.unlink(missing_ok=True)
58
+ raise LookupError("mobster-pcap couldn't be built here. Install Xcode's command line tools with "
59
+ "`xcode-select --install`.") from None
60
+ os.replace(partial, target)
61
+ return str(target)
62
+
63
+
64
+ def locate():
65
+ """``find()``, else ``build()``. LookupError says what is missing."""
66
+ return find() or build()