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
mobile_agent/alerts.py ADDED
@@ -0,0 +1,337 @@
1
+ """Alerts: a webhook post when a scheduled task fails, stops or waits on an approval for a minute, and when a phone
2
+ needs you (fleet_health.py: unplugged, locked, signed out, a security check, a region that doesn't match).
3
+
4
+ Opt-in: nothing is sent until MOBSTER_ALERT_WEBHOOK_URL holds an https address (Settings in the Mac app, or
5
+ the env file). The post is shaped for the service the address belongs to: Slack ({"text"}), Discord
6
+ ({"content"}), ntfy (plain text with a Title header), or anything else as JSON {workflow, status, reason, runId,
7
+ at, text}. It never carries the task's answer, a screenshot, typed text or any secret: the reason is one of
8
+ Mobster's own fixed sentences, chosen by the run's status. A phone alert names the phone and, for a profile, its
9
+ handle: the sentences are fixed, the names are the ones you gave.
10
+
11
+ Delivery never blocks or fails a run: it happens on its own thread, tries twice with a 5-second limit each, and
12
+ gives up quietly. Addresses on this Mac's network (localhost, private, link-local and reserved addresses, .local
13
+ names) are refused, the name is resolved once and that address is the one connected to, and redirects are not
14
+ followed, so an alert can't be turned against something on the local network.
15
+
16
+ `mobster alerts test` sends a test post (``add_arguments`` and ``run``, registered by devtools.py).
17
+ """
18
+
19
+ import datetime
20
+ import http.client
21
+ import ipaddress
22
+ import json
23
+ import os
24
+ import socket
25
+ import ssl
26
+ import sys
27
+ import threading
28
+ import time
29
+ from pathlib import Path
30
+ from urllib.parse import urlsplit
31
+
32
+ ENV = "MOBSTER_ALERT_WEBHOOK_URL"
33
+ TIMEOUT = 5.0
34
+ ATTEMPTS = 2
35
+ RETRY_PAUSE = 1.0
36
+ APPROVAL_WAIT_S = 60
37
+ # Statuses that need no alert: it worked, or the user ended it themselves.
38
+ QUIET = frozenset({"completed", "stopped", "approval_denied", "demo_complete", "queued", "running"})
39
+ REASONS = {
40
+ "blocked": "It couldn't finish the task.",
41
+ "max_steps": "It ran out of steps.",
42
+ "timeout": "It ran out of time.",
43
+ "spend_cap": "It reached its spending limit.",
44
+ "approval_timeout": "Nobody answered its approval in time.",
45
+ "approval_waiting": "It is waiting for your approval.",
46
+ "interrupted": "Mobster stopped while it ran.",
47
+ "run_expired": "Its task was deleted or left the history after the run finished, before Mobster read how it ended.",
48
+ "error": "It hit an error.",
49
+ "failed": "It failed.",
50
+ "test": "This is a test alert from Mobster.",
51
+ }
52
+ # Why a run stopped before it began, when the phone guard says (agent_hooks.GuardVerdict.code).
53
+ CODES = {
54
+ "phone_locked": "Your iPhone was locked.",
55
+ "passcode_failed": "The saved passcode didn't unlock your iPhone.",
56
+ "face_id": "An app asked for Face ID.",
57
+ "app_locked": "The app is locked on your iPhone.",
58
+ "apple_confirmation": "The App Store wanted you to confirm on your iPhone.",
59
+ "unplugged": "Your iPhone was unplugged.",
60
+ "keychain_locked": "Your Mac's Keychain was locked.",
61
+ "unlock_declined": "Unlocking the iPhone was declined.",
62
+ "blocked_app": "The task needed an app you told Mobster never to open.",
63
+ "system_dialog": "Your iPhone asked a question only you can answer.",
64
+ }
65
+ LOCAL_SUFFIXES = (".local", ".localhost", ".internal", ".lan", ".home.arpa", ".intranet", ".corp")
66
+
67
+
68
+ class AlertError(ValueError):
69
+ """Why an address can't take alerts, in one plain sentence."""
70
+
71
+
72
+ def validate_url(url):
73
+ """``url`` trimmed when it can take alerts (https, a name or a public address); names are checked at each send."""
74
+ text = str(url or "").strip()
75
+ if not text:
76
+ raise AlertError("Give the webhook's address.")
77
+ if len(text) > 2000 or any(ord(c) < 33 for c in text):
78
+ raise AlertError("That webhook address isn't valid.")
79
+ parts = urlsplit(text)
80
+ if parts.scheme.lower() != "https":
81
+ raise AlertError("Alerts go only to https addresses.")
82
+ if parts.username or parts.password:
83
+ raise AlertError("Put the webhook's secret in its path, not as user:password@.")
84
+ host = (parts.hostname or "").rstrip(".").lower()
85
+ if not host:
86
+ raise AlertError("That webhook address has no host.")
87
+ if host == "localhost" or host.endswith(LOCAL_SUFFIXES) or "." not in host and not _literal(host):
88
+ raise AlertError("Alerts never go to this Mac or its local network.")
89
+ address = _literal(host)
90
+ if address is not None and not _public(address):
91
+ raise AlertError("Alerts never go to this Mac or its local network.")
92
+ try:
93
+ parts.port
94
+ except ValueError:
95
+ raise AlertError("That webhook address has a bad port.") from None
96
+ return text
97
+
98
+
99
+ def _literal(host):
100
+ try:
101
+ return ipaddress.ip_address(host.strip("[]"))
102
+ except ValueError:
103
+ return None
104
+
105
+
106
+ def _public(address):
107
+ """Reachable on the internet; ``is_global`` also rules out shared 100.64.0.0/10 (Tailscale, carrier NAT)."""
108
+ if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
109
+ address = address.ipv4_mapped
110
+ return address.is_global and not (address.is_private or address.is_loopback or address.is_link_local
111
+ or address.is_multicast or address.is_reserved or address.is_unspecified
112
+ or getattr(address, "is_site_local", False))
113
+
114
+
115
+ def resolve_public(host, port, resolve=socket.getaddrinfo):
116
+ """The address to connect to: every address ``host`` resolves to must be public; the first is used."""
117
+ try:
118
+ infos = resolve(host, port, type=socket.SOCK_STREAM)
119
+ except (OSError, UnicodeError):
120
+ raise AlertError(f"{host} couldn't be found.") from None
121
+ addresses = [info[4][0] for info in infos]
122
+ if not addresses or not all(_public(ipaddress.ip_address(a.split("%")[0])) for a in addresses):
123
+ raise AlertError("Alerts never go to this Mac or its local network.")
124
+ return addresses[0]
125
+
126
+
127
+ def configured_url(env=None, data_dir=None, read_file=False):
128
+ """The alert address: $MOBSTER_ALERT_WEBHOOK_URL, else (``read_file``) the Mac app's agent.env, else None."""
129
+ env = os.environ if env is None else env
130
+ value = (env.get(ENV) or "").strip()
131
+ if value:
132
+ return value
133
+ if not read_file:
134
+ return None
135
+ try:
136
+ from . import env_secrets
137
+ from .config import read_env_values
138
+ from .paths import user_data_dir
139
+ file = (Path(data_dir) if data_dir else user_data_dir()) / "agent.env"
140
+ # Mobster for Mac keeps the address in the Keychain: it's a credential for Slack and Discord (env_secrets.py).
141
+ # The file's own value comes first, so `mobster alerts test` only reads the Keychain when it must.
142
+ return read_env_values(file).get(ENV) or env_secrets.load(file).get(ENV) or None
143
+ except (OSError, UnicodeDecodeError, ImportError):
144
+ return None
145
+
146
+
147
+ def reason_for(status, code=None):
148
+ if code in CODES:
149
+ return CODES[code]
150
+ return REASONS.get(status, "It didn't finish.")
151
+
152
+
153
+ def payload(workflow, status, run_id=None, code=None, at=None):
154
+ """The alert: never the answer, a screenshot or anything the task typed."""
155
+ at = time.time() if at is None else at
156
+ name = " ".join(str(workflow or "Saved task").split())[:120]
157
+ return {"workflow": name, "status": str(status)[:40], "reason": reason_for(status, code),
158
+ "runId": str(run_id)[:40] if run_id else None,
159
+ "at": datetime.datetime.fromtimestamp(at, datetime.timezone.utc).isoformat(timespec="seconds")}
160
+
161
+
162
+ def phone_payload(phone, code, message, action, at=None):
163
+ """The alert for a phone's condition (fleet_health.py): the phone's name, a code, and two fixed sentences."""
164
+ at = time.time() if at is None else at
165
+ name = " ".join(str(phone or "iPhone").split())[:120]
166
+ return {"workflow": name, "status": f"phone_{code}"[:40], "reason": f"{message} {action}"[:400], "runId": None,
167
+ "at": datetime.datetime.fromtimestamp(at, datetime.timezone.utc).isoformat(timespec="seconds")}
168
+
169
+
170
+ def headline(data):
171
+ if data["status"].startswith("phone_"):
172
+ return f"Mobster: {data['reason']}"
173
+ if data["status"] == "approval_waiting":
174
+ return f"Mobster: “{data['workflow']}” is waiting for your approval. Open the Mobster app to answer it."
175
+ if data["status"] == "challenge":
176
+ return (f"Mobster: {data['workflow']}. {data['reason']} Nothing runs as it until you resume it in the Mac "
177
+ "app's Queue.")
178
+ if data["status"] == "test":
179
+ return "Mobster: alerts work. You'll hear here when a scheduled task fails or waits for you."
180
+ if data["status"] == "run_expired":
181
+ # Mobster can't say it failed (workflows/health.py UNKNOWN_ENDS), so the alert never says it didn't finish.
182
+ return (f"Mobster: We can't tell how “{data['workflow']}” ended. {data['reason']} "
183
+ "Open the Mobster app to run it again.")
184
+ return (f"Mobster: “{data['workflow']}” didn't finish ({data['status']}). {data['reason']} "
185
+ "Open the Mobster app to see it.")
186
+
187
+
188
+ def shape(url, data):
189
+ """(body bytes, headers) for the service ``url`` belongs to."""
190
+ host = (urlsplit(url).hostname or "").lower()
191
+ line = headline(data)
192
+ if host == "hooks.slack.com" or host.endswith(".slack.com"):
193
+ body = {"text": line}
194
+ elif host in ("discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"):
195
+ body = {"content": line}
196
+ elif host == "ntfy.sh" or host.startswith("ntfy."):
197
+ title = f"Mobster: {data['workflow']}"[:120].encode("ascii", "replace").decode()
198
+ return line.encode("utf-8"), {"Content-Type": "text/plain; charset=utf-8", "Title": title,
199
+ "Tags": "iphone,warning"}
200
+ else:
201
+ body = {**data, "text": line}
202
+ return json.dumps(body, ensure_ascii=False).encode("utf-8"), {"Content-Type": "application/json"}
203
+
204
+
205
+ class _Pinned(http.client.HTTPSConnection):
206
+ """HTTPS to an address checked beforehand, with the name kept for TLS (SNI and the certificate check)."""
207
+
208
+ def __init__(self, host, address, port, timeout, context):
209
+ super().__init__(host, port=port, timeout=timeout, context=context)
210
+ self._address = address
211
+
212
+ def connect(self):
213
+ sock = socket.create_connection((self._address, self.port), self.timeout)
214
+ self.sock = self._context.wrap_socket(sock, server_hostname=self.host)
215
+
216
+
217
+ def _post(host, address, port, path, body, headers, timeout):
218
+ connection = _Pinned(host, address, port, timeout, ssl.create_default_context())
219
+ try:
220
+ connection.request("POST", path, body=body, headers={**headers, "User-Agent": "Mobster-alerts"})
221
+ response = connection.getresponse()
222
+ response.read(2048)
223
+ return response.status
224
+ finally:
225
+ connection.close()
226
+
227
+
228
+ def deliver(url, data, *, post=None, resolve=socket.getaddrinfo, sleep=time.sleep, attempts=ATTEMPTS):
229
+ """Send ``data`` to ``url``; True when the service answered 2xx. Never raises."""
230
+ try:
231
+ url = validate_url(url)
232
+ parts = urlsplit(url)
233
+ host, port = parts.hostname.rstrip("."), parts.port or 443
234
+ path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
235
+ body, headers = shape(url, data)
236
+ except Exception:
237
+ return False
238
+ post = post or _post
239
+ for attempt in range(attempts):
240
+ try:
241
+ address = resolve_public(host, port, resolve)
242
+ status = post(host, address, port, path, body, headers, TIMEOUT)
243
+ if 200 <= int(status) < 300:
244
+ return True
245
+ if 400 <= int(status) < 500 and status != 429:
246
+ return False # the address refuses it: trying again won't help
247
+ except AlertError:
248
+ return False
249
+ except Exception:
250
+ pass
251
+ if attempt + 1 < attempts:
252
+ sleep(RETRY_PAUSE)
253
+ return False
254
+
255
+
256
+ class Alerts:
257
+ """What workflows/service.py calls: reads the address at each alert and sends on a daemon thread, so no run waits
258
+ on it."""
259
+
260
+ def __init__(self, env=None, *, post=None, resolve=socket.getaddrinfo, background=True, clock=time.time):
261
+ self.env, self.post, self.resolve, self.background, self.clock = env, post, resolve, background, clock
262
+ self.sent = [] # (status, run id) of every alert started, for the log and tests
263
+
264
+ def url(self):
265
+ value = configured_url(self.env)
266
+ if not value:
267
+ return None
268
+ try:
269
+ return validate_url(value)
270
+ except AlertError:
271
+ return None
272
+
273
+ def run_finished(self, workflow, status, run_id, code=None):
274
+ """Alert when a scheduled run ended without finishing. True when an alert was started."""
275
+ if status in QUIET:
276
+ return False
277
+ return self._send(payload(workflow, status, run_id, code, self.clock()))
278
+
279
+ def approval_waiting(self, workflow, run_id):
280
+ return self._send(payload(workflow, "approval_waiting", run_id, None, self.clock()))
281
+
282
+ def challenge(self, title, reason, run_id):
283
+ """Alert that a profile was paused after the app challenged it (accounts/register.py)."""
284
+ return self._send({**payload(title, "challenge", run_id, None, self.clock()), "reason": reason})
285
+
286
+ def phone_alert(self, phone, code, message, action):
287
+ """Alert that a phone needs you (unplugged, locked, signed out, a check); True when an alert was started."""
288
+ return self._send(phone_payload(phone, code, message, action, self.clock()))
289
+
290
+ def _send(self, data):
291
+ url = self.url()
292
+ if not url:
293
+ return False
294
+ self.sent.append((data["status"], data["runId"]))
295
+
296
+ def work():
297
+ deliver(url, data, post=self.post, resolve=self.resolve)
298
+ if self.background:
299
+ threading.Thread(target=work, daemon=True, name="mobster-alert").start()
300
+ else:
301
+ work()
302
+ return True
303
+
304
+
305
+ def add_arguments(parser, helpers):
306
+ subs = parser.add_subparsers(dest="alerts_command", metavar="<subcommand>")
307
+ subs.required = True
308
+ test = subs.add_parser("test", help="send a test alert to your webhook",
309
+ description="Send a test alert to MOBSTER_ALERT_WEBHOOK_URL (or --url) and say whether "
310
+ "the service took it.",
311
+ epilog="exit codes: 0 sent, 1 the service didn't take it, 2 no usable address")
312
+ test.add_argument("--url", metavar="URL", help="send to this https address instead of the saved one")
313
+ test.add_argument("--json", action="store_true", help="print one JSON object")
314
+
315
+
316
+ def run(args, *, post=None, resolve=socket.getaddrinfo):
317
+ as_json = bool(getattr(args, "json", False))
318
+ url = getattr(args, "url", None) or configured_url(read_file=True)
319
+
320
+ def say(ok, message, code):
321
+ try:
322
+ if as_json:
323
+ print(json.dumps({"ok": ok, "message": message}), flush=True)
324
+ else:
325
+ print(("" if ok else "mobster alerts: ") + message, file=sys.stdout if ok else sys.stderr, flush=True)
326
+ except BrokenPipeError:
327
+ pass
328
+ return code
329
+ if not url:
330
+ return say(False, f"No alert address. Set {ENV} (Settings in the Mobster app), or pass --url.", 2)
331
+ try:
332
+ url = validate_url(url)
333
+ except AlertError as error:
334
+ return say(False, str(error), 2)
335
+ if deliver(url, payload("Test", "test"), post=post, resolve=resolve):
336
+ return say(True, f"Sent a test alert to {urlsplit(url).hostname}.", 0)
337
+ return say(False, f"{urlsplit(url).hostname} didn't take the test alert. Check the address, then try again.", 1)
@@ -0,0 +1,18 @@
1
+ """Android: emulators and USB phones through adb, behind MOBSTER_EXPERIMENTAL_ANDROID=1 until the owner turns it on."""
2
+
3
+ import os
4
+
5
+ FLAG = "MOBSTER_EXPERIMENTAL_ANDROID"
6
+
7
+
8
+ def enabled(environ=None):
9
+ environ = os.environ if environ is None else environ
10
+ return (environ.get(FLAG) or "").strip().casefold() in {"1", "true", "yes", "on"}
11
+
12
+
13
+ def add_test_flag(where):
14
+ """`mobster test --android`, shown only while the flag is on."""
15
+ if enabled():
16
+ where.add_argument("--android", nargs="?", const="", metavar="AVD_OR_SERIAL",
17
+ help="run on an Android emulator: an AVD name or its adb serial, or none for any idle "
18
+ "emulator (experimental)")
@@ -0,0 +1,268 @@
1
+ """adb: find the SDK tools, list devices, and run the few commands the driver needs. One argv per call, never a shell
2
+ string built on this side; the runner is injectable so the tests use a fake adb."""
3
+
4
+ import base64
5
+ import os
6
+ import re
7
+ import shlex
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ SERIAL = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:\-]{0,63}")
13
+ PACKAGE = re.compile(r"[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+")
14
+ DEFAULT_ROOTS = ("~/Library/Android/sdk", "~/Android/Sdk")
15
+ OUTPUT_LIMIT = 64_000_000
16
+ quote = shlex.quote
17
+
18
+
19
+ class AdbError(RuntimeError):
20
+ """An adb call failed or its answer was not understood. ``fix`` is one plain sentence (may be "")."""
21
+
22
+ def __init__(self, message, fix=""):
23
+ super().__init__(message)
24
+ self.fix = fix
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Result:
29
+ code: int
30
+ out: bytes = b""
31
+ err: bytes = b""
32
+
33
+ @property
34
+ def text(self):
35
+ return self.out.decode("utf-8", "replace")
36
+
37
+ @property
38
+ def message(self):
39
+ lines = (self.err or self.out).decode("utf-8", "replace").strip().splitlines()
40
+ return lines[-1] if lines else ""
41
+
42
+
43
+ def run_process(argv, timeout=30, stdin=None):
44
+ """The default runner. A timeout kills the child (subprocess.run does) and is reported as code 124."""
45
+ try:
46
+ done = subprocess.run(argv, input=stdin, capture_output=True, timeout=timeout, check=False)
47
+ except subprocess.TimeoutExpired:
48
+ return Result(124, b"", b"timed out")
49
+ except OSError as error:
50
+ return Result(127, b"", str(error).encode())
51
+ return Result(done.returncode, done.stdout[:OUTPUT_LIMIT], done.stderr[:4096])
52
+
53
+
54
+ def sdk_root(environ=None):
55
+ """The Android SDK folder: ANDROID_HOME, ANDROID_SDK_ROOT, else the Android Studio default; None when absent."""
56
+ environ = os.environ if environ is None else environ
57
+ for name in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
58
+ if environ.get(name):
59
+ path = Path(environ[name]).expanduser()
60
+ if path.is_dir():
61
+ return path
62
+ for default in DEFAULT_ROOTS:
63
+ path = Path(default).expanduser()
64
+ if path.is_dir():
65
+ return path
66
+ return None
67
+
68
+
69
+ def tool(name, root=None):
70
+ """The path of an SDK tool (adb, emulator, avdmanager, ...), or None."""
71
+ root = root if root is not None else sdk_root()
72
+ if root is None:
73
+ return None
74
+ for relative in {"adb": ("platform-tools/adb",), "emulator": ("emulator/emulator",),
75
+ "avdmanager": ("cmdline-tools/latest/bin/avdmanager", "cmdline-tools/bin/avdmanager")
76
+ }.get(name, (name,)):
77
+ path = Path(root) / relative
78
+ if path.is_file() and os.access(path, os.X_OK):
79
+ return str(path)
80
+ return None
81
+
82
+
83
+ def tool_in_build_tools(name, root=None):
84
+ """A tool from the newest build-tools folder (aapt2), or None."""
85
+ root = root if root is not None else sdk_root()
86
+ if root is None:
87
+ return None
88
+ for folder in sorted((Path(root) / "build-tools").glob("*"), reverse=True):
89
+ path = folder / name
90
+ if path.is_file() and os.access(path, os.X_OK):
91
+ return str(path)
92
+ return None
93
+
94
+
95
+ def valid_serial(serial):
96
+ if not isinstance(serial, str) or not SERIAL.fullmatch(serial):
97
+ raise AdbError(f"{serial!r} is not an adb serial.")
98
+ return serial
99
+
100
+
101
+ def valid_package(package):
102
+ if not isinstance(package, str) or len(package) > 255 or not PACKAGE.fullmatch(package):
103
+ raise AdbError(f"{package!r} is not an Android package name.")
104
+ return package
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class AndroidDevice:
109
+ serial: str
110
+ state: str # device, offline, unauthorized
111
+ kind: str # emulator or usb
112
+ model: str = ""
113
+ product: str = ""
114
+
115
+ @property
116
+ def ready(self):
117
+ return self.state == "device"
118
+
119
+
120
+ def parse_devices(text):
121
+ """`adb devices -l` as AndroidDevice rows. Lines that are not a serial and a state are ignored."""
122
+ devices = []
123
+ for line in text.splitlines():
124
+ parts = line.split()
125
+ if len(parts) < 2 or not SERIAL.fullmatch(parts[0]) or parts[0] == "List":
126
+ continue
127
+ extra = dict(item.split(":", 1) for item in parts[2:] if ":" in item)
128
+ devices.append(AndroidDevice(parts[0], parts[1], "emulator" if parts[0].startswith("emulator-") else "usb",
129
+ extra.get("model", ""), extra.get("product", "")))
130
+ return devices
131
+
132
+
133
+ class Adb:
134
+ """adb for one device (``serial``) or, with none, for listing. ``run`` is (argv, timeout, stdin) -> Result."""
135
+
136
+ def __init__(self, serial=None, run=run_process, binary=None):
137
+ self.serial = valid_serial(serial) if serial else None
138
+ self._run = run
139
+ self.binary = binary or tool("adb") or "adb"
140
+
141
+ def adb(self, *args, timeout=30, stdin=None, check=True):
142
+ argv = [self.binary, *(["-s", self.serial] if self.serial else []), *map(str, args)]
143
+ result = self._run(argv, timeout, stdin)
144
+ if check and result.code != 0:
145
+ raise AdbError(f"adb {args[0]} failed: {result.message or 'exit ' + str(result.code)}",
146
+ "Check that the device is online (`adb devices -l`) and unlocked.")
147
+ return result
148
+
149
+ def shell(self, *args, timeout=30, check=True):
150
+ return self.adb("shell", *args, timeout=timeout, check=check)
151
+
152
+ # -- devices
153
+
154
+ def devices(self):
155
+ return parse_devices(self.adb("devices", "-l", timeout=10).text)
156
+
157
+ def prop(self, name):
158
+ return self.shell("getprop", name, timeout=10).text.strip()
159
+
160
+ def booted(self):
161
+ return self.shell("getprop", "sys.boot_completed", timeout=10, check=False).text.strip() == "1"
162
+
163
+ # -- screen
164
+
165
+ def screen_px(self):
166
+ """(width, height, density dpi) of the display in pixels, from `wm size` and `wm density`."""
167
+ size = re.findall(r"(\d+)x(\d+)", self.shell("wm", "size", timeout=10).text)
168
+ density = re.findall(r"(\d+)", self.shell("wm", "density", timeout=10).text)
169
+ if not size or not density:
170
+ raise AdbError("The device did not report its screen size.")
171
+ width, height = map(int, size[-1])
172
+ return width, height, int(density[-1])
173
+
174
+ def dump_ui(self, timeout=15):
175
+ """The accessibility tree as uiautomator's XML, with the trailer line removed."""
176
+ text = self.adb("exec-out", "uiautomator", "dump", "/dev/tty", timeout=timeout).text
177
+ start, end = text.find("<?xml"), text.rfind("</hierarchy>")
178
+ if start < 0 or end < 0:
179
+ raise AdbError("uiautomator returned no accessibility tree.",
180
+ "Wait for the screen to settle, or restart the device's UI automation.")
181
+ return text[start:end + len("</hierarchy>")]
182
+
183
+ def screenshot_png(self, timeout=15):
184
+ data = self.adb("exec-out", "screencap", "-p", timeout=timeout).out
185
+ if not data.startswith(b"\x89PNG"):
186
+ raise AdbError("The device returned no screenshot.")
187
+ return data
188
+
189
+ def screenshot_data_uri(self, timeout=15):
190
+ return "data:image/png;base64," + base64.b64encode(self.screenshot_png(timeout)).decode()
191
+
192
+ def front_package(self):
193
+ """The package in front, from the focused window; "" when the device does not say."""
194
+ text = self.shell("dumpsys", "window", timeout=10, check=False).text
195
+ match = re.search(r"mCurrentFocus=Window\{\S+ \S+ ([A-Za-z0-9_.]+)/", text)
196
+ return match.group(1) if match else ""
197
+
198
+ # -- input
199
+
200
+ def tap(self, x, y):
201
+ self.shell("input", "tap", int(round(x)), int(round(y)), timeout=10)
202
+
203
+ def swipe(self, x1, y1, x2, y2, millis=300):
204
+ self.shell("input", "swipe", *(int(round(n)) for n in (x1, y1, x2, y2)), int(millis), timeout=10)
205
+
206
+ def long_press(self, x, y, millis=900):
207
+ self.swipe(x, y, x, y, millis)
208
+
209
+ def key(self, code):
210
+ self.shell("input", "keyevent", int(code), timeout=10)
211
+
212
+ def text(self, value):
213
+ """Type ASCII text at the focus. adb joins its arguments into one device shell line, so the text is single
214
+ quoted. `input text` cannot send other characters, and reads "%s" as a space, so those are refused."""
215
+ if not value.isascii() or "%" in value or any(ord(c) < 32 for c in value):
216
+ raise AdbError("Android can type plain ASCII text without % only.",
217
+ "Use text without accents, emoji or % until the keyboard helper ships.")
218
+ self.shell("input", "text", quote(value.replace(" ", "%s")), timeout=15)
219
+
220
+ # -- apps
221
+
222
+ def install(self, apk, timeout=180):
223
+ path = Path(apk)
224
+ if path.suffix != ".apk" or not path.is_file():
225
+ raise AdbError(f"{apk} is not an .apk file.")
226
+ result = self.adb("install", "-r", str(path), timeout=timeout, check=False)
227
+ if result.code != 0 or b"Success" not in result.out:
228
+ raise AdbError(f"adb install failed: {result.message or result.text.strip()[-200:]}")
229
+
230
+ def launch(self, package):
231
+ valid_package(package)
232
+ result = self.shell("monkey", "-p", package, "-c", "android.intent.category.LAUNCHER", "1", timeout=20)
233
+ if b"No activities found" in result.out + result.err:
234
+ raise AdbError(f"{package} has no launchable activity or is not installed.")
235
+
236
+ def stop(self, package):
237
+ self.shell("am", "force-stop", valid_package(package), timeout=10)
238
+
239
+ def installed_version(self, package):
240
+ """versionName of an installed package, or None when it is not installed."""
241
+ valid_package(package)
242
+ if "package:" not in self.shell("pm", "path", package, timeout=10, check=False).text:
243
+ return None
244
+ match = re.search(r"versionName=(\S+)", self.shell("dumpsys", "package", package, timeout=15).text)
245
+ return match.group(1) if match else ""
246
+
247
+ def clear_data(self, package):
248
+ result = self.shell("pm", "clear", valid_package(package), timeout=20, check=False)
249
+ if b"Success" not in result.out:
250
+ raise AdbError(f"Clearing {package}'s data failed: {result.message or result.text.strip()[-120:]}")
251
+
252
+ def uninstall(self, package):
253
+ self.adb("uninstall", valid_package(package), timeout=60, check=False)
254
+
255
+ def permission(self, package, permission, grant):
256
+ if not re.fullmatch(r"[A-Za-z0-9_.]+", permission):
257
+ raise AdbError(f"{permission!r} is not a permission name.")
258
+ result = self.shell("pm", "grant" if grant else "revoke", valid_package(package), permission, timeout=15,
259
+ check=False)
260
+ if result.code != 0 or b"Exception" in result.out + result.err or b"Error" in result.err:
261
+ raise AdbError(f"{'Granting' if grant else 'Revoking'} {permission} failed: "
262
+ f"{result.message or result.text.strip()[-120:]}")
263
+
264
+ def open_url(self, url, package=None):
265
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9+.\-]*:[^\s'\"`\\]+", url):
266
+ raise AdbError(f"{url!r} is not a link adb can open.")
267
+ self.shell("am", "start", "-a", "android.intent.action.VIEW", "-d", quote(url),
268
+ *(["-p", valid_package(package)] if package else []), timeout=15)