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,842 @@
1
+ """A workflow graph's saved shape: its node types, their ports and validation (docs/workflows.mdx › The graph).
2
+
3
+ The node types and their fields are graph/catalog.py. A graph is ``{"version": 2, "inputs", "vars", "nodes", "edges",
4
+ "limits"}``. Each node has an ``id``, a ``type`` from
5
+ ``TYPES``, its ``config``, a ``position`` the engine never reads, and may have a ``title``, ``notes``, a ``parent`` (the
6
+ loop it sits in) and a ``pin`` (test data, by output port). Each edge joins a source's output port to a target's input
7
+ port and may reshape the value on its way (``map``, an expression).
8
+
9
+ ``validate(graph, context)`` returns the graph as saved (config defaults filled in) or raises ``GraphError`` listing
10
+ every problem with the node, edge, port or field it is about, so the canvas can mark each one. ``catalog()`` is what
11
+ ``GET /api/workflows/node-types`` serves: every type with its fields and ports, so the canvas builds its palette and
12
+ inspector from the engine.
13
+
14
+ Screen text is data: a field that instructs a model (a phone step's words, a check's question, ``ai.ask``'s
15
+ instruction, a sub-workflow's values) may read only the person's own values (``$inputs``, ``$vars``, ``$run``, and a
16
+ loop's index, row or phone). Everything a step produced reaches a model only as fenced input.
17
+ """
18
+
19
+ import math
20
+ from dataclasses import dataclass
21
+ from typing import Callable
22
+
23
+ from ..workflows import starters
24
+ from . import expr
25
+ from .catalog import (
26
+ ANY,
27
+ BOOLEAN,
28
+ DEFAULT_LIMITS,
29
+ ERROR_OUT,
30
+ ID,
31
+ INPUT_TYPES,
32
+ INTEGER,
33
+ MAX_EDGES,
34
+ MAX_FANOUT,
35
+ MAX_GRAPH_BYTES,
36
+ MAX_INPUTS,
37
+ MAX_ITERATIONS,
38
+ MAX_NESTING,
39
+ MAX_NODES,
40
+ MAX_PIN_BYTES,
41
+ MAX_PINS_BYTES,
42
+ MAX_VARS,
43
+ MAX_VARS_BYTES,
44
+ MINUTES_RANGE,
45
+ NOTES_CHARS,
46
+ NUMBER,
47
+ POSITION,
48
+ RETRY_BACKOFF,
49
+ RETRY_TRIES,
50
+ RETRY_TYPES,
51
+ SPEND_RANGE,
52
+ STRING,
53
+ TITLE_CHARS,
54
+ TRIGGERS,
55
+ TRUSTED_ITEMS,
56
+ TRUSTED_ROOTS,
57
+ TYPES,
58
+ VAR,
59
+ VERSION,
60
+ Field,
61
+ Shapes,
62
+ _Fields,
63
+ _json_size,
64
+ _short,
65
+ array,
66
+ catalog,
67
+ pointer,
68
+ public_limits,
69
+ register_node_type,
70
+ )
71
+ from .expr import ExprError
72
+ from .infer import RUN_SCHEMA, infer, schema_kind
73
+
74
+ __all__ = ["VERSION", "TYPES", "TRIGGERS", "DEFAULT_LIMITS", "ERROR_OUT", "MAX_ITERATIONS", "GraphError", "Context",
75
+ "validate", "check_graph", "coerce", "catalog", "public_limits", "register_node_type", "pointer"]
76
+
77
+
78
+ class GraphError(ValueError):
79
+ """A graph that can't be saved."""
80
+
81
+ code = "invalid_graph"
82
+
83
+ def __init__(self, errors):
84
+ self.errors = list(errors)
85
+ more = len(self.errors) - 1
86
+ super().__init__(self.errors[0]["message"] + (f" (and {more} more)" if more else ""))
87
+
88
+
89
+ @dataclass
90
+ class Context:
91
+ """What validation asks the rest of Mobster."""
92
+ workflow_id: str | None = None
93
+ workflows: dict | None = None
94
+ app_ids: Callable | None = None
95
+ device: Callable | None = None
96
+ account: Callable | None = None
97
+ destination: Callable | None = None
98
+ connector: Callable | None = None
99
+ packs: dict | None = None
100
+ cron: Callable | None = None
101
+ timezone: Callable | None = None
102
+ deliveries: tuple = ("notification", "clipboard", "file", "destination")
103
+ plain: Callable | None = None
104
+
105
+
106
+ class _Validator:
107
+ def __init__(self, graph, ctx):
108
+ self.graph, self.context = graph, ctx
109
+ self.problems = []
110
+ self.nodes = {}
111
+ self.edges = []
112
+ self.expressions = []
113
+ self.field_types = {}
114
+ self._schemas = {}
115
+ self._input = {}
116
+ self._busy = set()
117
+
118
+ def problem(self, message, **where):
119
+ self.problems.append({**{k: v for k, v in where.items() if v is not None}, "message": str(message)})
120
+
121
+ def run(self):
122
+ graph = self.graph
123
+ if not isinstance(graph, dict):
124
+ raise GraphError([{"message": "A graph is one JSON object"}])
125
+ extra = set(graph) - {"version", "inputs", "vars", "nodes", "edges", "limits"}
126
+ if extra:
127
+ self.problem("A graph has only version, inputs, vars, nodes, edges and limits, not "
128
+ + ", ".join(sorted(extra)))
129
+ if graph.get("version") != VERSION:
130
+ self.problem(f"This is a graph of version {VERSION}")
131
+ try:
132
+ size = _json_size(graph)
133
+ except (TypeError, ValueError):
134
+ raise GraphError([{"message": "A graph is plain JSON"}]) from None
135
+ if size > MAX_GRAPH_BYTES:
136
+ raise GraphError([{"message": f"A graph is at most {MAX_GRAPH_BYTES // 1000} KB, pins included"}])
137
+ inputs = self.inputs(graph.get("inputs"))
138
+ variables = self.variables(graph.get("vars"))
139
+ limits = self.limits(graph.get("limits"))
140
+ nodes = self.read_nodes(graph.get("nodes"))
141
+ self.read_parents()
142
+ self.read_edges(graph.get("edges"))
143
+ if not self.problems:
144
+ self.structure()
145
+ if not self.problems:
146
+ self.values_schema = self.make_values_schema(inputs)
147
+ self.vars_schema = {"type": "object", "properties": {k: _schema_of_value(x) for k, x in variables.items()},
148
+ "additionalProperties": False}
149
+ self.check_expressions()
150
+ self.check_pins()
151
+ if self.problems:
152
+ raise GraphError(self.problems)
153
+ return {"version": VERSION, "inputs": inputs, "vars": variables, "nodes": nodes,
154
+ "edges": [edge for edge in self.edges], "limits": limits}
155
+
156
+ def inputs(self, value):
157
+ if value is None:
158
+ return []
159
+ if not isinstance(value, list) or len(value) > MAX_INPUTS:
160
+ self.problem(f"A graph has at most {MAX_INPUTS} inputs", field="inputs")
161
+ return []
162
+ out, seen = [], set()
163
+ for item in value:
164
+ try:
165
+ if not isinstance(item, dict) or set(item) - {"name", "type", "default", "description", "required"}:
166
+ raise ValueError("An input has a name, a type, and may have a default, a description and required")
167
+ name = _short(item.get("name"), "An input's name", starters.NAME_CHARS)
168
+ if not starters.NAMED.fullmatch(name):
169
+ raise ValueError(f"{name} can't be an input's name: start with a letter")
170
+ key = starters._key(name)
171
+ if key in seen:
172
+ raise ValueError(f"Two inputs are called {name}")
173
+ seen.add(key)
174
+ kind = item.get("type", "string")
175
+ if kind not in INPUT_TYPES:
176
+ raise ValueError("An input's type is " + ", ".join(INPUT_TYPES))
177
+ default = item.get("default")
178
+ if default is not None:
179
+ default = coerce(default, kind, name)
180
+ cleaned = {"name": name, "type": kind}
181
+ if default is not None:
182
+ cleaned["default"] = default
183
+ if item.get("description"):
184
+ cleaned["description"] = _short(item["description"], "A description", 200)
185
+ if item.get("required") is not None:
186
+ if type(item["required"]) is not bool:
187
+ raise ValueError("required is true or false")
188
+ cleaned["required"] = item["required"]
189
+ out.append(cleaned)
190
+ except ValueError as error:
191
+ self.problem(error, field="inputs")
192
+ return out
193
+
194
+ def variables(self, value):
195
+ if value is None:
196
+ return {}
197
+ if not isinstance(value, dict) or len(value) > MAX_VARS:
198
+ self.problem(f"A graph has at most {MAX_VARS} vars", field="vars")
199
+ return {}
200
+ for name in value:
201
+ if not isinstance(name, str) or not VAR.fullmatch(name):
202
+ self.problem(f"A var's name is letters, digits and _, starting with a letter: {str(name)[:40]!r}",
203
+ field="vars")
204
+ if _json_size(value) > MAX_VARS_BYTES:
205
+ self.problem(f"A graph's vars are at most {MAX_VARS_BYTES // 1000} KB in all", field="vars")
206
+ return dict(value)
207
+
208
+ def limits(self, value):
209
+ value = value if value is not None else {}
210
+ if not isinstance(value, dict) or set(value) - set(DEFAULT_LIMITS):
211
+ self.problem("A workflow's limits are maxSpendUsd and maxMinutes", field="limits")
212
+ return dict(DEFAULT_LIMITS)
213
+ spend = value.get("maxSpendUsd", DEFAULT_LIMITS["maxSpendUsd"])
214
+ if (isinstance(spend, bool) or not isinstance(spend, (int, float))
215
+ or not SPEND_RANGE[0] <= spend <= SPEND_RANGE[1]):
216
+ self.problem(f"maxSpendUsd is from ${SPEND_RANGE[0]:.2f} to ${SPEND_RANGE[1]:.0f}", field="limits")
217
+ spend = DEFAULT_LIMITS["maxSpendUsd"]
218
+ minutes = value.get("maxMinutes", DEFAULT_LIMITS["maxMinutes"])
219
+ if type(minutes) is not int or not MINUTES_RANGE[0] <= minutes <= MINUTES_RANGE[1]:
220
+ self.problem(f"maxMinutes is a whole number from {MINUTES_RANGE[0]} to {MINUTES_RANGE[1]:,}",
221
+ field="limits")
222
+ minutes = DEFAULT_LIMITS["maxMinutes"]
223
+ return {"maxSpendUsd": round(float(spend), 2), "maxMinutes": minutes}
224
+
225
+ def read_nodes(self, value):
226
+ if not isinstance(value, list) or not value:
227
+ self.problem("A graph has at least one node", field="nodes")
228
+ return []
229
+ if len(value) > MAX_NODES:
230
+ self.problem(f"A graph has at most {MAX_NODES} nodes", field="nodes")
231
+ return []
232
+ out = []
233
+ for raw in value:
234
+ node = self.read_node(raw)
235
+ if node is not None:
236
+ out.append(node)
237
+ kinds = [node["type"] for node in out if node["type"] in TRIGGERS]
238
+ if not kinds:
239
+ self.problem("Add a trigger: when the workflow runs")
240
+ for kind in set(kinds):
241
+ if kinds.count(kind) > 1:
242
+ self.problem(f"A graph has one {TYPES[kind].label} trigger at most")
243
+ if sum(1 for node in out if node["type"] == "output") > 1:
244
+ self.problem("A graph has one Result at most")
245
+ return out
246
+
247
+ def read_node(self, raw):
248
+ if not isinstance(raw, dict):
249
+ self.problem("Each node is an object")
250
+ return None
251
+ ident = raw.get("id")
252
+ if not isinstance(ident, str) or not ID.fullmatch(ident):
253
+ self.problem("Each node has an id: 1 to 24 letters, digits, - or _")
254
+ return None
255
+ if ident in self.nodes:
256
+ self.problem(f"Two nodes have the id {ident}", node=ident)
257
+ return None
258
+ extra = set(raw) - {"id", "type", "config", "position", "title", "parent", "pin", "notes", "retry"}
259
+ if extra:
260
+ self.problem("A node has only id, type, config, position, title, parent, pin, notes and retry", node=ident)
261
+ kind = raw.get("type")
262
+ node_type = TYPES.get(kind)
263
+ if node_type is None:
264
+ self.problem(f"Mobster has no node of type {str(kind)[:40]!r}", node=ident)
265
+ return None
266
+ node = {"id": ident, "type": kind, "config": {}, "position": self.position(raw.get("position"), ident)}
267
+ for key, high in (("title", TITLE_CHARS), ("notes", NOTES_CHARS)):
268
+ if raw.get(key) is not None:
269
+ try:
270
+ node[key] = _short(raw[key], "A node's " + key, high)
271
+ except ValueError as error:
272
+ self.problem(error, node=ident, field=key)
273
+ if raw.get("parent") is not None:
274
+ node["parent"] = raw["parent"]
275
+ if raw.get("pin") is not None:
276
+ node["pin"] = raw["pin"]
277
+ if raw.get("retry") is not None:
278
+ retry = self.retry(raw["retry"], kind, ident)
279
+ if retry is not None:
280
+ node["retry"] = retry
281
+ self.nodes[ident] = node
282
+ node["config"] = self.config(node, node_type, raw.get("config"))
283
+ return node
284
+
285
+ def retry(self, value, kind, ident):
286
+ if kind not in RETRY_TYPES:
287
+ self.problem(f"A {TYPES[kind].label} step can act outward, so Mobster never retries it", node=ident,
288
+ field="retry")
289
+ return None
290
+ if (not isinstance(value, dict) or set(value) - {"tries", "backoffSeconds"}
291
+ or type(value.get("tries")) is not int or not RETRY_TRIES[0] <= value["tries"] <= RETRY_TRIES[1]
292
+ or type(value.get("backoffSeconds", 30)) is not int
293
+ or not RETRY_BACKOFF[0] <= value.get("backoffSeconds", 30) <= RETRY_BACKOFF[1]):
294
+ self.problem(f"retry is {{tries: {RETRY_TRIES[0]} to {RETRY_TRIES[1]}, backoffSeconds: {RETRY_BACKOFF[0]} "
295
+ f"to {RETRY_BACKOFF[1]:,}}}", node=ident, field="retry")
296
+ return None
297
+ return {"tries": value["tries"], "backoffSeconds": value.get("backoffSeconds", 30)}
298
+
299
+ def position(self, value, ident):
300
+ if value is None:
301
+ return {"x": 0, "y": 0}
302
+ if (not isinstance(value, dict) or set(value) != {"x", "y"} or
303
+ any(isinstance(n, bool) or not isinstance(n, (int, float)) or not math.isfinite(n)
304
+ or abs(n) > POSITION for n in value.values())):
305
+ self.problem(f"A node's position is {{x, y}}, each within ±{POSITION:,}", node=ident, field="position")
306
+ return {"x": 0, "y": 0}
307
+ return {"x": value["x"], "y": value["y"]}
308
+
309
+ def config(self, node, node_type, value):
310
+ ident = node["id"]
311
+ value = value if value is not None else {}
312
+ if not isinstance(value, dict):
313
+ self.problem("A node's config is one object", node=ident, field="config")
314
+ return {}
315
+ for key in set(value) - set(node_type.fields):
316
+ self.problem(f"A {node_type.label} node has no field {key}", node=ident, field=key)
317
+ out = {}
318
+ node["config"] = out
319
+ failed = False
320
+ for name, f in node_type.fields.items():
321
+ given = value.get(name, f.default)
322
+ if given is None:
323
+ if f.required:
324
+ self.problem(f"{f.label} is needed", node=ident, field=name)
325
+ failed = True
326
+ out[name] = None
327
+ continue
328
+ try:
329
+ out[name] = getattr(_Fields, f.kind)(given, f, self.context, self, node)
330
+ except (ValueError, ExprError) as error:
331
+ self.problem(error, node=ident, field=name)
332
+ failed = True
333
+ out[name] = None
334
+ if failed:
335
+ return out
336
+ if node_type.check is not None:
337
+ try:
338
+ out.update(node_type.check(dict(out), self.context))
339
+ except (ValueError, ExprError) as error:
340
+ self.problem(error, node=ident)
341
+ return out
342
+ self.collect_expressions(node, node_type)
343
+ return out
344
+
345
+ def collect_expressions(self, node, node_type):
346
+ for name, f in node_type.fields.items():
347
+ if not f.expr or node["config"].get(name) is None:
348
+ continue
349
+ value = node["config"][name]
350
+ if f.expr == "bare":
351
+ self.expressions.append((node["id"], (name,), expr.compile(value), f.instructs, f.wants))
352
+ continue
353
+ for path, raw in _strings(value, (name,)) if f.expr == "deep" else [((name,), value)]:
354
+ try:
355
+ shape = expr.template(raw)
356
+ except ExprError as error:
357
+ self.problem(f"{error}", node=node["id"], field=".".join(map(str, path)))
358
+ continue
359
+ for parsed in shape.expressions:
360
+ self.expressions.append((node["id"], path, parsed, f.instructs, None))
361
+ if shape.whole is not None:
362
+ self.expressions.append((node["id"], path + ("whole",), shape.whole, False, None))
363
+ if node["type"] == "branch":
364
+ for case in node["config"].get("cases") or []:
365
+ self.expressions.append((node["id"], ("cases", case["id"]), expr.compile(case["when"]), False,
366
+ "boolean"))
367
+
368
+ def read_parents(self):
369
+ for node in list(self.nodes.values()):
370
+ parent = node.get("parent")
371
+ if parent is None:
372
+ continue
373
+ owner = self.nodes.get(parent) if isinstance(parent, str) else None
374
+ if owner is None or owner["type"] != "loop":
375
+ self.problem("A node sits inside a loop: its parent is a loop's id", node=node["id"], field="parent")
376
+ node.pop("parent", None)
377
+ continue
378
+ if node["type"] in TRIGGERS or node["type"] == "output":
379
+ self.problem(f"A {TYPES[node['type']].label} can't sit inside a loop", node=node["id"],
380
+ field="parent")
381
+ for node in self.nodes.values():
382
+ seen, at = {node["id"]}, node.get("parent")
383
+ depth = 0
384
+ while at is not None:
385
+ if at in seen:
386
+ self.problem("Loops can't sit inside each other in a circle", node=node["id"], field="parent")
387
+ node.pop("parent", None)
388
+ break
389
+ seen.add(at)
390
+ depth += 1
391
+ at = self.nodes[at].get("parent")
392
+ if node["type"] == "loop" and depth >= MAX_NESTING:
393
+ self.problem(f"Loops sit inside each other at most {MAX_NESTING} deep", node=node["id"])
394
+
395
+ def scopes(self, ident):
396
+ """The loops around a node, innermost first, ending with None (the top)."""
397
+ out, at = [], self.nodes[ident].get("parent")
398
+ while at is not None:
399
+ out.append(at)
400
+ at = self.nodes[at].get("parent")
401
+ return out + [None]
402
+
403
+ def lift(self, ident, level):
404
+ """The node at ``level`` (a loop's id, or None for the top) that holds ``ident``, or None."""
405
+ at = ident
406
+ while at is not None:
407
+ if self.nodes[at].get("parent") == level:
408
+ return at
409
+ at = self.nodes[at].get("parent")
410
+ return None
411
+
412
+ def outputs(self, ident):
413
+ node = self.nodes[ident]
414
+ node_type = TYPES[node["type"]]
415
+ ports = set(node_type.static)
416
+ if node["type"] == "branch":
417
+ ports |= {case["id"] for case in node["config"].get("cases") or []} | {"else"}
418
+ elif node["type"] == "loop":
419
+ ports |= {"item", "index", "out"}
420
+ elif node["type"] == "approval":
421
+ ports |= {"approved", "declined"}
422
+ elif node["type"] in ("merge", "wait", "workflow.call") or not ports:
423
+ ports |= {"out"} if node["type"] != "output" else set()
424
+ if not node_type.trigger:
425
+ ports.add("error")
426
+ return ports
427
+
428
+ def read_edges(self, value):
429
+ value = value if value is not None else []
430
+ if not isinstance(value, list) or len(value) > MAX_EDGES:
431
+ self.problem(f"A graph has at most {MAX_EDGES} edges", field="edges")
432
+ return
433
+ seen, pairs, fanout = set(), set(), {}
434
+ for raw in value:
435
+ if not isinstance(raw, dict):
436
+ self.problem("Each edge is an object")
437
+ continue
438
+ ident = raw.get("id")
439
+ if not isinstance(ident, str) or not ID.fullmatch(ident) or ident in seen:
440
+ self.problem("Each edge has its own id: 1 to 24 letters, digits, - or _")
441
+ continue
442
+ seen.add(ident)
443
+ extra = set(raw) - {"id", "source", "sourcePort", "target", "targetPort", "map"}
444
+ if extra:
445
+ self.problem("An edge has only id, source, sourcePort, target, targetPort and map", edge=ident)
446
+ source, target = raw.get("source"), raw.get("target")
447
+ sport, tport = raw.get("sourcePort", "out"), raw.get("targetPort", "in")
448
+ if source not in self.nodes or target not in self.nodes:
449
+ self.problem("An edge joins two nodes of this graph", edge=ident)
450
+ continue
451
+ if source == target:
452
+ self.problem("A node can't feed itself", edge=ident, node=source)
453
+ continue
454
+ if sport not in self.outputs(source):
455
+ self.problem(f"{self.name(source)} has no output called {sport}", edge=ident, node=source,
456
+ port=sport)
457
+ continue
458
+ target_type = TYPES[self.nodes[target]["type"]]
459
+ if target_type.trigger:
460
+ self.problem("Nothing runs into a trigger", edge=ident, node=target)
461
+ continue
462
+ if tport not in target_type.inputs:
463
+ self.problem(f"{self.name(target)} has no input called {tport}", edge=ident, node=target, port=tport)
464
+ continue
465
+ if (source, sport, target, tport) in pairs:
466
+ self.problem("Those two ports are joined twice", edge=ident)
467
+ continue
468
+ pairs.add((source, sport, target, tport))
469
+ fanout[(source, sport)] = fanout.get((source, sport), 0) + 1
470
+ if fanout[(source, sport)] == MAX_FANOUT + 1:
471
+ self.problem(f"One output feeds at most {MAX_FANOUT} steps", node=source, port=sport)
472
+ edge = {"id": ident, "source": source, "sourcePort": sport, "target": target, "targetPort": tport}
473
+ if raw.get("map") is not None:
474
+ try:
475
+ edge["map"] = _Fields.bare(raw["map"], Field("bare", "The edge's map"), self.context, self, None)
476
+ self.expressions.append(((ident, "edge"), ("map",), expr.compile(edge["map"]), False, None))
477
+ except (ValueError, ExprError) as error:
478
+ self.problem(error, edge=ident, field="map")
479
+ continue
480
+ if not self.scope_ok(edge):
481
+ continue
482
+ self.edges.append(edge)
483
+ for node in self.nodes.values():
484
+ node_type = TYPES[node["type"]]
485
+ for port in node_type.single:
486
+ count = sum(1 for e in self.edges if e["target"] == node["id"] and e["targetPort"] == port)
487
+ if count > 1:
488
+ self.problem(f"{self.name(node['id'])} takes one edge into {port}", node=node["id"], port=port)
489
+
490
+ def scope_ok(self, edge):
491
+ source, target, port = edge["source"], edge["target"], edge["sourcePort"]
492
+ inside = source in self.scopes(target)[:-1]
493
+ if self.nodes[source]["type"] == "loop" and port in ("item", "index"):
494
+ if not inside:
495
+ self.problem(f"A loop's {port} goes only to steps inside the loop", edge=edge["id"], node=source)
496
+ return False
497
+ return True
498
+ if inside:
499
+ self.problem("A loop's results leave through its out port, not into its own steps", edge=edge["id"],
500
+ node=source)
501
+ return False
502
+ if self.nodes[source].get("parent") not in self.scopes(target):
503
+ self.problem("A step inside a loop sends its results out through the loop's collect and out port",
504
+ edge=edge["id"], node=source)
505
+ return False
506
+ return True
507
+
508
+ def name(self, ident):
509
+ node = self.nodes[ident]
510
+ return f"“{node.get('title') or TYPES[node['type']].label}”"
511
+
512
+ def level_edges(self, level):
513
+ out = set()
514
+ for edge in self.edges:
515
+ if self.nodes[edge["source"]]["type"] == "loop" and edge["sourcePort"] in ("item", "index") \
516
+ and edge["source"] in self.scopes(edge["target"])[:-1]:
517
+ continue
518
+ a, b = self.lift(edge["source"], level), self.lift(edge["target"], level)
519
+ if a is not None and b is not None and a != b:
520
+ out.add((a, b))
521
+ return out
522
+
523
+ def levels(self):
524
+ return [None] + [ident for ident, node in self.nodes.items() if node["type"] == "loop"]
525
+
526
+ def structure(self):
527
+ self.order = {}
528
+ for level in self.levels():
529
+ members = [ident for ident, node in self.nodes.items() if node.get("parent") == level]
530
+ edges = self.level_edges(level)
531
+ order = _topological(members, edges)
532
+ if order is None:
533
+ cycle = _cycle(members, edges)
534
+ self.problem("These steps wait for each other in a circle: " + ", ".join(self.name(i) for i in cycle),
535
+ node=cycle[0])
536
+ return
537
+ self.order[level] = order
538
+ top = [ident for ident, node in self.nodes.items() if node.get("parent") is None]
539
+ reached = {ident for ident in top if self.nodes[ident]["type"] in TRIGGERS}
540
+ edges = self.level_edges(None)
541
+ changed = True
542
+ while changed:
543
+ changed = False
544
+ for a, b in edges:
545
+ if a in reached and b not in reached:
546
+ reached.add(b)
547
+ changed = True
548
+ for ident in top:
549
+ if ident not in reached:
550
+ self.problem(f"{self.name(ident)} never runs: no trigger leads to it", node=ident)
551
+ for node in self.nodes.values():
552
+ self.node_rules(node)
553
+
554
+ def incoming(self, ident, port=None):
555
+ return [e for e in self.edges if e["target"] == ident and (port is None or e["targetPort"] == port)]
556
+
557
+ def node_rules(self, node):
558
+ ident, config, kind = node["id"], node["config"], node["type"]
559
+ if kind == "merge" and len(self.incoming(ident)) < 2:
560
+ self.problem("A merge joins two or more paths", node=ident)
561
+ if kind == "branch" and len(self.incoming(ident, "in")) != 1:
562
+ self.problem("A branch takes exactly one edge", node=ident)
563
+ if kind == "loop":
564
+ items = self.incoming(ident, "items")
565
+ if config.get("over") == "items" and not items:
566
+ self.problem("Join a list to the loop's items", node=ident, port="items")
567
+ if config.get("over") != "items" and items:
568
+ self.problem("Only a loop over items takes the items input", node=ident, port="items")
569
+ for key in ("until", "collect"):
570
+ inner = config.get(key)
571
+ if inner is not None and (inner not in self.nodes or self.nodes[inner].get("parent") != ident):
572
+ self.problem(f"The loop's {key} is a step inside it", node=ident, field=key)
573
+ if config.get("over") == "until" and config.get("until") in self.nodes:
574
+ watched = self.nodes[config["until"]]
575
+ if TYPES[watched["type"]].trigger:
576
+ self.problem("The loop's until is a step inside it", node=ident, field="until")
577
+ if config.get("device") == "loop":
578
+ loop = next((self.nodes[s] for s in self.scopes(ident)[:-1]), None)
579
+ if loop is None or loop["config"].get("over") != "phones":
580
+ self.problem('A step runs on "loop" only inside a loop over phones', node=ident, field="device")
581
+
582
+ def make_values_schema(self, inputs):
583
+ kinds = {"string": STRING, "number": NUMBER, "boolean": BOOLEAN, "list": array(), "object": {"type": "object"}}
584
+ properties = {starters._key(item["name"]): kinds[item["type"]] for item in inputs}
585
+ for node in self.nodes.values():
586
+ for name, f in TYPES[node["type"]].fields.items():
587
+ if not f.blanks or node["config"].get(name) is None:
588
+ continue
589
+ for _path, raw in _strings(node["config"][name], ()):
590
+ for blank in expr.template(raw).blanks:
591
+ properties.setdefault(starters._key(blank[1]), STRING)
592
+ return {"type": "object", "properties": properties, "additionalProperties": False}
593
+
594
+ def schema_of(self, ident, port="out"):
595
+ key = (ident, port)
596
+ if key in self._schemas:
597
+ return self._schemas[key]
598
+ if port == "error":
599
+ return ERROR_OUT
600
+ if key in self._busy:
601
+ return ANY
602
+ self._busy.add(key)
603
+ try:
604
+ node = self.nodes[ident]
605
+ ports = TYPES[node["type"]].outputs(node, Shapes(self, node))
606
+ except (ValueError, ExprError, KeyError):
607
+ ports = {}
608
+ finally:
609
+ self._busy.discard(key)
610
+ for name, schema in ports.items():
611
+ self._schemas[(ident, name)] = schema if isinstance(schema, dict) else ANY
612
+ return self._schemas.get(key, ANY)
613
+
614
+ def edge_schema(self, edge):
615
+ schema = self.schema_of(edge["source"], edge["sourcePort"])
616
+ if edge.get("map"):
617
+ env = self.env_for_edge(edge, schema)
618
+ try:
619
+ schema = infer(edge["map"], env)
620
+ except ExprError:
621
+ return ANY
622
+ return schema
623
+
624
+ def input_schema(self, ident):
625
+ if ident in self._input:
626
+ return self._input[ident]
627
+ self._input[ident] = ANY
628
+ edges = self.incoming(ident, "in")
629
+ schemas = [self.edge_schema(edge) for edge in edges]
630
+ schema = array() if len(schemas) > 1 else schemas[0] if schemas else ANY
631
+ self._input[ident] = schema
632
+ return schema
633
+
634
+ def items_schema(self, ident):
635
+ edges = self.incoming(ident, "items")
636
+ return self.edge_schema(edges[0]) if edges else ANY
637
+
638
+ def ancestors(self, ident):
639
+ """Every node that has finished before ``ident`` starts, in any run where both run."""
640
+ out = set()
641
+ for level in self.scopes(ident):
642
+ here = self.lift(ident, level)
643
+ edges = self.level_edges(level)
644
+ before = {a: set() for a, _b in edges}
645
+ for a, b in edges:
646
+ before.setdefault(b, set()).add(a)
647
+ pending = list(before.get(here, ()))
648
+ while pending:
649
+ at = pending.pop()
650
+ if at not in out:
651
+ out.add(at)
652
+ pending.extend(before.get(at, ()))
653
+ return out
654
+
655
+ def env_for(self, ident):
656
+ loops = self.scopes(ident)[:-1]
657
+ env = {"input": self.input_schema(ident), "inputs": self.values_schema, "vars": self.vars_schema,
658
+ "run": RUN_SCHEMA, "node": {a: self.schema_of(a) for a in self.ancestors(ident)}}
659
+ if loops:
660
+ ports = TYPES["loop"].outputs(self.nodes[loops[0]], Shapes(self, self.nodes[loops[0]]))
661
+ env["item"], env["index"] = ports.get("item", ANY), INTEGER
662
+ return env
663
+
664
+ def env_for_edge(self, edge, schema):
665
+ env = self.env_for(edge["target"]) if edge["target"] in self.nodes else {}
666
+ return {**env, "input": schema}
667
+
668
+ def trusted(self, ident, roots):
669
+ loops = self.scopes(ident)[:-1]
670
+ allowed = set(TRUSTED_ROOTS)
671
+ if loops and self.nodes[loops[0]]["config"].get("over") in TRUSTED_ITEMS:
672
+ allowed.add("item")
673
+ return roots <= allowed
674
+
675
+ def check_expressions(self):
676
+ edges = {edge["id"]: edge for edge in self.edges}
677
+ for owner, path, parsed, instructs, wants in self.expressions:
678
+ if isinstance(owner, tuple):
679
+ edge = edges.get(owner[0])
680
+ if edge is None:
681
+ continue
682
+ where = {"edge": owner[0], "field": "map"}
683
+ env = self.env_for_edge(edge, self.schema_of(edge["source"], edge["sourcePort"]))
684
+ else:
685
+ where = {"node": owner, "field": ".".join(str(p) for p in path if p != "whole")}
686
+ env = self.env_for(owner)
687
+ if instructs and not self.trusted(owner, parsed.roots):
688
+ self.problem("Screen text can't become a step's instructions. Pass it as the step's input "
689
+ "instead.", **where)
690
+ continue
691
+ try:
692
+ schema = infer(parsed, env)
693
+ except ExprError as error:
694
+ self.problem(error, **where)
695
+ continue
696
+ kind = schema_kind(schema)
697
+ if wants and kind is not None and kind != wants:
698
+ self.problem(f"This gives {_kind_name(kind)}, and it needs {_kind_name(wants)}", **where)
699
+ if not isinstance(owner, tuple) and path and path[-1] == "whole":
700
+ self.field_types.setdefault(owner, {})[path[:-1]] = schema
701
+ for edge in self.edges:
702
+ if edge["targetPort"] == "items":
703
+ kind = schema_kind(self.edge_schema(edge))
704
+ if kind is not None and kind != "list":
705
+ self.problem(f"A loop's items is a list, and this edge carries {_kind_name(kind)}. "
706
+ "Reshape it with the edge's map.", edge=edge["id"])
707
+
708
+ def check_pins(self):
709
+ total = 0
710
+ for node in self.nodes.values():
711
+ pin = node.get("pin")
712
+ if pin is None:
713
+ continue
714
+ if not isinstance(pin, dict) or not pin:
715
+ self.problem("A pin is test data by output port, like {\"out\": ...}", node=node["id"], field="pin")
716
+ continue
717
+ try:
718
+ size = _json_size(pin)
719
+ except (TypeError, ValueError):
720
+ self.problem("A pin is plain JSON", node=node["id"], field="pin")
721
+ continue
722
+ total += size
723
+ if size > MAX_PIN_BYTES:
724
+ self.problem(f"A node's pin is at most {MAX_PIN_BYTES // 1000} KB", node=node["id"], field="pin")
725
+ for port, value in pin.items():
726
+ if port not in self.outputs(node["id"]) or port == "error":
727
+ self.problem(f"{self.name(node['id'])} has no output called {port}", node=node["id"], field="pin")
728
+ continue
729
+ kind = schema_kind(self.schema_of(node["id"], port))
730
+ if kind is not None and value is not None and expr.kind_of(value) != kind:
731
+ self.problem(f"The pin for {port} should be {_kind_name(kind)}", node=node["id"], field="pin")
732
+ if total > MAX_PINS_BYTES:
733
+ self.problem(f"A graph's pins are at most {MAX_PINS_BYTES // 1_000_000} MB in all", field="pins")
734
+
735
+
736
+ def _kind_name(kind):
737
+ return {"null": "null", "boolean": "true or false", "number": "a number", "string": "text", "list": "a list",
738
+ "object": "an object"}.get(kind, "something")
739
+
740
+
741
+ def _schema_of_value(value):
742
+ kind = expr.kind_of(value)
743
+ return {"type": {"list": "array"}.get(kind, kind)}
744
+
745
+
746
+ def _strings(value, path):
747
+ """(path, text) for every text inside a config value."""
748
+ if isinstance(value, str):
749
+ yield path, value
750
+ elif isinstance(value, dict):
751
+ for key, item in value.items():
752
+ yield from _strings(item, path + (key,))
753
+ elif isinstance(value, list):
754
+ for index, item in enumerate(value):
755
+ yield from _strings(item, path + (index,))
756
+
757
+
758
+ def _topological(members, edges):
759
+ """``members`` in an order where every edge's source comes first (ties keep their saved order), or None."""
760
+ position = {m: i for i, m in enumerate(members)}
761
+ needs = {m: 0 for m in members}
762
+ after = {m: [] for m in members}
763
+ for a, b in edges:
764
+ if a in needs and b in needs:
765
+ needs[b] += 1
766
+ after[a].append(b)
767
+ ready = sorted((m for m in members if needs[m] == 0), key=position.get)
768
+ out = []
769
+ while ready:
770
+ at = ready.pop(0)
771
+ out.append(at)
772
+ for b in after[at]:
773
+ needs[b] -= 1
774
+ if needs[b] == 0:
775
+ ready.append(b)
776
+ ready.sort(key=position.get)
777
+ return out if len(out) == len(members) else None
778
+
779
+
780
+ def _cycle(members, edges):
781
+ after = {m: [b for a, b in edges if a == m] for m in members}
782
+ state = {}
783
+
784
+ def visit(at, trail):
785
+ state[at] = 1
786
+ for b in after.get(at, ()):
787
+ if state.get(b) == 1:
788
+ return trail[trail.index(b):] + [b] if b in trail else [at, b]
789
+ if b not in state:
790
+ found = visit(b, trail + [b])
791
+ if found:
792
+ return found
793
+ state[at] = 2
794
+ return None
795
+
796
+ for m in members:
797
+ if m not in state:
798
+ found = visit(m, [m])
799
+ if found:
800
+ return found[:-1]
801
+ return members[:1]
802
+
803
+
804
+ def coerce(value, kind, name="That input"):
805
+ """A run's value for an input of ``kind``: text read as a number or true or false where the input says so."""
806
+ if kind == "string":
807
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
808
+ return format(value, "g")
809
+ if not isinstance(value, str):
810
+ raise ValueError(f"{name} is text")
811
+ return value
812
+ if kind == "number":
813
+ if isinstance(value, bool):
814
+ raise ValueError(f"{name} is a number")
815
+ if isinstance(value, (int, float)):
816
+ return value
817
+ try:
818
+ return expr.evaluate("number($input)", {"input": value})
819
+ except ExprError:
820
+ raise ValueError(f"{name} is a number, not {str(value)[:40]!r}") from None
821
+ if kind == "boolean":
822
+ try:
823
+ return expr.evaluate("bool($input)", {"input": value})
824
+ except ExprError:
825
+ raise ValueError(f"{name} is true or false") from None
826
+ if kind == "list":
827
+ if not isinstance(value, list):
828
+ raise ValueError(f"{name} is a list")
829
+ return value
830
+ if not isinstance(value, dict):
831
+ raise ValueError(f"{name} is an object")
832
+ return value
833
+
834
+
835
+ def validate(graph, context=None):
836
+ """The graph as saved, or GraphError listing every problem."""
837
+ return _Validator(graph, context or Context()).run()
838
+
839
+
840
+ def check_graph(graph):
841
+ """Whether ``graph`` (as saved) is a graph of this version: the light check readers make before trusting it."""
842
+ return isinstance(graph, dict) and graph.get("version") == VERSION and isinstance(graph.get("nodes"), list)