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,380 @@
1
+ """Workflows as files: each project's `.mobster/workflows/*.yaml` and the workflows Mobster runs, in step both ways.
2
+
3
+ Every 1.5 seconds Mobster looks at each project's workflow files (a stat each; a file is read only when it changed)
4
+ and at the workflows that have a file (the record's ``file``: where it is, the hash and the published revision last
5
+ in step, and how it stands). Then, for each file:
6
+
7
+ - **A new file** becomes a workflow, published, its schedule off until you turn it on. A file that doesn't parse or
8
+ check stays out and is listed with its problem.
9
+ - **The file changed** (a pull, an editor): its graph is checked as any save is and published as a new revision, so
10
+ Versions keeps the one before. If the workflow has an unpublished draft, or was published since, nothing is
11
+ overwritten: the file is ``conflict`` until you resolve it.
12
+ - **The workflow was published** (Publish, the card builder, Bring it back) or renamed: the file is written, unless
13
+ it changed on disk since (``conflict``) or the graph names a real iPhone or holds a secret (``blocked``).
14
+ - **The file was deleted**: the workflow stays and says so (``missing``); publishing writes the file again.
15
+ - **The workflow was deleted in Mobster**: its file goes too, only when it is as Mobster last left it.
16
+
17
+ Saving a draft never writes the file. Nothing is retried: a write that failed says why on the next look. The rest
18
+ of a workflow (its phone, profile, destination, schedule switch, pins) stays on the Mac. Events: ``workflow_file``
19
+ on the ``workflows`` topic.
20
+ """
21
+
22
+ import os
23
+ import threading
24
+ import time
25
+ from pathlib import Path
26
+
27
+ from ..api_errors import APIError
28
+ from ..paths import write_text_atomic
29
+ from . import folder
30
+ from .workflow_files import MAX_BYTES, SUFFIXES, FileError, blocked, digest, parse, render, slug
31
+
32
+ INTERVAL = 1.5
33
+ MAX_FILES = 200
34
+ FOLDER = Path(".mobster") / "workflows"
35
+ TOO_BIG = "is larger than 512 KB, so Mobster didn't read it."
36
+
37
+
38
+ PATH_PROBLEM = ("The workflow path contains a link or leaves this project. Keep .mobster/workflows and its files "
39
+ "inside the project without links, then publish again.")
40
+
41
+
42
+ def _safe_path(root, path):
43
+ """Workflow files stay under their registered project and never follow a link, including a parent folder."""
44
+ root, path = Path(root), Path(path)
45
+ try:
46
+ relative = path.relative_to(root)
47
+ except ValueError:
48
+ return False
49
+ return ".." not in relative.parts and not any(
50
+ root.joinpath(*relative.parts[:index]).is_symlink() for index in range(len(relative.parts) + 1))
51
+
52
+
53
+ def _live(record):
54
+ published = record.get("published")
55
+ return published["graph"] if isinstance(published, dict) and isinstance(published.get("graph"), dict) \
56
+ else record["graph"]
57
+
58
+
59
+ def _published_revision(record):
60
+ published = record.get("published")
61
+ return published.get("revision") if isinstance(published, dict) else None
62
+
63
+
64
+ class WorkflowSync:
65
+ """One project's workflow files and their workflows, for every project of a ProjectService."""
66
+
67
+ def __init__(self, service, workflows, *, interval=INTERVAL, clock=time.time):
68
+ self.service, self.workflows = service, workflows
69
+ self.interval, self.clock = interval, clock
70
+ self._files = {} # absolute path -> (mtime_ns, size, text or None, hash or None)
71
+ self._invalid = {} # (project id, path) -> (hash, sentence): new files that don't parse or check
72
+ self._lock = threading.RLock()
73
+ self._stop = threading.Event()
74
+ self._thread = None
75
+ workflows.deleted.append(self._deleted)
76
+
77
+ # -- the loop --------------------------------------------------------------------------------------------------
78
+
79
+ def start(self):
80
+ self._thread = threading.Thread(target=self._loop, name="mobster-workflow-files", daemon=True)
81
+ self._thread.start()
82
+
83
+ def close(self):
84
+ self._stop.set()
85
+ if self._deleted in self.workflows.deleted:
86
+ self.workflows.deleted.remove(self._deleted)
87
+ if self._thread is not None:
88
+ self._thread.join(2)
89
+
90
+ def _loop(self):
91
+ while not self._stop.wait(self.interval):
92
+ try:
93
+ self.tick()
94
+ except Exception: # noqa: BLE001 -- one bad look never stops the next
95
+ from .. import harness_api
96
+ harness_api.log.exception("workflow files: a look failed")
97
+
98
+ def tick(self):
99
+ """Look once at every project's workflow files and bring each pair in step."""
100
+ with self._lock:
101
+ records = self.workflows.file_records()
102
+ for project in self.service.store.projects():
103
+ if folder.state(project["path"], self.service.home)[0] != "ready":
104
+ continue
105
+ mine = [record for record in records if record["file"].get("projectId") == project["id"]]
106
+ self._project(project, mine)
107
+
108
+ def _project(self, project, records):
109
+ root = Path(project["path"])
110
+ if not _safe_path(root, root / FOLDER):
111
+ for record in records:
112
+ self._mark(project, record, "blocked", PATH_PROBLEM)
113
+ return
114
+ files = {}
115
+ try:
116
+ entries = sorted(os.scandir(root / FOLDER), key=lambda entry: entry.name)
117
+ except OSError:
118
+ entries = []
119
+ for entry in entries[:MAX_FILES]:
120
+ if (not entry.name.startswith(".") and entry.name.lower().endswith(SUFFIXES)
121
+ and entry.is_file(follow_symlinks=False)):
122
+ files[(FOLDER / entry.name).as_posix()] = Path(entry.path)
123
+ links = {record["file"].get("path"): record for record in records}
124
+ for path, absolute in files.items():
125
+ text, hashed = self._read(absolute, project["path"])
126
+ record = links.pop(path, None)
127
+ if record is None:
128
+ self._new(project, path, text, hashed)
129
+ else:
130
+ self._pair(project, record, path, absolute, text, hashed)
131
+ for path, record in links.items():
132
+ self._gone(project, record, root / path)
133
+ known = set(files)
134
+ for key in [key for key in self._invalid if key[0] == project["id"] and key[1] not in known]:
135
+ self._invalid.pop(key, None)
136
+
137
+ def _read(self, absolute, root):
138
+ """(text, hash) of a file, read again only when its size or time changed; (None, None) when it can't be read,
139
+ ("", TOO_BIG) when it is too large."""
140
+ if not _safe_path(root, absolute):
141
+ return None, None
142
+ try:
143
+ status = absolute.stat()
144
+ except OSError:
145
+ return None, None
146
+ kept = self._files.get(str(absolute))
147
+ if kept and kept[:2] == (status.st_mtime_ns, status.st_size):
148
+ return kept[2], kept[3]
149
+ if status.st_size > MAX_BYTES:
150
+ text, hashed = "", TOO_BIG
151
+ else:
152
+ try:
153
+ text = absolute.read_text(encoding="utf-8")
154
+ hashed = digest(text)
155
+ except (OSError, UnicodeDecodeError):
156
+ text, hashed = None, None
157
+ self._files[str(absolute)] = (status.st_mtime_ns, status.st_size, text, hashed)
158
+ return text, hashed
159
+
160
+ # -- each case -------------------------------------------------------------------------------------------------
161
+
162
+ def _link(self, project, path, record, *, hashed, revision, state="synced", problem=None):
163
+ file = {"projectId": project["id"], "projectName": project["name"], "path": path, "hash": hashed,
164
+ "revision": revision, "name": record["name"], "state": state, "problem": problem,
165
+ "syncedAt": round(self.clock() * 1000)}
166
+ before = record.get("file") or {}
167
+ if all(before.get(key) == file[key] for key in ("hash", "revision", "name", "state", "problem", "path")):
168
+ return
169
+ if self.workflows.set_file(record["id"], file) is not None:
170
+ self._publish(record["id"], state, revision)
171
+
172
+ def _mark(self, project, record, state, problem):
173
+ before = record["file"]
174
+ self._link(project, before["path"], record, hashed=before.get("hash"), revision=before.get("revision"),
175
+ state=state, problem=problem)
176
+
177
+ def _new(self, project, path, text, hashed):
178
+ key = (project["id"], path)
179
+ if text is None or self._invalid.get(key, (None,))[0] == hashed:
180
+ return
181
+ if hashed == TOO_BIG:
182
+ self._invalid[key] = (hashed, f"{path} {TOO_BIG}")
183
+ return
184
+ try:
185
+ name, graph = parse(text, path)
186
+ created = self.workflows.create({"name": name, "graph": graph, "publish": True})
187
+ except (FileError, ValueError, APIError) as error:
188
+ self._invalid[key] = (hashed, _sentence(path, error))
189
+ self._publish(None, "invalid", None, path=path, projectId=project["id"])
190
+ return
191
+ self._invalid.pop(key, None)
192
+ self._link(project, path, {**created, "file": None}, hashed=hashed, revision=created["revision"])
193
+
194
+ def _pair(self, project, record, path, absolute, text, hashed):
195
+ file = record["file"]
196
+ if text is None:
197
+ return # unreadable for a moment: the next look tells
198
+ file_changed = hashed != file.get("hash")
199
+ app_changed = _published_revision(record) != file.get("revision") or record["name"] != file.get("name")
200
+ if hashed == TOO_BIG:
201
+ if file_changed:
202
+ self._mark(project, record, "invalid", f"{path} {TOO_BIG}")
203
+ return
204
+ if file.get("state") == "conflict":
205
+ return # it waits for resolve()
206
+ if file_changed and (app_changed or record["graph"] != _live(record)):
207
+ self._mark(project, record, "conflict",
208
+ f"{path} changed while this workflow had changes of its own in Mobster. Keep the file's version "
209
+ "or Mobster's.")
210
+ elif file_changed:
211
+ self._take(project, record, path, text, hashed)
212
+ elif app_changed:
213
+ self._write(project, record, path, absolute)
214
+ elif file.get("state") != "synced":
215
+ self._mark(project, record, "synced", None)
216
+
217
+ def _take(self, project, record, path, text, hashed):
218
+ """The file's graph becomes the published one (a new revision; Versions keeps the one before)."""
219
+ if file_problem := self._invalid.get((project["id"], path)):
220
+ if file_problem[0] == hashed:
221
+ return
222
+ try:
223
+ name, graph = parse(text, path, previous=record["graph"])
224
+ saved = self.workflows.update(record["id"], {"revision": record["revision"], "graph": graph,
225
+ "publish": True, "name": name})
226
+ except APIError as error:
227
+ if error.code == "revision_conflict":
228
+ return # saved meanwhile: the next look sees both changes
229
+ self._mark(project, record, "invalid", _sentence(path, error))
230
+ return
231
+ except (FileError, ValueError) as error:
232
+ self._invalid[(project["id"], path)] = (hashed, _sentence(path, error))
233
+ file = record["file"]
234
+ self._link(project, path, record, hashed=file.get("hash"), revision=file.get("revision"), state="invalid",
235
+ problem=_sentence(path, error))
236
+ return
237
+ self._invalid.pop((project["id"], path), None)
238
+ self._link(project, path, {**record, "name": saved["name"]}, hashed=hashed, revision=saved["revision"])
239
+
240
+ def _write(self, project, record, path, absolute):
241
+ """The workflow's published graph is written to its file."""
242
+ if not _safe_path(project["path"], absolute):
243
+ self._mark(project, record, "blocked", PATH_PROBLEM)
244
+ return
245
+ graph = _live(record)
246
+ reason = blocked(graph)
247
+ if reason:
248
+ self._mark(project, record, "blocked", reason)
249
+ return
250
+ text = render(record["name"], graph)
251
+ try:
252
+ write_text_atomic(absolute, text)
253
+ except OSError as error:
254
+ self._mark(project, record, "blocked", f"Mobster couldn't write {path}: {error.strerror or error}. Check "
255
+ "the folder, then publish again.")
256
+ return
257
+ try:
258
+ status = absolute.stat()
259
+ self._files[str(absolute)] = (status.st_mtime_ns, status.st_size, text, digest(text))
260
+ except OSError:
261
+ pass
262
+ self._link(project, path, record, hashed=digest(text), revision=_published_revision(record))
263
+
264
+ def _gone(self, project, record, absolute):
265
+ file = record["file"]
266
+ if _published_revision(record) != file.get("revision") or record["name"] != file.get("name"):
267
+ self._write(project, record, file["path"], absolute)
268
+ elif file.get("state") != "missing":
269
+ self._mark(project, record, "missing",
270
+ f"{file['path']} isn't in the project any more. Publish the workflow to write it again, or "
271
+ "delete the workflow.")
272
+
273
+ def _deleted(self, record):
274
+ """A workflow deleted in Mobster takes its file with it, only when the file is as Mobster last left it."""
275
+ file = record.get("file")
276
+ if not isinstance(file, dict):
277
+ return
278
+ project = self.service.store.project(file.get("projectId"))
279
+ if project is None:
280
+ return
281
+ absolute = Path(project["path"]) / file["path"]
282
+ with self._lock:
283
+ text, hashed = self._read(absolute, project["path"])
284
+ if text is not None and hashed == file.get("hash"):
285
+ try:
286
+ absolute.unlink()
287
+ except OSError:
288
+ return
289
+ self._files.pop(str(absolute), None)
290
+
291
+ # -- asked from the API ----------------------------------------------------------------------------------------
292
+
293
+ def _record(self, workflow_id):
294
+ record = next((item for item in self.workflows.file_records() if item["id"] == workflow_id), None)
295
+ if record is None:
296
+ raise APIError("That workflow has no file in a project.", 404, "not_found")
297
+ project = self.service.store.project(record["file"].get("projectId"))
298
+ if project is None:
299
+ raise APIError("That workflow's project isn't in Mobster any more.", 404, "not_found")
300
+ return record, project
301
+
302
+ def resolve(self, workflow_id, keep):
303
+ """POST /api/projects/workflows/{id}/resolve {keep}: "file" publishes the file over Mobster's changes (they
304
+ stay in Versions); "mobster" publishes the workflow's draft and writes it over the file."""
305
+ if keep not in ("file", "mobster"):
306
+ raise ValueError('keep is "file" or "mobster"')
307
+ with self._lock:
308
+ record, project = self._record(workflow_id)
309
+ path = record["file"]["path"]
310
+ absolute = Path(project["path"]) / path
311
+ if keep == "file":
312
+ text, hashed = self._read(absolute, project["path"])
313
+ if text is None:
314
+ raise APIError(f"{path} isn't there to keep. Keep Mobster's version instead.", 409, "missing")
315
+ record = {**record, "file": {**record["file"], "state": "synced"}}
316
+ self._take(project, record, path, text, hashed)
317
+ else:
318
+ if record["graph"] != _live(record):
319
+ self.workflows.update(record["id"], {"revision": record["revision"], "graph": record["graph"],
320
+ "publish": True})
321
+ record = next(item for item in self.workflows.file_records() if item["id"] == workflow_id)
322
+ self._write(project, {**record, "file": {**record["file"], "revision": None}}, path, absolute)
323
+ return self.workflows.get(workflow_id)
324
+
325
+ def add(self, project, workflow_id):
326
+ """POST /api/projects/{id}/workflows {workflowId}: the workflow gets a file in the project, written from its
327
+ published graph on the next look."""
328
+ if not isinstance(workflow_id, str):
329
+ raise ValueError("Send the workflow's id")
330
+ with self._lock:
331
+ workflow = self.workflows.get(workflow_id)
332
+ if workflow.get("file"):
333
+ raise APIError(f"{workflow['name']} already has a file: {workflow['file']['path']}.", 409, "exists")
334
+ directory = Path(project["path"]) / FOLDER
335
+ taken = {entry.name for entry in _entries(directory)} if _safe_path(project["path"], directory) else set()
336
+ taken.update(Path(record["file"]["path"]).name for record in self.workflows.file_records()
337
+ if record["file"].get("projectId") == project["id"])
338
+ path = (FOLDER / slug(workflow["name"], taken)).as_posix()
339
+ self.workflows.set_file(workflow_id, {"projectId": project["id"], "projectName": project["name"],
340
+ "path": path, "hash": None, "revision": None,
341
+ "name": workflow["name"], "state": "pending", "problem": None,
342
+ "syncedAt": None})
343
+ self._publish(workflow_id, "pending", None)
344
+ return self.workflows.get(workflow_id)
345
+
346
+ def forget(self, project_id):
347
+ """The project was removed from Mobster: its workflows stay, without a file."""
348
+ with self._lock:
349
+ for record in self.workflows.file_records():
350
+ if record["file"].get("projectId") == project_id:
351
+ self.workflows.set_file(record["id"], None)
352
+ for key in [key for key in self._invalid if key[0] == project_id]:
353
+ self._invalid.pop(key, None)
354
+
355
+ def files(self, project_id):
356
+ """The project's workflow files: each workflow's and each file that isn't one, with its problem."""
357
+ out = [{"path": record["file"]["path"], "workflowId": record["id"], "name": record["name"],
358
+ "state": record["file"].get("state"), "problem": record["file"].get("problem")}
359
+ for record in self.workflows.file_records() if record["file"].get("projectId") == project_id]
360
+ out += [{"path": path, "workflowId": None, "name": None, "state": "invalid", "problem": problem}
361
+ for (project, path), (_, problem) in self._invalid.items() if project == project_id]
362
+ return sorted(out, key=lambda item: item["path"])
363
+
364
+ def _publish(self, workflow_id, state, revision, **fields):
365
+ publish = getattr(self.service.runtime, "publish", None)
366
+ if callable(publish):
367
+ publish("workflows", {"event": "workflow_file", "workflowId": workflow_id, "state": state,
368
+ "revision": revision, **fields})
369
+
370
+
371
+ def _entries(path):
372
+ try:
373
+ return list(os.scandir(path))
374
+ except OSError:
375
+ return []
376
+
377
+
378
+ def _sentence(path, error):
379
+ text = str(error)
380
+ return text if text.startswith(path) else f"{path}: {text}"
@@ -0,0 +1,270 @@
1
+ """A workflow as a file in its project: `.mobster/workflows/<name>.yaml`, written so a pull request's diff reads.
2
+
3
+ ``render(name, graph)`` writes the published graph; ``parse(text, where)`` reads a file back into (name, graph) or
4
+ raises FileError naming the line. The graph a file gives still goes through every check a save does (graph/schema.py)
5
+ before Mobster takes it. The format, version 1::
6
+
7
+ version: 1
8
+ name: Answer DMs
9
+ inputs:
10
+ - {name: who, type: string, default: Sam}
11
+ limits: {maxSpendUsd: 2, maxMinutes: 60}
12
+ nodes:
13
+ - id: start
14
+ type: trigger.manual
15
+ at: [0, 0]
16
+ - id: read
17
+ type: phone.task
18
+ at: [0, 152]
19
+ config:
20
+ text: Open Instagram and read the newest DM from {who}
21
+ edges:
22
+ - {from: start, to: read}
23
+ - {from: check.asked, to: tell}
24
+
25
+ Keys always come in the same order, a node's config leaves out what is at its default, an edge names its ends as
26
+ ``node`` or ``node.port`` (``out`` and ``in`` are left out), and a moved node changes one line. What stays on the Mac
27
+ and never goes in the file: the workflow's phone, profile and destination, whether its schedule is on, and pins (test
28
+ data). ``blocked(graph)`` refuses a graph that names a real iPhone or holds a secret, so neither reaches a repository.
29
+ """
30
+
31
+ import functools
32
+ import hashlib
33
+ import re
34
+
35
+ from ..graph import catalog
36
+ from ..secret_filter import redact
37
+
38
+ VERSION = 1
39
+ MAX_BYTES = 512_000
40
+ SUFFIXES = (".yaml", ".yml")
41
+ SHARED_DEVICES = (None, "any", "loop")
42
+ TOP = ("version", "name", "inputs", "vars", "limits", "nodes", "edges")
43
+ NODE = ("id", "type", "title", "notes", "parent", "at", "retry", "config")
44
+ EDGE = ("from", "to", "map", "id")
45
+ HEADER = "# A Mobster workflow. Mobster for Mac keeps this file and its canvas in step: edit either.\n"
46
+
47
+
48
+ class FileError(ValueError):
49
+ """A file that isn't a workflow: one sentence, with the file and the line where there is one."""
50
+
51
+
52
+ def digest(text):
53
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
54
+
55
+
56
+ def slug(name, taken=()):
57
+ """The file name for a workflow called ``name``: lowercase words joined by dashes, unique among ``taken``."""
58
+ base = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", name.lower())).strip("-")[:60].strip("-") or "workflow"
59
+ candidate, n = f"{base}.yaml", 2
60
+ taken = {name.casefold() for name in taken}
61
+ while candidate in taken:
62
+ candidate, n = f"{base}-{n}.yaml", n + 1
63
+ return candidate
64
+
65
+
66
+ def edge_id(source, source_port, target, target_port):
67
+ """An edge's id when its file gives none: the same ends always give the same id."""
68
+ key = f"{source}.{source_port}>{target}.{target_port}"
69
+ return "e" + hashlib.sha256(key.encode()).hexdigest()[:10]
70
+
71
+
72
+ # -- writing -------------------------------------------------------------------------------------------------------
73
+
74
+ def _config(node):
75
+ """The node's config without the fields at their default, in the catalog's order."""
76
+ node_type = catalog.TYPES.get(node["type"])
77
+ config = dict(node.get("config") or {})
78
+ if node_type is None:
79
+ return config
80
+ out = {}
81
+ for name, field in node_type.fields.items():
82
+ if name in config:
83
+ value = config.pop(name)
84
+ if field.required or value != field.default:
85
+ out[name] = value
86
+ out.update(config)
87
+ return out
88
+
89
+
90
+ def _end(node, port, default):
91
+ return node if port == default else f"{node}.{port}"
92
+
93
+
94
+ def blocked(graph):
95
+ """Why this graph can't go in a repository file, or None: a step names a real iPhone, or a value is a secret."""
96
+ for node in graph.get("nodes") or ():
97
+ config = node.get("config") or {}
98
+ if config.get("device") not in SHARED_DEVICES:
99
+ return (f"The step {node.get('title') or node['id']} runs on one named iPhone, and a project's files never "
100
+ "name a real phone. Set it to the workflow's phone, then publish.")
101
+ for text in _strings(graph):
102
+ if redact(text) != text:
103
+ return ("The workflow holds what looks like a key, a password or a card number, and Mobster never writes "
104
+ "one into a project. Keep it in the project's secrets (.mobster/env.example), then publish.")
105
+ return None
106
+
107
+
108
+ def _strings(value):
109
+ pending = [value]
110
+ while pending:
111
+ item = pending.pop()
112
+ if isinstance(item, str):
113
+ yield item
114
+ elif isinstance(item, dict):
115
+ pending.extend(item.values())
116
+ elif isinstance(item, list):
117
+ pending.extend(item)
118
+
119
+
120
+ def document(name, graph):
121
+ """The file's content as data, in the format's order."""
122
+ doc = {"version": VERSION, "name": name}
123
+ if graph.get("inputs"):
124
+ doc["inputs"] = [{key: item[key] for key in ("name", "type", "default", "description", "required")
125
+ if item.get(key) is not None} for item in graph["inputs"]]
126
+ if graph.get("vars"):
127
+ doc["vars"] = graph["vars"]
128
+ doc["limits"] = {key: _number(value) for key, value in (graph.get("limits") or catalog.DEFAULT_LIMITS).items()}
129
+ nodes = []
130
+ for node in graph.get("nodes") or ():
131
+ item = {"id": node["id"], "type": node["type"]}
132
+ for key in ("title", "notes", "parent"):
133
+ if node.get(key):
134
+ item[key] = node[key]
135
+ position = node.get("position") or {}
136
+ item["at"] = _Flow([_number(position.get("x", 0)), _number(position.get("y", 0))])
137
+ if node.get("retry"):
138
+ item["retry"] = node["retry"]
139
+ config = _config(node)
140
+ if config:
141
+ item["config"] = config
142
+ nodes.append(item)
143
+ doc["nodes"] = nodes
144
+ edges = []
145
+ for edge in graph.get("edges") or ():
146
+ item = {"from": _end(edge["source"], edge["sourcePort"], "out"),
147
+ "to": _end(edge["target"], edge["targetPort"], "in")}
148
+ if edge.get("map") is not None:
149
+ item["map"] = edge["map"]
150
+ edges.append(item)
151
+ doc["edges"] = edges
152
+ return doc
153
+
154
+
155
+ def render(name, graph):
156
+ """The file for a workflow called ``name`` whose published graph is ``graph``."""
157
+ import yaml
158
+ return HEADER + yaml.dump(document(name, graph), Dumper=_dumper(), sort_keys=False, allow_unicode=True, width=120,
159
+ default_flow_style=False)
160
+
161
+
162
+ def _number(value):
163
+ return int(value) if isinstance(value, float) and value.is_integer() else value
164
+
165
+
166
+ class _Flow(list):
167
+ """A list written on one line: a node's ``at``."""
168
+
169
+
170
+ @functools.cache
171
+ def _dumper():
172
+ """yaml's safe dumper, with a node's ``at`` on one line. Built on first use, so yaml loads only when a file is
173
+ written."""
174
+ import yaml
175
+
176
+ class Dumper(yaml.SafeDumper):
177
+ pass
178
+
179
+ Dumper.add_representer(_Flow, lambda dumper, value: dumper.represent_sequence(
180
+ "tag:yaml.org,2002:seq", value, flow_style=True))
181
+ return Dumper
182
+
183
+
184
+ # -- reading -------------------------------------------------------------------------------------------------------
185
+
186
+ def parse(text, where, *, previous=None):
187
+ """(name, graph) from a file's text. ``where`` names the file in sentences. ``previous`` is the workflow's graph
188
+ as Mobster has it: its edges keep their ids and its nodes their pins when the file leaves them as they were.
189
+ FileError with what is wrong; the graph is then checked as any save is."""
190
+ import yaml
191
+ if len(text.encode("utf-8")) > MAX_BYTES:
192
+ raise FileError(f"{where} is larger than {MAX_BYTES // 1000} KB.")
193
+ try:
194
+ data = yaml.safe_load(text)
195
+ except yaml.YAMLError as error:
196
+ mark = getattr(error, "problem_mark", None)
197
+ line = f" line {mark.line + 1}" if mark is not None else ""
198
+ raise FileError(f"{where}{line}: {getattr(error, 'problem', None) or 'not valid YAML'}") from None
199
+ except (RecursionError, ValueError):
200
+ raise FileError(f"{where}: nested too deeply") from None
201
+ if not isinstance(data, dict):
202
+ raise FileError(f"{where} is empty or isn't a workflow: it needs version, name, nodes and edges.")
203
+ unknown = [key for key in data if key not in TOP]
204
+ if unknown:
205
+ raise FileError(f"{where}: {unknown[0]!r} isn't a workflow field. A workflow has " + ", ".join(TOP) + ".")
206
+ if data.get("version") != VERSION:
207
+ raise FileError(f"{where}: this is a version {data.get('version')!r} file, and Mobster reads version "
208
+ f"{VERSION}. Update Mobster, or set version: 1.")
209
+ name = data.get("name")
210
+ if not isinstance(name, str) or not 1 <= len(name.strip()) <= 120:
211
+ raise FileError(f"{where}: name is the workflow's name, 1 to 120 characters.")
212
+ previous_nodes = {node["id"]: node for node in (previous or {}).get("nodes") or ()}
213
+ nodes = []
214
+ for index, item in enumerate(_list(data, "nodes", where)):
215
+ if not isinstance(item, dict):
216
+ raise FileError(f"{where}: node {index + 1} is a mapping with id, type and at.")
217
+ unknown = [key for key in item if key not in NODE]
218
+ if unknown:
219
+ raise FileError(f"{where}: node {item.get('id', index + 1)} has {unknown[0]!r}; a node has "
220
+ + ", ".join(NODE) + ".")
221
+ node = {key: item[key] for key in ("id", "type", "title", "notes", "parent", "retry") if key in item}
222
+ at = item.get("at", [0, 0])
223
+ if not (isinstance(at, list) and len(at) == 2
224
+ and all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in at)):
225
+ raise FileError(f"{where}: node {item.get('id', index + 1)}'s at is [x, y], two numbers.")
226
+ node["position"] = {"x": at[0], "y": at[1]}
227
+ node["config"] = item.get("config") if item.get("config") is not None else {}
228
+ if not isinstance(node.get("id"), str):
229
+ raise FileError(f"{where}: node {index + 1}'s id is text.")
230
+ before = previous_nodes.get(node["id"])
231
+ if before is not None and before.get("type") == node.get("type") and before.get("pin") is not None:
232
+ node["pin"] = before["pin"]
233
+ nodes.append(node)
234
+ ids = {node.get("id") for node in nodes}
235
+ previous_edges = {(e["source"], e["sourcePort"], e["target"], e["targetPort"]): e["id"]
236
+ for e in (previous or {}).get("edges") or ()}
237
+ edges = []
238
+ for index, item in enumerate(_list(data, "edges", where)):
239
+ if not isinstance(item, dict) or not isinstance(item.get("from"), str) or not isinstance(item.get("to"), str):
240
+ raise FileError(f"{where}: edge {index + 1} is {{from: node, to: node}}, with .port after a node when it "
241
+ "isn't out or in.")
242
+ unknown = [key for key in item if key not in EDGE]
243
+ if unknown:
244
+ raise FileError(f"{where}: edge {index + 1} has {unknown[0]!r}; an edge has from, to and map.")
245
+ source, source_port = _split(item["from"], "out")
246
+ target, target_port = _split(item["to"], "in")
247
+ for end in (source, target):
248
+ if end not in ids:
249
+ raise FileError(f"{where}: edge {index + 1} joins {end!r}, and no node has that id.")
250
+ key = (source, source_port, target, target_port)
251
+ edge = {"id": item.get("id") or previous_edges.get(key) or edge_id(*key), "source": source,
252
+ "sourcePort": source_port, "target": target, "targetPort": target_port}
253
+ if item.get("map") is not None:
254
+ edge["map"] = item["map"]
255
+ edges.append(edge)
256
+ graph = {"version": catalog.VERSION, "inputs": data.get("inputs") or [], "vars": data.get("vars") or {},
257
+ "nodes": nodes, "edges": edges, "limits": data.get("limits") or dict(catalog.DEFAULT_LIMITS)}
258
+ return " ".join(name.split()), graph
259
+
260
+
261
+ def _list(data, key, where):
262
+ value = data.get(key) if data.get(key) is not None else []
263
+ if not isinstance(value, list):
264
+ raise FileError(f"{where}: {key} is a list.")
265
+ return value
266
+
267
+
268
+ def _split(end, default):
269
+ node, dot, port = end.strip().partition(".")
270
+ return node, (port if dot else default)