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/fleet.py ADDED
@@ -0,0 +1,841 @@
1
+ """The server's devices (SPEC §3): one ``Phone`` per device, and the ``Fleet`` that lists and resolves them."""
2
+
3
+ import contextlib
4
+ import json
5
+ import shutil
6
+ import threading
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from . import devices as registry
11
+ from . import ownership
12
+ from .devices import REASONS
13
+ from .fleet_phone import Phone, PrimaryPhone
14
+
15
+ # The thread class as imported: tests that park the runtime's workers (patching threading.Thread) don't count the
16
+ # background refresh of the simulator list as one.
17
+ _Thread = threading.Thread
18
+
19
+
20
+ class Fleet:
21
+ """Every device this server can run tasks on, and which one a request means."""
22
+
23
+ USB_TTL = 2.0
24
+ SIM_TTL = 5.0
25
+
26
+ def __init__(self, runtime):
27
+ self.runtime = runtime
28
+ self.config = runtime.config
29
+ self.lock = threading.RLock()
30
+ self.primary = PrimaryPhone(self)
31
+ self.phones = {} # id -> Phone: USB phones set up here, simulators, WDA addresses
32
+ self.cache = {}
33
+ self.sims_lock = threading.Lock()
34
+ self.closed = False
35
+ manager = getattr(runtime, "manager", None)
36
+ self.data_dir = None
37
+ if manager is not None:
38
+ from .paths import user_data_dir
39
+ self.data_dir = Path(getattr(self.config, "data_dir", None) or user_data_dir())
40
+ self.store = registry.DeviceStore(self.data_dir) if self.data_dir is not None else None
41
+ from .fleet_health import FleetHealth
42
+ self.health = FleetHealth(self)
43
+ # A phone's own settings (a nickname, the default, Disconnect): devices.json in the data folder, else beside the
44
+ # journal (a server without USB phones), else in memory only.
45
+ state_db = getattr(self.config, "state_db", None)
46
+ self.prefs_store = self.store if self.store is not None else registry.DeviceStore(
47
+ Path(state_db).parent if isinstance(state_db, str) and state_db else None)
48
+ self.prefs_cache = None
49
+ stored = self.store.read() if self.store is not None else {"phones": [], "lastUsed": None}
50
+ self.last_used = stored["lastUsed"]
51
+ preferred = getattr(self.config, "device", None) # serve --device
52
+ self.preferred = preferred if isinstance(preferred, str) and preferred else None
53
+ if manager is not None:
54
+ manager.claimed = lambda: {phone.udid for phone in list(self.phones.values())
55
+ if phone.kind == "usb" and phone.manager is not None}
56
+ for entry in stored["phones"]:
57
+ if entry["udid"] == self._chosen():
58
+ continue
59
+ try:
60
+ self._usb_phone(entry)
61
+ except Exception as error: # one phone's folder never stops the others, or the server
62
+ import sys
63
+ print(f"mobster: an iPhone set up earlier couldn't be loaded ({type(error).__name__})",
64
+ file=sys.stderr, flush=True)
65
+ if getattr(runtime, "pool", None) is None:
66
+ for spec in registry.static_specs(self.config):
67
+ if spec.wda_url.rstrip("/") != (self.primary.wda_url or ""):
68
+ self.phones.setdefault(spec.id, Phone(self, id=spec.id, kind="wda", wda_url=spec.wda_url,
69
+ mjpeg_url=spec.mjpeg_url or None,
70
+ name=spec.label or None))
71
+
72
+ # -- USB phones set up here -------------------------------------------------------------------------
73
+
74
+ def _usb_phone(self, entry):
75
+ from .device_info import DeviceInfo
76
+ from .device_manager import DeviceManager
77
+ from .installed_apps import InstalledApps, InventoryCache
78
+ from .setup_service import SetupService
79
+ udid = entry["udid"]
80
+ url, mjpeg = registry.wda_url(entry["wdaPort"]), registry.wda_url(entry["mjpegPort"])
81
+ folder = registry.phone_folder(self.data_dir, udid)
82
+ folder.mkdir(parents=True, exist_ok=True, mode=0o700)
83
+ manager = DeviceManager(self.data_dir, url, udid=udid, root=folder, video_port=entry["mjpegPort"])
84
+ primary = self.runtime.manager
85
+ manager.cache = primary.cache # one Xcode, team and USB reading for every phone
86
+ phone = Phone(self, id=udid, kind="usb", wda_url=url, mjpeg_url=mjpeg, udid=udid, name=entry.get("name"),
87
+ manager=manager)
88
+ env_file = self.runtime.setup.env_file if self.runtime.setup is not None else None
89
+ phone.setup = SetupService(self.runtime, manager, env_file, video=lambda: phone.video,
90
+ busy=lambda: self.busy(phone) is not None,
91
+ stop=lambda message: self.runtime.stop_phone_run(phone, message),
92
+ idle=lambda: self.runtime.release_idle(phone))
93
+ phone.device_info = DeviceInfo(manager=manager)
94
+ if not getattr(self.config, "socket", None):
95
+ phone.installed_apps = InstalledApps(manager.device, cache=InventoryCache(self.data_dir / "installed-apps"),
96
+ over_wifi=manager.wifi_udid)
97
+ self.phones[udid] = phone
98
+ threading.Thread(target=manager.watch, args=(manager.watching,), name=f"mobster-device-watch-{udid[-6:]}",
99
+ daemon=True).start()
100
+ return phone
101
+
102
+ def adopt(self, udid):
103
+ manager = self.runtime.manager
104
+ if manager is None:
105
+ raise LookupError("This Mobster doesn't manage USB iPhones. Start it with --manage-device.")
106
+ with self.lock:
107
+ if udid in self.phones and self.phones[udid].manager is not None:
108
+ return self.phones[udid]
109
+ chosen = self._chosen()
110
+ if not chosen or chosen == udid:
111
+ manager.choose(udid)
112
+ return self.primary
113
+ listing = {item["udid"]: item for item in self._usb_listing()}
114
+ if udid not in listing:
115
+ raise LookupError("That iPhone is not connected")
116
+ entry = self.store.add(udid, listing[udid].get("name"))
117
+ return self._usb_phone(entry)
118
+
119
+ def forget(self, query):
120
+ phone = self.find(query)
121
+ if phone.kind != "usb" or phone.manager is None:
122
+ if phone.kind == "simulator":
123
+ raise LookupError("Mobster's simulators are managed with `mobster sim`. Remove it there.")
124
+ if phone.kind == "wda":
125
+ raise LookupError("This device comes from MOBSTER_WDA_DEVICES. Remove it there.")
126
+ raise LookupError("This iPhone isn't set up in Mobster, so there's nothing to forget.")
127
+ # Checked and dropped under the runtime's lock, which admits tasks: none can start on it in between.
128
+ with self.runtime.lock:
129
+ if self.busy(phone) is not None:
130
+ raise LookupError("A task is running on it. Stop it first.")
131
+ if not phone.primary:
132
+ with self.lock:
133
+ self.phones.pop(phone.id, None)
134
+ udid, forgotten = phone.udid, phone.id
135
+ if phone.primary:
136
+ self._forget_primary(phone)
137
+ else:
138
+ phone.close()
139
+ self.store.remove(udid)
140
+ with contextlib.suppress(OSError):
141
+ shutil.rmtree(registry.phone_folder(self.data_dir, udid))
142
+ self._drop_prefs(forgotten, udid)
143
+ self.release_profiles(forgotten)
144
+ with contextlib.suppress(OSError):
145
+ (self.data_dir / "installed-apps" / f"{udid}.json").unlink()
146
+ self.cache.pop("usb", None) # the USB listing only: every phone keeps its lastUsedAt
147
+ return forgotten
148
+
149
+ def _forget_primary(self, phone):
150
+ manager = phone.manager
151
+ manager.stop_runner(remember=True)
152
+ settings = {key: value for key, value in manager.settings().items()
153
+ if key not in ("udid", "device_name", "built_for", "built_patches", "autostart", "expires_at")}
154
+ with contextlib.suppress(OSError):
155
+ registry_dir = manager.settings_path.parent
156
+ registry_dir.mkdir(parents=True, exist_ok=True)
157
+ tmp = manager.settings_path.with_suffix(".tmp")
158
+ tmp.write_text(json.dumps(settings, indent=1))
159
+ tmp.chmod(0o600)
160
+ tmp.replace(manager.settings_path)
161
+ with contextlib.suppress(OSError):
162
+ shutil.rmtree(manager.derived)
163
+ phone.id = phone.udid = None
164
+ phone.name = None
165
+
166
+ def release_profiles(self, device_id):
167
+ # to the default phone, so a task as one of them starts instead of being refused for a phone that is gone
168
+ from .accounts.store import store_for
169
+ with contextlib.suppress(OSError):
170
+ store_for(self.runtime).forget_device(device_id)
171
+
172
+ def _drop_prefs(self, *ids):
173
+ for device_id in {item for item in ids if item}:
174
+ with contextlib.suppress(OSError, ValueError):
175
+ self.prefs_store.remove(device_id)
176
+ if self.last_used == device_id:
177
+ self.last_used = None
178
+ self.prefs_cache = None
179
+
180
+ # -- a phone's own settings (Settings › Phones) ------------------------------------------------------------
181
+
182
+ def stored(self):
183
+ """{prefs, defaultDevice, disconnected, chosen}: read from devices.json at most once per change of the file."""
184
+ store = self.prefs_store
185
+ stamp = None
186
+ if store.path is not None: # in memory there is no file to stamp, and reading is free
187
+ try:
188
+ stamp = store.path.stat().st_mtime_ns
189
+ except OSError:
190
+ pass
191
+ cached = self.prefs_cache
192
+ if cached is not None and cached[0] == stamp:
193
+ return cached[1]
194
+ data = store.read()
195
+ value = {"prefs": data["prefs"], "defaultDevice": data["defaultDevice"], "disconnected": data["disconnected"],
196
+ "chosen": data["chosen"]}
197
+ if store.path is not None:
198
+ self.prefs_cache = (stamp, value)
199
+ return value
200
+
201
+ def prefs(self, phone):
202
+ return dict(self.stored()["prefs"].get(phone.id) or {}) if phone is not None and phone.id else {}
203
+
204
+ def is_disconnected(self, phone):
205
+ return bool(phone is not None and phone.id and phone.id in self.stored()["disconnected"])
206
+
207
+ def set_prefs(self, phone, **changes):
208
+ if not phone.id:
209
+ raise LookupError("This phone has no identity yet. Plug it in and unlock it.")
210
+ value = self.prefs_store.set_prefs(phone.id, **changes)
211
+ self.prefs_cache = None
212
+ if "nickname" in changes:
213
+ phone.name = value.get("nickname") or phone.name
214
+ return value
215
+
216
+ def set_default(self, phone):
217
+ """Make ``phone`` the one a new task runs on unless another is picked (None: no default of your own)."""
218
+ device_id = phone.id if phone is not None else None
219
+ if phone is not None and not device_id:
220
+ raise LookupError("This phone has no identity yet. Plug it in and unlock it.")
221
+ self.prefs_store.set_default(device_id)
222
+ self.prefs_cache = None
223
+ return device_id
224
+
225
+ # -- the composer's phone (the Mac app) -----------------------------------------------------------------------
226
+
227
+ def choose(self, device_ids):
228
+ clean = registry.clean_chosen(device_ids) if isinstance(device_ids, (list, tuple)) else None
229
+ if clean is None or len(clean) != len(device_ids):
230
+ raise ValueError(f"devices is a list of at most {registry.MAX_CHOSEN} different device ids")
231
+ self.prefs_store.set_chosen(clean)
232
+ self.prefs_cache = None
233
+ return clean
234
+
235
+ def chosen(self, records=None):
236
+ """Keep a connected choice, except one needing setup while another is set up; never save the fallback."""
237
+ records = self.public() if records is None else records
238
+ listed = {item["id"]: item for item in records if item.get("id")}
239
+ kept = [device_id for device_id in self.stored()["chosen"]
240
+ if device_id in listed and listed[device_id]["state"] != "disconnected"]
241
+ if kept and not (all(listed[device_id]["state"] == "needs_setup" for device_id in kept)
242
+ and any(item["state"] not in ("needs_setup", "disconnected") for item in records)):
243
+ return kept
244
+ if not records: # the fallback below is never saved, so the chosen phone comes back when plugged in
245
+ return []
246
+ rank = {"ready": 0, "busy": 1, "connected": 2, "needs_setup": 3, "disconnected": 4}
247
+ best = min(enumerate(records), key=lambda pair: (rank.get(pair[1]["state"], 4), not pair[1].get("default"),
248
+ not pair[1].get("primary"), pair[0]))[1]
249
+ return [best["id"]] if best.get("id") else []
250
+
251
+ def disconnect(self, phone):
252
+ if not phone.id:
253
+ raise LookupError("This phone has no identity yet.")
254
+ if phone.kind == "usb" and phone.manager is None:
255
+ raise LookupError("This iPhone isn't set up yet, so it isn't in use.")
256
+ with self.runtime.lock:
257
+ if self.busy(phone) is not None:
258
+ raise LookupError("A task is running on it. Stop it first.")
259
+ self._set_disconnected(phone.id, True)
260
+ if phone.manager is not None:
261
+ with contextlib.suppress(Exception):
262
+ phone.manager.stop_runner(remember=True)
263
+ if not phone.primary:
264
+ for name in ("_video", "_control"):
265
+ item = getattr(phone, name, None)
266
+ if item is not None:
267
+ with contextlib.suppress(Exception):
268
+ item.close()
269
+ setattr(phone, name, None)
270
+ phone.health_at = 0.0
271
+ return phone.id
272
+
273
+ def connect(self, phone):
274
+ if not phone.id:
275
+ raise LookupError("This phone has no identity yet.")
276
+ self._set_disconnected(phone.id, False)
277
+ note = None
278
+ manager = phone.manager
279
+ if manager is not None:
280
+ with contextlib.suppress(OSError):
281
+ manager.save_settings(autostart=True) # the watcher starts it when the phone is back
282
+ try:
283
+ manager.start_runner()
284
+ except LookupError as error:
285
+ note = str(error)
286
+ except Exception: # noqa: BLE001 -- the watcher tries again
287
+ note = "Mobster will start its helper on it in a moment."
288
+ phone.health_at = 0.0
289
+ return note
290
+
291
+ def _set_disconnected(self, device_id, value):
292
+ self.prefs_store.set_disconnected(device_id, value)
293
+ self.prefs_cache = None
294
+
295
+ # -- discovery --------------------------------------------------------------------------------------
296
+
297
+ def _usb_listing(self):
298
+ manager = getattr(self.runtime, "manager", None)
299
+ if manager is None:
300
+ return []
301
+ cached = self.cache.get("usb")
302
+ if cached and time.monotonic() - cached[0] < self.USB_TTL:
303
+ return cached[1]
304
+ try:
305
+ listing = [item for item in manager.devices() if isinstance(item, dict)
306
+ and isinstance(item.get("udid"), str)]
307
+ except Exception:
308
+ listing = []
309
+ self.cache["usb"] = (time.monotonic(), listing)
310
+ return listing
311
+
312
+ def _sims_wanted(self):
313
+ url = self.primary.wda_url or ""
314
+ # No WDA target (a scripted phone, the terminal UI's demo): no simulators beside it.
315
+ return getattr(self.runtime, "manager", None) is not None or url.startswith(("http://", "https://"))
316
+
317
+ def _read_sims(self):
318
+ try:
319
+ rows = registry.simulator_rows()
320
+ except Exception:
321
+ rows = []
322
+ self.cache["sims"] = (time.monotonic(), rows)
323
+ return rows
324
+
325
+ def _sim_rows(self):
326
+ # simctl takes up to a second: a stale list is served while a fresh one is read in the background
327
+ if not self._sims_wanted():
328
+ return []
329
+ cached = self.cache.get("sims")
330
+ if cached is None:
331
+ with self.sims_lock:
332
+ cached = self.cache.get("sims")
333
+ return cached[1] if cached is not None else self._read_sims()
334
+ if time.monotonic() - cached[0] >= self.SIM_TTL and self.sims_lock.acquire(blocking=False):
335
+ def refresh():
336
+ try:
337
+ self._read_sims()
338
+ finally:
339
+ self.sims_lock.release()
340
+ _Thread(target=refresh, name="mobster-fleet-sims", daemon=True).start()
341
+ return cached[1]
342
+
343
+ def _sim_phone(self, row):
344
+ with self.lock:
345
+ phone = self.phones.get(row["udid"])
346
+ if phone is None or phone.kind != "simulator" or phone.wda_url != row["wda_url"].rstrip("/"):
347
+ phone = Phone(self, id=row["udid"], kind="simulator", wda_url=row["wda_url"],
348
+ mjpeg_url=row.get("mjpeg_url"), udid=row["udid"], name=row.get("name"),
349
+ model=row.get("device_type"),
350
+ ios=str(row.get("runtime") or "").removeprefix("iOS ").strip() or None)
351
+ self.phones[row["udid"]] = phone
352
+ return phone
353
+
354
+ def _chosen(self):
355
+ """The UDID chosen in Setup for the primary phone, or None."""
356
+ manager = getattr(self.runtime, "manager", None)
357
+ try:
358
+ udid = manager.settings().get("udid") if manager is not None else None
359
+ except Exception:
360
+ return None
361
+ return udid if isinstance(udid, str) and udid else None
362
+
363
+ def _primary_identity(self):
364
+ manager = getattr(self.runtime, "manager", None)
365
+ url = self.primary.wda_url
366
+ if manager is not None:
367
+ listing = self._usb_listing()
368
+ udid = self._chosen()
369
+ if udid is None:
370
+ try:
371
+ found = manager.device(listing)
372
+ except Exception:
373
+ found = None
374
+ udid = found.get("udid") if isinstance(found, dict) else None
375
+ if not isinstance(udid, str) or not udid:
376
+ return None
377
+ device = next((item for item in listing if item["udid"] == udid), None)
378
+ saved = manager.settings().get("device_name")
379
+ return {"id": udid, "kind": "usb", "udid": udid, "device": device,
380
+ "name": (device or {}).get("name") or (saved if isinstance(saved, str) else None)}
381
+ if not url:
382
+ return None
383
+ for row in self._sim_rows():
384
+ if row["wda_url"].rstrip("/") == url:
385
+ return {"id": row["udid"], "kind": "simulator", "udid": row["udid"], "row": row,
386
+ "name": row.get("name")}
387
+ return {"id": registry.device_id_for_url(url), "kind": "wda", "udid": None, "name": None}
388
+
389
+ def busy(self, phone):
390
+ run_on = getattr(self.runtime, "run_on", None)
391
+ if callable(run_on):
392
+ return run_on(phone)
393
+ return next((run_id for run_id, key in list(self.runtime.active_runs.items()) if key == phone.key), None)
394
+
395
+ def in_use(self, phone):
396
+ if self.busy(phone) is not None:
397
+ return True
398
+ url = phone.wda_url
399
+ return bool(url) and not self._lease_free(url, phone.udid)
400
+
401
+ def _lease_free(self, url, udid=None):
402
+ # Under the runtime's lock: Runtime.create takes the same lease under it, and two flocks in one process
403
+ # on one file would make a task refused while this probe holds it for a moment. Never called with that
404
+ # lock held (create resolves its device first).
405
+ with self.runtime.lock:
406
+ return registry.lease_free(url, udid)
407
+
408
+ def inventory(self):
409
+ found = []
410
+ identity = self._primary_identity()
411
+ primary_url = self.primary.wda_url
412
+ if identity is not None:
413
+ phone = self.primary
414
+ phone.id, phone.kind, phone.udid = identity["id"], identity["kind"], identity["udid"]
415
+ phone.name = identity.get("name") or phone.name
416
+ found.append((phone, identity))
417
+ with self.lock:
418
+ usb_phones = [phone for phone in self.phones.values() if phone.kind == "usb" and phone.manager is not None]
419
+ claimed = {phone.udid for phone in usb_phones}
420
+ if identity is not None and identity["kind"] == "usb":
421
+ claimed.add(identity["udid"])
422
+ listing = {item["udid"]: item for item in self._usb_listing()}
423
+ for phone in usb_phones:
424
+ device = listing.get(phone.udid)
425
+ if device is not None:
426
+ phone.name = device.get("name") or phone.name
427
+ phone.model, phone.ios = device.get("model"), device.get("ios")
428
+ found.append((phone, {"device": device}))
429
+ for udid, device in listing.items():
430
+ if udid in claimed:
431
+ continue
432
+ with self.lock:
433
+ loose = self.phones.get(udid)
434
+ if loose is None or loose.manager is not None:
435
+ loose = self.phones[udid] = Phone(self, id=udid, kind="usb", udid=udid)
436
+ loose.name, loose.model, loose.ios = device.get("name"), device.get("model"), device.get("ios")
437
+ found.append((loose, {"device": device, "loose": True}))
438
+ seen = {primary_url} if primary_url else set()
439
+ seen |= {phone.wda_url for phone in usb_phones}
440
+ for row in self._sim_rows():
441
+ if row["wda_url"].rstrip("/") in seen:
442
+ continue
443
+ phone = self._sim_phone(row)
444
+ found.append((phone, {"row": row}))
445
+ seen.add(phone.wda_url)
446
+ with self.lock:
447
+ static = [phone for phone in self.phones.values() if phone.kind == "wda"]
448
+ for phone in static:
449
+ if phone.wda_url not in seen:
450
+ found.append((phone, {}))
451
+ seen.add(phone.wda_url)
452
+ # A nickname given in Settings › Phones is the phone's name everywhere (the device list, runs, conversations).
453
+ nicknames = self.stored()["prefs"]
454
+ for phone, _ in found:
455
+ nickname = (nicknames.get(phone.id) or {}).get("nickname") if phone.id else None
456
+ if nickname:
457
+ phone.name = nickname
458
+ # Devices that went away (a simulator deleted, a phone unplugged before it was set up) are dropped.
459
+ live = {id(phone) for phone, _ in found}
460
+ with self.lock:
461
+ for key in [key for key, phone in self.phones.items() if id(phone) not in live and phone.manager is None
462
+ and phone.kind != "wda" and self.busy(phone) is None]:
463
+ self.phones.pop(key, None)
464
+ return found
465
+
466
+ def usb_managers(self):
467
+ out = []
468
+ manager = getattr(self.runtime, "manager", None)
469
+ udid = self._chosen() if manager is not None else None
470
+ if udid:
471
+ out.append((self.primary, udid, manager))
472
+ with self.lock:
473
+ others = [phone for phone in self.phones.values() if phone.kind == "usb" and phone.manager is not None]
474
+ out += [(phone, phone.udid, phone.manager) for phone in others if phone.udid and phone.udid != udid]
475
+ return out
476
+
477
+ def _wifi(self):
478
+ return registry.wifi_phones(self.data_dir) if self.data_dir is not None else None
479
+
480
+ def entries(self, found=None):
481
+ from .device_info import MODEL_NAMES
482
+ # one phone's probe (``entry``) isn't every phone: the alerts' baseline needs every one
483
+ complete = found is None
484
+ found = self.inventory() if found is None else found
485
+ default = self.default(found)
486
+ chosen_default = self.stored()["defaultDevice"] # Settings › Phones › Set as default (not the phone used last)
487
+ wifi = self._wifi()
488
+ rows = []
489
+ for phone, context in found:
490
+ if context.get("loose"):
491
+ device = context["device"]
492
+ item = registry.record(
493
+ id=phone.id, kind="usb", name=phone.name, udid=phone.udid, model=phone.model,
494
+ model_name=MODEL_NAMES.get(phone.model), ios=phone.ios, state="needs_setup",
495
+ reason=REASONS["not_built"] if device.get("trusted") else REASONS["untrusted"],
496
+ wda_url_value=None, set_up=False)
497
+ else:
498
+ item = self._record(phone, context, wifi)
499
+ run_id = self.busy(phone)
500
+ run = self.runtime.runs.get(run_id) if run_id else None
501
+ approval = None
502
+ if run is not None:
503
+ with run.condition:
504
+ approval = dict(run.approval) if run.approval else None
505
+ item = {key: value for key, value in item.items() if key not in ("wdaUrl", "mjpegUrl")}
506
+ prefs = self.prefs(phone)
507
+ disconnected = self.is_disconnected(phone)
508
+ if disconnected and run_id is None and item["state"] != "needs_setup":
509
+ item.update(state="disconnected", reason=REASONS["paused"])
510
+ item.update(default=phone is default, activeRunId=run_id, approval=approval,
511
+ lastUsedAt=self.last_used_at(phone), nickname=prefs.get("nickname"),
512
+ disconnected=disconnected, askBeforeActing=prefs.get("askBeforeActing"),
513
+ apps=prefs.get("apps"), defaultSet=bool(phone.id) and phone.id == chosen_default)
514
+ if wifi is not None and item["kind"] == "usb":
515
+ item.update(self._transport_fields(phone, context, wifi))
516
+ # How the screen and the taps reach it: "wda" (Pro), "mirroring" or "cable_bt" (Cable + Bluetooth). A
517
+ # connection that sets its own on the phone wins.
518
+ item["connection"] = getattr(phone, "connection", None) or "wda"
519
+ item.update(self._ownership_fields(phone, item))
520
+ item.update(self._reliability_fields(phone))
521
+ rows.append((phone, item))
522
+ # Last, so the fields other work adds to a record (the owner, the holder) are there to read.
523
+ try:
524
+ self.health.apply(rows, complete=complete)
525
+ except Exception: # noqa: BLE001 -- the device list never fails for health
526
+ pass
527
+ return rows
528
+
529
+ def _ownership_fields(self, phone, item):
530
+ read = getattr(self.runtime, "phone_ownership", None)
531
+ if not callable(read) or item.get("state") == "needs_setup":
532
+ return {}
533
+ try:
534
+ fields = read(phone)
535
+ except Exception: # noqa: BLE001 -- a device list is still a device list without ownership
536
+ return {}
537
+ if fields["holder"] is None and item.get("state") == "busy" and item.get("reason") == REASONS["busy_elsewhere"]:
538
+ fields = {**fields, "holder": ownership.elsewhere_holder()}
539
+ return fields
540
+
541
+ @staticmethod
542
+ def _reliability_fields(phone):
543
+ manager = getattr(phone, "manager", None)
544
+ fields = {}
545
+ if getattr(manager, "presence", None) is not None:
546
+ fields["owner"] = manager.presence.snapshot()
547
+ if getattr(manager, "watchdog", None) is not None:
548
+ fields["watchdog"] = manager.watchdog.snapshot()
549
+ renewal = getattr(manager, "renewal", None)
550
+ if renewal is not None:
551
+ try:
552
+ fields["renewal"] = renewal.public()
553
+ except Exception: # noqa: BLE001 -- a record never fails for a field that is only advice
554
+ pass
555
+ return fields
556
+
557
+ @staticmethod
558
+ def _transport_fields(phone, context, wifi):
559
+ from .wireless import transport
560
+ link = transport.link(phone.udid) if phone.udid else None
561
+ manager = phone.manager
562
+ now = getattr(manager, "transport", None) if manager is not None and manager.runner is not None else None
563
+ if now is None and context.get("device") is not None:
564
+ now = "usb"
565
+ return {"transport": now, "wifi": {"enabled": str(phone.udid or "").upper() in wifi,
566
+ "reachable": bool(link is not None and link.reachable
567
+ and link.transport == "wifi")}}
568
+
569
+ def _record(self, phone, context, wifi=None):
570
+ from .device_info import MODEL_NAMES
571
+ if phone.kind == "simulator":
572
+ return self._sim_record(phone, context["row"])
573
+ if phone.kind == "wda":
574
+ return self._static_record(phone)
575
+ device = context.get("device")
576
+ wifi_on = wifi is not None and str(phone.udid or "").upper() in wifi
577
+ manager = phone.manager
578
+ if device is None and wifi_on and manager is not None and getattr(manager, "transport", None) == "wifi":
579
+ # Unplugged, and its runner runs over Wi-Fi: the phone is here, through the encrypted tunnel.
580
+ device = {"udid": phone.udid, "trusted": True}
581
+ if self.busy(phone) is not None:
582
+ state, reason = "busy", REASONS["busy"]
583
+ else:
584
+ target = phone.target_status() if device is not None and device.get("trusted") else {}
585
+ answered = bool(target.get("ready") or target.get("health"))
586
+ state, reason = registry.usb_state(device, phone.manager, lambda: answered,
587
+ lambda: self._lease_free(phone.wda_url, phone.udid),
588
+ locked=bool(target.get("health")))
589
+ if state == "connected" and target.get("health"):
590
+ # The locked sentence is REASONS["locked"], which fleet_health keys its alert on.
591
+ reason = REASONS["locked"] if phone.manager.locked() else target["health"]
592
+ if state == "disconnected" and wifi_on:
593
+ reason = REASONS["wifi_unreachable"]
594
+ built = phone.manager is not None and phone.manager.built()
595
+ return registry.record(id=phone.id, kind="usb", name=phone.name, udid=phone.udid, model=phone.model,
596
+ model_name=MODEL_NAMES.get(phone.model), ios=phone.ios, state=state, reason=reason,
597
+ wda_url_value=phone.wda_url, mjpeg_url=phone.mjpeg_url, primary=phone.primary,
598
+ set_up=built)
599
+
600
+ def _sim_record(self, phone, row):
601
+ busy_here = self.busy(phone) is not None
602
+ ready = None
603
+ if not busy_here and row.get("state") == "Booted":
604
+ ready = phone.target_status()["ready"]
605
+ item = registry.simulator_record(row, ready=ready, free=None if busy_here else self._lease_free(phone.wda_url)
606
+ if ready else True, busy_here=busy_here)
607
+ if phone.start_state["state"] == "starting" and item["state"] != "ready":
608
+ item.update(state="connected", reason="Starting the simulator and Mobster's helper on it.")
609
+ elif phone.start_state["state"] == "failed" and item["state"] != "ready":
610
+ item["reason"] = phone.start_state["error"] or item["reason"]
611
+ return {**item, "primary": phone.primary}
612
+
613
+ def _static_record(self, phone):
614
+ busy_here = self.busy(phone) is not None
615
+ if busy_here:
616
+ item = registry.record(id=phone.id, kind="wda", name=phone.name or f"WebDriverAgent on port "
617
+ f"{registry.port_of(phone.wda_url)}", state="busy", reason=REASONS["busy"],
618
+ wda_url_value=phone.wda_url)
619
+ else:
620
+ ready = phone.target_status()["ready"]
621
+ item = registry.static_record(phone.wda_url, id=phone.id, name=phone.name, ready=ready,
622
+ free=self._lease_free(phone.wda_url) if ready else True)
623
+ return {**item, "primary": phone.primary}
624
+
625
+ # -- what a request means -----------------------------------------------------------------------------
626
+
627
+ def public(self):
628
+ return [item for _, item in self.entries()]
629
+
630
+ def find(self, query, found=None):
631
+ found = self.inventory() if found is None else found
632
+ rows = [(phone, self._name_row(phone)) for phone, _ in found]
633
+ item = registry.resolve([row for _, row in rows], query)
634
+ return next(phone for phone, row in rows if row is item)
635
+
636
+ def lookup(self, query):
637
+ known = self._known(query)
638
+ return known if known is not None else self.find(query)
639
+
640
+ def _known(self, query):
641
+ folded = str(query or "").strip().casefold()
642
+ if not folded:
643
+ return None
644
+ primary = self.primary
645
+ if primary.id and folded in {primary.id.casefold(), str(primary.udid or "").casefold()}:
646
+ manager = getattr(self.runtime, "manager", None)
647
+ stable = manager is None or (primary.kind == "usb" and self._chosen() == primary.udid)
648
+ return primary if stable else None
649
+ with self.lock:
650
+ phones = list(self.phones.values())
651
+ for phone in phones:
652
+ if phone.kind == "usb" and phone.manager is None: # not set up: Setup may adopt it any moment
653
+ continue
654
+ if phone.id and folded in {phone.id.casefold(), str(phone.udid or "").casefold()}:
655
+ return phone
656
+ return None
657
+
658
+ def entry(self, query):
659
+ """(phone, public record) for one device, probing only that one."""
660
+ found = self.inventory()
661
+ phone = self.find(query, found)
662
+ return next(row for row in self.entries([(candidate, context) for candidate, context in found
663
+ if candidate is phone]))
664
+
665
+ def default(self, found=None, sticky=True):
666
+ """Resolve explicit preferences before a usable last-used phone; unattended tasks keep the primary fallback."""
667
+ found = self.inventory() if found is None else found
668
+ phones = [phone for phone, _ in found]
669
+ if self.preferred:
670
+ try:
671
+ return self.find(self.preferred, found)
672
+ except LookupError:
673
+ pass
674
+ chosen = self.stored()["defaultDevice"] # Settings › Phones › Set as default
675
+ if chosen:
676
+ match = next((phone for phone in phones if phone.id == chosen), None)
677
+ if match is not None:
678
+ return match
679
+ if len(phones) == 1:
680
+ return phones[0]
681
+ if not sticky:
682
+ return self.primary
683
+ unready = {id(phone) for phone, context in found if self._needs_setup(phone, context)}
684
+ usable = [phone for phone in phones if id(phone) not in unready]
685
+ if self.last_used:
686
+ match = next((phone for phone in phones if phone.id == self.last_used), None)
687
+ if match is not None and (id(match) not in unready or not usable):
688
+ return match
689
+ if not usable or any(phone is self.primary for phone in usable):
690
+ return self.primary
691
+ return max(usable, key=lambda phone: self.last_used_at(phone) or 0)
692
+
693
+ @staticmethod
694
+ def _needs_setup(phone, context):
695
+ """Whether identity or build state requires setup, without probing the phone."""
696
+ if context.get("loose"):
697
+ return True
698
+ if phone.kind != "usb":
699
+ return False
700
+ device = context.get("device")
701
+ if device is not None and not device.get("trusted"):
702
+ return True
703
+ manager = phone.manager
704
+ try:
705
+ return manager is None or not manager.built()
706
+ except Exception: # noqa: BLE001 -- a helper folder it can't read is a phone it can't use yet
707
+ return True
708
+
709
+ @staticmethod
710
+ def _name_row(phone):
711
+ return {"id": phone.id or "", "udid": phone.udid, "name": phone.name}
712
+
713
+ def for_run(self, query):
714
+ if query is None:
715
+ return self.default()
716
+ if not isinstance(query, str) or not 1 <= len(query.strip()) <= 128:
717
+ raise ValueError("device must be a device's id, UDID or name")
718
+ return self.find(query) # a phone plugged in but not set up too: admission refuses it as not ready
719
+
720
+ # -- Setup, per device ---------------------------------------------------------------------------------
721
+
722
+ def setup_state(self, phone):
723
+ """What Setup shows for this device. LookupError for a device Mobster doesn't set up (a WDA address)."""
724
+ if phone.kind == "usb":
725
+ if phone.primary or (phone.setup is None and not self._primary_chosen()):
726
+ if self.runtime.setup is None:
727
+ raise LookupError("Setup is managed outside this app")
728
+ return self.runtime.setup.state()
729
+ service = phone.setup if phone.setup is not None else self._loose_setup(phone)
730
+ return service.state()
731
+ if phone.kind == "simulator":
732
+ row = next((row for row in self._sim_rows() if row["udid"] == phone.udid), None)
733
+ item = self._sim_record(phone, row) if row is not None else None
734
+ runner = ("starting" if phone.start_state["state"] in ("starting", "stopping") else
735
+ "running" if item is not None and item["state"] in ("ready", "busy") else
736
+ "failed" if phone.start_state["state"] == "failed" else "stopped")
737
+ return {"kind": "simulator", "complete": runner == "running",
738
+ "device": {key: value for key, value in (item or {}).items() if key not in ("wdaUrl", "mjpegUrl")},
739
+ "runner": {"state": runner, "error": phone.start_state.get("error")}}
740
+ raise LookupError("Setup is managed outside this app")
741
+
742
+ def _primary_chosen(self):
743
+ return self._chosen() is not None
744
+
745
+ def _loose_setup(self, phone):
746
+ from .device_manager import DeviceManager
747
+ from .setup_service import SetupService
748
+ if self.runtime.setup is None:
749
+ raise LookupError("Setup is managed outside this app")
750
+ manager = DeviceManager(self.data_dir, registry.wda_url(0), udid=phone.udid,
751
+ root=registry.phone_folder(self.data_dir, phone.udid))
752
+ manager.cache = self.runtime.manager.cache
753
+ return SetupService(self.runtime, manager, self.runtime.setup.env_file, video=lambda: None,
754
+ busy=lambda: False)
755
+
756
+ def setup_action(self, phone, action, body):
757
+ if phone.kind == "usb":
758
+ if action == "build" and set(body) <= {"team"}:
759
+ target = phone if phone.primary else self.adopt(phone.udid)
760
+ target.manager.start_build(body.get("team"))
761
+ self.cache.pop("usb", None)
762
+ return target
763
+ manager = phone.manager
764
+ if manager is None:
765
+ if action == "refresh" and not body:
766
+ # "Check again" on a new phone's first steps (Trust, Developer Mode, the tools): read them anew.
767
+ self._loose_setup(phone).refresh()
768
+ self.cache.pop("usb", None)
769
+ return phone
770
+ raise LookupError("Set this iPhone up first: install Mobster's helper on it.")
771
+ if action == "start" and not body:
772
+ manager.start_runner()
773
+ elif action == "stop" and not body:
774
+ manager.stop_runner(remember=True)
775
+ elif action == "refresh" and not body:
776
+ (phone.setup or self.runtime.setup).refresh()
777
+ elif action == "photos" and not body:
778
+ (phone.setup or self.runtime.setup).allow_photos(phone.id)
779
+ else:
780
+ raise KeyError(action)
781
+ phone.health_at = 0.0
782
+ return phone
783
+ if phone.kind == "simulator":
784
+ if action == "start" and not body:
785
+ self._simulator_job(phone, "starting")
786
+ elif action == "stop" and not body:
787
+ if self.busy(phone) is not None:
788
+ raise LookupError("A task is running on it. Stop it first.")
789
+ self._simulator_job(phone, "stopping")
790
+ else:
791
+ raise KeyError(action)
792
+ return phone
793
+ raise LookupError("Setup is managed outside this app")
794
+
795
+ def _simulator_job(self, phone, kind):
796
+ """Boot a simulator and start its WebDriverAgent, or shut it down, in the background."""
797
+ with self.lock:
798
+ if phone.start_state["state"] in ("starting", "stopping"):
799
+ return
800
+ phone.start_state = {"state": kind, "error": None}
801
+
802
+ def work():
803
+ try:
804
+ from .sim import SimulatorManager
805
+ manager = SimulatorManager(data_dir=registry.simulator_folder())
806
+ if kind == "starting":
807
+ manager.acquire_udid(phone.udid, timeout=30).release()
808
+ else:
809
+ manager.shutdown(phone.udid)
810
+ phone.start_state = {"state": "idle", "error": None}
811
+ except Exception as error:
812
+ phone.start_state = {"state": "failed", "error": " ".join(
813
+ part for part in (str(error), getattr(error, "fix", "")) if part)[:300]}
814
+ finally:
815
+ self._read_sims()
816
+ phone.health_at = 0.0
817
+ threading.Thread(target=work, name=f"mobster-sim-{kind}", daemon=True).start()
818
+
819
+ def mark_used(self, phone):
820
+ if phone is None or not phone.id:
821
+ return
822
+ self.last_used = phone.id
823
+ self.cache.setdefault("used_at", {})[phone.id] = round(time.time() * 1000)
824
+ if self.store is not None:
825
+ try:
826
+ self.store.set_last_used(phone.id)
827
+ except OSError:
828
+ pass
829
+
830
+ def last_used_at(self, phone):
831
+ return self.cache.get("used_at", {}).get(phone.id)
832
+
833
+ def videos(self):
834
+ return [phone._video for phone in list(self.phones.values()) if phone._video is not None]
835
+
836
+ def close(self, keep_runner=False):
837
+ self.closed = True
838
+ with self.lock:
839
+ phones = list(self.phones.values())
840
+ for phone in phones:
841
+ phone.close(keep_runner)