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/fanout.py ADDED
@@ -0,0 +1,663 @@
1
+ """One task on several phones at once: one run per phone through Runtime.create, shown and summarized as one task."""
2
+
3
+ import contextlib
4
+ import ctypes
5
+ import ctypes.util
6
+ import json
7
+ import logging
8
+ import os
9
+ import re
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ import uuid
14
+ from pathlib import Path
15
+
16
+ from .api_errors import APIError
17
+
18
+ log = logging.getLogger("mobster.fanout")
19
+
20
+ MAX_PHONES = 100
21
+ SOFT_LIMIT = 10
22
+ CPU_HOLD = 85.0
23
+ HOLD_MAX_S = 300.0
24
+ HOLD_POLL_S = 1.0
25
+ HOLD_STAGGER_S = 1.0
26
+ KEEP = 200
27
+ GROUP_ID = re.compile(r"[a-f0-9]{12}")
28
+ STORE = "groups.json"
29
+ DONE = frozenset({"completed"})
30
+
31
+ HELD = "Your Mac is busy (CPU {cpu:.0f}%). Mobster starts the other phones when it drops below {limit:.0f}%."
32
+ GAVE_UP = "Your Mac stayed busy, so this phone didn't start. Nothing ran on it. Try again when the Mac is less busy."
33
+ SOFT = ("{count} tasks at once is more than {limit}. Each phone streams its screen and sends screenshots, so a busy "
34
+ "Mac or USB hub can slow every task down.")
35
+
36
+
37
+ class MacLoad:
38
+ MIN_GAP = 0.5
39
+
40
+ def __init__(self):
41
+ self.lock = threading.Lock()
42
+ self.last = None
43
+ self.value = None
44
+ self._host = self._libc = None
45
+ try:
46
+ libc = ctypes.CDLL(ctypes.util.find_library("System") or "/usr/lib/libSystem.B.dylib")
47
+ libc.mach_host_self.restype = ctypes.c_uint
48
+ self._host, self._libc = libc.mach_host_self(), libc
49
+ except (OSError, AttributeError):
50
+ pass
51
+
52
+ def ticks(self):
53
+ if self._libc is not None:
54
+ info = (ctypes.c_uint * 4)() # user, system, idle, nice (HOST_CPU_LOAD_INFO)
55
+ count = ctypes.c_uint(4)
56
+ if self._libc.host_statistics(self._host, 3, ctypes.byref(info), ctypes.byref(count)) == 0:
57
+ user, system, idle, nice = list(info)
58
+ return user + system + nice, user + system + idle + nice
59
+ try:
60
+ with open("/proc/stat") as stream:
61
+ values = [int(part) for part in stream.readline().split()[1:8]]
62
+ idle = values[3] + values[4]
63
+ return sum(values) - idle, sum(values)
64
+ except (OSError, ValueError, IndexError):
65
+ return None
66
+
67
+ def percent(self):
68
+ with self.lock:
69
+ now = time.monotonic()
70
+ if self.last is not None and now - self.last[0] < self.MIN_GAP and self.value is not None:
71
+ return self.value
72
+ ticks = self.ticks()
73
+ if ticks is None:
74
+ try:
75
+ self.value = min(100.0, os.getloadavg()[0] / (os.cpu_count() or 1) * 100)
76
+ except OSError:
77
+ self.value = 0.0
78
+ return self.value
79
+ if self.last is None:
80
+ # The first read has nothing to compare with: the load average stands in.
81
+ self.last = (now, ticks)
82
+ try:
83
+ self.value = round(min(100.0, os.getloadavg()[0] / (os.cpu_count() or 1) * 100), 1)
84
+ except OSError:
85
+ self.value = 0.0
86
+ return self.value
87
+ first = self.last[1]
88
+ busy = (ticks[0] - first[0]) % 2 ** 32 if ticks[0] < first[0] else ticks[0] - first[0]
89
+ total = (ticks[1] - first[1]) % 2 ** 32 if ticks[1] < first[1] else ticks[1] - first[1]
90
+ if total > 0:
91
+ self.value = round(100.0 * busy / total, 1)
92
+ elif self.value is None:
93
+ self.value = 0.0
94
+ self.last = (time.monotonic(), ticks)
95
+ return self.value
96
+
97
+
98
+ BULLET = re.compile(r"^\s*(?:[-*•·–]|\d{1,3}[.)])\s+")
99
+ TRIM = re.compile(r"^[\W_]+|[\W_]+$", re.UNICODE)
100
+
101
+
102
+ def _key(line):
103
+ return TRIM.sub("", " ".join(BULLET.sub("", line).split()).casefold())
104
+
105
+
106
+ def answer_of(summary):
107
+ summary = summary if isinstance(summary, dict) else {}
108
+ data = summary.get("data")
109
+ if isinstance(data, str) and data.strip():
110
+ return data.strip()
111
+ full = summary.get("full_answer") or summary.get("answer")
112
+ return full.strip() if isinstance(full, str) and full.strip() else None
113
+
114
+
115
+ def merge_results(results):
116
+ answers, lines, order, rows, seen_rows, tables = [], {}, [], [], set(), True
117
+ completed, cost, priced = 0, 0.0, False
118
+ for item in results:
119
+ summary = item.get("summary") if isinstance(item.get("summary"), dict) else {}
120
+ status = item.get("status") or summary.get("status") or "error"
121
+ text = answer_of(summary)
122
+ answers.append({"device": item.get("device"), "deviceName": item.get("deviceName"),
123
+ **({"profile": item["profile"], "profileName": item.get("profileName")}
124
+ if item.get("profile") else {}), "status": status,
125
+ "answer": text[:3000] if text else None,
126
+ "reason": None if text or status in DONE else (str(summary.get("reason") or "")[:500] or None)})
127
+ if isinstance(item.get("costUsd"), (int, float)):
128
+ cost, priced = cost + float(item["costUsd"]), True
129
+ if status not in DONE:
130
+ continue
131
+ completed += 1
132
+ data = summary.get("data")
133
+ if isinstance(data, list) and all(isinstance(row, dict) for row in data):
134
+ for row in data:
135
+ key = json.dumps(row, sort_keys=True, ensure_ascii=False, default=str)
136
+ if key not in seen_rows:
137
+ seen_rows.add(key)
138
+ rows.append(row)
139
+ else:
140
+ tables = False
141
+ for line in (text or "").splitlines():
142
+ key = _key(line)
143
+ # A heading ("Your notes:") introduces each phone's list; the lines under it are what they found.
144
+ if not key or line.rstrip().endswith(":"):
145
+ continue
146
+ if key not in lines:
147
+ lines[key] = {"text": BULLET.sub("", line).strip(), "phones": []}
148
+ order.append(key)
149
+ if item.get("device") not in lines[key]["phones"]:
150
+ lines[key]["phones"].append(item.get("device"))
151
+ return {"phones": len(results), "completed": completed, "answers": answers,
152
+ "lines": [lines[key] for key in order][:400], "rows": rows[:1000] if tables and completed else None,
153
+ "costUsd": round(cost, 6) if priced else None}
154
+
155
+
156
+ PART_STATUS = re.compile(r"[a-z_]{1,32}")
157
+ MAX_PART_ROWS = 1000
158
+
159
+
160
+ def merge_parts(parts):
161
+ """One summary for a task run as one group on each of several Macs (the console's fan-out across Macs): ``parts``
162
+ are the groups' summaries as the console shows them, phones by team id ({answers: [{device, deviceName, status,
163
+ answer, reason}], rows, costUsd}). The answers are merged as ``merge_results`` merges one Mac's phones; the rows
164
+ are every part's once, when every part has rows; the cost is the sum of the parts' known costs."""
165
+ results, rows, seen, tables, cost, priced = [], [], set(), True, 0.0, False
166
+ for part in parts:
167
+ for item in part["answers"]:
168
+ summary = {"status": item["status"], "answer": item.get("answer"), "reason": item.get("reason")}
169
+ results.append({"device": item["device"], "deviceName": item.get("deviceName"), "status": item["status"],
170
+ "summary": summary})
171
+ if isinstance(part.get("rows"), list):
172
+ for row in part["rows"]:
173
+ key = json.dumps(row, sort_keys=True, ensure_ascii=False, default=str)
174
+ if key not in seen:
175
+ seen.add(key)
176
+ rows.append(row)
177
+ else:
178
+ tables = False
179
+ if isinstance(part.get("costUsd"), (int, float)):
180
+ cost, priced = cost + float(part["costUsd"]), True
181
+ merged = merge_results(results)
182
+ merged["rows"] = rows[:MAX_PART_ROWS] if tables and merged["completed"] else None
183
+ merged["costUsd"] = round(cost, 6) if priced else None
184
+ return merged
185
+
186
+
187
+ def new_id():
188
+ return uuid.uuid4().hex[:12]
189
+
190
+
191
+ def retained_groups(groups):
192
+ finished = [group for group in groups if group.get("finishedAt") is not None]
193
+ kept = {group["id"] for group in finished[-KEEP:]}
194
+ return [group for group in groups if group.get("finishedAt") is None or group["id"] in kept]
195
+
196
+
197
+ class GroupStore:
198
+ def __init__(self, folder):
199
+ self.path = Path(folder) / STORE if folder else None
200
+
201
+ def read(self):
202
+ if self.path is None:
203
+ return []
204
+ try:
205
+ data = json.loads(self.path.read_text())
206
+ except (OSError, ValueError):
207
+ return []
208
+ groups = data.get("groups") if isinstance(data, dict) else None
209
+ return [group for group in groups or [] if isinstance(group, dict)
210
+ and isinstance(group.get("id"), str) and GROUP_ID.fullmatch(group["id"])
211
+ and isinstance(group.get("members"), list)]
212
+
213
+ def write(self, groups):
214
+ if self.path is None:
215
+ return
216
+ try:
217
+ self.path.parent.mkdir(parents=True, exist_ok=True)
218
+ fd, temporary = tempfile.mkstemp(prefix=".groups.", suffix=".json", dir=self.path.parent)
219
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
220
+ os.fchmod(stream.fileno(), 0o600)
221
+ stream.write(json.dumps({"version": 1, "groups": retained_groups(groups)}, ensure_ascii=False))
222
+ os.replace(temporary, self.path)
223
+ except OSError:
224
+ log.warning("mobster: could not save the task groups")
225
+ with contextlib.suppress(OSError, UnboundLocalError):
226
+ os.unlink(temporary)
227
+
228
+
229
+ class Fanout:
230
+ def __init__(self, runtime, load=None):
231
+ self.runtime = runtime
232
+ state_db = getattr(runtime.config, "state_db", None)
233
+ self.store = GroupStore(Path(state_db).parent if isinstance(state_db, str) and state_db else None)
234
+ self.lock = threading.RLock()
235
+ self.groups = {group["id"]: group for group in self.store.read()}
236
+ self.keys = {group["key"]: group["id"] for group in self.groups.values() if group.get("key")}
237
+ self.load = load or MacLoad()
238
+ self.wake = threading.Event()
239
+ self.dispatcher = None
240
+ self.closed = False
241
+ # Phones held by a group that was interrupted by a restart never start later: say so.
242
+ for group in list(self.groups.values()):
243
+ for member in group["members"]:
244
+ if member.get("state") in ("held", "pending"):
245
+ member.update(state="refused", code="interrupted",
246
+ error="Mobster restarted before this phone's task was confirmed. "
247
+ "Inspect the phone before starting another task; it was not retried.")
248
+ self.finish_if_done(group, notify=False)
249
+
250
+ def running(self):
251
+ with self.runtime.lock:
252
+ return len(self.runtime.active_runs)
253
+
254
+ def limits(self):
255
+ cpu = self.load.percent()
256
+ return {"running": self.running(), "softLimit": SOFT_LIMIT, "cpuPercent": cpu, "holdAbovePercent": CPU_HOLD,
257
+ "holding": cpu > CPU_HOLD, "maxPhones": MAX_PHONES}
258
+
259
+ def get(self, group_id):
260
+ with self.lock:
261
+ group = self.groups.get(group_id)
262
+ if group is None:
263
+ raise APIError("That task group doesn't exist here.", 404, "group_not_found")
264
+ return group
265
+
266
+ def has_request(self, key):
267
+ with self.lock:
268
+ return key in self.keys
269
+
270
+ def public(self, group):
271
+ runs = self.runtime.runs
272
+ members, counts = [], {"phones": 0, "running": 0, "waiting": 0, "completed": 0, "ended": 0, "refused": 0,
273
+ "held": 0}
274
+ for member in group["members"]:
275
+ run = runs.get(member.get("runId")) if member.get("runId") else None
276
+ entry = {key: member.get(key) for key in ("device", "deviceName", "runId", "state", "error", "code")}
277
+ if member.get("profile"):
278
+ entry.update(profile=member["profile"], profileName=member.get("profileName"))
279
+ approval = None
280
+ if run is not None:
281
+ with run.condition:
282
+ approval = dict(run.approval) if run.approval else None
283
+ entry.update(status=run.status, finishedAt=run.finished_at * 1000 if run.finished_at else None,
284
+ costUsd=run.cost_usd, approval=approval, estimateUsd=run.estimate_usd)
285
+ else:
286
+ entry.update(status=None, finishedAt=None, costUsd=member.get("costUsd"), approval=None,
287
+ estimateUsd=None)
288
+ counts["phones"] += 1
289
+ if member["state"] == "refused":
290
+ counts["refused"] += 1
291
+ elif member["state"] == "held":
292
+ counts["held"] += 1
293
+ elif run is not None and run.finished_at is None:
294
+ counts["waiting" if approval else "running"] += 1
295
+ elif run is not None and run.status in DONE or member.get("status") in DONE:
296
+ counts["completed"] += 1
297
+ else:
298
+ counts["ended"] += 1
299
+ members.append(entry)
300
+ state = ("waiting" if counts["waiting"] else "running" if counts["running"] else "held" if counts["held"]
301
+ else "finished" if group.get("finishedAt") is not None else "running")
302
+ estimates = [item["estimateUsd"] for item in members if isinstance(item.get("estimateUsd"), (int, float))]
303
+ estimate = group.get("estimateUsd") if group.get("estimateUsd") is not None else (
304
+ max(estimates) if estimates else None)
305
+ started = sum(1 for item in members if item["state"] != "refused")
306
+ return {"id": group["id"], "goal": group["goal"], "appId": group.get("appId"), "engine": group.get("engine"),
307
+ "origin": group.get("origin"), "threadId": group.get("threadId"), "createdAt": group["createdAt"],
308
+ "finishedAt": group.get("finishedAt"), "state": state, "counts": counts, "members": members,
309
+ "warning": group.get("warning"), "held": group.get("held"),
310
+ "estimateUsd": estimate,
311
+ "estimateTotalUsd": round(estimate * started, 6) if estimate is not None else None,
312
+ "summary": group.get("summary"),
313
+ **({"teamGroup": group["teamGroup"]} if group.get("teamGroup") else {})}
314
+
315
+ def recent(self, limit=20):
316
+ with self.lock:
317
+ groups = sorted(self.groups.values(), key=lambda group: group["createdAt"], reverse=True)[:limit]
318
+ return [self.public(group) for group in groups]
319
+
320
+ def resolve(self, phones):
321
+ if not isinstance(phones, list) or not 1 <= len(phones) <= MAX_PHONES:
322
+ raise ValueError(f"phones is a list of 1 to {MAX_PHONES} devices (their ids, UDIDs or names)")
323
+ if not all(isinstance(item, str) and 1 <= len(item.strip()) <= 128 for item in phones):
324
+ raise ValueError("Name each phone by its id, UDID or name")
325
+ fleet = getattr(self.runtime, "fleet", None)
326
+ if fleet is None or getattr(self.runtime, "pool", None) is not None:
327
+ raise ValueError("This Mobster runs every task on its own device; leave phones out")
328
+ resolved, seen = [], set()
329
+ for name in phones:
330
+ phone = self.runtime.resolve_device(name)
331
+ if phone.id in seen:
332
+ continue
333
+ seen.add(phone.id)
334
+ resolved.append(phone)
335
+ return resolved
336
+
337
+ def start(self, phones, app_id, goal, *, idempotency_key=None, fingerprint=None, thread_id=None, team_group=None,
338
+ **create):
339
+ """Start ``goal`` on every phone of ``phones``: (group, replayed). ``team_group``: the console's id for the
340
+ same task on its other Macs' phones (12 hex), kept on the group."""
341
+ if team_group is not None and (not isinstance(team_group, str) or not GROUP_ID.fullmatch(team_group)):
342
+ raise ValueError("teamGroup is 12 lowercase hex characters")
343
+ resolved = self.resolve(phones)
344
+ members = [{"device": phone.id, "deviceName": phone.name, "runId": None, "state": "pending",
345
+ "error": None, "code": None} for phone in resolved]
346
+ return self._open(members, app_id, goal, idempotency_key, fingerprint, thread_id, create, team_group=team_group)
347
+
348
+ def start_profiles(self, profiles, app_id, goal, *, idempotency_key=None, fingerprint=None, thread_id=None,
349
+ **create):
350
+ # Profiles that share a phone run in turn. Nothing is retried: an unknown outcome is that profile's result.
351
+ resolved = self.resolve_profiles(profiles)
352
+ members, first_on = [], {}
353
+ for account in resolved:
354
+ device = account.get("device")
355
+ member = {"device": device, "deviceName": account.get("deviceName"), "profile": account["id"],
356
+ "profileName": account.get("handle"), "runId": None, "state": "pending", "error": None,
357
+ "code": None}
358
+ if not device:
359
+ member.update(state="refused", code="no_phone",
360
+ error=f"@{str(account.get('handle') or '').lstrip('@')} "
361
+ "has no phone yet. Choose its phone on the Profiles page, then run it again.")
362
+ elif device in first_on:
363
+ member["state"] = "queued" # starts when the profile ahead of it on this phone has finished
364
+ else:
365
+ first_on[device] = member
366
+ members.append(member)
367
+ return self._open(members, app_id, goal, idempotency_key, fingerprint, thread_id, create)
368
+
369
+ def resolve_profiles(self, profiles):
370
+ from .accounts.store import AccountNotFound, AmbiguousAccount, resolve, store_for
371
+ stored = store_for(self.runtime).all()
372
+ if profiles == "all":
373
+ chosen = list(stored)
374
+ else:
375
+ if not isinstance(profiles, list) or not 1 <= len(profiles) <= MAX_PHONES or not all(
376
+ isinstance(item, str) and 1 <= len(item.strip()) <= 128 for item in profiles):
377
+ raise ValueError(f"profiles is \"all\", or a list of 1 to {MAX_PHONES} profiles (their ids or handles)")
378
+ chosen = []
379
+ for name in profiles:
380
+ try:
381
+ chosen.append(resolve(stored, name))
382
+ except (AccountNotFound, AmbiguousAccount) as error:
383
+ raise ValueError(str(error)) from None
384
+ seen, out = set(), []
385
+ for account in chosen:
386
+ if account["id"] not in seen:
387
+ seen.add(account["id"])
388
+ out.append(account)
389
+ if not out:
390
+ raise ValueError("There are no profiles yet. Add one on the Profiles page, then run it on every profile.")
391
+ return out[:MAX_PHONES]
392
+
393
+ def _open(self, members, app_id, goal, idempotency_key, fingerprint, thread_id, create, *, team_group=None):
394
+ if not isinstance(goal, str) or not 1 <= len(goal.strip()) <= 4000:
395
+ raise ValueError("Choose an app, a task (1–4000 characters), and demo/live mode")
396
+ resolved = members
397
+ with self.lock:
398
+ if idempotency_key:
399
+ prior = self.keys.get(idempotency_key)
400
+ if prior is not None and prior in self.groups:
401
+ if self.groups[prior].get("fingerprint") != fingerprint:
402
+ raise APIError("This request key belongs to a different task", code="idempotency_conflict")
403
+ return self.groups[prior], True
404
+ running = self.running()
405
+ group = {"id": new_id(), "key": idempotency_key, "fingerprint": fingerprint, "goal": goal.strip(),
406
+ "appId": app_id, "engine": create.get("engine"), "origin": create.get("origin", "api"),
407
+ "threadId": thread_id, "createdAt": time.time() * 1000, "finishedAt": None, "summary": None,
408
+ **({"teamGroup": team_group} if team_group else {}),
409
+ "warning": SOFT.format(count=running + len(resolved), limit=SOFT_LIMIT)
410
+ if running + len(resolved) > SOFT_LIMIT else None, "held": None, "estimateUsd": None,
411
+ "members": members,
412
+ "create": {"app_id": app_id, "goal": goal.strip(), **{key: value for key, value in create.items()
413
+ if key in RUN_KEYS}}}
414
+ self.groups[group["id"]] = group
415
+ if idempotency_key:
416
+ self.keys[idempotency_key] = group["id"]
417
+ cpu = self.load.percent()
418
+ if cpu > CPU_HOLD:
419
+ with self.lock:
420
+ for member in group["members"]:
421
+ if member["state"] == "pending":
422
+ member["state"] = "held"
423
+ if any(member["state"] == "held" for member in group["members"]):
424
+ group["held"] = {"cpuPercent": cpu, "since": time.time() * 1000,
425
+ "reason": HELD.format(cpu=cpu, limit=CPU_HOLD)}
426
+ else:
427
+ for member in group["members"]:
428
+ self._admit(group, member)
429
+ with self.lock:
430
+ self.save()
431
+ if not any(member["state"] in ("started", "held", "queued") for member in group["members"]):
432
+ # Nothing started: fail as a one-phone start would, with the first refusal.
433
+ with self.lock:
434
+ self.groups.pop(group["id"], None)
435
+ if idempotency_key:
436
+ self.keys.pop(idempotency_key, None)
437
+ self.save()
438
+ first = group["members"][0]
439
+ raise APIError(first.get("error") or "None of those phones could start this task.", 409,
440
+ first.get("code") or "group_refused",
441
+ refused=[{key: member.get(key) for key in ("device", "deviceName", "error", "code")}
442
+ for member in group["members"]])
443
+ self.notify(group, "created")
444
+ if group["held"] is not None:
445
+ self._dispatch()
446
+ return group, False
447
+
448
+ def _admit_queued(self, group, member):
449
+ with self.lock:
450
+ if member["state"] != "queued":
451
+ return
452
+ member["state"] = "pending"
453
+ self._admit(group, member)
454
+ with self.lock:
455
+ self.save()
456
+ self.notify(group, "updated")
457
+
458
+ def _admit(self, group, member):
459
+ # Stop uses this same lock: it either refuses this member before admission or sees its new run id.
460
+ with self.lock:
461
+ if self.closed or member["state"] not in ("pending", "held"):
462
+ return None
463
+ create = dict(group["create"])
464
+ if member.get("profile"):
465
+ # A task as this profile: its own phone, the profile confirmed on screen before it commits.
466
+ create["extras"] = {**(create.get("extras") or {}), "account": member["profile"]}
467
+ create["engine"] = create.get("engine") or "smart"
468
+ try:
469
+ run = self.runtime.create(create.pop("app_id"), create.pop("goal"), "live", device=member["device"],
470
+ group_id=group["id"], **create)
471
+ except APIError as error:
472
+ member.update(state="refused", error=str(error), code=error.code)
473
+ return None
474
+ except ValueError as error:
475
+ member.update(state="refused", error=str(error), code="invalid")
476
+ return None
477
+ run = run[0] if isinstance(run, tuple) else run
478
+ member.update(state="started", runId=run.id, deviceName=run.device_name or member.get("deviceName"),
479
+ error=None, code=None)
480
+ if group.get("estimateUsd") is None and run.estimate_usd is not None:
481
+ group["estimateUsd"] = run.estimate_usd
482
+ return run
483
+
484
+ def _dispatch(self):
485
+ self.wake.set()
486
+ with self.lock:
487
+ if self.dispatcher is not None and self.dispatcher.is_alive():
488
+ return
489
+ self.dispatcher = threading.Thread(target=self._held_loop, name="mobster-fanout-held", daemon=True)
490
+ self.dispatcher.start()
491
+
492
+ def _held_loop(self):
493
+ while not self.closed:
494
+ with self.lock:
495
+ waiting = [(group, member) for group in self.groups.values() for member in group["members"]
496
+ if member["state"] == "held"]
497
+ if not waiting:
498
+ self.dispatcher = None
499
+ return
500
+ group, member = waiting[0]
501
+ held = group.get("held") or {}
502
+ since = held.get("since") or time.time() * 1000
503
+ if time.time() * 1000 - since > HOLD_MAX_S * 1000:
504
+ with self.lock:
505
+ if member["state"] == "held":
506
+ member.update(state="refused", code="mac_busy", error=GAVE_UP)
507
+ self._settle(group)
508
+ continue
509
+ cpu = self.load.percent()
510
+ if cpu <= CPU_HOLD and not getattr(self.runtime, "closing", False):
511
+ self._admit(group, member)
512
+ self._settle(group)
513
+ # The next phone waits a moment, so the CPU read before it sees this one's start.
514
+ self.wake.clear()
515
+ self.wake.wait(HOLD_STAGGER_S)
516
+ continue
517
+ with self.lock:
518
+ if group.get("held") is not None:
519
+ group["held"].update(cpuPercent=cpu, reason=HELD.format(cpu=cpu, limit=CPU_HOLD))
520
+ self.wake.clear()
521
+ self.wake.wait(HOLD_POLL_S)
522
+
523
+ def _settle(self, group):
524
+ with self.lock:
525
+ if not any(member["state"] == "held" for member in group["members"]):
526
+ group["held"] = None
527
+ self.save()
528
+ self.notify(group, "updated")
529
+ self.finish_if_done(group)
530
+
531
+ def stop(self, group_id):
532
+ group = self.get(group_id)
533
+ with self.lock:
534
+ for member in group["members"]:
535
+ if member["state"] in ("held", "pending", "queued"):
536
+ member.update(state="refused", code="stopped", error="Stopped before it started on this phone.")
537
+ for member in group["members"]:
538
+ if member.get("runId") and member["runId"] in self.runtime.runs:
539
+ with contextlib.suppress(APIError):
540
+ self.runtime.stop_run(member["runId"])
541
+ with self.lock:
542
+ group["held"] = None
543
+ self.save()
544
+ self.notify(group, "updated")
545
+ self.finish_if_done(group)
546
+ return group
547
+
548
+ def answer_approvals(self, group_id, approve, approvals):
549
+ # Only an approval still waiting with exactly the id the person saw is answered.
550
+ group = self.get(group_id)
551
+ if type(approve) is not bool:
552
+ raise ValueError("approve is true or false")
553
+ if not isinstance(approvals, dict) or not 1 <= len(approvals) <= MAX_PHONES:
554
+ raise ValueError("approvals maps each phone's run id to the approval id that was shown")
555
+ members = {member.get("runId"): member for member in group["members"] if member.get("runId")}
556
+ requests = []
557
+ for run_id, approval_id in approvals.items():
558
+ if run_id not in members or not isinstance(approval_id, str):
559
+ raise ValueError("Each approval belongs to one of this group's runs")
560
+ run = self.runtime.runs.get(run_id)
561
+ if run is not None:
562
+ with run.condition:
563
+ pending = dict(run.approval) if run.approval else None
564
+ if pending is not None and pending.get("id") == approval_id and pending.get("kind") == "clarify":
565
+ raise ValueError("Mobster's questions are answered one at a time, on their phone's tab")
566
+ requests.append((run_id, approval_id, run))
567
+ # Validate the whole request before answering any card: a later invalid entry must not approve an earlier one.
568
+ out = []
569
+ for run_id, approval_id, run in requests:
570
+ answered = bool(run is not None and run.answer_approval(approval_id, approve))
571
+ out.append({"runId": run_id, "device": members[run_id].get("device"), "answered": answered,
572
+ "decision": None if answered or run is None else run.approval_decision(approval_id)})
573
+ return out
574
+
575
+ def member_finished(self, run):
576
+ group_id = getattr(run, "group_id", None)
577
+ if not group_id:
578
+ return
579
+ with self.lock:
580
+ group = self.groups.get(group_id)
581
+ if group is None:
582
+ return
583
+ following = None
584
+ for member in group["members"]:
585
+ if member.get("runId") == run.id:
586
+ member.update(status=run.status, costUsd=run.cost_usd)
587
+ following = next((other for other in group["members"] if other["state"] == "queued"
588
+ and other.get("device") == member.get("device")), None)
589
+ self.save()
590
+ if following is not None:
591
+ self._admit_queued(group, following)
592
+ if not self.finish_if_done(group):
593
+ self.notify(group, "updated")
594
+
595
+ def finish_if_done(self, group, *, notify=True):
596
+ runs = self.runtime.runs
597
+ with self.lock:
598
+ if group.get("finishedAt") is not None:
599
+ return False
600
+ for member in group["members"]:
601
+ if member["state"] in ("held", "pending", "queued"):
602
+ return False
603
+ run = runs.get(member.get("runId")) if member.get("runId") else None
604
+ if run is not None and run.finished_at is None:
605
+ return False
606
+ results = []
607
+ for member in group["members"]:
608
+ run = runs.get(member.get("runId")) if member.get("runId") else None
609
+ results.append({"device": member["device"], "deviceName": member.get("deviceName"),
610
+ "profile": member.get("profile"), "profileName": member.get("profileName"),
611
+ "status": run.status if run is not None else ("refused" if member["state"] == "refused"
612
+ else member.get("status")),
613
+ "summary": (run.summary if run is not None else None) or (
614
+ {"status": "refused", "reason": member.get("error")}
615
+ if member["state"] == "refused" else {}),
616
+ "costUsd": run.cost_usd if run is not None else member.get("costUsd")})
617
+ group["summary"] = merge_results(results)
618
+ group["finishedAt"] = time.time() * 1000
619
+ self.save()
620
+ if notify:
621
+ self.notify(group, "finished")
622
+ return True
623
+
624
+ def notify(self, group, event):
625
+ self.runtime.publish("groups", {"event": f"group_{event}", "groupId": group["id"],
626
+ **({"threadId": group["threadId"]} if group.get("threadId") else {})})
627
+ public = self.public(group)
628
+ for listener in list(LISTENERS):
629
+ try:
630
+ listener(self.runtime, public, event)
631
+ except Exception: # noqa: BLE001 -- a listener never changes the group
632
+ log.exception("a group listener failed")
633
+
634
+ def save(self):
635
+ # Call with ``lock`` held. A held run's ``create`` options stay in memory only.
636
+ ordered = sorted(self.groups.values(), key=lambda group: group["createdAt"])
637
+ retained = retained_groups(ordered)
638
+ if len(retained) < len(ordered):
639
+ kept = {group["id"] for group in retained}
640
+ for old in ordered:
641
+ if old["id"] in kept:
642
+ continue
643
+ self.groups.pop(old["id"], None)
644
+ if old.get("key"):
645
+ self.keys.pop(old["key"], None)
646
+ self.store.write([{key: value for key, value in group.items() if key != "create"} for group in retained])
647
+
648
+ def close(self):
649
+ self.closed = True
650
+ self.wake.set()
651
+
652
+
653
+ RUN_KEYS = frozenset({"output_schema", "output_format", "allowed_bundles", "engine", "extras", "origin"})
654
+
655
+ # Called with (runtime, public group, event) on "created", "updated" and "finished".
656
+ LISTENERS = []
657
+
658
+
659
+ def add_listener(fn):
660
+ if not callable(fn):
661
+ raise ValueError("A group listener is a function")
662
+ if fn not in LISTENERS:
663
+ LISTENERS.append(fn)
@@ -0,0 +1,2 @@
1
+ """In-app feedback (docs/feedback.mdx): the diagnostics bundle (bundle.py), sending and the outbox (outbox.py), and the
2
+ local API the Mac app calls (routes.py)."""