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,705 @@
1
+ """Failure evidence: what a run keeps to say why a check went red, not just that it did."""
2
+
3
+ import json
4
+ import os
5
+ import plistlib
6
+ import re
7
+ import shutil
8
+ import signal
9
+ import subprocess
10
+ import tempfile
11
+ import threading
12
+ import time
13
+ from collections import deque
14
+ from dataclasses import dataclass
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+
18
+ from .. import secret_filter
19
+ from . import crashes
20
+
21
+ LOG_FILE = "logs/app.log"
22
+ CRASH_DIR = "crashes"
23
+ LOG_CAP_BYTES = 1_000_000
24
+ TAIL_LINES = 2_000
25
+ LINE_LIMIT = 2_000
26
+ # The log process is reaped on a thread of its own: waiting for `simctl spawn` cost a check 0.35 s (16 Oct).
27
+ DRAIN_WAIT = .3
28
+ WINDOW_MS = 10_000
29
+ WINDOW_LINES = 60
30
+ STOP_WAIT = 3.0
31
+ # ReportCrash wrote a simulator's report 1-4 min after the crash under load (16 Oct); `mobster test` looks again.
32
+ EXIT_WAIT = 2.0
33
+ REPORT_WAIT = {"simulator": 8.0, "iphone": 15.0}
34
+ CRASH_POLL = .5
35
+ # A phone's clock and the Mac's can disagree by a little.
36
+ CLOCK_SLACK = 5.0
37
+ # SpringBoard's line when an app's process ends: "[app<id>:pid] Process exited: ... domain:signal(2) code:SIGTRAP(5)".
38
+ # A crash is a signal other than a kill, a watchdog or jetsam; Mobster's own relaunch shows as a force-quit.
39
+ EXITED = re.compile(r"\[app<(?P<bundle>[^>(]+)(?:\([^)]*\))?>:(?P<pid>\d+)\] Process exited: .*?"
40
+ r"domain:(?P<domain>[\w-]+)\(\d+\) code:(?P<code>[\w-]+)(?:\((?P<number>[^)]*)\))?")
41
+ NOT_CRASHES = frozenset({"SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT"})
42
+ LEVELS = {"default": "notice", "notice": "notice", "info": "info", "debug": "debug", "error": "error",
43
+ "fault": "fault", "warning": "error", "critical": "fault"}
44
+
45
+
46
+ @dataclass
47
+ class Line:
48
+ at_ms: int
49
+ level: str
50
+ process: str
51
+ text: str
52
+ own: bool = False # from the app's own code (a subsystem not Apple's), which the window keeps first
53
+ driver: bool = False # XCTest's own lines in the app's process (com.apple.dt.*): Mobster's driver, not the app
54
+ apple: bool = False # an Apple framework's subsystem (com.apple.*), even inside the app's process: accessibility
55
+
56
+ def render(self):
57
+ return f"{self.at_ms / 1000:9.3f} {self.level:<6} {self.process}: {self.text}"
58
+
59
+ def to_dict(self):
60
+ return {"atMs": self.at_ms, "level": self.level, "process": self.process, "text": self.text}
61
+
62
+
63
+ @dataclass
64
+ class Exit:
65
+ """An app process ending, from SpringBoard's log."""
66
+ at_ms: int
67
+ pid: int
68
+ domain: str
69
+ code: str
70
+
71
+ @property
72
+ def crashed(self):
73
+ if self.domain == "signal":
74
+ return self.code not in NOT_CRASHES
75
+ if self.domain == "jetsam":
76
+ return True
77
+ return self.domain == "frontboard" and "8badf00d" in self.code.lower()
78
+
79
+ @property
80
+ def why(self):
81
+ """'SIGTRAP', 'killed for using too much memory (jetsam)', 'the watchdog (0x8badf00d)'."""
82
+ if self.domain == "jetsam":
83
+ return "killed for using too much memory (jetsam)"
84
+ if "8badf00d" in self.code.lower():
85
+ return "killed by the watchdog (0x8badf00d)"
86
+ return self.code
87
+
88
+
89
+ def exit_event(text, bundle_id):
90
+ """The Exit in a SpringBoard line about ``bundle_id``'s process (at_ms 0), or None."""
91
+ match = EXITED.search(text or "")
92
+ if not match or match.group("bundle") != bundle_id:
93
+ return None
94
+ code = match.group("code")
95
+ if match.group("domain") == "frontboard" and match.group("number"):
96
+ code = match.group("number")
97
+ return Exit(0, int(match.group("pid")), match.group("domain"), code)
98
+
99
+
100
+ def _predicate_text(value):
101
+ return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"') + '"'
102
+
103
+
104
+ class SimulatorTools:
105
+ """A simulator's evidence: its unified log and the Mac's crash reports for its processes."""
106
+
107
+ kind = "simulator"
108
+
109
+ def __init__(self, udid, *, popen=subprocess.Popen, run=subprocess.run, reports=None):
110
+ self.udid = udid
111
+ self.popen, self.run = popen, run
112
+ self.reports = Path(reports) if reports else crashes.DIAGNOSTIC_REPORTS
113
+
114
+ def executable(self, app):
115
+ return getattr(app, "executable", None)
116
+
117
+ def log_argv(self, executable, bundle_id):
118
+ """The app's own log, and SpringBoard's line when its process exits."""
119
+ predicate = (f"process == {_predicate_text(executable)} OR (process == \"SpringBoard\" AND "
120
+ f"eventMessage CONTAINS \"Process exited\" AND "
121
+ f"eventMessage CONTAINS {_predicate_text(bundle_id)})")
122
+ return ["/usr/bin/xcrun", "simctl", "spawn", self.udid, "log", "stream", "--style", "ndjson", "--level",
123
+ "info", "--predicate", predicate]
124
+
125
+ def parse(self, raw, arrival, t0_wall):
126
+ try:
127
+ event = json.loads(raw)
128
+ except ValueError:
129
+ return None
130
+ if not isinstance(event, dict) or event.get("eventType") not in (None, "logEvent", "activityCreateEvent"):
131
+ return None
132
+ stamp = _log_time(event.get("timestamp")) or arrival
133
+ process = Path(str(event.get("processImagePath") or "")).name or "app"
134
+ where = "/".join(filter(None, [event.get("subsystem"), event.get("category")]))
135
+ text = str(event.get("eventMessage") or "")
136
+ if where:
137
+ text = f"[{where}] {text}"
138
+ level = LEVELS.get(str(event.get("messageType") or "default").lower(), "notice")
139
+ subsystem = str(event.get("subsystem") or "")
140
+ own = bool(subsystem) and not subsystem.startswith("com.apple.")
141
+ return Line(round((stamp - t0_wall) * 1000), level, process, text, own, subsystem.startswith("com.apple.dt."),
142
+ subsystem.startswith("com.apple."))
143
+
144
+ def crash_reports(self, executable, since):
145
+ """The crash reports that may be this run's: the Mac's own files, read in place (no copy)."""
146
+ return crashes.candidates(self.reports, executable, since - CLOCK_SLACK), None
147
+
148
+
149
+ class PhoneTools:
150
+ # idevicecrashreport --keep copies the phone's crash reports and never removes them from the phone.
151
+ kind = "iphone"
152
+
153
+ def __init__(self, udid, *, network=False, popen=subprocess.Popen, run=subprocess.run, tool=None):
154
+ self.udid, self.network = udid, network
155
+ self.popen, self.run = popen, run
156
+ if tool is None:
157
+ from ..mac_tools import tool
158
+ self._tool = tool
159
+
160
+ def _base(self, name):
161
+ path = self._tool(name)
162
+ if not path:
163
+ raise LookupError(f"{name} isn't installed")
164
+ return [path, "-u", self.udid] + (["-n"] if self.network else [])
165
+
166
+ def executable(self, app):
167
+ """The app's CFBundleExecutable, read from the phone (``ideviceinstaller list -b``); None when it can't."""
168
+ known = getattr(app, "executable", None)
169
+ if known:
170
+ return known
171
+ try:
172
+ done = self.run(self._base("ideviceinstaller") + ["list", "--all", "--xml", "-b", app.bundle_id,
173
+ "-a", "CFBundleIdentifier", "-a", "CFBundleExecutable"],
174
+ capture_output=True, timeout=20, stdin=subprocess.DEVNULL)
175
+ items = plistlib.loads(done.stdout) if done.returncode == 0 and done.stdout else []
176
+ except (OSError, subprocess.SubprocessError, LookupError, ValueError, plistlib.InvalidFileException):
177
+ return None
178
+ for item in items if isinstance(items, list) else ():
179
+ if isinstance(item, dict) and item.get("CFBundleIdentifier") == app.bundle_id:
180
+ name = item.get("CFBundleExecutable")
181
+ return name if isinstance(name, str) and name else None
182
+ return None
183
+
184
+ def log_argv(self, executable, bundle_id):
185
+ """The app's process and SpringBoard's (for the line when the app exits; the rest of SpringBoard's is dropped as
186
+ it is read)."""
187
+ return self._base("idevicesyslog") + ["--no-colors", "-p", f"{executable}|SpringBoard"]
188
+
189
+ SYSLOG = re.compile(r"^\w{3} +\d+ [\d:.]+ \S+ (?P<process>[^\[(\s]+)(?:\([^)]*\))?\[(?P<pid>\d+)\] "
190
+ r"<(?P<level>\w+)>: ?(?P<text>.*)$")
191
+
192
+ def parse(self, raw, arrival, t0_wall):
193
+ # Stamped when the Mac read it: the phone's clock isn't the run's.
194
+ text = raw.rstrip("\n")
195
+ if not text.strip() or text.startswith("[connected") or text.startswith("[disconnected"):
196
+ return None
197
+ match = self.SYSLOG.match(text)
198
+ at = round((arrival - t0_wall) * 1000)
199
+ if not match:
200
+ return Line(at, "", "", text.strip())
201
+ return Line(at, LEVELS.get(match.group("level").lower(), "notice"), match.group("process"),
202
+ match.group("text"))
203
+
204
+ def crash_reports(self, executable, since):
205
+ """Copies of the phone's crash reports for ``executable`` (``idevicecrashreport --keep --filter``) in a
206
+ temporary folder the caller removes."""
207
+ folder = tempfile.mkdtemp(prefix="mobster-crashes-")
208
+ try:
209
+ self.run(self._base("idevicecrashreport") + ["--keep", "--filter", executable, folder],
210
+ capture_output=True, timeout=60, stdin=subprocess.DEVNULL)
211
+ except (OSError, subprocess.SubprocessError, LookupError):
212
+ return [], folder
213
+ found = []
214
+ for path in Path(folder).rglob("*.ips"):
215
+ if path.name.startswith(f"{executable}-"):
216
+ found.append(path)
217
+ return sorted(found, reverse=True), folder
218
+
219
+
220
+ def _log_time(text):
221
+ if not isinstance(text, str):
222
+ return None
223
+ try:
224
+ return datetime.strptime(text.strip(), "%Y-%m-%d %H:%M:%S.%f%z").timestamp()
225
+ except ValueError:
226
+ return None
227
+
228
+
229
+ class LogCapture:
230
+ """One log process, read on its own thread into ``logs/app.log`` (capped) and kept in memory for the window."""
231
+
232
+ def __init__(self, tools, executable, bundle_id, path, *, t0_wall, clock=time.time, on_exit=None):
233
+ self.tools, self.executable, self.bundle_id, self.path = tools, executable, bundle_id, Path(path)
234
+ self.t0_wall, self.clock = t0_wall, clock
235
+ self.on_exit = on_exit or (lambda event: None)
236
+ self.lines = [] # what the file holds, in order
237
+ self.tail = deque(maxlen=TAIL_LINES)
238
+ self.dropped = 0
239
+ self.bytes = 0
240
+ self.error = None
241
+ self._process = self._thread = None
242
+ self._handle = None
243
+ self._lock = threading.Lock()
244
+ self._stopped = False
245
+ self._last = None # the entry a continuation line belongs to
246
+
247
+ def start(self):
248
+ argv = self.tools.log_argv(self.executable, self.bundle_id)
249
+ self.path.parent.mkdir(parents=True, exist_ok=True)
250
+ self._handle = open(self.path, "w", encoding="utf-8")
251
+ try:
252
+ self._process = self.tools.popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
253
+ stderr=subprocess.DEVNULL, start_new_session=True, text=True,
254
+ encoding="utf-8", errors="replace", bufsize=1)
255
+ except BaseException:
256
+ self._handle.close()
257
+ self._handle = None
258
+ raise
259
+ self._thread = threading.Thread(target=self._read, name="mobster-evidence-log", daemon=True)
260
+ self._thread.start()
261
+
262
+ def _read(self):
263
+ try:
264
+ for raw in self._process.stdout:
265
+ line = self.tools.parse(raw, self.clock(), self.t0_wall)
266
+ if line is not None:
267
+ self._take(line)
268
+ except (OSError, ValueError, AttributeError) as error:
269
+ self.error = type(error).__name__
270
+
271
+ def _take(self, line):
272
+ if not line.level and not line.process: # the next line of the entry before
273
+ if self._last is None:
274
+ return
275
+ line = Line(self._last.at_ms, self._last.level, self._last.process, line.text)
276
+ elif line.process == "SpringBoard":
277
+ event = exit_event(line.text, self.bundle_id)
278
+ self._last = None
279
+ if event is None:
280
+ return
281
+ event.at_ms = line.at_ms
282
+ self.on_exit(event)
283
+ # SpringBoard's own words hold "code:SIGTRAP(5)", which the secret filter would take for a code.
284
+ line = Line(line.at_ms, line.level, "SpringBoard", f"{self.bundle_id} process {event.pid} exited: "
285
+ f"{event.domain} {event.code}" + (" (a crash)" if event.crashed else ""))
286
+ self._last = line if line.process != "SpringBoard" else None
287
+ line.text = secret_filter.redact(line.text[:LINE_LIMIT])
288
+ with self._lock:
289
+ if self._stopped:
290
+ return
291
+ size = len(line.render().encode("utf-8")) + 1
292
+ if self.bytes + size <= LOG_CAP_BYTES and not self.tail:
293
+ self.lines.append(line)
294
+ self.bytes += size
295
+ self._handle.write(line.render() + "\n")
296
+ else:
297
+ if len(self.tail) == self.tail.maxlen:
298
+ self.dropped += 1
299
+ self.tail.append(line)
300
+
301
+ def stop(self):
302
+ """End the log process and finish the file (idempotent): the head, a note of what was dropped, the tail."""
303
+ process, self._process = self._process, None
304
+ if process is not None:
305
+ _signal(process)
306
+ threading.Thread(target=_end, args=(process,), name="mobster-evidence-reap", daemon=True).start()
307
+ if self._thread is not None:
308
+ self._thread.join(DRAIN_WAIT)
309
+ with self._lock:
310
+ if self._stopped:
311
+ return
312
+ self._stopped = True
313
+ handle, self._handle = self._handle, None
314
+ if handle is None:
315
+ return
316
+ try:
317
+ if self.dropped:
318
+ handle.write(f"[{self.dropped} lines dropped: the log passed {LOG_CAP_BYTES // 1000} kB; the last "
319
+ f"{len(self.tail)} follow]\n")
320
+ for line in self.tail:
321
+ text = line.render()
322
+ handle.write(text + "\n")
323
+ self.bytes += len(text.encode("utf-8")) + 1
324
+ finally:
325
+ handle.close()
326
+
327
+ @property
328
+ def count(self):
329
+ with self._lock:
330
+ return len(self.lines) + len(self.tail) + self.dropped
331
+
332
+ def all(self):
333
+ with self._lock:
334
+ return list(self.lines) + list(self.tail)
335
+
336
+ def window(self, center_ms, half=WINDOW_MS, limit=WINDOW_LINES):
337
+ near = [line for line in self.all() if abs(line.at_ms - center_ms) <= half and not line.driver]
338
+ if len(near) > limit:
339
+ def rank(i):
340
+ line = near[i]
341
+ return (0 if line.own or line.level in ("error", "fault") or line.process == "SpringBoard" else 1,
342
+ abs(line.at_ms - center_ms))
343
+ keep = sorted(range(len(near)), key=rank)[:limit]
344
+ near = [near[i] for i in sorted(keep)]
345
+ return near
346
+
347
+
348
+ def _signal(process):
349
+ """SIGTERM to the log process's group (it was started in a session of its own)."""
350
+ try:
351
+ if process.poll() is None:
352
+ try:
353
+ if not isinstance(process, subprocess.Popen):
354
+ raise TypeError("not a process this module started")
355
+ os.killpg(process.pid, signal.SIGTERM)
356
+ except (OSError, TypeError):
357
+ process.terminate()
358
+ except Exception:
359
+ pass
360
+
361
+
362
+ def _end(process):
363
+ """Reap the log process, killing it when it hasn't ended STOP_WAIT seconds after SIGTERM."""
364
+ try:
365
+ try:
366
+ process.wait(timeout=STOP_WAIT)
367
+ except subprocess.TimeoutExpired:
368
+ process.kill()
369
+ process.wait(timeout=STOP_WAIT)
370
+ except Exception:
371
+ pass
372
+
373
+
374
+ class Evidence:
375
+ """The evidence of one run."""
376
+
377
+ def __init__(self, tools, app, run_dir, *, t0_wall, project_root=None, log=None, clock=time.time,
378
+ sleep=time.sleep):
379
+ self.tools, self.app, self.run_dir = tools, app, Path(run_dir)
380
+ self.t0_wall, self.root = t0_wall, project_root
381
+ self._log = log or (lambda line: None)
382
+ self.clock, self.sleep = clock, sleep
383
+ self.executable = None
384
+ self.capture = None
385
+ self.note = None # why there's no log, when there isn't one
386
+ self.pids = set() # the app's processes the run saw
387
+ self.exits = [] # Exit events from SpringBoard's log
388
+ self.crash_summary = None
389
+ self._looked = False
390
+ self._starter = None
391
+ self._closed = False # stop() came: a log the starter hasn't started yet never starts
392
+ self._lock = threading.Lock()
393
+
394
+ @property
395
+ def kind(self):
396
+ return self.tools.kind
397
+
398
+ def start(self):
399
+ """Start the log on a thread of its own, so the launch never waits for it (an iPhone's executable name is one
400
+ ideviceinstaller call)."""
401
+ self._starter = threading.Thread(target=self._start, name="mobster-evidence-start", daemon=True)
402
+ self._starter.start()
403
+
404
+ def _start(self):
405
+ try:
406
+ self.executable = self.tools.executable(self.app)
407
+ except Exception as error:
408
+ self._log(f"evidence: no executable name ({type(error).__name__})")
409
+ if not self.executable:
410
+ self.note = "No log: Mobster couldn't read the app's executable name."
411
+ return
412
+ capture = LogCapture(self.tools, self.executable, self.app.bundle_id, self.run_dir / LOG_FILE,
413
+ t0_wall=self.t0_wall, clock=self.clock, on_exit=self._exited)
414
+ with self._lock:
415
+ if self._closed:
416
+ return
417
+ try:
418
+ capture.start()
419
+ except Exception as error:
420
+ self.note = "No log: " + ("the log tool isn't installed." if isinstance(error, LookupError)
421
+ else f"the log didn't start ({type(error).__name__}).")
422
+ self._log(f"evidence: no log ({type(error).__name__}: {error})")
423
+ return
424
+ self.capture = capture
425
+
426
+ def _exited(self, event):
427
+ with self._lock:
428
+ self.exits.append(event)
429
+ if event.crashed:
430
+ self.saw(event.pid)
431
+ self._log(f"evidence: {self.app.bundle_id} process {event.pid} exited: {event.why}")
432
+
433
+ def saw(self, pid):
434
+ """A process of the app the run saw (WebDriverAgent's foreground app): its crash is this run's."""
435
+ if isinstance(pid, int) and not isinstance(pid, bool) and pid > 0:
436
+ with self._lock:
437
+ self.pids.add(pid)
438
+
439
+ def _ready(self, wait=STOP_WAIT * 4):
440
+ if self._starter is not None:
441
+ self._starter.join(wait)
442
+
443
+ def crashed(self):
444
+ """The first crashed Exit the log showed, or None."""
445
+ with self._lock:
446
+ return next((event for event in self.exits if event.crashed), None)
447
+
448
+ def crash(self, *, suspect=False):
449
+ """This run's crash summary, or None. Looks once; with ``suspect`` it waits EXIT_WAIT, then REPORT_WAIT."""
450
+ with self._lock:
451
+ if self._looked:
452
+ return self.crash_summary
453
+ self._ready()
454
+ exited = self.crashed()
455
+ if exited is None and suspect and self.capture is not None:
456
+ deadline = self.clock() + EXIT_WAIT
457
+ while exited is None and self.clock() < deadline:
458
+ self.sleep(CRASH_POLL)
459
+ exited = self.crashed()
460
+ summary = None
461
+ executable = self.executable or getattr(self.app, "executable", None)
462
+ if exited is not None or (suspect and self.capture is None):
463
+ wait = REPORT_WAIT.get(self.kind, 8.0) if exited is not None else 0
464
+ summary = self._report(executable, self.clock() + wait) if executable else None
465
+ if summary is None and exited is not None:
466
+ summary = {"type": None, "signal": exited.why, "subtype": None, "message": None, "thread": None,
467
+ "process": executable, "pid": exited.pid, "frames": [], "symbolicated": False,
468
+ "atMs": exited.at_ms, "report": None}
469
+ elif summary is not None and exited is not None and not summary.get("signal"):
470
+ summary["signal"] = exited.why
471
+ with self._lock:
472
+ self._looked = True
473
+ self.crash_summary = summary
474
+ return summary
475
+
476
+ def _report(self, executable, deadline):
477
+ """The crash report's summary, looking until ``deadline``; None when it hasn't appeared."""
478
+ while True:
479
+ report = self._find(executable)
480
+ if report is not None:
481
+ return self._summarize(report, executable)
482
+ if self.clock() >= deadline:
483
+ return None
484
+ self.sleep(CRASH_POLL)
485
+
486
+ def _summarize(self, report, executable):
487
+ try:
488
+ dsyms = crashes.find_dsyms(getattr(self.app, "path", None), self.root)
489
+ summary = crashes.summary(report, executable=executable, dsyms=dsyms, run=self.tools.run)
490
+ copy = crashes.write_copy(report, self.run_dir / CRASH_DIR)
491
+ except Exception as error:
492
+ self._log(f"evidence: the crash report couldn't be read ({type(error).__name__}: {error})")
493
+ return None
494
+ summary["report"] = copy.relative_to(self.run_dir).as_posix()
495
+ summary["atMs"] = round((report.wall - self.t0_wall) * 1000)
496
+ self._log(f"evidence: crash report {report.path.name} ({summary.get('type')})")
497
+ return summary
498
+
499
+ def _find(self, executable):
500
+ try:
501
+ paths, scratch = self.tools.crash_reports(executable, self.t0_wall)
502
+ except Exception as error:
503
+ self._log(f"evidence: no crash reports ({type(error).__name__})")
504
+ return None
505
+ try:
506
+ with self._lock:
507
+ pids = set(self.pids)
508
+ udid = getattr(self.tools, "udid", None) if self.kind == "simulator" else None
509
+ for path in paths:
510
+ report = crashes.read(path)
511
+ if report is not None and crashes.belongs(
512
+ report, bundle_id=self.app.bundle_id, executable=executable,
513
+ since=self.t0_wall - (CLOCK_SLACK if self.kind == "iphone" else 0),
514
+ until=self.clock() + CLOCK_SLACK, pids=pids, udid=udid):
515
+ return report
516
+ return None
517
+ finally:
518
+ if scratch:
519
+ shutil.rmtree(scratch, ignore_errors=True)
520
+
521
+ def stop(self):
522
+ """Stop the log (idempotent). A log still starting never starts."""
523
+ self._ready(STOP_WAIT)
524
+ with self._lock:
525
+ self._closed = True
526
+ capture = self.capture
527
+ if capture is not None:
528
+ capture.stop()
529
+
530
+ def lines(self):
531
+ """The captured log as dicts for the per-step proof (verify/proof.py)."""
532
+ with self._lock:
533
+ capture = self.capture
534
+ return [{"atMs": line.at_ms, "level": line.level, "process": line.process, "text": line.text, "own": line.own,
535
+ "driver": line.driver} for line in capture.all()] if capture is not None else []
536
+
537
+ def summary(self, *, center_ms=None, steps=(), failed=False):
538
+ """results.json's ``evidence``: the log file, and for a run that didn't pass, the window around the failure, the
539
+ step it came in and the crash."""
540
+ with self._lock:
541
+ capture, crash = self.capture, self.crash_summary
542
+ out = {"source": self.kind, "startedAt": round(self.t0_wall * 1000)}
543
+ if capture is not None:
544
+ out["log"] = {"path": LOG_FILE, "lines": capture.count, "bytes": capture.bytes, "dropped": capture.dropped}
545
+ elif self.note:
546
+ out["note"] = self.note
547
+ if crash is not None:
548
+ out["crash"] = dict(crash)
549
+ if not failed and crash is None:
550
+ return out
551
+ center = crash.get("atMs") if crash else center_ms
552
+ if center is None:
553
+ return out
554
+ out["atMs"] = center
555
+ step = failing_step(steps, center)
556
+ if step is not None:
557
+ out["step"] = step
558
+ if capture is not None:
559
+ # "app": the app under test's own line (its process, not an Apple framework's subsystem). The Mac app shows
560
+ # those first and folds the rest (accessibility, system daemons) behind Show system log (ICP-3).
561
+ executable = getattr(capture, "executable", None)
562
+ out["window"] = {"fromMs": center - WINDOW_MS, "toMs": center + WINDOW_MS,
563
+ "lines": [{**line.to_dict(), "app": bool(executable) and line.process == executable
564
+ and not line.apple} for line in capture.window(center)]}
565
+ return out
566
+
567
+
568
+ def failing_step(steps, at_ms):
569
+ """The step the failure came in: the first one recorded at or after ``at_ms``, else the last."""
570
+ # A step is recorded once its action and settle end, so a crash its tap causes comes before the step's own time.
571
+ timed = [(step.get("at_ms"), step.get("index")) for step in steps or ()
572
+ if isinstance(step.get("at_ms"), (int, float))]
573
+ for at, index in timed:
574
+ if at >= at_ms:
575
+ return index
576
+ return timed[-1][1] if timed else None
577
+
578
+
579
+ def for_run(manager, target, app, run_dir, *, t0_wall, project_root=None, log=None):
580
+ """The run's Evidence, or None when its manager offers none (a WebDriverAgent address, a fake)."""
581
+ offer = getattr(manager, "evidence_tools", None)
582
+ if not callable(offer):
583
+ return None
584
+ try:
585
+ tools = offer(target)
586
+ except Exception:
587
+ return None
588
+ if tools is None:
589
+ return None
590
+ return Evidence(tools, app, run_dir, t0_wall=t0_wall, project_root=project_root, log=log)
591
+
592
+
593
+ # `mobster test` waits this long, once, at the end of a suite, for crash reports macOS hadn't written when their run
594
+ # ended. A Mac under load wrote them 1-4 min after the crash (16 Oct); an idle one, within seconds.
595
+ LATE_WAIT = 60.0
596
+ LATE_POLL = 1.0
597
+
598
+
599
+ def late_reports(results, *, progress=None, budget=LATE_WAIT, clock=time.time, sleep=time.sleep, tools_for=None):
600
+ pending = [cell for check in results.get("checks") or () for cell in check.get("results") or ()
601
+ if _waiting(cell.get("evidence"))]
602
+ if not pending:
603
+ return 0
604
+ progress = progress or (lambda line: None)
605
+ count = len(pending)
606
+ progress(f"Waiting up to {budget:.0f} s for {count} crash report{'' if count == 1 else 's'}: macOS writes "
607
+ "them after the crash")
608
+ deadline, found = clock() + budget, 0
609
+ while pending:
610
+ for cell in list(pending):
611
+ try:
612
+ attached = _attach(cell, tools_for=tools_for or _tools_for)
613
+ except Exception:
614
+ attached = None
615
+ if attached is not False and attached is not None:
616
+ pending.remove(cell)
617
+ found += 1
618
+ elif attached is False:
619
+ pending.remove(cell) # its run folder is gone or unreadable: nothing to wait for
620
+ if not pending or clock() >= deadline:
621
+ break
622
+ sleep(LATE_POLL)
623
+ if pending:
624
+ progress(f"{len(pending)} crash report{'' if len(pending) == 1 else 's'} didn't arrive in time; macOS keeps "
625
+ "them in ~/Library/Logs/DiagnosticReports")
626
+ return found
627
+
628
+
629
+ def _waiting(evidence):
630
+ crash = evidence.get("crash") if isinstance(evidence, dict) else None
631
+ return bool(crash) and not crash.get("report") and bool(evidence.get("runDir"))
632
+
633
+
634
+ def _tools_for(source, udid):
635
+ if source == "simulator":
636
+ return SimulatorTools(udid)
637
+ if source == "iphone":
638
+ return PhoneTools(udid)
639
+ return None
640
+
641
+
642
+ def _attach(cell, *, tools_for):
643
+ """One crash report, looked for once: True when found and written, None when not there yet, False when the run can't
644
+ take one."""
645
+ from types import SimpleNamespace
646
+
647
+ from .checks import project_root
648
+ from .crashes import headline
649
+ from .report import write_report
650
+ evidence = cell["evidence"]
651
+ run_dir = Path(evidence["runDir"])
652
+ try:
653
+ result = json.loads((run_dir / "result.json").read_text(encoding="utf-8"))
654
+ except (OSError, ValueError):
655
+ return False
656
+ stored = result.get("evidence") or {}
657
+ crash, app, device = stored.get("crash") or {}, result.get("app") or {}, result.get("device") or {}
658
+ tools = tools_for(stored.get("source"), device.get("udid"))
659
+ if tools is None or not stored.get("startedAt") or not crash.get("process"):
660
+ return False
661
+ lookup = Evidence(tools, SimpleNamespace(bundle_id=app.get("bundle_id"), executable=crash["process"],
662
+ path=app.get("path")),
663
+ run_dir, t0_wall=stored["startedAt"] / 1000, project_root=project_root(run_dir))
664
+ lookup.executable = crash["process"]
665
+ if isinstance(crash.get("pid"), int):
666
+ lookup.saw(crash["pid"])
667
+ summary = lookup._report(crash["process"], deadline=0)
668
+ if summary is None:
669
+ return None
670
+ if not summary.get("signal"):
671
+ summary["signal"] = crash.get("signal")
672
+ stored["crash"] = summary
673
+ # The report's own time is the crash's: SpringBoard logs the exit seconds later.
674
+ stored["atMs"] = summary["atMs"]
675
+ step = failing_step(result.get("steps"), summary["atMs"])
676
+ if step is not None:
677
+ stored["step"] = step
678
+ message = headline(summary, app.get("name") or app.get("bundle_id") or "The app")
679
+ if (result.get("reason") or {}).get("class") == "app_crashed":
680
+ result["reason"]["message"] = message
681
+ result["summary"] = message
682
+ result["evidence"] = stored
683
+ from ..paths import write_text_atomic
684
+ write_text_atomic(run_dir / "result.json", json.dumps(result, indent=1, ensure_ascii=False, allow_nan=False) + "\n")
685
+ write_report(run_dir, result)
686
+ evidence["crash"] = summary
687
+ evidence["atMs"] = stored["atMs"]
688
+ if "step" in stored:
689
+ evidence["step"] = stored["step"]
690
+ if (cell.get("reason") or {}).get("class") == "app_crashed":
691
+ cell["reason"]["message"] = message
692
+ for run in cell.get("runs") or ():
693
+ if run.get("runDir") == str(run_dir) and run.get("class") == "app_crashed":
694
+ run["message"] = message
695
+ return True
696
+
697
+
698
+ def log_text(lines):
699
+ """Window lines as plain text, for JUnit's <system-out>."""
700
+ return "\n".join(Line(item.get("atMs") or 0, item.get("level") or "", item.get("process") or "",
701
+ item.get("text") or "").render() for item in lines or ())
702
+
703
+
704
+ __all__ = ["Evidence", "LogCapture", "Line", "Exit", "SimulatorTools", "PhoneTools", "for_run", "failing_step",
705
+ "exit_event", "late_reports", "log_text", "WINDOW_MS", "LOG_FILE"]