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,729 @@
1
+ """The health of every phone: the alerts and pre-run checks the Phones page shows (Mobster for Mac › Phones)."""
2
+
3
+ import contextlib
4
+ import errno
5
+ import fcntl
6
+ import functools
7
+ import json
8
+ import os
9
+ import re
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ from .api_errors import APIError
17
+ from .devices import REASONS
18
+
19
+ _Thread = threading.Thread
20
+
21
+ CODES = ("unplugged", "locked", "signed_out", "challenge", "region_mismatch", "owner_active")
22
+ ATTENTION, INFO = "attention", "info"
23
+ SEVERITY = {code: ATTENTION for code in CODES} | {"owner_active": INFO}
24
+ STORE = "fleet-health.json"
25
+ DEBOUNCE_S = 20.0
26
+ # How long a phone's region, language and time zone stand before they are read again. The pre-run check reads again
27
+ # after PRE_RUN_TTL, so a person who just changed a setting isn't refused on the old value for long.
28
+ LOCALE_TTL = 300.0
29
+ PRE_RUN_TTL = 5.0
30
+ READ_TIMEOUT = 2.0
31
+ GOAL_CHARS = 140
32
+ NOT_TAKEN = "No action was taken."
33
+
34
+
35
+ # -- reading the screen ------------------------------------------------------------------------------------------
36
+
37
+ def _clean(text):
38
+ text = (text or "").replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"')
39
+ return re.sub(r"\s+", " ", text).strip().casefold().strip(" .!?:")
40
+
41
+
42
+ # A security check or a block, in the apps' own words (English UI). Each pattern has to match one whole label (a title
43
+ # or a button, never a row or a field's value), so a message or a post that merely contains the words is
44
+ # not a challenge.
45
+ CHALLENGE = tuple(re.compile(pattern) for pattern in (
46
+ r"we suspect automated behavior( on your account)?",
47
+ r"suspicious (login|activity)( attempt| detected)?",
48
+ r"we detected unusual (login )?activity( on your account)?",
49
+ r"(please )?confirm it'?s you",
50
+ r"verify it'?s you",
51
+ r"help us confirm (you own this account|it'?s you)",
52
+ r"confirm your identity",
53
+ r"verify your identity",
54
+ r"(take|record|upload) a (video )?selfie",
55
+ r"upload a photo of your (id|government id)",
56
+ r"captcha",
57
+ r"(i'?m|i am) not a robot",
58
+ r"(confirm|prove|verify) (that )?you'?re (a )?(human|not a robot)",
59
+ r"verify you are (a )?human",
60
+ r"select all (the )?(images|squares|pictures) (with|containing) .{1,40}",
61
+ r"your account (has been|is|was) (temporarily )?(locked|suspended|disabled|restricted)",
62
+ r"we (have )?(temporarily )?(locked|restricted) your account",
63
+ ))
64
+ # One of these alone says the session ended.
65
+ SIGNED_OUT_STRONG = tuple(re.compile(pattern) for pattern in (
66
+ r"you'?ve been logged out",
67
+ r"you have been logged out",
68
+ r"your session (has )?expired",
69
+ r"session expired",
70
+ r"(please )?(log|sign) in again( to .{1,40})?",
71
+ ))
72
+ # A sign-in page shows at least two of these together (a feed never does).
73
+ SIGN_IN_PARTS = (
74
+ re.compile(r"(log ?in|sign in)( to .{1,30})?"),
75
+ re.compile(r"forgot (your )?password"),
76
+ re.compile(r"create (new |a )?account|sign up|get started"),
77
+ re.compile(r"(phone number|mobile number|username|email)(,| or ).{0,40}"),
78
+ re.compile(r"log into another account|use another profile|switch accounts"),
79
+ )
80
+ # What each kind of element can say. A title says what the screen is; a button is what the person can press; a field's
81
+ # label is its prompt ("Phone number, username or email address"). Cells and text views hold content (a post, a message,
82
+ # a row), and no element's value is read: those can carry anything another person wrote.
83
+ TITLES = frozenset({"StaticText", "NavigationBar", "Other", "Link"})
84
+ PROMPTS = TITLES | {"Button", "TextField"}
85
+ MAX_LABEL = 100
86
+ MAX_LABELS = 400
87
+
88
+
89
+ def _whole(patterns, text):
90
+ return any(pattern.fullmatch(text) for pattern in patterns)
91
+
92
+
93
+ def classify_screen(labels):
94
+ # screen text is data: only whole, short titles and buttons match, and nothing read is passed on as an instruction
95
+ pairs = []
96
+ for pair in list(labels or ())[:MAX_LABELS]:
97
+ if (isinstance(pair, (tuple, list)) and len(pair) == 2 and all(isinstance(part, str) for part in pair)
98
+ and len(pair[1]) <= 3 * MAX_LABEL):
99
+ text = _clean(pair[1])
100
+ if text and len(text) <= MAX_LABEL:
101
+ pairs.append((pair[0], text))
102
+ buttons = [text for role, text in pairs if role == "Button"]
103
+ titles = [text for role, text in pairs if role in TITLES]
104
+ if buttons and any(_whole(CHALLENGE, text) for text in titles):
105
+ return "challenge"
106
+ if any(_whole(SIGNED_OUT_STRONG, text) for role, text in pairs if role in TITLES or role == "Button"):
107
+ return "signed_out"
108
+ seen = {index for role, text in pairs if role in PROMPTS
109
+ for index, part in enumerate(SIGN_IN_PARTS) if part.fullmatch(text)}
110
+ return "signed_out" if len(seen) >= 2 else None
111
+
112
+
113
+ def snapshot_labels(snapshot):
114
+ out = []
115
+ for element in list(getattr(snapshot, "elements", None) or ())[:MAX_LABELS]:
116
+ role, label = getattr(element, "role", None), getattr(element, "label", None)
117
+ if isinstance(role, str) and isinstance(label, str) and label:
118
+ out.append((role, label))
119
+ return out
120
+
121
+
122
+ # -- region, language and time zone ------------------------------------------------------------------------------
123
+
124
+ REGION = re.compile(r"[A-Z]{2}")
125
+ SETTING = {"region": "Settings › General › Language & Region › Region",
126
+ "language": "Settings › General › Language & Region › Preferred Languages",
127
+ "timezone": "Settings › General › Date & Time"}
128
+ WORD = {"region": "region", "language": "language", "timezone": "time zone"}
129
+
130
+
131
+ def expected_of(profile):
132
+ """The values a profile expects: ``{region, language, timezone}``, each a string or None."""
133
+ out = {}
134
+ for key in ("region", "language", "timezone"):
135
+ value = (profile or {}).get(key)
136
+ out[key] = value if isinstance(value, str) and value else None
137
+ return out
138
+
139
+
140
+ @functools.lru_cache(maxsize=1)
141
+ def _iana_names():
142
+ try:
143
+ from zoneinfo import available_timezones
144
+ return frozenset(available_timezones())
145
+ except Exception: # noqa: BLE001 -- no tz data on this Mac: no zone can be told from an abbreviation
146
+ return frozenset()
147
+
148
+
149
+ def iana_zone(name):
150
+ # an abbreviation such as CST names no zone, so it is never compared with the zone a profile expects
151
+ if not isinstance(name, str):
152
+ return None
153
+ name = name.strip()
154
+ return name if (("/" in name or name in ("UTC", "GMT")) and name in _iana_names()) else None
155
+
156
+
157
+ def parse_locale(info):
158
+ # /wda/device/info answers for the runner: its locale has the phone's Region but the runner's own language
159
+ info = info if isinstance(info, dict) else {}
160
+ region = None
161
+ identifier = info.get("currentLocale")
162
+ if isinstance(identifier, str):
163
+ parts = re.split(r"[_-]", identifier.split("@")[0].split(".")[0].strip())
164
+ found = [part for part in parts[1:] if REGION.fullmatch(part.upper()) and len(part) == 2]
165
+ region = found[-1].upper() if found else None
166
+ return {"region": region, "language": None, "timezone": iana_zone(info.get("timeZone"))}
167
+
168
+
169
+ def _offsets(name):
170
+ """The UTC offsets of an IANA zone on the first of four months this year, or None for a name that isn't one."""
171
+ try:
172
+ from zoneinfo import ZoneInfo
173
+ zone = ZoneInfo(name)
174
+ except Exception: # noqa: BLE001 -- an unknown name or no tz data: compared by name only
175
+ return None
176
+ year = datetime.now(timezone.utc).year
177
+ return tuple(datetime(year, month, 1, 12, tzinfo=zone).utcoffset() for month in (1, 4, 7, 10))
178
+
179
+
180
+ def zones_match(expected, actual):
181
+ if expected == actual:
182
+ return True
183
+ left, right = _offsets(expected), _offsets(actual)
184
+ return left is not None and left == right
185
+
186
+
187
+ def compare(profile, locale):
188
+ wanted = expected_of(profile)
189
+ locale = locale or {}
190
+ checks = []
191
+ for key in ("region", "language", "timezone"):
192
+ if wanted[key] is None:
193
+ continue
194
+ actual = locale.get(key)
195
+ if actual is None:
196
+ state = "unknown"
197
+ elif key == "timezone":
198
+ state = "match" if zones_match(wanted[key], actual) else "mismatch"
199
+ else:
200
+ state = "match" if wanted[key].casefold() == actual.casefold() else "mismatch"
201
+ checks.append({"field": key, "expected": wanted[key], "actual": actual, "state": state})
202
+ return checks
203
+
204
+
205
+ def region_state(checks):
206
+ if any(check["state"] == "mismatch" for check in checks):
207
+ return "mismatch"
208
+ if any(check["state"] == "unknown" for check in checks):
209
+ return "unknown"
210
+ return "ok" if checks else "none"
211
+
212
+
213
+ def mismatch_message(profile_name, phone_name, checks):
214
+ """The refusal: what differs (both values), what to change, and that nothing was done."""
215
+ bad = [check for check in checks if check["state"] == "mismatch"]
216
+ parts = [f"{WORD[c['field']]} {c['actual']} where {c['expected']} is expected" for c in bad]
217
+ changes = " and ".join(dict.fromkeys(SETTING[c["field"]] for c in bad))
218
+ return (f"{phone_name} doesn't match {profile_name}: it is set to {_join(parts)}. Change {changes} on the phone, "
219
+ f"or change what the profile expects in Settings › Profiles, then run the task again. {NOT_TAKEN}")
220
+
221
+
222
+ def _join(parts):
223
+ return parts[0] if len(parts) == 1 else ", ".join(parts[:-1]) + " and " + parts[-1]
224
+
225
+
226
+ def read_locale(wda_url, *, timeout=READ_TIMEOUT):
227
+ from . import native_helpers
228
+ if not wda_url or not native_helpers.devices_allowed():
229
+ return None
230
+ from .transport import HTTP
231
+ client = None
232
+ try:
233
+ client = HTTP(wda_url)
234
+ info = client.request("GET", "/wda/device/info", timeout=timeout).get("value")
235
+ except Exception: # noqa: BLE001 -- unknown, not blocking
236
+ return None
237
+ finally:
238
+ if client is not None:
239
+ client.close()
240
+ locale = parse_locale(info)
241
+ return locale if any(locale.values()) else None
242
+
243
+
244
+ # -- the alerts of one phone -------------------------------------------------------------------------------------
245
+
246
+ def _alert(code, message, action, since=None, profile=None):
247
+ return {"code": code, "severity": SEVERITY[code], "message": message, "action": action, "since": since,
248
+ **({"profile": profile} if profile else {})}
249
+
250
+
251
+ def state_alerts(item, name):
252
+ """The alerts the device list's own state gives: unplugged, locked, owner active."""
253
+ alerts = []
254
+ state, reason = item.get("state"), item.get("reason")
255
+ if state == "disconnected" and item.get("kind") == "usb" and not item.get("disconnected"):
256
+ away = reason == REASONS["wifi_unreachable"]
257
+ alerts.append(_alert("unplugged", f"{name} isn't reachable over Wi-Fi." if away else f"{name} is unplugged.",
258
+ "Keep it unlocked and on the same Wi-Fi as this Mac, or plug it in." if away
259
+ else "Plug it into this Mac with a cable."))
260
+ elif state == "connected" and reason == REASONS["locked"]:
261
+ alerts.append(_alert("locked", f"{name} is locked.", "Unlock it to run tasks. Mobster never unlocks a phone."))
262
+ owner = item.get("owner")
263
+ if isinstance(owner, dict) and owner.get("state") == "owner_active":
264
+ alerts.append(_alert("owner_active", f"You're using {name}.", "Mobster waits until you're done with it.",
265
+ since=owner.get("since") if isinstance(owner.get("since"), (int, float)) else None))
266
+ return alerts
267
+
268
+
269
+ def _app_name(profile):
270
+ app = (profile or {}).get("app")
271
+ name = app.get("name") if isinstance(app, dict) else None
272
+ return name if isinstance(name, str) and name else "the app"
273
+
274
+
275
+ def _profile_name(profile):
276
+ app = (profile or {}).get("app")
277
+ handle = (profile or {}).get("handle") or "profile"
278
+ return f"@{handle} on {_app_name(profile)}" if isinstance(app, dict) and app.get("name") else f"@{handle}"
279
+
280
+
281
+ def signal_alert(signal, profile_name, app_name, phone_name, since, profile_id):
282
+ if signal == "challenge":
283
+ return _alert("challenge", f"{app_name} asked for a security check for {profile_name} on {phone_name}.",
284
+ "Finish the check on the phone yourself, then choose Mark as handled. Tasks for this profile "
285
+ "wait until you do.", since, profile_id)
286
+ return _alert("signed_out", f"{profile_name} is signed out on {phone_name}.",
287
+ f"Open {app_name} on the phone and sign in yourself. Mobster never signs in for you.", since,
288
+ profile_id)
289
+
290
+
291
+ def region_alert(profile_name, phone_name, checks, since, profile_id):
292
+ bad = [check for check in checks if check["state"] == "mismatch"]
293
+ parts = [f"{WORD[c['field']]} {c['actual']} instead of {c['expected']}" for c in bad]
294
+ changes = " and ".join(dict.fromkeys(SETTING[c["field"]] for c in bad))
295
+ return _alert("region_mismatch", f"{phone_name} is set to {_join(parts)}, which {profile_name} doesn't expect.",
296
+ f"Change {changes} on the phone, or what the profile expects in Settings › Profiles.", since,
297
+ profile_id)
298
+
299
+
300
+ # -- persisted signals -------------------------------------------------------------------------------------------
301
+
302
+ LOCK_TIMEOUT = 5.0
303
+ BLANK = {"signal": None, "since": None, "device": None, "confirmedAt": None}
304
+
305
+
306
+ class Signals:
307
+ """fleet-health.json: per profile id, ``{signal, since, device, confirmedAt}``."""
308
+
309
+ def __init__(self, folder, clock=time.time):
310
+ self.path = Path(folder) / STORE if folder is not None else None
311
+ self.lock_path = self.path.with_suffix(".lock") if self.path is not None else None
312
+ self.clock = clock
313
+ self.lock = threading.Lock() # the in-memory copy, used only without a folder
314
+ self.memory = {}
315
+
316
+ def _load(self):
317
+ if self.path is None:
318
+ return {key: dict(entry) for key, entry in self.memory.items()}
319
+ try:
320
+ raw = json.loads(self.path.read_text())
321
+ except (OSError, ValueError):
322
+ return {}
323
+ profiles = raw.get("profiles") if isinstance(raw, dict) else None
324
+ out = {}
325
+ for key, entry in (profiles.items() if isinstance(profiles, dict) else ()):
326
+ if isinstance(key, str) and isinstance(entry, dict):
327
+ signal = entry.get("signal") if entry.get("signal") in ("challenge", "signed_out") else None
328
+ out[key] = {"signal": signal,
329
+ "since": entry.get("since") if isinstance(entry.get("since"), int) else None,
330
+ "device": entry.get("device") if isinstance(entry.get("device"), str) else None,
331
+ "confirmedAt": (entry.get("confirmedAt") if isinstance(entry.get("confirmedAt"), int)
332
+ else None)}
333
+ return out
334
+
335
+ def _save(self, data):
336
+ if self.path is None:
337
+ self.memory = data
338
+ return
339
+ fd, temporary = tempfile.mkstemp(prefix=".fleet-health.", suffix=".json", dir=self.path.parent)
340
+ try:
341
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
342
+ os.fchmod(stream.fileno(), 0o600)
343
+ stream.write(json.dumps({"version": 1, "profiles": data}, indent=1) + "\n")
344
+ os.replace(temporary, self.path)
345
+ except BaseException:
346
+ with contextlib.suppress(OSError):
347
+ os.unlink(temporary)
348
+ raise
349
+
350
+ @contextlib.contextmanager
351
+ def _edit(self):
352
+ # the Mac app's server and `mobster mcp` both write: every edit locks, reads again and writes atomically
353
+ if self.path is None:
354
+ with self.lock:
355
+ data = self._load()
356
+ before = json.dumps(data, sort_keys=True)
357
+ yield data
358
+ if json.dumps(data, sort_keys=True) != before:
359
+ self._save(data)
360
+ return
361
+ self.path.parent.mkdir(parents=True, exist_ok=True)
362
+ fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
363
+ try:
364
+ end = time.monotonic() + LOCK_TIMEOUT
365
+ while True:
366
+ try:
367
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
368
+ break
369
+ except OSError as exc:
370
+ if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or time.monotonic() >= end:
371
+ raise OSError("Another Mobster process is holding the phone health file") from None
372
+ time.sleep(.02)
373
+ data = self._load()
374
+ before = json.dumps(data, sort_keys=True)
375
+ yield data
376
+ if json.dumps(data, sort_keys=True) != before:
377
+ self._save(data)
378
+ finally:
379
+ os.close(fd)
380
+
381
+ def all(self):
382
+ """Every profile's entry, read from the file now."""
383
+ if self.path is None:
384
+ with self.lock:
385
+ return self._load()
386
+ return self._load()
387
+
388
+ def get(self, profile_id):
389
+ return dict(self.all().get(profile_id) or {})
390
+
391
+ def mark(self, profile_id, signal, device):
392
+ """Record ``signal``; a challenge is never replaced by the milder signed_out."""
393
+ now = round(self.clock() * 1000)
394
+ with self._edit() as data:
395
+ entry = data.setdefault(profile_id, dict(BLANK))
396
+ if entry["signal"] == "challenge" and signal == "signed_out":
397
+ return
398
+ if entry["signal"] != signal:
399
+ entry.update(signal=signal, since=now)
400
+ entry["device"] = device
401
+
402
+ def confirm(self, profile_id, device, clear=True):
403
+ now = round(self.clock() * 1000)
404
+ with self._edit() as data:
405
+ entry = data.setdefault(profile_id, dict(BLANK))
406
+ entry["confirmedAt"], entry["device"] = now, device
407
+ if clear and entry["signal"] != "challenge":
408
+ entry["signal"], entry["since"] = None, None
409
+
410
+ def clear(self, profile_id, signal=None):
411
+ with self._edit() as data:
412
+ entry = data.get(profile_id)
413
+ if not entry or not entry["signal"] or (signal and entry["signal"] != signal):
414
+ return False
415
+ entry["signal"], entry["since"] = None, None
416
+ return True
417
+
418
+ def forget(self, profile_id):
419
+ with self._edit() as data:
420
+ data.pop(profile_id, None)
421
+
422
+
423
+ # -- the service object -------------------------------------------------------------------------------------------
424
+
425
+ def _goal(text):
426
+ text = " ".join(str(text or "").split())
427
+ return text if len(text) <= GOAL_CHARS else text[:GOAL_CHARS - 1].rstrip() + "…"
428
+
429
+
430
+ class FleetHealth:
431
+ """Computes ``health`` for the fleet's phones and fires the edge-triggered alerts. One per Fleet."""
432
+
433
+ def __init__(self, fleet, clock=time.time, monotonic=time.monotonic, read=read_locale, alerts=None, publish=None,
434
+ background=True, debounce=DEBOUNCE_S):
435
+ self.fleet = fleet
436
+ self.clock, self.monotonic, self.read = clock, monotonic, read
437
+ self._alerts, self._publish, self.background, self.debounce = alerts, publish, background, debounce
438
+ self.signals = Signals(getattr(fleet, "data_dir", None), clock)
439
+ self.lock = threading.Lock()
440
+ self.locales = {} # phone id -> (monotonic time read, locale or None)
441
+ self.reading = set() # phone ids being read now
442
+ self.active = {} # (phone id, code, profile id) -> {first, sent, gone}
443
+ self.baselined = False
444
+ self.sent = [] # (phone id, code, profile id) of every alert delivered, for tests and the log
445
+
446
+ # -- inputs --------------------------------------------------------------------------------------------------
447
+
448
+ @property
449
+ def runtime(self):
450
+ return self.fleet.runtime
451
+
452
+ def profiles(self):
453
+ try:
454
+ from .accounts.store import store_for
455
+ return store_for(self.runtime).all()
456
+ except Exception: # noqa: BLE001 -- Profiles off: no profile checks
457
+ return []
458
+
459
+ def profiles_on(self, phone, item, profiles=None):
460
+ profiles = self.profiles() if profiles is None else profiles
461
+ return [p for p in profiles if p.get("device") == phone.id or (not p.get("device") and item.get("default"))]
462
+
463
+ def locale(self, phone, ttl=LOCALE_TTL, read_now=False):
464
+ """The phone's cached locale, or None. ``read_now`` reads again when the cache is older than ``ttl``."""
465
+ with self.lock:
466
+ cached = self.locales.get(phone.id)
467
+ fresh = cached is not None and self.monotonic() - cached[0] < ttl
468
+ if fresh or not read_now:
469
+ return cached[1] if cached else None
470
+ locale = self.read(phone.wda_url)
471
+ with self.lock:
472
+ self.locales[phone.id] = (self.monotonic(), locale)
473
+ return locale
474
+
475
+ def readable(self, phone):
476
+ try:
477
+ disconnected = getattr(self.fleet, "is_disconnected", None)
478
+ if callable(disconnected) and disconnected(phone):
479
+ return False
480
+ in_use = getattr(self.fleet, "in_use", None)
481
+ if callable(in_use):
482
+ return not in_use(phone)
483
+ busy = getattr(self.fleet, "busy", None)
484
+ return not (callable(busy) and busy(phone) is not None)
485
+ except Exception: # noqa: BLE001 -- fail closed: nothing is read from a phone whose state can't be told
486
+ return False
487
+
488
+ def _refresh(self, phone, item, wanted):
489
+ """Read an idle ready phone's locale in the background when a profile on it expects one and the cache is old."""
490
+ if (not wanted or item.get("state") != "ready" or item.get("activeRunId") or item.get("disconnected")
491
+ or not phone.wda_url):
492
+ return
493
+ with self.lock:
494
+ cached = self.locales.get(phone.id)
495
+ if phone.id in self.reading or (cached is not None and self.monotonic() - cached[0] < LOCALE_TTL):
496
+ return
497
+ self.reading.add(phone.id)
498
+ if not self.readable(phone):
499
+ with self.lock:
500
+ self.reading.discard(phone.id)
501
+ return
502
+
503
+ def work():
504
+ try:
505
+ self.locale(phone, ttl=0, read_now=True)
506
+ finally:
507
+ with self.lock:
508
+ self.reading.discard(phone.id)
509
+ if self.background:
510
+ _Thread(target=work, name="mobster-locale", daemon=True).start()
511
+ else:
512
+ work()
513
+
514
+ def last_good(self, phone):
515
+ runs = getattr(self.runtime, "runs", None) or {}
516
+ best = None
517
+ for run in list(runs.values()):
518
+ if getattr(run, "status", None) != "completed" or getattr(run, "finished_at", None) is None:
519
+ continue
520
+ if getattr(run, "mode", "live") != "live":
521
+ continue
522
+ device = getattr(run, "device_id", None)
523
+ if device != phone.id and not (device is None and phone.primary):
524
+ continue
525
+ if best is None or run.finished_at > best.finished_at:
526
+ best = run
527
+ if best is None:
528
+ return None
529
+ return {"runId": best.id, "at": round(best.finished_at * 1000), "summary": _goal(best.goal)}
530
+
531
+ def current_task(self, item):
532
+ run_id = item.get("activeRunId")
533
+ run = (getattr(self.runtime, "runs", None) or {}).get(run_id) if run_id else None
534
+ if run is not None:
535
+ return {"runId": run_id, "goal": _goal(run.goal), "since": round(run.created_at * 1000)}
536
+ holder = item.get("holder")
537
+ if isinstance(holder, dict) and isinstance(holder.get("label"), str):
538
+ since = holder.get("since")
539
+ return {"runId": holder.get("runId") if isinstance(holder.get("runId"), str) else None,
540
+ "goal": _goal(holder["label"]), "kind": holder.get("kind") if isinstance(holder.get("kind"), str)
541
+ else None, "since": since if isinstance(since, (int, float)) else None}
542
+ return None
543
+
544
+ # -- one phone's health --------------------------------------------------------------------------------------
545
+
546
+ def health_of(self, phone, item, profiles=None, signals=None):
547
+ """``health`` for one phone's record. Reads nothing from the phone: its locale comes from the cache."""
548
+ signals = self.signals.all() if signals is None else signals
549
+ name = item.get("name") or "This iPhone"
550
+ mine = self.profiles_on(phone, item, profiles)
551
+ wanted = any(any(expected_of(p).values()) for p in mine)
552
+ self._refresh(phone, item, wanted)
553
+ locale = self.locale(phone)
554
+ alerts = state_alerts(item, name)
555
+ report, rows = [], []
556
+ for profile in mine:
557
+ label = _profile_name(profile)
558
+ stored = signals.get(profile["id"]) or {}
559
+ signal = stored.get("signal")
560
+ checks = compare(profile, locale)
561
+ report.append({"profile": profile["id"], "name": label, "state": region_state(checks), "checks": checks})
562
+ if signal:
563
+ alerts.append(signal_alert(signal, label, _app_name(profile), name, stored.get("since"),
564
+ profile["id"]))
565
+ if any(check["state"] == "mismatch" for check in checks):
566
+ alerts.append(region_alert(label, name, checks, None, profile["id"]))
567
+ expected = expected_of(profile)
568
+ rows.append({"id": profile["id"], "name": label, "handle": profile.get("handle"), "app": _app_name(profile),
569
+ "signedIn": False if signal else (True if stored.get("confirmedAt") else None), **expected})
570
+ overall = "none"
571
+ for entry in report:
572
+ if entry["state"] == "mismatch":
573
+ overall = "mismatch"
574
+ elif entry["state"] == "unknown" and overall != "mismatch":
575
+ overall = "unknown"
576
+ elif entry["state"] == "ok" and overall == "none":
577
+ overall = "ok"
578
+ region = {"state": overall, "profiles": report, "phone": {**{k: (locale or {}).get(k) for k in
579
+ ("region", "language", "timezone")}, "readAt": self._read_at(phone)}}
580
+ return {"alerts": alerts, "lastGoodRun": self.last_good(phone), "profiles": rows, "region": region,
581
+ "currentTask": self.current_task(item)}
582
+
583
+ def _read_at(self, phone):
584
+ with self.lock:
585
+ cached = self.locales.get(phone.id)
586
+ if cached is None or cached[1] is None:
587
+ return None
588
+ return round((self.clock() - (self.monotonic() - cached[0])) * 1000)
589
+
590
+ def apply(self, rows, complete=True):
591
+ profiles, signals = self.profiles(), self.signals.all()
592
+ current = {}
593
+ for phone, item in rows:
594
+ if not phone.id:
595
+ continue
596
+ health = self.health_of(phone, item, profiles, signals)
597
+ for alert in health["alerts"]:
598
+ current[(phone.id, alert["code"], alert.get("profile"))] = (item, alert)
599
+ item["health"] = health
600
+ self.observe(current, None if complete else {phone.id for phone, _ in rows if phone.id})
601
+ # The alerts' ``since``: when each condition was first seen, unless the source knew better.
602
+ for phone, item in rows:
603
+ for alert in (item.get("health") or {}).get("alerts", ()):
604
+ if alert["since"] is None:
605
+ seen = self.active.get((phone.id, alert["code"], alert.get("profile")))
606
+ alert["since"] = round(seen["first_wall"] * 1000) if seen else round(self.clock() * 1000)
607
+
608
+ # -- edge-triggered alerts -----------------------------------------------------------------------------------
609
+
610
+ def observe(self, current, phones=None):
611
+ now, wall = self.monotonic(), self.clock()
612
+ delivered = []
613
+ with self.lock:
614
+ first_pass = not self.baselined
615
+ if first_pass:
616
+ if phones is not None: # the baseline is a read of every phone: one phone's can't tell what held
617
+ return []
618
+ self.baselined = True
619
+ for key, (item, alert) in current.items():
620
+ entry = self.active.get(key)
621
+ if entry is None:
622
+ entry = self.active[key] = {"first": now, "first_wall": wall, "sent": first_pass, "gone": None}
623
+ elif entry["gone"] is not None and not entry["sent"]:
624
+ entry["first"], entry["first_wall"] = now, wall
625
+ entry["gone"] = None
626
+ if not entry["sent"] and now - entry["first"] >= self.debounce:
627
+ entry["sent"] = True
628
+ if alert["severity"] == ATTENTION:
629
+ delivered.append((key, item, alert))
630
+ for key in [k for k in self.active if k not in current and (phones is None or k[0] in phones)]:
631
+ entry = self.active[key]
632
+ if entry["gone"] is None:
633
+ entry["gone"] = now
634
+ elif now - entry["gone"] >= self.debounce:
635
+ del self.active[key]
636
+ for key, item, alert in delivered:
637
+ self._deliver(key, item, alert)
638
+ return [key for key, _, _ in delivered]
639
+
640
+ def _deliver(self, key, item, alert):
641
+ self.sent.append(key)
642
+ name = item.get("name") or "iPhone"
643
+ body = f"{alert['message']} {alert['action']}"
644
+ event = {"event": "phone_alert", "device": key[0], "deviceName": name, "code": alert["code"],
645
+ "profile": key[2], "title": f"Mobster: {name}", "body": body}
646
+ with contextlib.suppress(Exception):
647
+ if self._publish is not None:
648
+ self._publish("devices", event)
649
+ else:
650
+ self.runtime.publish("devices", event)
651
+ with contextlib.suppress(Exception):
652
+ alerts = self._alerts
653
+ if alerts is None:
654
+ from .alerts import Alerts
655
+ alerts = self._alerts = Alerts()
656
+ alerts.phone_alert(name, alert["code"], alert["message"], alert["action"])
657
+
658
+ # -- the runs ------------------------------------------------------------------------------------------------
659
+
660
+ def note_screen(self, account, device, signal):
661
+ if signal in ("challenge", "signed_out") and isinstance(account, dict) and account.get("id"):
662
+ with contextlib.suppress(Exception):
663
+ self.signals.mark(account["id"], signal, device)
664
+
665
+ def note_run(self, run):
666
+ """A finished run: keep what it learned about its profile. Never raises."""
667
+ try:
668
+ self._note_run(run)
669
+ except Exception: # noqa: BLE001 -- health is never part of a run's outcome
670
+ pass
671
+
672
+ def _note_run(self, run):
673
+ account = (getattr(run, "extras", None) or {}).get("account")
674
+ if not isinstance(account, dict) or not account.get("id"):
675
+ return
676
+ device = getattr(run, "device_id", None)
677
+ summary = run.summary if isinstance(run.summary, dict) else {}
678
+ latest = None
679
+ for event in list(run.events):
680
+ if event.get("event") == "account_signal" and event.get("account") == account["id"]:
681
+ if event.get("signal") in ("challenge", "signed_out"):
682
+ latest = "challenge" if event["signal"] == "challenge" or latest == "challenge" else "signed_out"
683
+ elif (event.get("event") == "account_checked" and event.get("account") == account["id"]
684
+ and event.get("state") == "confirmed" and latest != "challenge"):
685
+ latest = "confirmed"
686
+ if (summary.get("stop_code") or summary.get("code")) in ("account_unconfirmed", "account_mismatch") \
687
+ and latest not in ("challenge", "signed_out"):
688
+ latest = "signed_out"
689
+ if latest in ("challenge", "signed_out"):
690
+ self.signals.mark(account["id"], latest, device)
691
+ elif latest == "confirmed":
692
+ self.signals.confirm(account["id"], device)
693
+
694
+ def acknowledge(self, profile_id, code=None):
695
+ """The person says they handled a challenge or a sign-in. True when an alert was cleared."""
696
+ if code is not None and code not in ("challenge", "signed_out"):
697
+ raise ValueError("code is challenge or signed_out")
698
+ return self.signals.clear(profile_id, code)
699
+
700
+ # -- the pre-run check ---------------------------------------------------------------------------------------
701
+
702
+ def preflight(self, account, phone):
703
+ stored = next((p for p in self.profiles() if p["id"] == account.get("id")), None) or account
704
+ name = phone.name or "This iPhone"
705
+ label = _profile_name(stored)
706
+ signal = self.signals.get(stored["id"]).get("signal") if stored.get("id") else None
707
+ if signal == "challenge":
708
+ raise APIError(f"{_app_name(stored)} asked for a security check for {label}. Finish it on the phone "
709
+ f"yourself, then choose Mark as handled on the Phones page. {NOT_TAKEN}", 409,
710
+ "profile_challenge", device=phone.id, profile=stored["id"])
711
+ wanted = expected_of(stored)
712
+ if not any(wanted.values()):
713
+ return
714
+ if not self.readable(phone):
715
+ return # disconnected, or a task runs there: nothing is read, and the run waits or is refused on its own
716
+ checks = compare(stored, self.locale(phone, ttl=PRE_RUN_TTL, read_now=True))
717
+ if any(check["state"] == "mismatch" for check in checks):
718
+ raise APIError(mismatch_message(label, name, checks), 409, "profile_region_mismatch", device=phone.id,
719
+ profile=stored["id"], checks=checks)
720
+
721
+ # -- the API -------------------------------------------------------------------------------------------------
722
+
723
+ def report(self):
724
+ """GET /api/fleet/health: ``{devices: [{id, name, state, health}], checkedAt}``, cheap (Fleet.entries)."""
725
+ rows = self.fleet.entries()
726
+ return {"devices": [{"id": item["id"], "name": item.get("name"), "state": item.get("state"),
727
+ "health": item.get("health")} for _, item in rows],
728
+ "checkedAt": round(self.clock() * 1000)}
729
+