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,676 @@
1
+ """Every phone Mobster can drive, their ports, and naming one (SPEC §3, docs/devices.md)."""
2
+
3
+ import contextlib
4
+ import copy
5
+ import errno
6
+ import fcntl
7
+ import json
8
+ import os
9
+ import re
10
+ import socket
11
+ import stat
12
+ import tempfile
13
+ import threading
14
+ import time
15
+ from pathlib import Path
16
+ from urllib.parse import urlsplit
17
+
18
+ STATES = ("ready", "busy", "connected", "needs_setup", "disconnected")
19
+ STORE = "devices.json"
20
+ STORE_LOCK = "devices.lock"
21
+ LOCK_TIMEOUT = 10.0
22
+ WDA_BASE, MJPEG_BASE = 8100, 9100
23
+ FIRST_SLOT, LAST_SLOT = 1, 99
24
+ # The relay and WDA listen here only. Without it iproxy listened on every interface
25
+ # of the Mac (lsof: *:8100) and WDA on the phone's Wi-Fi address; neither has auth.
26
+ LOOPBACK = "127.0.0.1"
27
+ ID_PATTERN = re.compile(r"[A-Za-z0-9._-]{1,64}")
28
+ UDID_PATTERN = re.compile(r"[0-9A-Fa-f-]{24,40}")
29
+
30
+ # The one sentence each state says, when nothing more precise is known.
31
+ REASONS = {
32
+ # Plain words: the app shows these under a phone's name (dashboard/src/lib/devices.ts), and the CLI's
33
+ # `mobster devices` prints them. "Mobster's helper" is the on-phone runner (WebDriverAgent).
34
+ "busy": "A task is running on it.",
35
+ "busy_elsewhere": "Another Mobster app or command is using it.",
36
+ "untrusted": "Unlock it and tap Trust on it.",
37
+ "not_built": "Set it up once. Mobster walks you through it.",
38
+ "built_for_other": "Set it up again: Mobster's helper on it was made for another iPhone.",
39
+ # Both are refreshed by themselves while the phone is plugged in and unlocked (runner_renewal.py).
40
+ "out_of_date": "It needs a quick refresh after Mobster's update. Mobster does it by itself while it's plugged in "
41
+ "and unlocked.",
42
+ "expired": "It needs a quick refresh. Free Apple accounts ask for this every 7 days. Mobster does it by itself "
43
+ "while it's plugged in and unlocked.",
44
+ "starting": "Mobster's helper on it isn't answering yet. Keep it unlocked and connected.",
45
+ # iOS waits for automation mode (runner_hints.AUTOMATION_WAIT_S): the switch to turn on, said while it waits.
46
+ "ui_automation": "Turn on Settings › Developer › Enable UI Automation on it. If it asks for its passcode, enter "
47
+ "it. Mobster's helper starts by itself after that.",
48
+ "stopped": "Mobster's helper isn't running on it. Start it again to see the screen.",
49
+ # Its helper's first start (device_manager.prearm): the switch iOS asks about, said before the first launch.
50
+ "prearm": "Its helper is starting for the first time. Open Settings › Developer on it and turn on Enable UI "
51
+ "Automation, so it starts on the first try.",
52
+ "unplugged": "It's unplugged. Plug it into this Mac with a cable.",
53
+ "sim_off": "The simulator is shut down. Start it to use it.",
54
+ "sim_no_wda": "The simulator is on, but Mobster's helper isn't running on it. Start it to use it.",
55
+ "wda_down": "Nothing answers at its address.",
56
+ "locked": "Unlock it to run tasks and see its screen.",
57
+ # Only with Wi-Fi for iPhones on (MOBSTER_WIFI_TRANSPORT) and Wi-Fi turned on for the phone (wireless/).
58
+ "wifi_unreachable": "It isn't reachable over Wi-Fi. Keep it unlocked and on the same Wi-Fi as this Mac, or plug "
59
+ "it in.",
60
+ # Disconnected in Settings › Phones: still known, not used until Connect.
61
+ "paused": "Disconnected in Mobster. Connect it to use it again.",
62
+ }
63
+
64
+ # A phone's own settings in Mobster (devices.json "prefs", by device id): a nickname shown instead of its name, whether
65
+ # a task on it asks before acting (None: the Mac-wide Safety setting), and the apps a task on it may use (None: all).
66
+ NICKNAME_MAX = 64
67
+ PREF_APPS_MAX = 64
68
+
69
+
70
+ class DeviceNotFound(LookupError):
71
+ """No device has that id, UDID or name."""
72
+
73
+
74
+ class AmbiguousDevice(LookupError):
75
+ """More than one device has that name."""
76
+
77
+
78
+ def wda_url(port):
79
+ return f"http://{LOOPBACK}:{int(port)}"
80
+
81
+
82
+ def port_of(url):
83
+ try:
84
+ return urlsplit(url or "").port
85
+ except ValueError:
86
+ return None
87
+
88
+
89
+ def slot_ports(slot):
90
+ """(WDA port, MJPEG port) of a USB slot: 8100+N and 9100+N."""
91
+ return WDA_BASE + slot, MJPEG_BASE + slot
92
+
93
+
94
+ def listening(port, host=LOOPBACK, timeout=.2):
95
+ """Whether something accepts connections on this Mac's loopback at ``port``."""
96
+ try:
97
+ with socket.create_connection((host, int(port)), timeout=timeout):
98
+ return True
99
+ except OSError:
100
+ return False
101
+
102
+
103
+ def allocate_slot(entries, busy=None):
104
+ busy = busy or listening
105
+ taken = {int(entry["slot"]) for entry in entries if isinstance(entry.get("slot"), int)}
106
+ for slot in range(FIRST_SLOT, LAST_SLOT + 1):
107
+ if slot in taken:
108
+ continue
109
+ wda, mjpeg = slot_ports(slot)
110
+ if busy(wda) or busy(mjpeg):
111
+ continue # another program's port: skipped, never taken over
112
+ return slot
113
+ raise LookupError(f"Every port from {WDA_BASE + FIRST_SLOT} to {WDA_BASE + LAST_SLOT} is taken, so Mobster "
114
+ "has no room for another iPhone. Forget a phone you no longer use.")
115
+
116
+
117
+ def device_id_for_url(url):
118
+ """The id of a device known only by its WDA address: wda-<port>."""
119
+ port = port_of(url)
120
+ return f"wda-{port}" if port else "wda"
121
+
122
+
123
+ # -- devices.json --------------------------------------------------------------------------------------------
124
+
125
+ MAX_CHOSEN = 100 # the composer's phones for one task on each (fanout's own limits apply when it runs)
126
+
127
+
128
+ def clean_chosen(items):
129
+ """Device ids in order, each once, at most MAX_CHOSEN; anything else is dropped."""
130
+ out = []
131
+ for item in items:
132
+ if isinstance(item, str) and ID_PATTERN.fullmatch(item) and item not in out:
133
+ out.append(item)
134
+ return out[:MAX_CHOSEN]
135
+
136
+
137
+ class DeviceStore:
138
+ """devices.json in the data folder: the phones set up besides the primary, their slots, and the device used last."""
139
+
140
+ def __init__(self, data_dir):
141
+ self.data_dir = Path(data_dir) if data_dir is not None else None
142
+ self.path = self.data_dir / STORE if self.data_dir is not None else None
143
+ self.lock_path = self.data_dir / STORE_LOCK if self.data_dir is not None else None
144
+ self.memory = self._empty() if self.data_dir is None else None
145
+ self.memory_lock = threading.Lock()
146
+
147
+ @staticmethod
148
+ def _empty():
149
+ return {"version": 1, "phones": [], "lastUsed": None, "defaultDevice": None, "disconnected": [],
150
+ "prefs": {}, "chosen": []}
151
+
152
+ def read(self):
153
+ if self.data_dir is None:
154
+ with self.memory_lock:
155
+ return copy.deepcopy(self.memory)
156
+ try:
157
+ data = json.loads(self.path.read_text())
158
+ except (OSError, ValueError):
159
+ data = None
160
+ if not isinstance(data, dict): # damaged: it holds no secret and nothing that can't be set up again
161
+ return self._empty()
162
+ entries = data.get("phones")
163
+ if not isinstance(entries, list):
164
+ entries = []
165
+ phones = [entry for entry in entries if isinstance(entry, dict)
166
+ and isinstance(entry.get("udid"), str) and UDID_PATTERN.fullmatch(entry["udid"])
167
+ and type(entry.get("slot")) is int and FIRST_SLOT <= entry["slot"] <= LAST_SLOT
168
+ and type(entry.get("wdaPort")) is int and type(entry.get("mjpegPort")) is int
169
+ and (entry["wdaPort"], entry["mjpegPort"]) == slot_ports(entry["slot"])]
170
+ last, default = data.get("lastUsed"), data.get("defaultDevice")
171
+ paused = data.get("disconnected")
172
+ prefs = data.get("prefs") if isinstance(data.get("prefs"), dict) else {}
173
+ chosen = data.get("chosen")
174
+ return {"version": 1, "phones": phones,
175
+ "lastUsed": last if isinstance(last, str) and ID_PATTERN.fullmatch(last) else None,
176
+ "defaultDevice": default if isinstance(default, str) and ID_PATTERN.fullmatch(default) else None,
177
+ "disconnected": sorted({item for item in paused if isinstance(item, str)
178
+ and ID_PATTERN.fullmatch(item)})
179
+ if isinstance(paused, list) else [],
180
+ "prefs": {key: clean for key, value in prefs.items() if isinstance(key, str)
181
+ and ID_PATTERN.fullmatch(key)
182
+ for clean in [clean_prefs(value)] if clean},
183
+ "chosen": clean_chosen(chosen) if isinstance(chosen, list) else []}
184
+
185
+ def read_strict(self):
186
+ """Fresh policy for trusted suite admission; a damaged saved rule never becomes allow-all."""
187
+ if self.data_dir is None:
188
+ with self.memory_lock:
189
+ return copy.deepcopy(self.memory)
190
+ from .transport import decode_json
191
+ try:
192
+ fd = os.open(self.path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
193
+ except FileNotFoundError:
194
+ return self._empty() # A new Mac has no device preferences yet.
195
+ except OSError:
196
+ raise ValueError("Saved device rules cannot be read") from None
197
+ try:
198
+ with os.fdopen(fd, "rb") as stream:
199
+ info = os.fstat(stream.fileno())
200
+ if not stat.S_ISREG(info.st_mode) or info.st_size > 1_000_000:
201
+ raise ValueError("Saved device rules are not a bounded regular file")
202
+ data = decode_json(stream.read(1_000_001))
203
+ except (OSError, UnicodeError, ValueError, RecursionError):
204
+ raise ValueError("Saved device rules are damaged or unreadable") from None
205
+ if (not isinstance(data, dict) or type(data.get("version")) is not int or data["version"] != 1 or
206
+ set(data) != {"version", "phones", "lastUsed", "defaultDevice", "disconnected", "prefs", "chosen"} or
207
+ not isinstance(data["phones"], list) or not isinstance(data["disconnected"], list) or
208
+ not isinstance(data["prefs"], dict) or not isinstance(data["chosen"], list)):
209
+ raise ValueError("Saved device rules are damaged")
210
+ if any(not isinstance(entry, dict) or not isinstance(entry.get("udid"), str) or
211
+ not UDID_PATTERN.fullmatch(entry["udid"]) or type(entry.get("slot")) is not int or
212
+ not FIRST_SLOT <= entry["slot"] <= LAST_SLOT or
213
+ (entry.get("wdaPort"), entry.get("mjpegPort")) != slot_ports(entry["slot"])
214
+ for entry in data["phones"]):
215
+ raise ValueError("Saved device rules are damaged")
216
+ ids = data["disconnected"] + data["chosen"]
217
+ if (any(not isinstance(item, str) or not ID_PATTERN.fullmatch(item) for item in ids) or
218
+ any(value is not None and (not isinstance(value, str) or not ID_PATTERN.fullmatch(value))
219
+ for value in (data["lastUsed"], data["defaultDevice"])) or
220
+ len(set(data["chosen"])) != len(data["chosen"]) or
221
+ len(clean_chosen(data["chosen"])) != len(data["chosen"])):
222
+ raise ValueError("Saved device rules are damaged")
223
+ for key, value in data["prefs"].items():
224
+ if not ID_PATTERN.fullmatch(key) or not isinstance(value, dict) or set(value) - {
225
+ "nickname", "askBeforeActing", "apps"}:
226
+ raise ValueError("Saved device rules are damaged")
227
+ clean_prefs(value, strict=True)
228
+ return data
229
+
230
+ def phones(self):
231
+ return self.read()["phones"]
232
+
233
+ @contextlib.contextmanager
234
+ def edit(self):
235
+ if self.data_dir is None:
236
+ with self.memory_lock:
237
+ yield self.memory
238
+ return
239
+ self.data_dir.mkdir(parents=True, exist_ok=True)
240
+ fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
241
+ try:
242
+ end = time.monotonic() + LOCK_TIMEOUT
243
+ while True:
244
+ try:
245
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
246
+ break
247
+ except OSError as exc:
248
+ if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or time.monotonic() >= end:
249
+ raise OSError("Another Mobster process is holding the device list") from None
250
+ time.sleep(.05)
251
+ data = self.read()
252
+ before = json.dumps(data, sort_keys=True)
253
+ yield data
254
+ if json.dumps(data, sort_keys=True) != before:
255
+ self._write(data)
256
+ finally:
257
+ os.close(fd)
258
+
259
+ def _write(self, data):
260
+ fd, temporary = tempfile.mkstemp(prefix=".devices.", suffix=".json", dir=self.data_dir)
261
+ try:
262
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
263
+ os.fchmod(stream.fileno(), 0o600)
264
+ stream.write(json.dumps(data, indent=1) + "\n")
265
+ os.replace(temporary, self.path)
266
+ except BaseException:
267
+ with contextlib.suppress(OSError):
268
+ os.unlink(temporary)
269
+ raise
270
+
271
+ def add(self, udid, name=None, busy=None):
272
+ """The phone's entry, allocating its slot the first time."""
273
+ with self.edit() as data:
274
+ entry = next((item for item in data["phones"] if item["udid"] == udid), None)
275
+ if entry is None:
276
+ slot = allocate_slot(data["phones"], busy)
277
+ wda, mjpeg = slot_ports(slot)
278
+ entry = {"udid": udid, "name": name, "slot": slot, "wdaPort": wda, "mjpegPort": mjpeg,
279
+ "addedAt": round(time.time() * 1000)}
280
+ data["phones"].append(entry)
281
+ elif name and entry.get("name") != name:
282
+ entry["name"] = name
283
+ return dict(entry)
284
+
285
+ def remove(self, udid):
286
+ with self.edit() as data:
287
+ data["phones"] = [item for item in data["phones"] if item["udid"] != udid]
288
+ for key in ("lastUsed", "defaultDevice"):
289
+ if data.get(key) == udid:
290
+ data[key] = None
291
+ data["disconnected"] = [item for item in data["disconnected"] if item != udid]
292
+ data["prefs"].pop(udid, None)
293
+ data["chosen"] = [item for item in data["chosen"] if item != udid]
294
+
295
+ def set_default(self, device_id):
296
+ """The phone a new task runs on unless another is picked (None: no choice, Fleet.default decides)."""
297
+ if device_id is not None and not ID_PATTERN.fullmatch(str(device_id)):
298
+ raise ValueError("A device id is 1 to 64 letters, digits, dots, dashes or underscores")
299
+ with self.edit() as data:
300
+ data["defaultDevice"] = device_id
301
+
302
+ def set_chosen(self, device_ids):
303
+ if not isinstance(device_ids, (list, tuple)) or len(device_ids) > MAX_CHOSEN or not all(
304
+ isinstance(item, str) and ID_PATTERN.fullmatch(item) for item in device_ids) or len(
305
+ set(device_ids)) != len(device_ids):
306
+ raise ValueError(f"Choose at most {MAX_CHOSEN} different phones by their ids")
307
+ with self.edit() as data:
308
+ data["chosen"] = clean_chosen(device_ids)
309
+
310
+ def set_disconnected(self, device_id, value):
311
+ if not ID_PATTERN.fullmatch(str(device_id)):
312
+ raise ValueError("A device id is 1 to 64 letters, digits, dots, dashes or underscores")
313
+ with self.edit() as data:
314
+ others = [item for item in data["disconnected"] if item != device_id]
315
+ data["disconnected"] = sorted(others + [device_id]) if value else others
316
+
317
+ def prefs(self, device_id):
318
+ return dict(self.read()["prefs"].get(device_id) or {})
319
+
320
+ def set_prefs(self, device_id, **changes):
321
+ """Change a phone's own settings (``clean_prefs``); a value of None removes it. The phone's settings after."""
322
+ if not ID_PATTERN.fullmatch(str(device_id)):
323
+ raise ValueError("A device id is 1 to 64 letters, digits, dots, dashes or underscores")
324
+ unknown = set(changes) - {"nickname", "askBeforeActing", "apps"}
325
+ if unknown:
326
+ raise ValueError("A phone's settings are nickname, askBeforeActing and apps")
327
+ with self.edit() as data:
328
+ current = dict(data["prefs"].get(device_id) or {})
329
+ for key, value in changes.items():
330
+ if value is None:
331
+ current.pop(key, None)
332
+ else:
333
+ current[key] = value
334
+ clean = clean_prefs(current, strict=True)
335
+ if clean:
336
+ data["prefs"][device_id] = clean
337
+ else:
338
+ data["prefs"].pop(device_id, None)
339
+ return dict(clean)
340
+
341
+ def set_last_used(self, device_id):
342
+ with self.edit() as data:
343
+ data["lastUsed"] = device_id
344
+
345
+ def set_wifi(self, udid, value):
346
+ """Set (a dict) or clear (None) the phone's ``wifi`` (wireless/store.py). LookupError when it has no entry."""
347
+ with self.edit() as data:
348
+ entry = next((item for item in data["phones"] if item["udid"].upper() == str(udid).upper()), None)
349
+ if entry is None:
350
+ raise LookupError("not_set_up")
351
+ if value is None:
352
+ entry.pop("wifi", None)
353
+ else:
354
+ entry["wifi"] = dict(value)
355
+
356
+
357
+ def clean_nickname(value):
358
+ """A nickname as kept: its words joined by single spaces, 1 to 64 characters, no control characters."""
359
+ if not isinstance(value, str):
360
+ raise ValueError("A name is text")
361
+ text = " ".join(value.split())
362
+ if not 1 <= len(text) <= NICKNAME_MAX or any(ord(char) < 32 or 0x7f <= ord(char) < 0xa0 for char in text):
363
+ raise ValueError(f"A name is 1 to {NICKNAME_MAX} characters")
364
+ return text
365
+
366
+
367
+ def clean_prefs(value, strict=False):
368
+ from .state import validate_bundle_id
369
+ if not isinstance(value, dict):
370
+ if strict:
371
+ raise ValueError("A phone's settings are an object")
372
+ return {}
373
+ out = {}
374
+ if value.get("nickname") is not None:
375
+ try:
376
+ out["nickname"] = clean_nickname(value["nickname"])
377
+ except ValueError:
378
+ if strict:
379
+ raise
380
+ if value.get("askBeforeActing") is not None:
381
+ if type(value["askBeforeActing"]) is bool:
382
+ out["askBeforeActing"] = value["askBeforeActing"]
383
+ elif strict:
384
+ raise ValueError("askBeforeActing is true, false or null (the Mac-wide setting)")
385
+ if value.get("apps") is not None:
386
+ apps = value["apps"]
387
+ try:
388
+ if not isinstance(apps, list) or not 1 <= len(apps) <= PREF_APPS_MAX:
389
+ raise ValueError
390
+ out["apps"] = sorted({validate_bundle_id(bundle) for bundle in apps})
391
+ except (ValueError, TypeError):
392
+ if strict:
393
+ raise ValueError(f"apps is null (every app) or 1 to {PREF_APPS_MAX} bundle identifiers") from None
394
+ return out
395
+
396
+
397
+ def phone_folder(data_dir, udid):
398
+ """Where a phone other than the primary keeps its runner build, settings and logs."""
399
+ return Path(data_dir) / "devices" / udid
400
+
401
+
402
+ # -- reading the machine ---------------------------------------------------------------------------------------
403
+
404
+ def record(*, id, kind, name, wda_url_value, udid=None, model=None, model_name=None, ios=None, state="disconnected",
405
+ reason=None, mjpeg_url=None, primary=False, set_up=True, enclosure=None):
406
+ if state not in STATES:
407
+ raise ValueError(f"unknown device state {state}")
408
+ return {"id": id, "kind": kind, "name": name or ("iPhone" if kind == "usb" else id), "udid": udid,
409
+ "model": model, "modelName": model_name, "ios": ios, "enclosure": enclosure, "state": state,
410
+ "reason": None if state == "ready" else (reason or None), "primary": bool(primary),
411
+ "setUp": bool(set_up), "wdaUrl": wda_url_value, "mjpegUrl": mjpeg_url}
412
+
413
+
414
+ def usb_state(device, manager, wda_ready, lease_free, locked=None):
415
+ if device is None:
416
+ return "disconnected", REASONS["unplugged"]
417
+ if not device.get("trusted"):
418
+ return "needs_setup", REASONS["untrusted"]
419
+ if manager is None:
420
+ return "needs_setup", REASONS["not_built"]
421
+ settings = manager.settings()
422
+ built_for = settings.get("built_for", settings.get("udid"))
423
+ if manager.expired():
424
+ return "needs_setup", REASONS["expired"]
425
+ if not manager.built():
426
+ if built_for and built_for != device.get("udid"):
427
+ return "needs_setup", REASONS["built_for_other"]
428
+ if manager.out_of_date():
429
+ return "needs_setup", REASONS["out_of_date"]
430
+ return "needs_setup", REASONS["not_built"]
431
+ if not wda_ready():
432
+ waiting = getattr(manager, "runner_waiting", None)
433
+ if callable(waiting) and waiting() == "unlock":
434
+ return "connected", REASONS["locked"] # its helper starts when the phone is unlocked
435
+ running = manager.runner is not None and manager.runner.running
436
+ if running and getattr(manager, "runner_cause", None) == "ui_automation_off":
437
+ return "connected", REASONS["ui_automation"]
438
+ prearm = getattr(manager, "prearm", None)
439
+ if running and callable(prearm) and prearm():
440
+ return "connected", REASONS["prearm"]
441
+ return "connected", REASONS["starting"] if running else REASONS["stopped"]
442
+ if not lease_free():
443
+ return "busy", REASONS["busy_elsewhere"]
444
+ if locked:
445
+ return "connected", REASONS["locked"]
446
+ return "ready", None
447
+
448
+
449
+ def wda_answers(url, timeout=1.0):
450
+ """WDA's /status says ready, asked under an absolute deadline (a phone that drips bytes can't extend it)."""
451
+ from .transport import read_kept
452
+ try:
453
+ return read_kept(url, "/status", timeout=timeout).get("value", {}).get("ready") is True
454
+ except Exception:
455
+ return False
456
+
457
+
458
+ def lease_free(url, udid=None):
459
+ from .journal import JournalError, Lease, LeaseHeld
460
+ try:
461
+ lease = Lease.device(url, wait=0, udid=udid)
462
+ except LeaseHeld:
463
+ return False
464
+ except JournalError:
465
+ return True # an unusable lease folder guards nothing; the run says so when it starts
466
+ lease.close()
467
+ return True
468
+
469
+
470
+ def simulator_folder():
471
+ from .paths import dev_data_dir
472
+ return dev_data_dir()
473
+
474
+
475
+ def simulator_rows(data_dir=None):
476
+ try:
477
+ from .sim.registry import Registry
478
+ except ImportError:
479
+ return []
480
+ folder = Path(data_dir) if data_dir else simulator_folder()
481
+ if not (folder / "simulators.json").exists():
482
+ return [] # never runs simctl on a Mac with no Mobster simulator
483
+ try:
484
+ entries = Registry(folder).read()
485
+ except Exception:
486
+ return []
487
+ states = {}
488
+ try:
489
+ from .sim import SimulatorManager
490
+ states = {item.udid: item.state for item in SimulatorManager(data_dir=folder)._devices()}
491
+ except Exception:
492
+ pass
493
+ return [{"udid": entry["udid"], "name": entry.get("name"), "device_type": entry.get("device_type"),
494
+ "runtime": entry.get("runtime"), "state": states.get(entry["udid"], "Missing" if states else "Unknown"),
495
+ "wda_url": wda_url(entry["wda_port"]), "mjpeg_url": wda_url(entry["mjpeg_port"])}
496
+ for entry in entries if entry.get("wda_port") and entry.get("mjpeg_port")]
497
+
498
+
499
+ def simulator_record(row, *, ready=None, free=None, busy_here=False):
500
+ """One simulator row (simulator_rows) as a device. ``ready``/``free`` default to probing its WDA and lease."""
501
+ booted = row.get("state") == "Booted"
502
+ if busy_here:
503
+ state, reason = "busy", REASONS["busy"]
504
+ elif not booted:
505
+ state, reason = "disconnected", REASONS["sim_off"]
506
+ elif not (wda_answers(row["wda_url"]) if ready is None else ready):
507
+ state, reason = "connected", REASONS["sim_no_wda"]
508
+ elif not (lease_free(row["wda_url"]) if free is None else free):
509
+ state, reason = "busy", REASONS["busy_elsewhere"]
510
+ else:
511
+ state, reason = "ready", None
512
+ runtime = str(row.get("runtime") or "")
513
+ return record(id=row["udid"], kind="simulator", name=row.get("name"), udid=row["udid"],
514
+ model=row.get("device_type"), model_name=row.get("device_type"),
515
+ ios=runtime.removeprefix("iOS ").strip() or None, state=state, reason=reason,
516
+ wda_url_value=row["wda_url"], mjpeg_url=row.get("mjpeg_url"))
517
+
518
+
519
+ def simulator_records(probe=True):
520
+ return [simulator_record(row, **({} if probe else {"ready": True, "free": True})) for row in simulator_rows()]
521
+
522
+
523
+ def static_specs(config=None):
524
+ """WDA devices given by address: ``MOBSTER_WDA_DEVICES`` (pool.wda_specs_from_env) and ``config.devices``."""
525
+ from .pool import wda_specs_from_config
526
+ try:
527
+ return wda_specs_from_config(config)
528
+ except ValueError:
529
+ return []
530
+
531
+
532
+ def static_record(url, *, id=None, name=None, mjpeg_url=None, ready=None, free=None):
533
+ ready = wda_answers(url) if ready is None else ready
534
+ if not ready:
535
+ state, reason = "disconnected", REASONS["wda_down"]
536
+ elif not (lease_free(url) if free is None else free):
537
+ state, reason = "busy", REASONS["busy_elsewhere"]
538
+ else:
539
+ state, reason = "ready", None
540
+ return record(id=id or device_id_for_url(url), kind="wda", name=name or f"WebDriverAgent on port {port_of(url)}",
541
+ state=state, reason=reason, wda_url_value=url.rstrip("/"), mjpeg_url=mjpeg_url)
542
+
543
+
544
+ def discover(data_dir=None, *, sims=True, extra_urls=(), probe=True):
545
+ from .device_info import MODEL_NAMES
546
+ from .device_manager import DeviceManager
547
+ from .mac_tools import WDA_PORT
548
+ from .paths import user_data_dir
549
+ data_dir = Path(data_dir) if data_dir else user_data_dir()
550
+ records, seen_urls = [], set()
551
+
552
+ def add(item):
553
+ identity = (item.get("wdaUrl") or "").rstrip("/")
554
+ if identity and identity in seen_urls:
555
+ return
556
+ if identity:
557
+ seen_urls.add(identity)
558
+ records.append(item)
559
+
560
+ primary = DeviceManager(data_dir, wda_url(WDA_PORT))
561
+ store = DeviceStore(data_dir)
562
+ extras = store.phones()
563
+ claimed = {entry["udid"] for entry in extras}
564
+ primary.claimed = lambda: claimed
565
+ try:
566
+ connected = primary.devices()
567
+ except Exception:
568
+ connected = []
569
+ by_udid = {item["udid"]: item for item in connected}
570
+ wifi = wifi_phones(data_dir)
571
+
572
+ def usb(udid, manager, url, mjpeg, is_primary, saved_name):
573
+ device = by_udid.get(udid)
574
+ over_wifi = False
575
+ if device is None and wifi is not None and udid.upper() in wifi and probe and wda_answers(url):
576
+ # Unplugged, but the Mac app's Wi-Fi relay answers on its port: the phone is there over Wi-Fi.
577
+ device, over_wifi = {"udid": udid, "trusted": True}, True
578
+ state, reason = usb_state(device, manager, (lambda: wda_answers(url)) if probe else (lambda: True),
579
+ (lambda: lease_free(url)) if probe else (lambda: True))
580
+ if wifi is not None and udid.upper() in wifi and state == "disconnected":
581
+ reason = REASONS["wifi_unreachable"]
582
+ model = (device or {}).get("model")
583
+ item = record(id=udid, kind="usb", name=(device or {}).get("name") or saved_name, udid=udid, model=model,
584
+ model_name=MODEL_NAMES.get(model), ios=(device or {}).get("ios"),
585
+ enclosure=(device or {}).get("enclosure"), state=state, reason=reason,
586
+ wda_url_value=url, mjpeg_url=mjpeg, primary=is_primary,
587
+ set_up=manager is not None and manager.built())
588
+ if wifi is not None:
589
+ item["transport"] = "wifi" if over_wifi else "usb" if udid in by_udid else None
590
+ return item
591
+
592
+ chosen = primary.device(connected)
593
+ primary_udid = primary.settings().get("udid") or (chosen or {}).get("udid")
594
+ if primary_udid:
595
+ add(usb(primary_udid, primary, wda_url(WDA_PORT), wda_url(MJPEG_BASE), True,
596
+ primary.settings().get("device_name")))
597
+ for entry in extras:
598
+ if entry["udid"] == primary_udid:
599
+ continue
600
+ add(usb(entry["udid"], manager_for(entry["udid"], data_dir), wda_url(entry["wdaPort"]),
601
+ wda_url(entry["mjpegPort"]), False, entry.get("name")))
602
+ for device in connected:
603
+ if device["udid"] in claimed or device["udid"] == primary_udid:
604
+ continue
605
+ add(record(id=device["udid"], kind="usb", name=device.get("name"), udid=device["udid"],
606
+ model=device.get("model"), model_name=MODEL_NAMES.get(device.get("model")), ios=device.get("ios"),
607
+ enclosure=device.get("enclosure"), state="needs_setup", reason=REASONS["untrusted"]
608
+ if not device.get("trusted") else REASONS["not_built"],
609
+ wda_url_value=None, set_up=False))
610
+ if sims:
611
+ for item in simulator_records(probe=probe):
612
+ add(item)
613
+ for spec in static_specs():
614
+ add(static_record(spec.wda_url, id=spec.id, name=spec.label or None, mjpeg_url=spec.mjpeg_url or None,
615
+ **({} if probe else {"ready": True, "free": True})))
616
+ for url in extra_urls:
617
+ if url and url.rstrip("/") not in seen_urls:
618
+ add(static_record(url, **({} if probe else {"ready": True, "free": True})))
619
+ return records
620
+
621
+
622
+ def wifi_phones(data_dir):
623
+ from .wireless import store, transport_on
624
+ if not transport_on():
625
+ return None
626
+ try:
627
+ return store.enabled_udids(data_dir)
628
+ except Exception:
629
+ return set()
630
+
631
+
632
+ def manager_for(udid, data_dir=None):
633
+ """The DeviceManager of a USB phone set up besides the primary one (devices.json), or None."""
634
+ from .device_manager import DeviceManager
635
+ from .paths import user_data_dir
636
+ data_dir = Path(data_dir) if data_dir else user_data_dir()
637
+ entry = next((item for item in DeviceStore(data_dir).phones() if item["udid"] == udid), None)
638
+ if entry is None:
639
+ return None
640
+ return DeviceManager(data_dir, wda_url(entry["wdaPort"]), udid=udid, root=phone_folder(data_dir, udid),
641
+ video_port=entry["mjpegPort"])
642
+
643
+
644
+ def resolve(records, query):
645
+ text = str(query or "").strip()
646
+ if not text:
647
+ raise DeviceNotFound("Name a device: its id, UDID or name.")
648
+ folded = text.casefold()
649
+ for item in records:
650
+ if folded in {str(item.get("id") or "").casefold(), str(item.get("udid") or "").casefold()}:
651
+ return item
652
+ named = [item for item in records if str(item.get("name") or "").casefold() == folded]
653
+ if len(named) == 1:
654
+ return named[0]
655
+ if named:
656
+ raise AmbiguousDevice(f"{len(named)} devices are named “{text}”. Name one by its id: "
657
+ + ", ".join(item["id"] for item in named) + ".")
658
+ known = ", ".join(f"{item['name']} ({item['id']})" for item in records[:8])
659
+ raise DeviceNotFound(f"No device is named or has the id “{text}”." +
660
+ (f" Devices: {known}." if known else " Mobster sees no device: plug in an iPhone or "
661
+ "prepare a simulator (`mobster sim prepare`)."))
662
+
663
+
664
+ def describe(item):
665
+ """One line for a terminal: name, kind, state and reason."""
666
+ kind = {"usb": "iPhone (USB)", "simulator": "Simulator", "wda": "WebDriverAgent",
667
+ "android": "Android emulator" if item.get("emulator") else "Android phone"}.get(item["kind"], item["kind"])
668
+ if item["kind"] == "usb" and item.get("transport") == "wifi":
669
+ kind = "iPhone (Wi-Fi)"
670
+ details = ", ".join(part for part in (item.get("modelName") or item.get("model"),
671
+ f"iOS {item['ios']}" if item.get("ios") else None) if part)
672
+ state = item["state"].replace("_", " ")
673
+ line = f"{item['name']} [{kind}{', ' + details if details else ''}] {state}"
674
+ if item.get("reason"):
675
+ line += f": {item['reason']}"
676
+ return line