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,309 @@
1
+ """Set or remove one table of a TOML file, or one entry of a YAML mapping, as a text edit."""
2
+
3
+ import copy
4
+ import json
5
+ import re
6
+ import tomllib
7
+
8
+
9
+ class EditError(ValueError):
10
+ """The file can't be edited safely. The message says why."""
11
+
12
+
13
+ # ---------------------------------------------------------------------------------------------------- TOML
14
+
15
+ HEADER = re.compile(r"^\s*(\[\[?)\s*(.+?)\s*(\]\]?)\s*(?:#.*)?$")
16
+
17
+
18
+ def _toml_key_path(raw):
19
+ """``a."b c".d`` as ["a", "b c", "d"]; None when it can't be read."""
20
+ parts, pos = [], 0
21
+ raw = raw.strip()
22
+ while pos < len(raw):
23
+ if raw[pos] == '"':
24
+ end = pos + 1
25
+ while end < len(raw) and raw[end] != '"':
26
+ end += 2 if raw[end] == "\\" else 1
27
+ try:
28
+ parts.append(json.loads(raw[pos:end + 1]))
29
+ except ValueError:
30
+ return None
31
+ pos = end + 1
32
+ elif raw[pos] == "'":
33
+ end = raw.find("'", pos + 1)
34
+ if end < 0:
35
+ return None
36
+ parts.append(raw[pos + 1:end])
37
+ pos = end + 1
38
+ else:
39
+ match = re.match(r"[A-Za-z0-9_-]+", raw[pos:])
40
+ if not match:
41
+ return None
42
+ parts.append(match.group())
43
+ pos += match.end()
44
+ rest = re.match(r"\s*(\.)?\s*", raw[pos:])
45
+ pos += rest.end()
46
+ if not rest.group(1) and pos < len(raw):
47
+ return None
48
+ return parts
49
+
50
+
51
+ def toml_string(value):
52
+ """A TOML basic string."""
53
+ return json.dumps(str(value), ensure_ascii=False).replace("\x7f", "\\u007F")
54
+
55
+
56
+ def toml_value(value):
57
+ if isinstance(value, bool):
58
+ return "true" if value else "false"
59
+ if isinstance(value, (int, float)):
60
+ return repr(value)
61
+ if isinstance(value, list):
62
+ return "[" + ", ".join(toml_value(item) for item in value) + "]"
63
+ if isinstance(value, dict):
64
+ return "{ " + ", ".join(f"{_toml_key(k)} = {toml_value(v)}" for k, v in value.items()) + " }"
65
+ return toml_string(value)
66
+
67
+
68
+ def _toml_key(key):
69
+ return key if re.fullmatch(r"[A-Za-z0-9_-]+", key) else toml_string(key)
70
+
71
+
72
+ def toml_table(path, value):
73
+ """``[a.b]`` with ``value``'s scalar and list keys, then a sub-table for each dict value."""
74
+ lines = ["[" + ".".join(_toml_key(part) for part in path) + "]"]
75
+ tables = []
76
+ for key, item in value.items():
77
+ if isinstance(item, dict):
78
+ tables.append((key, item))
79
+ else:
80
+ lines.append(f"{_toml_key(key)} = {toml_value(item)}")
81
+ for key, item in tables:
82
+ lines.append("")
83
+ lines.extend(toml_table([*path, key], item).splitlines())
84
+ return "\n".join(lines) + "\n"
85
+
86
+
87
+ def toml_loads(text):
88
+ try:
89
+ return tomllib.loads(text)
90
+ except tomllib.TOMLDecodeError as error:
91
+ raise EditError(f"it isn't valid TOML ({error})") from None
92
+
93
+
94
+ def _dig(data, path):
95
+ for part in path:
96
+ if not isinstance(data, dict) or part not in data:
97
+ return None
98
+ data = data[part]
99
+ return data
100
+
101
+
102
+ def _toml_cut(text, path):
103
+ """``text`` without the table at ``path`` and its sub-tables; and the line index where the first one began."""
104
+ lines = text.splitlines(keepends=True)
105
+ kept, cut_at, cutting, in_string = [], None, False, None
106
+ for line in lines:
107
+ # A header inside a multi-line string isn't a header.
108
+ if in_string is None:
109
+ match = HEADER.match(line)
110
+ if match:
111
+ key = _toml_key_path(match.group(2))
112
+ cutting = key is not None and match.group(1) == "[" and key[:len(path)] == list(path)
113
+ if cutting and cut_at is None:
114
+ cut_at = len(kept)
115
+ for quote in ('"""', "'''"):
116
+ count = line.count(quote)
117
+ if count % 2 == 1 and in_string in (None, quote):
118
+ in_string = None if in_string == quote else quote
119
+ if not cutting:
120
+ kept.append(line)
121
+ if cut_at is not None:
122
+ # Drop the blank lines the cut table left behind it.
123
+ while cut_at > 0 and not kept[cut_at - 1].strip() and (cut_at >= len(kept) or not kept[cut_at].strip()):
124
+ kept.pop(cut_at - 1)
125
+ cut_at -= 1
126
+ return "".join(kept), cut_at
127
+
128
+
129
+ def _pruned(data, path):
130
+ """``data`` without the tables along ``path`` that are left empty."""
131
+ data = copy.deepcopy(data)
132
+ for depth in range(len(path), 0, -1):
133
+ parent = _dig(data, path[:depth - 1])
134
+ if isinstance(parent, dict) and parent.get(path[depth - 1]) == {}:
135
+ del parent[path[depth - 1]]
136
+ return data
137
+
138
+
139
+ def toml_set_table(text, path, value):
140
+ """``text`` with the table at ``path`` (such as ("mcp_servers", "mobster")) holding exactly ``value``."""
141
+ data = toml_loads(text)
142
+ if _dig(data, path) == value:
143
+ return text
144
+ expected = copy.deepcopy(data)
145
+ target = expected
146
+ for part in path[:-1]:
147
+ target = target.setdefault(part, {})
148
+ if not isinstance(target, dict):
149
+ raise EditError(f"{part} is not a table")
150
+ target[path[-1]] = copy.deepcopy(value)
151
+ cut, cut_at = _toml_cut(text, path)
152
+ if _dig(toml_loads(cut), path) is not None:
153
+ raise EditError(f"{'.'.join(path)} is written inline or with dotted keys; remove it by hand first")
154
+ table = toml_table(path, value)
155
+ if cut_at is None:
156
+ body = cut.rstrip("\n")
157
+ new_text = (body + "\n\n" if body else "") + table
158
+ else:
159
+ lines = cut.splitlines(keepends=True)
160
+ before, after = "".join(lines[:cut_at]), "".join(lines[cut_at:])
161
+ if before and not before.endswith("\n"):
162
+ before += "\n"
163
+ if before.strip() and not before.endswith("\n\n"):
164
+ before += "\n"
165
+ new_text = before + table + ("\n" + after if after.strip() else after)
166
+ if toml_loads(new_text) != expected:
167
+ raise EditError("the edit would change more than Mobster's entry")
168
+ return new_text
169
+
170
+
171
+ def toml_remove_table(text, path):
172
+ """``text`` without the table at ``path``; ``text`` itself when there is none."""
173
+ data = toml_loads(text)
174
+ if _dig(data, path) is None:
175
+ return text
176
+ expected = copy.deepcopy(data)
177
+ del _dig(expected, path[:-1])[path[-1]]
178
+ new_text, _ = _toml_cut(text, path)
179
+ new_text = re.sub(r"\n{3,}", "\n\n", new_text)
180
+ try:
181
+ ok = toml_loads(new_text) in (expected, _pruned(expected, path[:-1]))
182
+ except EditError:
183
+ ok = False
184
+ if not ok:
185
+ raise EditError(f"{'.'.join(path)} is written inline or with dotted keys; remove it by hand")
186
+ return new_text
187
+
188
+
189
+ # ---------------------------------------------------------------------------------------------------- YAML
190
+
191
+ def yaml_loads(text):
192
+ import yaml
193
+ try:
194
+ data = yaml.safe_load(text) if text.strip() else {}
195
+ except yaml.YAMLError as error:
196
+ raise EditError(f"it isn't valid YAML ({str(error).splitlines()[0]})") from None
197
+ if data is None:
198
+ return {}
199
+ if not isinstance(data, dict):
200
+ raise EditError("it holds something other than a YAML mapping")
201
+ return data
202
+
203
+
204
+ def _indent(line):
205
+ return len(line) - len(line.lstrip(" "))
206
+
207
+
208
+ def _content(line):
209
+ stripped = line.strip()
210
+ return bool(stripped) and not stripped.startswith("#")
211
+
212
+
213
+ def _yaml_entry(key, value, indent):
214
+ import yaml
215
+ dumped = yaml.safe_dump({key: value}, sort_keys=False, default_flow_style=False, allow_unicode=True, width=10_000)
216
+ return "".join(" " * indent + line + "\n" for line in dumped.splitlines())
217
+
218
+
219
+ def _yaml_find(lines, section, key):
220
+ """(section line, end of the section, the child indent, the key's start, the key's end) by line index."""
221
+ top = re.compile(rf"^{re.escape(section)}\s*:\s*(?:#.*)?$")
222
+ head = next((i for i, line in enumerate(lines) if top.match(line.rstrip("\n"))), None)
223
+ if head is None:
224
+ return None
225
+ end, child = len(lines), None
226
+ for i in range(head + 1, len(lines)):
227
+ if _content(lines[i]) and _indent(lines[i]) == 0:
228
+ end = i
229
+ break
230
+ if child is None and _content(lines[i]):
231
+ child = _indent(lines[i])
232
+ start = stop = None
233
+ if child:
234
+ pattern = re.compile(rf"^ {{{child}}}{re.escape(key)}\s*:\s*(?:#.*)?$")
235
+ for i in range(head + 1, end):
236
+ if start is None and pattern.match(lines[i].rstrip("\n")):
237
+ start = i
238
+ elif start is not None and _content(lines[i]) and _indent(lines[i]) <= child:
239
+ stop = i
240
+ break
241
+ if start is not None and stop is None:
242
+ stop = end
243
+ while stop > start + 1 and not _content(lines[stop - 1]):
244
+ stop -= 1
245
+ return head, end, child, start, stop
246
+
247
+
248
+ def yaml_set_entry(text, section, key, value):
249
+ """``text`` with ``section: {key: value}`` at the top level (Goose's ``extensions:``)."""
250
+ data = yaml_loads(text)
251
+ existing = data.get(section)
252
+ if existing is not None and not isinstance(existing, dict):
253
+ raise EditError(f"{section} is not a mapping")
254
+ if isinstance(existing, dict) and existing.get(key) == value:
255
+ return text
256
+ expected = copy.deepcopy(data)
257
+ expected.setdefault(section, None)
258
+ expected[section] = dict(expected[section] or {})
259
+ expected[section][key] = copy.deepcopy(value)
260
+ lines = text.splitlines(keepends=True)
261
+ if lines and not lines[-1].endswith("\n"):
262
+ lines[-1] += "\n"
263
+ found = _yaml_find(lines, section, key)
264
+ empty = re.compile(rf"^{re.escape(section)}\s*:\s*\{{\s*\}}\s*(?:#.*)?$")
265
+ if found is None and data.get(section) == {}:
266
+ lines = [f"{section}:\n" if empty.match(line.rstrip("\n")) else line for line in lines]
267
+ found = _yaml_find(lines, section, key)
268
+ if found is None:
269
+ if section in data:
270
+ raise EditError(f"{section} is written in a form this installer doesn't edit; edit it by hand")
271
+ body = "".join(lines)
272
+ new_text = (body + ("" if not body or body.endswith("\n") else "\n") + f"{section}:\n"
273
+ + _yaml_entry(key, value, 2))
274
+ else:
275
+ head, end, child, start, stop = found
276
+ indent = child or 2
277
+ entry = _yaml_entry(key, value, indent)
278
+ if start is not None:
279
+ lines[start:stop] = [entry]
280
+ else:
281
+ at = end
282
+ while at > head + 1 and not _content(lines[at - 1]):
283
+ at -= 1
284
+ lines[at:at] = [entry]
285
+ new_text = "".join(lines)
286
+ if yaml_loads(new_text) != expected:
287
+ raise EditError("the edit would change more than Mobster's entry")
288
+ return new_text
289
+
290
+
291
+ def yaml_remove_entry(text, section, key):
292
+ """``text`` without ``key`` under ``section``; ``text`` itself when there is none."""
293
+ data = yaml_loads(text)
294
+ if not isinstance(data.get(section), dict) or key not in data[section]:
295
+ return text
296
+ expected = copy.deepcopy(data)
297
+ del expected[section][key]
298
+ lines = text.splitlines(keepends=True)
299
+ found = _yaml_find(lines, section, key)
300
+ if found is None or found[3] is None:
301
+ raise EditError(f"{section}.{key} is written in a form this installer doesn't edit; remove it by hand")
302
+ head, _end, _child, start, stop = found
303
+ del lines[start:stop]
304
+ if not expected[section]:
305
+ lines[head] = f"{section}: {{}}\n"
306
+ new_text = "".join(lines)
307
+ if yaml_loads(new_text) != expected:
308
+ raise EditError("the edit would change more than Mobster's entry")
309
+ return new_text
@@ -0,0 +1,17 @@
1
+ """Deterministic path first: before tapping, ask whether the goal has a link that gets there (dark).
2
+
3
+ ``atlas`` reads what the apps on a phone declare (URL schemes, associated domains) as "discovered" links, a layer
4
+ under ``deeplinks.LINKS``. ``callback`` builds and reads x-callback-url returns. ``plan`` proposes a link for a goal.
5
+ Everything is off unless ``MOBSTER_EXPERIMENTAL_INTENTS=1``. ``deeplinks.decide`` still governs every link; a
6
+ discovered link is never "verified" (that word is for links opened on a real iPhone, with a date).
7
+ """
8
+
9
+ import os
10
+
11
+ ENV_FLAG = "MOBSTER_EXPERIMENTAL_INTENTS"
12
+
13
+
14
+ def enabled():
15
+ """Whether the experimental deterministic-path layer is on (``MOBSTER_EXPERIMENTAL_INTENTS=1``)."""
16
+ return os.environ.get(ENV_FLAG, "").strip().casefold() in ("1", "true", "yes", "on")
17
+
@@ -0,0 +1,216 @@
1
+ """The link atlas: the schemes and universal-link paths the apps on one phone declare.
2
+
3
+ Sources, none of which needs the network here:
4
+ - an app's Info.plist ``CFBundleURLTypes`` (schemes) and, when the bundle carries it, its entitlements'
5
+ ``com.apple.developer.associated-domains`` (``applinks:`` domains; a simulator build keeps
6
+ ``archived-expanded-entitlements.xcent``);
7
+ - an apple-app-site-association document (the caller fetched it, when the network is allowed): the paths each
8
+ domain sends to the app.
9
+ A simulator is read through ``xcrun simctl listapps``. Every entry is "discovered" until a run opens it.
10
+ """
11
+
12
+ import fnmatch
13
+ import json
14
+ import plistlib
15
+ import re
16
+ import tempfile
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from urllib.parse import urlsplit
20
+
21
+ from ..deeplinks import BLOCKED_SCHEMES, SCHEME
22
+
23
+ SCHEME_NAME = re.compile(r"^[a-z][a-z0-9+.-]{0,40}$")
24
+ DOMAIN = re.compile(r"^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$")
25
+ ENTITLEMENTS = "archived-expanded-entitlements.xcent"
26
+ STATE = "discovered"
27
+
28
+
29
+ @dataclass
30
+ class Entry:
31
+ bundle: str
32
+ name: str
33
+ schemes: list = field(default_factory=list)
34
+ domains: list = field(default_factory=list)
35
+ paths: dict = field(default_factory=dict) # domain -> [{"pattern": "/invite/*", "exclude": False}]
36
+
37
+ def public(self):
38
+ return {"bundleId": self.bundle, "app": self.name, "schemes": self.schemes, "domains": self.domains,
39
+ "state": STATE}
40
+
41
+
42
+ def _schemes(info):
43
+ found = []
44
+ for kind in info.get("CFBundleURLTypes") or ():
45
+ for scheme in (kind.get("CFBundleURLSchemes") or ()) if isinstance(kind, dict) else ():
46
+ scheme = str(scheme).strip().casefold()
47
+ if SCHEME_NAME.match(scheme) and scheme not in BLOCKED_SCHEMES and scheme not in found:
48
+ found.append(scheme)
49
+ return found
50
+
51
+
52
+ def _domains(entitlements):
53
+ found = []
54
+ for item in (entitlements or {}).get("com.apple.developer.associated-domains") or ():
55
+ kind, _, host = str(item).partition(":")
56
+ host = host.split("?", 1)[0].strip().casefold()
57
+ if kind == "applinks" and DOMAIN.match(host) and not host.startswith("*") and host not in found:
58
+ found.append(host)
59
+ return found
60
+
61
+
62
+ def entry_from_bundle(bundle, name, info, entitlements=None):
63
+ """An ``Entry`` from an app's Info.plist (a dict) and its entitlements; None when it declares no link."""
64
+ entry = Entry(bundle, str(info.get("CFBundleDisplayName") or info.get("CFBundleName") or name or bundle),
65
+ _schemes(info), _domains(entitlements))
66
+ return entry if entry.schemes or entry.domains else None
67
+
68
+
69
+ def read_app_folder(folder):
70
+ """An ``Entry`` from an unpacked ``.app`` folder; None when it is unreadable or declares nothing."""
71
+ folder = Path(folder)
72
+ try:
73
+ info = plistlib.loads((folder / "Info.plist").read_bytes())
74
+ bundle = str(info["CFBundleIdentifier"])
75
+ except (OSError, ValueError, KeyError, plistlib.InvalidFileException):
76
+ return None
77
+ try:
78
+ entitlements = plistlib.loads((folder / ENTITLEMENTS).read_bytes())
79
+ except (OSError, ValueError, plistlib.InvalidFileException):
80
+ entitlements = None
81
+ return entry_from_bundle(bundle, folder.stem, info, entitlements)
82
+
83
+
84
+ def parse_aasa(document, bundle):
85
+ """The path rules an apple-app-site-association document (text or dict) gives ``bundle``: a list of
86
+ {"pattern", "exclude"}. Old ``paths`` ("NOT /x" excludes) and ``components`` ("/" patterns) are both read."""
87
+ try:
88
+ data = json.loads(document) if isinstance(document, (str, bytes)) else document
89
+ details = data["applinks"]["details"]
90
+ except (ValueError, KeyError, TypeError):
91
+ return []
92
+ rules = []
93
+ for detail in details if isinstance(details, list) else ():
94
+ ids = list(detail.get("appIDs") or ()) + ([detail["appID"]] if detail.get("appID") else [])
95
+ if not any(str(i).partition(".")[2] == bundle for i in ids):
96
+ continue
97
+ for path in detail.get("paths") or ():
98
+ text = str(path)
99
+ rules.append({"pattern": text.removeprefix("NOT ").strip(), "exclude": text.startswith("NOT ")})
100
+ for part in detail.get("components") or ():
101
+ if isinstance(part, dict) and isinstance(part.get("/"), str):
102
+ rules.append({"pattern": part["/"], "exclude": bool(part.get("exclude"))})
103
+ return [r for r in rules if r["pattern"].startswith(("/", "*"))]
104
+
105
+
106
+ def path_allowed(rules, path):
107
+ """Whether the first rule that matches ``path`` includes it (a rule list that never matches refuses)."""
108
+ for rule in rules:
109
+ if fnmatch.fnmatchcase(path, rule["pattern"]):
110
+ return not rule["exclude"]
111
+ return False
112
+
113
+
114
+ class Atlas:
115
+ def __init__(self, entries=()):
116
+ self.entries = {entry.bundle: entry for entry in entries}
117
+
118
+ def add_aasa(self, bundle, domain, document):
119
+ entry = self.entries.get(bundle)
120
+ if entry is not None and domain in entry.domains:
121
+ entry.paths[domain] = parse_aasa(document, bundle)
122
+
123
+ def find(self, url, installed=None):
124
+ """The ``Entry`` that declares ``url``'s scheme, or whose associated domain and paths send it to the app;
125
+ None otherwise. ``installed``: bundle ids to limit to (an app that left the phone claims nothing)."""
126
+ text = str(url or "").strip()
127
+ found = SCHEME.match(text)
128
+ if not found:
129
+ return None
130
+ scheme = found.group(1).casefold()
131
+ for entry in self.entries.values():
132
+ if installed is not None and entry.bundle not in installed:
133
+ continue
134
+ if scheme in ("http", "https"):
135
+ parts = urlsplit(text)
136
+ host = (parts.hostname or "").casefold()
137
+ if host in entry.domains and path_allowed(entry.paths.get(host, ()), parts.path or "/"):
138
+ return entry
139
+ elif scheme in entry.schemes:
140
+ return entry
141
+ return None
142
+
143
+ def by_app(self, words):
144
+ """Entries whose app name or bundle id holds every word of ``words``."""
145
+ wants = [w for w in re.split(r"\W+", str(words or "").casefold()) if w]
146
+ return [e for e in self.entries.values()
147
+ if wants and all(w in f"{e.name} {e.bundle}".casefold() for w in wants)]
148
+
149
+ def named_in(self, goal):
150
+ """Entries whose app name is a whole word run in ``goal`` ("open Things and add milk" finds Things)."""
151
+ said = " " + " ".join(re.split(r"\W+", str(goal or "").casefold())) + " "
152
+ return [e for e in self.entries.values() if f" {' '.join(re.split(r'\W+', e.name.casefold()))} " in said]
153
+
154
+ def to_json(self):
155
+ return json.dumps({"entries": [vars(e) for e in self.entries.values()]}, indent=1)
156
+
157
+ @classmethod
158
+ def from_json(cls, text):
159
+ try:
160
+ rows = json.loads(text)["entries"]
161
+ return cls(Entry(**row) for row in rows)
162
+ except (ValueError, KeyError, TypeError):
163
+ return cls()
164
+
165
+
166
+ def path_for(key):
167
+ from ..paths import dev_data_dir
168
+ return dev_data_dir() / "link-atlas" / (re.sub(r"[^A-Za-z0-9._-]", "_", key) + ".json")
169
+
170
+
171
+ def load(key):
172
+ try:
173
+ return Atlas.from_json(path_for(key).read_text())
174
+ except OSError:
175
+ return Atlas()
176
+
177
+
178
+ def save(key, atlas):
179
+ from ..paths import write_text_atomic
180
+ write_text_atomic(path_for(key), atlas.to_json())
181
+
182
+
183
+ def harvest_simulator(udid, run):
184
+ """The atlas of a simulator's apps. ``run(argv, timeout)`` is ``sim.simctl.run``'s shape (a ``Result``).
185
+ Reads Info.plist and entitlements of each app ``simctl listapps`` names; an unreadable app is skipped."""
186
+ listed = run(["xcrun", "simctl", "listapps", udid], timeout=60)
187
+ if not listed.ok:
188
+ return Atlas()
189
+ with tempfile.TemporaryDirectory(prefix="mobster-atlas-") as folder:
190
+ source = Path(folder) / "apps.plist"
191
+ source.write_text(listed.out)
192
+ converted = run(["plutil", "-convert", "json", "-o", "-", str(source)], timeout=30)
193
+ try:
194
+ apps = json.loads(converted.out) if converted.ok else {}
195
+ except ValueError:
196
+ apps = {}
197
+ entries = []
198
+ for bundle, app in apps.items() if isinstance(apps, dict) else ():
199
+ where = str(app.get("Path") or "") if isinstance(app, dict) else ""
200
+ entry = read_app_folder(where) if where else None
201
+ if entry is None and isinstance(app, dict):
202
+ entry = entry_from_bundle(bundle, app.get("CFBundleDisplayName") or "", app, None)
203
+ if entry is not None and entry.bundle == bundle:
204
+ entries.append(entry)
205
+ return Atlas(entries)
206
+
207
+
208
+ def discovered(args):
209
+ """The atlas entries (as dicts) for an ``open_url`` listing's ``app`` and ``query`` on its ``device``."""
210
+ words = f"{args.get('app') or ''} {args.get('query') or ''}"
211
+ return [e.public() for e in load(args.get("device") or "default").by_app(words)]
212
+
213
+
214
+ def describe(rows):
215
+ lines = [f"{r['app']}: {', '.join(s + '://' for s in r['schemes']) or 'no scheme'}" for r in rows]
216
+ return "Discovered in the apps' own files, not verified. open_url still asks as usual:\n" + "\n".join(lines)
@@ -0,0 +1,44 @@
1
+ """x-callback-url: ask an app to hand control back to Mobster's own scheme, so the answer needs no screen read.
2
+
3
+ ``with_callback(url, token)`` adds ``x-success``, ``x-error`` and ``x-cancel`` to a link that is already allowed by
4
+ ``deeplinks.decide``; ``parse(url)`` reads what came back. Receiving the callback needs the runner to own the
5
+ ``mobster-callback`` scheme; that part is pending a proof on a simulator. Mobster never opens a callback link
6
+ itself: it only reads one the runner reports.
7
+ """
8
+
9
+ import re
10
+ from urllib.parse import parse_qsl, quote, urlsplit
11
+
12
+ from ..deeplinks import LinkError, decide
13
+
14
+ SCHEME = "mobster-callback"
15
+ TOKEN = re.compile(r"^[A-Za-z0-9_-]{6,64}$")
16
+ STATUSES = ("success", "error", "cancel")
17
+
18
+
19
+ def with_callback(url, token):
20
+ """``url`` with the three return addresses added. LinkError for a link Mobster never opens, a web page (it has no
21
+ callback) or a bad token. The link keeps its decision: ``decide(result)`` asks exactly when ``decide(url)`` did."""
22
+ if not TOKEN.match(str(token or "")):
23
+ raise LinkError("A callback token is 6 to 64 letters, digits, - or _.")
24
+ decision = decide(url)
25
+ scheme = decision.url.partition(":")[0].casefold()
26
+ if scheme in ("http", "https"):
27
+ raise LinkError("A web page can't return to Mobster. Use an app's link.")
28
+ base, _, query = decision.url.partition("?")
29
+ returns = "&".join(f"x-{status}={quote(f'{SCHEME}://{token}/{status}', safe='')}" for status in STATUSES)
30
+ return f"{base}?{query}&{returns}" if query else f"{base}?{returns}"
31
+
32
+
33
+ def parse(url):
34
+ """{"token", "status", "params"} for a callback address, or None when it isn't one."""
35
+ parts = urlsplit(str(url or "").strip())
36
+ status = parts.path.strip("/")
37
+ if parts.scheme.casefold() != SCHEME or status not in STATUSES or not TOKEN.match(parts.netloc):
38
+ return None
39
+ return {"token": parts.netloc, "status": status, "params": dict(parse_qsl(parts.query))}
40
+
41
+
42
+ def shortcut_run(name, token):
43
+ """The x-callback link that runs a shortcut and returns. It acts, so it asks every time (``decide``)."""
44
+ return with_callback("shortcuts://x-callback-url/run-shortcut?name=" + quote(str(name).strip(), safe=""), token)
@@ -0,0 +1,55 @@
1
+ """`mobster intents harvest`: read a simulator's apps and their associated domains into the link atlas.
2
+
3
+ Hidden unless MOBSTER_EXPERIMENTAL_INTENTS=1. The AASA fetch is the only network use; `--no-network` skips it.
4
+ Exit codes: 0 harvested, 3 couldn't (no simulator, no Xcode, usage too).
5
+ """
6
+
7
+ import json
8
+ import sys
9
+
10
+ from ..devkit import DevError, main, say, subcommands
11
+
12
+
13
+ def add_arguments(parser, helpers):
14
+ add = subcommands(parser, "intents_command")
15
+ harvest = add("harvest", help="read a simulator's apps and domains into the link atlas",
16
+ description="Read each app's URL schemes and associated domains on a simulator through simctl, "
17
+ "fetch each domain's apple-app-site-association (https), and keep the atlas under "
18
+ "Mobster's data folder. Discovered links are never marked verified.")
19
+ harvest.add_argument("--sim", metavar="UDID", help="the simulator (default: the one booted simulator)")
20
+ harvest.add_argument("--force", action="store_true", help="read again although the atlas is under a day old")
21
+ harvest.add_argument("--no-network", action="store_true", help="skip the AASA fetch")
22
+ harvest.add_argument("--json", action="store_true", help="print the atlas as JSON")
23
+
24
+
25
+ def run(args):
26
+ return main("intents", {"harvest": _harvest}, args)
27
+
28
+
29
+ def booted(run):
30
+ """The UDID of the one booted simulator; DevError otherwise."""
31
+ result = run(["xcrun", "simctl", "list", "devices", "booted", "-j"], timeout=30)
32
+ try:
33
+ devices = [d["udid"] for group in json.loads(result.out)["devices"].values() for d in group
34
+ if d.get("state") == "Booted"] if result.ok else []
35
+ except (ValueError, KeyError, TypeError):
36
+ devices = []
37
+ if len(devices) != 1:
38
+ raise DevError("Mobster needs one booted simulator to read.", "Pass --sim UDID, or boot just one.")
39
+ return devices[0]
40
+
41
+
42
+ def _harvest(args):
43
+ from ..sim.simctl import run as simctl_run
44
+ from . import harvest
45
+ udid = args.sim or booted(simctl_run)
46
+ atlas = harvest.harvest(udid, simctl_run, fetch=None if args.no_network else harvest.fetch_aasa,
47
+ log=lambda line: say(line, sys.stderr), force=args.force)
48
+ if args.json:
49
+ say(atlas.to_json())
50
+ else:
51
+ for entry in atlas.entries.values():
52
+ say(f"{entry.name}: {', '.join(s + '://' for s in entry.schemes) or '-'}"
53
+ f" {', '.join(entry.domains) or '-'} discovered")
54
+ say(f"{len(atlas.entries)} apps declare links.")
55
+ return 0