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,901 @@
1
+ """iOSWorld, run by Mobster: the published iOS agent benchmark, on its own terms.
2
+
3
+ iOSWorld (arXiv 2606.09764; github.com/ljang0/iOSWorld, CC BY 4.0): 133 tasks over 26
4
+ SwiftUI apps on an iPhone 17 Pro simulator, scored by its own LLM judge (gpt-5.4-mini by
5
+ default) from each run's trajectory -- per-step screenshots, actions, final answer --
6
+ against the task's rubric. Published best: 51.9% (Claude Opus 4.6 + XCUITest tree).
7
+
8
+ What is iOSWorld's and unchanged: the tasks (goals verbatim), the start state (its own
9
+ ``reset_app_data`` + ``reseed_apps``, then Home, as its runner does), the 50-step limit,
10
+ the trajectory format and the judge (``scripts/judge_trajectories.py``, run as is).
11
+
12
+ What is Mobster's: its agent (mobile_agent.frontier.FrontierAgent, composed as the app's
13
+ Smart engine with FrameClock), driving the simulator through WDA, capped per task by
14
+ ``task_cost_cap``. The task's app list becomes the apps the agent may open -- iOSWorld hands
15
+ its own agent the same list -- and the first one is opened, recorded as a ``launch_app`` step
16
+ as iOSWorld's agent would take it. ``--policy`` takes one value, ``frontier``, the default.
17
+
18
+ Screenshots exist only for the judge: taken with ``simctl io`` (never through WDA, never
19
+ shown to Mobster) before each action and once at the end; their time is excluded from
20
+ Mobster's reported latency. The simulator runs headless: nothing here opens a window.
21
+
22
+ python -m mobile_agent.bench.iosworld run --repo ../iOSWorld --udid <UDID> \\
23
+ --wda-url http://127.0.0.1:8200 --mjpeg-url http://127.0.0.1:9200 \\
24
+ --out research/iosworld-runs/NAME [--only caltrack-001,...] [--limit N] --env-file <agent.env> \\
25
+ [--policy frontier --model gpt-5.6-luna --reasoning low]
26
+ python -m mobile_agent.bench.iosworld pool --repo ../iOSWorld --sims UDID:WDA_PORT:MJPEG_PORT,... \\
27
+ --out research/iosworld-runs/NAME [--only ...] [--limit N] [--policy frontier ...] --env-file <agent.env>
28
+ python -m mobile_agent.bench.iosworld judge --repo ../iOSWorld --run research/iosworld-runs/NAME
29
+ python -m mobile_agent.bench.iosworld report --run research/iosworld-runs/NAME
30
+ python -m mobile_agent.bench.iosworld compare --a research/iosworld-runs/OLD-* --b research/iosworld-runs/NEW-*
31
+ python -m mobile_agent.bench.iosworld estimate research/iosworld-runs/* --model gpt-5.6-sol
32
+
33
+ A task passes by iOSWorld's own rule: every rubric criterion satisfied (judge_trajectories.py
34
+ ``_aggregate``), not the judge's ``success`` flag; see bench/iosworld_stats.py.
35
+ """
36
+
37
+ import argparse
38
+ import base64
39
+ import dataclasses
40
+ import glob
41
+ import json
42
+ import os
43
+ import pathlib
44
+ import plistlib
45
+ import re
46
+ import statistics
47
+ import subprocess
48
+ import sys
49
+ import time
50
+ import urllib.request
51
+
52
+ from ..engines import SMART_CONFIG, frontier_kwargs
53
+ from ..sim import simctl
54
+ from . import tape
55
+
56
+ # The frontier policy is the app's Smart engine: its model, reasoning, and limits come from
57
+ # engines.SMART_CONFIG (tests/test_engines.py proves both build the same agent).
58
+ DEFAULT_FRONTIER_MODEL = SMART_CONFIG.model
59
+ MAX_TASK_COST_USD = 0.60 # a runaway task stops here (24 Sep: 50-step loops cost ~$0.80 each on gpt-5.5)
60
+
61
+
62
+ def task_cost_cap(bundles, model=None):
63
+ """A spend cap that grows with the apps a task spans: a flat $0.25 cut five-app tasks off
64
+ mid-way (multi-006, mem-005, 24 Sep) while one-app tasks finish for $0.03-0.13. It is set in
65
+ gpt-5.6-terra dollars and scaled up by a model's input price, so a pricier model gets as many
66
+ turns: iOSWorld limits turns, not spend (the cap stopped 9 of 21 gpt-5.6-sol runs, 28 Sep).
67
+ None, no cap, for a model Mobster has no price for (a provider's "<provider>:<model>"): the frontier would count it
68
+ at the highest listed rates, every cached token at the dearest input rate (model_clients.ceiling_cost_usd), and
69
+ stop it after a fraction of a priced model's turns. On 17 Oct a Surplus claude-sonnet-5.5 run stopped at a
70
+ "$2.63" ceiling count for $0.29 of tokens. Such a run is bounded by iOSWorld's own 50 steps and the 900 s limit
71
+ (engines.SMART_CONFIG); its spend is read from its tokens afterwards."""
72
+ from ..model_clients import PRICES, cost_usd
73
+ base, rate = PRICES.get("gpt-5.6-terra"), PRICES.get(model or "")
74
+ if rate is None and model and cost_usd(model, {"prompt_tokens": 1}) is None:
75
+ return None
76
+ scale = rate[0] / base[0] if base and rate and base[0] else 1.0
77
+ return min(MAX_TASK_COST_USD, 0.10 + 0.08 * max(1, len(bundles))) * max(1.0, scale)
78
+ TASK_DIR_WIDTH = 3
79
+ _APP_ALIASES = {"letterboxd": "cinephile", "whatsapp": "quickchat", "linkedin": "lockedin"}
80
+
81
+
82
+ def load_tasks(repo):
83
+ return json.loads((pathlib.Path(repo) / "tasks.json").read_text(encoding="utf-8"))
84
+
85
+
86
+ def load_manifest(repo):
87
+ """{app: {bundle_id, app_path, ...}} written by iOSWorld's bootstrap."""
88
+ for candidate in sorted(pathlib.Path(repo).rglob(".app_manifest.json")):
89
+ return json.loads(candidate.read_text(encoding="utf-8"))
90
+ raise FileNotFoundError("No .app_manifest.json: run iOSWorld's bootstrap first")
91
+
92
+
93
+ def register_names(manifest):
94
+ """Each app's display name (its Info.plist), so the agent's app-switch choices read "CalTrack", not a bundle."""
95
+ import plistlib
96
+
97
+ from ..catalog import register_app_names
98
+ names = {}
99
+ for key, entry in manifest.items():
100
+ name = key
101
+ try:
102
+ with open(pathlib.Path(entry.get("app_path", "")) / "Info.plist", "rb") as handle:
103
+ info = plistlib.load(handle)
104
+ name = info.get("CFBundleDisplayName") or info.get("CFBundleName") or key
105
+ except (OSError, ValueError, plistlib.InvalidFileException):
106
+ pass
107
+ if entry.get("bundle_id"):
108
+ names[entry["bundle_id"]] = name
109
+ register_app_names(names)
110
+ return names
111
+
112
+
113
+ def iosworld_module(repo):
114
+ """iOSWorld's own runner module (stdlib-only at import), for its reset functions."""
115
+ scripts = str(pathlib.Path(repo) / "scripts")
116
+ if scripts not in sys.path:
117
+ sys.path.insert(0, scripts)
118
+ import appium_agent
119
+ return appium_agent
120
+
121
+
122
+ def app_entry(manifest, name):
123
+ return manifest.get(_APP_ALIASES.get(name, name)) or manifest.get(name) or {}
124
+
125
+
126
+ SIM_DEVICES = pathlib.Path.home() / "Library/Developer/CoreSimulator/Devices"
127
+
128
+
129
+ def foreign_containers(udid, manifest):
130
+ """Apps whose data containers on ``udid`` resolve outside that simulator's own folder, as [(bundle, path)].
131
+ A ``simctl clone`` keeps the source's LaunchServices paths, so one worker's reset wipes another's task."""
132
+ own = os.path.realpath(SIM_DEVICES / udid) + os.sep # both sides resolved: a home under /var is /private/var
133
+ foreign = []
134
+ for entry in manifest.values():
135
+ bundle = entry.get("bundle_id")
136
+ if not bundle:
137
+ continue
138
+ proc = subprocess.run(["xcrun", "simctl", "get_app_container", udid, bundle, "data"],
139
+ capture_output=True, text=True)
140
+ path = proc.stdout.strip()
141
+ if proc.returncode == 0 and path and not os.path.realpath(path).startswith(own):
142
+ foreign.append((bundle, path))
143
+ return foreign
144
+
145
+
146
+ def check_isolation(udid, manifest):
147
+ """Refuse a simulator whose apps live in another simulator's containers (see foreign_containers)."""
148
+ foreign = foreign_containers(udid, manifest)
149
+ if foreign:
150
+ bundle, path = foreign[0]
151
+ raise SystemExit(f"simulator {udid} is not isolated: {len(foreign)} apps resolve to another device's "
152
+ f"containers (e.g. {bundle} -> {path}). It was likely made with `simctl clone`: "
153
+ f"create a fresh simulator and run iOSWorld's bootstrap on it.")
154
+
155
+
156
+ CONTAINER_METADATA = ".com.apple.mobile_container_manager.metadata.plist"
157
+ STATE_PARTS = ("Documents", "Library/Application Support") # JSON state files and SQLite stores (with -wal, -shm)
158
+ STATE_SETTLE_SECONDS = 1.0 # apps write on leaving the foreground (SwiftData's autosave) or on a background queue
159
+ SECRET_DEFAULTS = re.compile(r"api[_-]?key|token|secret|password", re.I) # iOSWorld writes openai_api_key into each app
160
+
161
+
162
+ def containers(root):
163
+ """{identifier: folder} for the containers under ``root``, named by each one's metadata plist, the way
164
+ iOSWorld's mcps/_data_layer.py finds an app's data (Data/Application) or the shared envelope (Shared/AppGroup)."""
165
+ found = {}
166
+ for folder in sorted(root.iterdir()) if root.is_dir() else ():
167
+ try:
168
+ identifier = plistlib.loads((folder / CONTAINER_METADATA).read_bytes()).get("MCMMetadataIdentifier")
169
+ except (OSError, ValueError, plistlib.InvalidFileException):
170
+ continue
171
+ if identifier:
172
+ found[identifier] = folder
173
+ return found
174
+
175
+
176
+ def utc(epoch):
177
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)) if epoch is not None else None
178
+
179
+
180
+ def save_end_state(udid, bundles, folder, wda_url=None, started=None):
181
+ """Copy the task's apps' end state into ``folder``/state, before the next task's reset wipes it, so a run can be
182
+ graded by code: each app's Documents/, Library/Application Support/ and UserDefaults plist (state/apps/<bundle>/),
183
+ and the app groups of the apps' vendor (state/groups/<group>/: the cloud apps' shared workspace envelope). With
184
+ ``wda_url``, the app in front is sent home first and given STATE_SETTLE_SECONDS to write; then the task's apps are
185
+ closed, so nothing writes during the copy, and their preferences get as long again to reach disk. ``started`` (the
186
+ task's start, epoch seconds) is recorded, since the shared app groups are never reset and keep earlier runs'
187
+ records. It never fails a run: any error is written to state/ERROR."""
188
+ import shutil
189
+ out = pathlib.Path(folder) / "state"
190
+ began = time.monotonic()
191
+ try:
192
+ if wda_url:
193
+ try: # WDA's sessionless home button: the run's session is closed by now
194
+ urllib.request.urlopen(urllib.request.Request(wda_url + "/wda/homescreen", data=b"{}", method="POST",
195
+ headers={"Content-Type": "application/json"}), timeout=10)
196
+ except (OSError, ValueError):
197
+ pass
198
+ time.sleep(STATE_SETTLE_SECONDS)
199
+ for bundle in bundles:
200
+ try:
201
+ subprocess.run(["xcrun", "simctl", "terminate", udid, bundle], capture_output=True, timeout=20)
202
+ except (OSError, subprocess.SubprocessError):
203
+ pass
204
+ time.sleep(STATE_SETTLE_SECONDS)
205
+ root = SIM_DEVICES / udid / "data/Containers"
206
+ apps = containers(root / "Data/Application")
207
+ saved, missing = {}, []
208
+ for bundle in bundles:
209
+ home = apps.get(bundle)
210
+ if home is None:
211
+ missing.append(bundle)
212
+ continue
213
+ for rel in (*STATE_PARTS, f"Library/Preferences/{bundle}.plist"):
214
+ source, target = home / rel, out / "apps" / bundle / rel
215
+ if source.is_dir():
216
+ shutil.copytree(source, target, dirs_exist_ok=True)
217
+ elif source.is_file(): # the UserDefaults plist, without keys
218
+ prefs = plistlib.loads(source.read_bytes())
219
+ target.parent.mkdir(parents=True, exist_ok=True)
220
+ target.write_bytes(plistlib.dumps({k: v for k, v in prefs.items() if not SECRET_DEFAULTS.search(k)},
221
+ fmt=plistlib.FMT_BINARY))
222
+ else:
223
+ continue
224
+ saved.setdefault(bundle, []).append(rel)
225
+ vendors = {"group." + bundle.rsplit(".", 1)[0] for bundle in bundles if "." in bundle}
226
+ shared = containers(root / "Shared/AppGroup")
227
+ groups = [group for group in shared if group.rsplit(".", 1)[0] in vendors]
228
+ for group in groups:
229
+ shutil.copytree(shared[group], out / "groups" / group, dirs_exist_ok=True,
230
+ ignore=shutil.ignore_patterns(CONTAINER_METADATA))
231
+ out.mkdir(parents=True, exist_ok=True)
232
+ record = {"udid": udid, "started": utc(started), "saved": utc(time.time()),
233
+ "seconds": round(time.monotonic() - began, 2), "apps": saved, "missing": missing, "groups": groups}
234
+ (out / "state.json").write_text(json.dumps(record, indent=1), encoding="utf-8")
235
+ except Exception as error: # a snapshot never costs a run
236
+ try:
237
+ out.mkdir(parents=True, exist_ok=True)
238
+ (out / "ERROR").write_text(f"{type(error).__name__}: {error}\n", encoding="utf-8")
239
+ except OSError:
240
+ pass
241
+
242
+
243
+ RESET_ATTEMPTS = 2
244
+
245
+
246
+ # MOBSTER_BENCH_RESET: how each task's start state is made.
247
+ # - "iosworld" (default): iOSWorld's own reset, wiping and re-seeding every app (43-66 s a task).
248
+ # - "restore": put back a checkpoint of the freshly seeded apps (bench/evals/checkpoint.py, ~4.5 s), checked by
249
+ # digest. The checkpoint is made once per simulator, from iOSWorld's own reset, in MOBSTER_BENCH_CHECKPOINTS
250
+ # (seed-<UDID>). A restore whose digest differs from the save falls back to iOSWorld's reset.
251
+ RESET_MODES = ("iosworld", "restore")
252
+
253
+
254
+ def reset_mode():
255
+ mode = os.environ.get("MOBSTER_BENCH_RESET", "").strip() or "iosworld"
256
+ if mode not in RESET_MODES:
257
+ raise ValueError(f"MOBSTER_BENCH_RESET={mode!r}: use iosworld or restore")
258
+ return mode
259
+
260
+
261
+ def reset(repo, udid, manifest, mode=None):
262
+ """The task's clean slate (see MOBSTER_BENCH_RESET); returns {"mode", "seconds", ...} for the record."""
263
+ mode = reset_mode() if mode is None else mode
264
+ started = time.monotonic()
265
+ if mode == "restore":
266
+ from .evals import checkpoint
267
+ bundles = {app: entry["bundle_id"] for app, entry in manifest.items() if entry.get("bundle_id")}
268
+ root = pathlib.Path(os.environ.get("MOBSTER_BENCH_CHECKPOINTS", "")).expanduser()
269
+ if not str(root) or str(root) == ".":
270
+ raise ValueError("MOBSTER_BENCH_RESET=restore needs MOBSTER_BENCH_CHECKPOINTS (a folder for the seeds)")
271
+ seed = root / f"seed-{udid}"
272
+ if not (seed / "checkpoint.json").exists():
273
+ iosworld_reset(repo, udid, manifest)
274
+ checkpoint.save(udid, bundles, seed)
275
+ return {"mode": "restore", "seeded": True, "seconds": round(time.monotonic() - started, 1)}
276
+ seconds, ok = checkpoint.restore(udid, bundles, seed)
277
+ if ok:
278
+ return {"mode": "restore", "seconds": seconds, "digest_ok": True}
279
+ iosworld_reset(repo, udid, manifest)
280
+ return {"mode": "restore", "digest_ok": False, "fell_back": True,
281
+ "seconds": round(time.monotonic() - started, 1)}
282
+ iosworld_reset(repo, udid, manifest)
283
+ return {"mode": "iosworld", "seconds": round(time.monotonic() - started, 1)}
284
+
285
+
286
+ def iosworld_reset(repo, udid, manifest):
287
+ """iOSWorld's clean slate: wipe every app's data, re-seed each, as its runner does per task."""
288
+ runner = iosworld_module(repo)
289
+ apps = list(manifest)
290
+ for attempt in range(RESET_ATTEMPTS):
291
+ try:
292
+ runner.reset_app_data(udid, apps, manifest)
293
+ runner.reseed_apps(udid, apps, manifest)
294
+ return
295
+ except RuntimeError:
296
+ # Seeding under load can leave apps empty ("no seed data after 3 attempts": four
297
+ # simulators busy, 24 Sep); a full second pass cleared it.
298
+ if attempt + 1 == RESET_ATTEMPTS:
299
+ raise
300
+
301
+
302
+ class Shots:
303
+ """PNG screenshots through ``simctl io`` (not WDA, so Mobster's device channel is untouched)."""
304
+
305
+ def __init__(self, udid, folder):
306
+ self.udid, self.folder = udid, pathlib.Path(folder).resolve()
307
+ self.folder.mkdir(parents=True, exist_ok=True)
308
+ self.count = 0
309
+ self.seconds = 0.0
310
+ self.trace = None # the driver's call_trace: shots go in it, to be told apart from the agent's time
311
+
312
+ def take(self, name):
313
+ started = time.monotonic()
314
+ path = self.folder / f"{name}.png"
315
+ if not simctl.screenshot(self.udid, path, timeout=20).ok:
316
+ path = None
317
+ self.seconds += time.monotonic() - started
318
+ self.count += 1
319
+ if self.trace is not None:
320
+ self.trace.append(("shot", started, (time.monotonic() - started) * 1000))
321
+ return str(path) if path else None
322
+
323
+
324
+ def describe(operation, target=None, text=None):
325
+ """One iOSWorld-style action dict for a Mobster operation."""
326
+ if operation == "LAUNCH_APP":
327
+ return {"type": "launch_app", "bundle_id": str(target)}
328
+ if operation.startswith("SWIPE_"):
329
+ return {"type": "swipe", "direction": operation.split("_", 1)[1].lower()}
330
+ if operation in ("HOME", "BACK", "VOLUME_UP", "VOLUME_DOWN"):
331
+ return {"type": operation.lower()}
332
+ action = {"type": {"TAP": "tap", "TYPE": "type", "TYPE_SUBMIT": "type", "SUBMIT": "submit",
333
+ "LONG_PRESS": "long_press"}.get(operation, operation.lower())}
334
+ if target is not None and hasattr(target, "label"):
335
+ action["element"] = {"label": (target.label or "")[:120], "role": target.role}
336
+ if text is not None:
337
+ action["text"] = text
338
+ if operation == "TYPE_SUBMIT":
339
+ action["submit"] = True
340
+ return action
341
+
342
+
343
+ class RecordingDriver:
344
+ """The simulator's WDA driver, with each dispatch recorded as an iOSWorld trajectory step."""
345
+
346
+ def __init__(self, driver, shots):
347
+ self._driver, self.shots = driver, shots
348
+ self.steps = []
349
+ # The model turn now acting (set from frontier_decision events): screenshots are named by
350
+ # it, since a turn may dispatch several actions and a refused turn none.
351
+ self.turn = None
352
+
353
+ def __getattr__(self, name):
354
+ return getattr(self._driver, name)
355
+
356
+ def _record(self, action):
357
+ """Screenshot and record one step; returns the screenshot's seconds."""
358
+ index = len(self.steps) + 1
359
+ name = f"step-{index:03d}" + (f"-turn-{self.turn:02d}" if self.turn is not None else "")
360
+ started = time.monotonic()
361
+ self.steps.append({"step": index, "screenshot": self.shots.take(name), "actions": [action],
362
+ **({"turn": self.turn} if self.turn is not None else {})})
363
+ return time.monotonic() - started
364
+
365
+ def execute(self, operation, target, snapshot, text=None, timeout=10):
366
+ spent = self._record(describe(operation, target, text))
367
+ # The judge's screenshot (0.2-2.5 s) does not age the observation: without it the action goes
368
+ # that much sooner, so the driver's re-read of an observation over 1 s old (WDA_FRESH_SECONDS)
369
+ # would not run (it ran after 0.6-1.8 s screenshots in exec-new2's megamart-001).
370
+ if hasattr(snapshot, "captured_at"):
371
+ snapshot = dataclasses.replace(snapshot, captured_at=snapshot.captured_at + spent)
372
+ return self._driver.execute(operation, target, snapshot, text=text, timeout=timeout)
373
+
374
+ def long_press(self, target, snapshot, timeout=10):
375
+ self._record({"type": "long_press", "element": {"label": (target.label or "")[:120], "role": target.role}})
376
+ return self._driver.long_press(target, snapshot, timeout=timeout)
377
+
378
+ def tap_point(self, x, y, snapshot, timeout=10, purpose="dismiss"):
379
+ self._record({"type": "tap_xy", "x": round(x, 3), "y": round(y, 3), "purpose": purpose})
380
+ return self._driver.tap_point(x, y, snapshot, timeout=timeout)
381
+
382
+ def set_value(self, target, value, timeout=10):
383
+ self._record({"type": "set_picker", "element": {"label": (target.value or "")[:120], "role": target.role},
384
+ "value": str(value)[:60]})
385
+ return self._driver.set_value(target, value, timeout=timeout)
386
+
387
+ def clear_text(self, target, timeout=10):
388
+ self._record({"type": "clear_text", "element": {"label": (target.label or "")[:120], "role": target.role}})
389
+ return self._driver.clear_text(target, timeout=timeout)
390
+
391
+ def write_text(self, target, snapshot, text, append=False, submit=False, timeout=30):
392
+ # One step, as the typed text: its paste, read-back and repair are how the typing is done.
393
+ action = describe("TYPE_SUBMIT" if submit else "TYPE", target, text)
394
+ if not append:
395
+ action["replace"] = True
396
+ self._record(action if text else {"type": "clear_text", "element": action.get("element")})
397
+ return self._driver.write_text(target, snapshot, text, append=append, submit=submit, timeout=timeout)
398
+
399
+ def replace_text(self, target, text, timeout=10):
400
+ self._record({**describe("TYPE", target, text), "replace": True})
401
+ done = self._driver.replace_text(target, text, timeout=timeout)
402
+ if not done:
403
+ self.steps.pop() # nothing was typed: the caller clears and types, recorded as such
404
+ return done
405
+
406
+ def call(self, method, path, body=None, timeout=10):
407
+ if method == "POST" and path == "/wda/apps/activate" and isinstance(body, dict):
408
+ self._record(describe("LAUNCH_APP", body.get("bundleId")))
409
+ elif method == "POST" and path == "/url" and isinstance(body, dict):
410
+ self._record({"type": "open_url", "url": str(body.get("url", ""))[:300]})
411
+ return self._driver.call(method, path, body, timeout)
412
+
413
+
414
+ def judge_shots_apart(timing, seconds):
415
+ """The frontier's timing with the judge's screenshots (taken before each dispatch, inside its
416
+ exec bucket) moved to their own bucket: they are not the agent's time (0.4-2.5 s each)."""
417
+ if not timing or "exec" not in timing:
418
+ return timing
419
+ return {**timing, "exec": round(timing["exec"] - seconds, 2), "judge_shots": round(seconds, 2)}
420
+
421
+
422
+ def turn_tokens(usage):
423
+ """One call's usage, compact, for the trace: input, output, cached and cache-written tokens, and with
424
+ MOBSTER_BENCH_TTFT=on the time to first token and the call's total (bench/stream_timing.py)."""
425
+ out = {"in": usage.get("prompt_tokens"), "out": usage.get("completion_tokens"),
426
+ "cached": usage.get("cached_tokens"), "write": usage.get("cache_write_tokens")}
427
+ for key in ("ttft_ms", "text_ttft_ms", "first_action_ms", "total_ms"):
428
+ if usage.get(key) is not None:
429
+ out[key] = usage[key]
430
+ return out
431
+
432
+
433
+ def build_id(path=None):
434
+ """The code's commit (and whether its tree has local changes), so every record names its build."""
435
+ folder = pathlib.Path(path or __file__).resolve().parent
436
+ try:
437
+ sha = subprocess.run(["git", "-C", str(folder), "rev-parse", "HEAD"], capture_output=True, text=True,
438
+ timeout=10).stdout.strip()
439
+ dirty = subprocess.run(["git", "-C", str(folder), "status", "--porcelain", "--untracked-files=no"],
440
+ capture_output=True, text=True, timeout=10).stdout.strip()
441
+ except (OSError, subprocess.SubprocessError):
442
+ return None
443
+ return (sha[:12] + ("+dirty" if dirty else "")) if sha else None
444
+
445
+
446
+ def answer_text(result):
447
+ data = result.get("data")
448
+ if data is None:
449
+ return None
450
+ if isinstance(data, str):
451
+ return data
452
+ return json.dumps(data, ensure_ascii=False)
453
+
454
+
455
+ def clear_alerts(driver, attempts=5):
456
+ """Accept system prompts the re-seed launches left up (CityRide's location prompt covered
457
+ CalTrack in the first smoke run). iOSWorld's bootstrap clicks these with AppleScript, which
458
+ needs the Simulator window; here WDA's alert endpoint does it headlessly, before the task."""
459
+ from ..transport import TransportError
460
+ for _ in range(attempts):
461
+ try:
462
+ text = driver.call("GET", "/alert/text", timeout=5)
463
+ except TransportError:
464
+ return
465
+ if not text:
466
+ return
467
+ try:
468
+ driver.call("POST", "/alert/accept", {"name": "Allow While Using App"}, timeout=5)
469
+ except TransportError:
470
+ driver.call("POST", "/alert/accept", {}, timeout=5)
471
+ time.sleep(.5)
472
+
473
+
474
+ # MOBSTER_BENCH_SESSION: the capabilities of each task's WDA session. Every task starts a new session, so two arms of
475
+ # an A/B on one simulator never share one.
476
+ # - "product" (the default since 10 Oct): as the app makes it (drivers.resolve_wda_session): no quiescence wait, so the
477
+ # bench measures the session users run. (A first SWIPE_UP on a live score feed still hung 95 s with it, 1 of 1
478
+ # such runs on 10 Oct: that hang is the swipe's, not the session's.)
479
+ # - "legacy": WDA's default capabilities, as every run before 10 Oct had them.
480
+ SESSION_CAPABILITIES = {"product": {"platformName": "iOS", "shouldWaitForQuiescence": False, "waitForIdleTimeout": 0},
481
+ "legacy": {}}
482
+
483
+
484
+ def session_mode():
485
+ mode = os.environ.get("MOBSTER_BENCH_SESSION", "").strip() or "product"
486
+ if mode not in SESSION_CAPABILITIES:
487
+ raise ValueError(f"MOBSTER_BENCH_SESSION={mode!r}: use product or legacy")
488
+ return mode
489
+
490
+
491
+ def wda_session(wda_url, mode=None):
492
+ """A new WDA session with ``mode``'s capabilities (session_mode() by default)."""
493
+ mode = session_mode() if mode is None else mode
494
+ if mode not in SESSION_CAPABILITIES:
495
+ raise ValueError(f"MOBSTER_BENCH_SESSION={mode!r}: use product or legacy")
496
+ request = urllib.request.Request(wda_url + "/session", method="POST",
497
+ data=json.dumps(
498
+ {"capabilities": {"alwaysMatch": SESSION_CAPABILITIES[mode]}}).encode(),
499
+ headers={"Content-Type": "application/json"})
500
+ created = json.load(urllib.request.urlopen(request, timeout=30))
501
+ return created.get("sessionId") or (created.get("value") or {}).get("sessionId")
502
+
503
+
504
+ def sim_restarter(udid, wda_url, mjpeg_url, xctestrun, log_path=None):
505
+ """The simulator's WDA restarter (sim/wda.py), or None without its xctestrun or in the open-source tree,
506
+ which leaves restarting a runner to whoever started it."""
507
+ if not xctestrun or not mjpeg_url:
508
+ return None
509
+ from urllib.parse import urlsplit
510
+ try:
511
+ from ..sim.wda import SimulatorWDA
512
+ except ImportError:
513
+ return None
514
+ return SimulatorWDA(udid, urlsplit(wda_url).port, urlsplit(mjpeg_url).port, xctestrun, log_path=log_path)
515
+
516
+
517
+ def frontier_agent(driver, names, emit, bundles, model=None, reasoning=None, recorder=None, request=None):
518
+ """(FrontierAgent, client): engines.SMART_CONFIG, the app's Smart engine, with this harness's per-task cost cap."""
519
+ from ..frontier import FrontierAgent
520
+ from ..model_clients import chat_client
521
+ client = chat_client(model or DEFAULT_FRONTIER_MODEL, reasoning=reasoning or SMART_CONFIG.reasoning)
522
+ if os.environ.get("MOBSTER_BENCH_TTFT", "").lower() in ("1", "on", "true", "yes"):
523
+ from .stream_timing import timed
524
+ client = timed(client) # the same request, streamed, to time the first token (both arms alike)
525
+ kwargs = frontier_kwargs(max_cost_usd=task_cost_cap(bundles, client.model))
526
+ if recorder is not None:
527
+ recorder.header(request=request, apps=names, model=client.model, frontier=kwargs, driver=driver)
528
+ driver, client = tape.TapeDriver(driver, recorder), tape.TapeClient(client, recorder)
529
+ return FrontierAgent(driver, client, apps=names, emit=emit, **kwargs), client
530
+
531
+
532
+ def taping():
533
+ """Whether a run keeps its tape (tape.jsonl, for bench/tape_replay.py): on unless MOBSTER_TAPE is off."""
534
+ return os.environ.get("MOBSTER_TAPE", "").strip().casefold() not in ("off", "0", "false", "no")
535
+
536
+
537
+ def run_task(task, *, repo, udid, manifest, wda_url, mjpeg_url, folder, policy="frontier", frontier_model=None,
538
+ reasoning="low", wda_xctestrun=None):
539
+ from ..catalog import app_label
540
+ from ..compose import build_target_driver, close_all
541
+ from ..model_clients import cost_usd
542
+ from ..wda.frame_clock import attach_frame_clock
543
+
544
+ folder = pathlib.Path(folder)
545
+ folder.mkdir(parents=True, exist_ok=True)
546
+ task_started = time.time()
547
+ t_reset = time.monotonic()
548
+ reset_info = reset(repo, udid, manifest)
549
+ reset_s = time.monotonic() - t_reset
550
+
551
+ bundles = [app_entry(manifest, app).get("bundle_id") for app in task.get("apps", [])]
552
+ bundles = [b for b in bundles if b]
553
+ costs, events = [], []
554
+
555
+ def emit(event):
556
+ kind = event.get("event")
557
+ if kind == "frontier_prompt":
558
+ # Kept out of task.json: prompts.jsonl, one line per turn, and the image the model saw as
559
+ # screens/prompt-turn-NN.jpg, so a decision can be replayed offline (bench/replay.py).
560
+ event = dict(event)
561
+ image = event.pop("image", None)
562
+ if image and image.startswith("data:image/jpeg;base64,"):
563
+ name = f"prompt-turn-{event.get('step', 0):02d}.jpg"
564
+ (folder / "screens").mkdir(parents=True, exist_ok=True)
565
+ (folder / "screens" / name).write_bytes(base64.b64decode(image.split(",", 1)[1]))
566
+ event["image"] = f"screens/{name}"
567
+ with open(folder / "prompts.jsonl", "a", encoding="utf-8") as prompts:
568
+ prompts.write(json.dumps(event, ensure_ascii=False) + "\n")
569
+ return
570
+ if kind == "inference_finished" and isinstance(event.get("cost_nanodollars"), int):
571
+ costs.append(event["cost_nanodollars"] / 1e9)
572
+ if kind in ("frontier_decision", "frontier_error", "frontier_refused", "frontier_action", "frontier_failed",
573
+ "frontier_chunk_stop", "frontier_checklist", "frontier_verify",
574
+ "frontier_text", "frontier_tuned", "frontier_wda_recovery",
575
+ "frontier_contract", "frontier_contract_update", "frontier_screen",
576
+ "frontier_open_url", "frontier_retry", "frontier_split",
577
+ "frontier_app_relaunched"):
578
+ usage = event.get("usage")
579
+ event = {k: v for k, v in event.items() if k != "usage"}
580
+ if kind == "frontier_decision" and isinstance(usage, dict) and usage:
581
+ event["tokens"] = turn_tokens(usage)
582
+ trace = getattr(driver, "call_trace", None)
583
+ if trace and kind in ("frontier_decision", "frontier_action", "frontier_failed"):
584
+ # Every WDA request, read and judge screenshot since the last event: [kind, start ms, ms].
585
+ items = trace[:]
586
+ del trace[:len(items)] # the verify read's thread may append meanwhile
587
+ event["wda"] = [[name, round((at - started) * 1000), round(ms)] for name, at, ms in items]
588
+ events.append(event)
589
+ if kind == "frontier_decision" and recording is not None:
590
+ recording.turn = event.get("step")
591
+ return
592
+ if kind in ("result", "error"):
593
+ kept = {k: v for k, v in event.items() if isinstance(v, (str, int, float, bool)) or v is None}
594
+ if isinstance(kept.get("text"), str):
595
+ kept["text"] = kept["text"][:600]
596
+ events.append(kept)
597
+
598
+ shots = Shots(udid, folder / "screens")
599
+ driver = recording = calls = None
600
+ model_name = frontier_model or DEFAULT_FRONTIER_MODEL
601
+ started = time.monotonic()
602
+ try:
603
+ session = wda_session(wda_url)
604
+ driver = build_target_driver(wda_url=wda_url, session=session)
605
+ driver.call_trace = shots.trace = []
606
+ # A runner that dies mid-task is restarted as it was started, and its session taken the way this
607
+ # harness takes one (frontier.FrontierAgent._recover).
608
+ driver.restarter = sim_restarter(udid, wda_url, mjpeg_url, wda_xctestrun or os.environ.get(
609
+ "MOBSTER_SIM_WDA_XCTESTRUN"), log_path=str(folder / "wda-restart.log"))
610
+ driver.new_session = lambda: wda_session(wda_url)
611
+ from ..app_killer import simulator as simulator_killer
612
+ driver.killer = simulator_killer(udid) # a stuck app WDA can't close is closed by simctl
613
+ attach_frame_clock(driver, wda_url=wda_url, session=session, mjpeg_url=mjpeg_url,
614
+ log_path=str(folder / "frameclock.jsonl"))
615
+ clear_alerts(driver)
616
+ driver.call("POST", "/wda/pressButton", {"name": "home"}, timeout=10)
617
+ recording = RecordingDriver(driver, shots)
618
+ if bundles:
619
+ recording.call("POST", "/wda/apps/activate", {"bundleId": bundles[0]}, timeout=20)
620
+ names = {b: app_label(b).split(" (")[0] for b in bundles}
621
+ started, shot_seconds = time.monotonic(), shots.seconds
622
+ recorder = tape.Tape(folder / tape.NAME) if taping() else None
623
+ agent, client = frontier_agent(recording, names, emit, bundles, model_name, reasoning, recorder=recorder,
624
+ request=task["goal"])
625
+ outcome = agent.run(task["goal"])
626
+ if recorder is not None:
627
+ recorder.end(outcome)
628
+ costs.append(cost_usd(client.model, outcome["usage"]) or 0.0)
629
+ calls = outcome["usage"].get("calls") # FrontierAgent emits no inference_finished: one cost, many calls
630
+ result = {"status": outcome["status"], "data": outcome.get("answer"),
631
+ "reason": outcome.get("reason", ""), "actions": outcome["actions"],
632
+ "decisions": outcome["steps"], "usage": outcome["usage"], "notes": outcome["notes"],
633
+ "timing": judge_shots_apart(outcome.get("timing"), shots.seconds - shot_seconds)}
634
+ except Exception as error:
635
+ result = {"status": "error", "reason": f"{type(error).__name__}: {error}"}
636
+ finally:
637
+ elapsed = time.monotonic() - started
638
+ close_all(driver)
639
+ steps = recording.steps if recording is not None else []
640
+ final = shots.take("final")
641
+ save_end_state(udid, bundles, folder, wda_url=wda_url, started=task_started)
642
+ if steps:
643
+ steps[-1]["post_action_screenshot"] = final
644
+ else:
645
+ steps = [{"step": 1, "screenshot": final, "actions": [], "post_action_screenshot": final}]
646
+ answer = answer_text(result)
647
+ if answer is not None:
648
+ steps.append({"step": len(steps) + 1, "actions": [{"type": "stop", "answer": answer}],
649
+ "post_action_screenshot": final})
650
+ (folder / "trajectory.json").write_text(json.dumps(steps, indent=1), encoding="utf-8")
651
+ # "ok" means the agent ran to a result; iOSWorld's judge then decides success.
652
+ record = {"task": task["name"], "goal": task["goal"], "apps": task.get("apps", []),
653
+ "category": task.get("category"), "difficulty": task.get("difficulty"),
654
+ "status": "ok", "agent_answer": answer,
655
+ "mobster": {"status": result.get("status"), "reason": str(result.get("reason") or "")[:400],
656
+ "data_status": result.get("data_status"), "actions": result.get("actions"),
657
+ "decisions": result.get("decisions"),
658
+ "agent_seconds": round(max(0.0, elapsed - shots.seconds), 2),
659
+ "screenshot_seconds": round(shots.seconds, 2), "reset_seconds": round(reset_s, 1),
660
+ "reset": reset_info,
661
+ "cost_usd": round(sum(costs), 6),
662
+ "model_calls": calls if calls is not None else len(costs), "policy": policy,
663
+ "model": model_name, "usage": result.get("usage"), "build": build_id(),
664
+ "ttft": os.environ.get("MOBSTER_BENCH_TTFT", "") or None,
665
+ "wda_session": session_mode(),
666
+ "notes": result.get("notes"), "timing": result.get("timing"),
667
+ "events": events[-400:]}}
668
+ (folder / "task.json").write_text(json.dumps(record, indent=1), encoding="utf-8")
669
+ return record
670
+
671
+
672
+ def load_envs(paths):
673
+ from ..config import load_env_file
674
+ for path in paths or ():
675
+ load_env_file(os.path.expanduser(path))
676
+
677
+
678
+ def cmd_run(args):
679
+ # OPENAI_API_KEY goes into the apps' UserDefaults, as iOSWorld's runner does (its apps' in-app replies).
680
+ load_envs(args.env_file)
681
+ tasks = all_tasks = load_tasks(args.repo)
682
+ all_names = [t["name"] for t in all_tasks] # folder numbers follow the full task list
683
+ if args.only:
684
+ wanted = set(args.only.split(","))
685
+ tasks = [t for t in tasks if t["name"] in wanted]
686
+ if args.limit:
687
+ tasks = tasks[:args.limit]
688
+ manifest = load_manifest(args.repo)
689
+ register_names(manifest)
690
+ check_isolation(args.udid, manifest)
691
+ out = pathlib.Path(args.out)
692
+ out.mkdir(parents=True, exist_ok=True)
693
+ for task in tasks:
694
+ folder = out / f"{all_names.index(task['name']) + 1:0{TASK_DIR_WIDTH}d}-{task['name']}"
695
+ if (folder / "task.json").exists() and not args.force:
696
+ print(f"skip {task['name']} (done)", flush=True)
697
+ continue
698
+ try:
699
+ record = run_task(task, repo=args.repo, udid=args.udid, manifest=manifest, wda_url=args.wda_url,
700
+ mjpeg_url=args.mjpeg_url, folder=folder, policy=args.policy, frontier_model=args.model,
701
+ reasoning=args.reasoning, wda_xctestrun=args.wda_xctestrun)
702
+ except RuntimeError as error:
703
+ # A failed reset is the harness's, not the agent's: no task.json, so a rerun retries it,
704
+ # and the rest of this simulator's queue still runs.
705
+ print(f"{task['name']:24} NOT RUN (reset failed: {str(error)[:120]})", flush=True)
706
+ continue
707
+ m = record["mobster"]
708
+ print(f"{task['name']:24} {m['status']:24} {m['agent_seconds']:6.1f}s ${m['cost_usd']:.4f} "
709
+ f"steps={m['decisions']} answer={str(record['agent_answer'])[:80]!r}", flush=True)
710
+
711
+
712
+ def cmd_pool(args):
713
+ """One ``run`` per simulator (UDID:WDA-port:MJPEG-port), the tasks dealt round-robin."""
714
+ tasks = [t["name"] for t in load_tasks(args.repo)]
715
+ if args.only:
716
+ wanted = set(args.only.split(","))
717
+ tasks = [t for t in tasks if t in wanted]
718
+ if args.limit:
719
+ tasks = tasks[:args.limit]
720
+ workers = [spec.split(":") for spec in args.sims.split(",")]
721
+ if len({udid for udid, _, _ in workers}) < len(workers):
722
+ raise SystemExit("--sims lists a simulator twice")
723
+ manifest = load_manifest(args.repo)
724
+ for udid, _, _ in workers: # before any worker starts: one shared simulator taints every worker's results
725
+ check_isolation(udid, manifest)
726
+ procs = []
727
+ for index, (udid, port, mjpeg) in enumerate(workers):
728
+ share = tasks[index::len(workers)]
729
+ if not share:
730
+ continue
731
+ command = [sys.executable, "-u", "-m", "mobile_agent.bench.iosworld", "run", "--repo", args.repo,
732
+ "--udid", udid, "--wda-url", f"http://127.0.0.1:{port}", "--mjpeg-url", f"http://127.0.0.1:{mjpeg}",
733
+ "--out", args.out, "--only", ",".join(share)]
734
+ for env_file in args.env_file or ():
735
+ command += ["--env-file", env_file]
736
+ command += (["--policy", args.policy, "--reasoning", args.reasoning]
737
+ + (["--model", args.model] if args.model else []))
738
+ if args.wda_xctestrun:
739
+ command += ["--wda-xctestrun", args.wda_xctestrun]
740
+ if args.force:
741
+ command.append("--force")
742
+ log = open(pathlib.Path(args.out).with_suffix(f".worker{index}.log"), "w")
743
+ procs.append((subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT), log))
744
+ for proc, log in procs:
745
+ proc.wait()
746
+ log.close()
747
+ return max((proc.returncode for proc, _ in procs), default=0)
748
+
749
+
750
+ def cmd_judge(args):
751
+ """iOSWorld's own judge, unmodified, in its own environment."""
752
+ load_envs(args.env_file) # the judge reads OPENAI_API_KEY from this process's environment
753
+ python = pathlib.Path(args.repo) / ".venv" / "bin" / "python"
754
+ command = [str(python if python.exists() else sys.executable), "scripts/judge_trajectories.py",
755
+ "--run-dir", str(pathlib.Path(args.run).resolve())]
756
+ if args.force:
757
+ command.append("--re-judge") # judge_trajectories.py has no --force
758
+ code = subprocess.call(command, cwd=args.repo, env=dict(os.environ))
759
+ # The judge's own summary counts passes by the official rule; this adds where its success flag differs.
760
+ from .iosworld_stats import load_runs, report_lines
761
+ for line in report_lines(load_runs([args.run])):
762
+ print(line)
763
+ return code
764
+
765
+
766
+ def cmd_report(args):
767
+ """One run folder: official passes (iOSWorld's every-criterion rule), the judge's own flag where
768
+ it disagrees, by category and difficulty, then time and cost."""
769
+ from .iosworld_stats import load_runs, report_lines
770
+ runs = load_runs([args.run])
771
+ for line in report_lines(runs):
772
+ print(line)
773
+ judged = [r for r in runs if r.judged]
774
+ for key in ("category", "difficulty"):
775
+ groups = {}
776
+ for r in judged:
777
+ groups.setdefault(getattr(r, key), []).append(r.official)
778
+ print(key + ": " + ", ".join(f"{k} {sum(v)}/{len(v)}" for k, v in sorted(groups.items(), key=str)))
779
+ from ..model_clients import cost_usd
780
+ rows = [json.loads(p.read_text(encoding="utf-8")) for p in sorted(pathlib.Path(args.run).glob("*/task.json"))]
781
+ seconds = [r["mobster"]["agent_seconds"] for r in rows]
782
+ costs = [r["mobster"]["cost_usd"] or cost_usd(r["mobster"].get("model"), r["mobster"].get("usage")) or 0.0
783
+ for r in rows]
784
+ if seconds:
785
+ print(f"agent seconds p50 {statistics.median(seconds):.1f}, mean {statistics.mean(seconds):.1f}; "
786
+ f"$/task mean {statistics.mean(costs):.4f}, total ${sum(costs):.2f}")
787
+
788
+
789
+ def _run_dirs(patterns):
790
+ """Run folders from arguments that may be globs (a folder holds NNN-task/task.json)."""
791
+ dirs = []
792
+ for pattern in patterns:
793
+ for path in sorted(glob.glob(pattern)) or [pattern]:
794
+ if pathlib.Path(path).is_dir() and path not in dirs:
795
+ dirs.append(path)
796
+ return dirs
797
+
798
+
799
+ def cmd_compare(args):
800
+ """A paired A/B: arm A's run folders against arm B's, on the tasks both judged."""
801
+ from .iosworld_stats import compare, format_compare, load_overrides, load_runs, read_worker_logs
802
+ overrides = load_overrides(args.commit_labels)
803
+ arms = {arm: _run_dirs(getattr(args, arm)) for arm in ("a", "b")}
804
+ runs = {arm: load_runs(dirs, model=args.model, overrides=overrides) for arm, dirs in arms.items()}
805
+ logs = {}
806
+ for arm, dirs in arms.items():
807
+ merged = {"not_run": [], "tracebacks": 0}
808
+ for folder in dirs:
809
+ info = read_worker_logs(folder)
810
+ merged["not_run"] += info["not_run"]
811
+ merged["tracebacks"] += info["tracebacks"]
812
+ logs[arm] = merged
813
+ result = compare(runs["a"], runs["b"], n_boot=args.bootstrap, seed=args.seed)
814
+ if args.json:
815
+ print(json.dumps({**result, "logs": logs, "arms": arms}, indent=1, default=str))
816
+ return 0
817
+ print(f"A: {', '.join(arms['a'])}\nB: {', '.join(arms['b'])}\n")
818
+ print(format_compare(result, args.a_name, args.b_name, logs=logs))
819
+ return 0
820
+
821
+
822
+ def cmd_estimate(args):
823
+ """The official pass rate over any set of run folders, reweighted to iOSWorld's 133-task mix."""
824
+ from .iosworld_stats import FULL_MIX, estimate, format_estimate, load_runs
825
+ mix = dict(FULL_MIX)
826
+ if args.repo:
827
+ mix = {c: 0 for c in FULL_MIX}
828
+ for task in load_tasks(args.repo):
829
+ mix[task.get("category")] = mix.get(task.get("category"), 0) + 1
830
+ runs = load_runs(_run_dirs(args.runs), model=args.model)
831
+ for weighting in ("task", "run") if args.weighting == "both" else (args.weighting,):
832
+ result = estimate(runs, mix=mix, weighting=weighting, n_boot=args.bootstrap, seed=args.seed)
833
+ if args.json:
834
+ print(json.dumps(result, indent=1, default=str))
835
+ else:
836
+ print(format_estimate(result))
837
+ return 0
838
+
839
+
840
+ def main(argv=None):
841
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
842
+ sub = parser.add_subparsers(dest="command", required=True)
843
+ run = sub.add_parser("run")
844
+ run.add_argument("--repo", required=True)
845
+ run.add_argument("--udid", required=True)
846
+ run.add_argument("--wda-url", required=True)
847
+ run.add_argument("--mjpeg-url")
848
+ run.add_argument("--out", required=True)
849
+ run.add_argument("--only")
850
+ run.add_argument("--limit", type=int)
851
+ run.add_argument("--force", action="store_true")
852
+ run.add_argument("--env-file", action="append")
853
+ run.add_argument("--policy", choices=("frontier",), default="frontier")
854
+ run.add_argument("--model")
855
+ run.add_argument("--reasoning", default=SMART_CONFIG.reasoning)
856
+ run.add_argument("--wda-xctestrun", help="the simulator runner's .xctestrun: a runner that dies is restarted "
857
+ "(also MOBSTER_SIM_WDA_XCTESTRUN)")
858
+ pool = sub.add_parser("pool")
859
+ pool.add_argument("--repo", required=True)
860
+ pool.add_argument("--sims", required=True, help="UDID:WDA_PORT:MJPEG_PORT,...")
861
+ pool.add_argument("--out", required=True)
862
+ pool.add_argument("--only")
863
+ pool.add_argument("--limit", type=int)
864
+ pool.add_argument("--force", action="store_true")
865
+ pool.add_argument("--env-file", action="append")
866
+ pool.add_argument("--policy", choices=("frontier",), default="frontier")
867
+ pool.add_argument("--model")
868
+ pool.add_argument("--reasoning", default=SMART_CONFIG.reasoning)
869
+ pool.add_argument("--wda-xctestrun")
870
+ judge = sub.add_parser("judge")
871
+ judge.add_argument("--repo", required=True)
872
+ judge.add_argument("--run", required=True)
873
+ judge.add_argument("--force", action="store_true")
874
+ judge.add_argument("--env-file", action="append")
875
+ report = sub.add_parser("report")
876
+ report.add_argument("--run", required=True)
877
+ ab = sub.add_parser("compare", help="paired A/B over two sets of run folders (offline)")
878
+ ab.add_argument("--a", nargs="+", required=True, help="arm A's run folders (globs allowed)")
879
+ ab.add_argument("--b", nargs="+", required=True, help="arm B's run folders (globs allowed)")
880
+ ab.add_argument("--a-name", default="A")
881
+ ab.add_argument("--b-name", default="B")
882
+ ab.add_argument("--model", help="only runs of this model")
883
+ ab.add_argument("--commit-labels", help='JSON {task: {control label: true|false}}: hand labels for "requested"')
884
+ ab.add_argument("--bootstrap", type=int, default=10000)
885
+ ab.add_argument("--seed", type=int, default=20260926)
886
+ ab.add_argument("--json", action="store_true")
887
+ pooled = sub.add_parser("estimate", help="official pass rate reweighted to the 133-task mix (offline)")
888
+ pooled.add_argument("runs", nargs="+", help="run folders (globs allowed)")
889
+ pooled.add_argument("--model", help="only runs of this model, e.g. gpt-5.6-sol")
890
+ pooled.add_argument("--repo", help="iOSWorld checkout: take the category mix from its tasks.json")
891
+ pooled.add_argument("--weighting", choices=("task", "run", "both"), default="both")
892
+ pooled.add_argument("--bootstrap", type=int, default=10000)
893
+ pooled.add_argument("--seed", type=int, default=20260926)
894
+ pooled.add_argument("--json", action="store_true")
895
+ args = parser.parse_args(argv)
896
+ return {"run": cmd_run, "pool": cmd_pool, "judge": cmd_judge, "report": cmd_report, "compare": cmd_compare,
897
+ "estimate": cmd_estimate}[args.command](args) or 0
898
+
899
+
900
+ if __name__ == "__main__":
901
+ sys.exit(main())