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,472 @@
1
+ """Commands from outside: a person in Slack or Discord starts one of your workflows, and Mobster answers where
2
+ they asked.
3
+
4
+ ``/mobster run Running late person=Sam`` in Slack (or Discord) reaches the relay, which checks the platform's signature
5
+ and hands the command to this Mac over Remote's socket (``{"t": "inbound", "rid", "channel", "sender", "label", "verb",
6
+ "text", "key", "reply"}``, answered ``inbound.res``). This module decides what it may do. Off until
7
+ ``MOBSTER_EXPERIMENTAL_INBOUND=1``.
8
+
9
+ - **Who.** A person is allowed after they type a link code you made (``new_code``) in Slack or Discord, where the
10
+ platform signs who they are. ``inbound_senders`` holds them; ``forget`` removes one and tells the relay. A
11
+ command from anyone else starts nothing.
12
+ - **What.** Only a workflow you turned on for that channel (``inbound_workflows``). The text names it and fills its
13
+ blanks, ``name=value`` each, and nothing else: a value is short masked text, never a step. A word the workflow has no
14
+ blank for refuses the command instead of being ignored.
15
+ - **How.** The run's origin is ``hook`` (harness_api.FORCED_ASK_ORIGINS): it asks before it sends, buys, posts or
16
+ deletes whatever Ask before acting says, and the card waits in Mobster on this Mac. Nothing here approves anything.
17
+ - **Once.** The platform's command id is kept (``inbound_seen``); the same command twice starts one run.
18
+ - **The answer.** The run's start comes back at once, privately to the sender. When the run ends, its status and last
19
+ answer (masked, cut) are posted once to the address the platform gave (inbound_reply.py), privately unless you
20
+ made the sender public. The address is good for 15 minutes (Discord) or 30 (Slack); a run that outlives it posts
21
+ nothing, and its result stays in Mobster. Mobster quitting drops the wait the same way.
22
+ """
23
+
24
+ import hashlib
25
+ import logging
26
+ import os
27
+ import re
28
+ import secrets
29
+ import sqlite3
30
+ import threading
31
+ import time
32
+
33
+ from .. import secret_filter
34
+ from ..api_errors import APIError
35
+ from ..journal import JournalError
36
+ from ..workflows import starters
37
+ from . import hooks, net
38
+ from . import inbound_reply as reply_to
39
+
40
+ log = logging.getLogger("mobster.outlets.inbound")
41
+
42
+ SCHEMA = "outlet_inbound"
43
+ VERSION = 1
44
+ FLAG = "MOBSTER_EXPERIMENTAL_INBOUND"
45
+ PLATFORMS = ("slack", "discord")
46
+ NAMES = {"slack": "Slack", "discord": "Discord"}
47
+ SENDER = {"slack": re.compile(r"[A-Z0-9]{3,24}:[A-Z0-9]{3,24}"), "discord": re.compile(r"\d{15,25}")}
48
+ VERBS = ("run", "list", "link")
49
+ CODE_CHARS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # Crockford base32, as the relay reads a typed code
50
+ CODE_SECONDS = 600.0
51
+ MAX_CODES = 5
52
+ SEEN_KEEP = 3600.0
53
+ TEXT_MAX = 1000
54
+ ANSWER_CHARS = 1500
55
+ POLL_SECONDS = 2.0
56
+ WORKFLOW_ID = re.compile(r"[0-9a-f]{12}")
57
+ OFF = "Commands aren't turned on in this Mobster."
58
+ STRANGER = "Mobster doesn't know this account. In Mobster for Mac, open Settings › Commands and link it."
59
+
60
+
61
+ def on(environ=None):
62
+ return (os.environ if environ is None else environ).get(FLAG) == "1"
63
+
64
+
65
+ def migrate(connection, from_version):
66
+ if from_version < 1:
67
+ connection.execute("""CREATE TABLE IF NOT EXISTS inbound_senders (
68
+ channel TEXT NOT NULL, sender TEXT NOT NULL, label TEXT NOT NULL, public INTEGER NOT NULL DEFAULT 0,
69
+ created_at REAL NOT NULL, last_at REAL, uses INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (channel, sender))""")
70
+ connection.execute("""CREATE TABLE IF NOT EXISTS inbound_workflows (
71
+ channel TEXT NOT NULL, workflow_id TEXT NOT NULL, created_at REAL NOT NULL,
72
+ PRIMARY KEY (channel, workflow_id))""")
73
+ connection.execute("""CREATE TABLE IF NOT EXISTS inbound_seen (
74
+ channel TEXT NOT NULL, key TEXT NOT NULL, at REAL NOT NULL, PRIMARY KEY (channel, key))""")
75
+
76
+
77
+ def refuse(status, message):
78
+ return {"status": status, "message": message, "public": False}
79
+
80
+
81
+ def code_hash(code):
82
+ """What the relay is told of a link code: the hash of its normalized form (``normalize``)."""
83
+ return hashlib.sha256(code.encode()).hexdigest()
84
+
85
+
86
+ def normalize(text):
87
+ """A typed link code as the Mac made it: upper case, no dash or space, O as 0 and I or L as 1."""
88
+ return re.sub(r"[\s-]", "", str(text).upper()).replace("O", "0").replace("I", "1").replace("L", "1")
89
+
90
+
91
+ def words(text):
92
+ return " ".join(str(text).split())
93
+
94
+
95
+ class Store:
96
+ def __init__(self, journal):
97
+ self.journal = journal
98
+
99
+ def _read(self, sql, args=()):
100
+ with self.journal.lock:
101
+ if self.journal.connection is None:
102
+ raise JournalError("The run journal is closed")
103
+ try:
104
+ return self.journal.connection.execute(sql, args).fetchall()
105
+ except sqlite3.Error:
106
+ raise JournalError("Mobster could not read its commands") from None
107
+
108
+ def senders(self, channel=None):
109
+ where = " WHERE channel=?" if channel else ""
110
+ rows = self._read("SELECT channel, sender, label, public, created_at, last_at, uses FROM inbound_senders"
111
+ + where + " ORDER BY created_at", (channel,) if channel else ())
112
+ return [{"channel": c, "sender": s, "label": label, "public": bool(p), "createdAt": round(made * 1000),
113
+ "lastAt": round(last * 1000) if last else None, "uses": uses}
114
+ for c, s, label, p, made, last, uses in rows]
115
+
116
+ def sender(self, channel, sender):
117
+ return next((row for row in self.senders(channel) if row["sender"] == sender), None)
118
+
119
+ def add_sender(self, channel, sender, label, now):
120
+ with self.journal.transaction() as connection:
121
+ connection.execute("INSERT INTO inbound_senders (channel, sender, label, created_at) VALUES (?,?,?,?) "
122
+ "ON CONFLICT (channel, sender) DO UPDATE SET label=excluded.label",
123
+ (channel, sender, label, now))
124
+
125
+ def set_public(self, channel, sender, public):
126
+ with self.journal.transaction() as connection:
127
+ connection.execute("UPDATE inbound_senders SET public=? WHERE channel=? AND sender=?",
128
+ (1 if public else 0, channel, sender))
129
+
130
+ def used(self, channel, sender, now):
131
+ with self.journal.transaction() as connection:
132
+ connection.execute("UPDATE inbound_senders SET last_at=?, uses=uses+1 WHERE channel=? AND sender=?",
133
+ (now, channel, sender))
134
+
135
+ def remove_sender(self, channel, sender):
136
+ with self.journal.transaction() as connection:
137
+ connection.execute("DELETE FROM inbound_senders WHERE channel=? AND sender=?", (channel, sender))
138
+
139
+ def workflow_ids(self, channel):
140
+ query = "SELECT workflow_id FROM inbound_workflows WHERE channel=? ORDER BY created_at"
141
+ return [row[0] for row in self._read(query, (channel,))]
142
+
143
+ def enable(self, channel, workflow_id, now):
144
+ with self.journal.transaction() as connection:
145
+ connection.execute("INSERT OR IGNORE INTO inbound_workflows VALUES (?,?,?)", (channel, workflow_id, now))
146
+
147
+ def disable(self, channel, workflow_id):
148
+ with self.journal.transaction() as connection:
149
+ connection.execute("DELETE FROM inbound_workflows WHERE channel=? AND workflow_id=?",
150
+ (channel, workflow_id))
151
+
152
+ def seen(self, channel, key, now):
153
+ """True when this command id came before (a replay); else remember it. Old ones go."""
154
+ with self.journal.transaction() as connection:
155
+ connection.execute("DELETE FROM inbound_seen WHERE at < ?", (now - SEEN_KEEP,))
156
+ try:
157
+ connection.execute("INSERT INTO inbound_seen VALUES (?,?,?)", (channel, key, now))
158
+ except sqlite3.IntegrityError:
159
+ return True
160
+ return False
161
+
162
+
163
+ def read_values(workflow, rest):
164
+ """({blank: text}, None) from the words after a workflow's name, or (None, why). ``name=value`` or ``name: value``
165
+ pairs; with one blank and no pair, the whole rest is its value. Anything else is refused, never guessed."""
166
+ names = [blank["name"] for blank in starters.blanks(workflow.get("goal"))]
167
+ if not rest:
168
+ return {}, None
169
+ if not names:
170
+ return None, f"{workflow['name']} takes no values. Send `run {workflow['name']}` alone."
171
+ alternatives = "|".join(re.escape(name) for name in sorted(names, key=len, reverse=True))
172
+ marks = list(re.finditer(rf"(?:^|(?<=\s))({alternatives})\s*[=:]\s*", rest, re.IGNORECASE))
173
+ if not marks:
174
+ if len(names) == 1:
175
+ return {names[0]: rest}, None
176
+ return None, f"Say which blank each value fills: {', '.join(f'{name}=…' for name in names)}."
177
+ if marks[0].start() != 0:
178
+ return None, f"Start with a blank's name: {', '.join(f'{name}=…' for name in names)}."
179
+ found = {}
180
+ for here, mark in enumerate(marks):
181
+ end = marks[here + 1].start() if here + 1 < len(marks) else len(rest)
182
+ value = rest[mark.end():end].strip()
183
+ if len(value) > 1 and value[0] == value[-1] and value[0] in "\"'":
184
+ value = value[1:-1]
185
+ found[mark.group(1)] = value
186
+ return found, None
187
+
188
+
189
+ class Watch:
190
+ def __init__(self, channel, reply, public, name, check, deadline):
191
+ self.channel, self.reply, self.public, self.name = channel, reply, public, name
192
+ self.check, self.deadline = check, deadline
193
+
194
+
195
+ class InboundService:
196
+ """The commands this Mac takes. One per Runtime (outlets.register); ``remote`` is Remote's service."""
197
+
198
+ def __init__(self, runtime, *, clock=time.time, remote=None, call=None, interval=POLL_SECONDS):
199
+ self.runtime = runtime
200
+ self.store = Store(runtime.journal)
201
+ self.clock = clock
202
+ self._remote = remote
203
+ self.call = call or net.call
204
+ self.interval = interval
205
+ self._codes = {} # code hash -> (channel, expires)
206
+ self._watches = []
207
+ self._lock = threading.Condition()
208
+ self._thread = None
209
+ self._closed = False
210
+
211
+ # -- lifecycle ---------------------------------------------------------------------------------------------------
212
+
213
+ def close(self):
214
+ with self._lock:
215
+ self._closed = True
216
+ self._lock.notify_all()
217
+ if self._thread is not None:
218
+ self._thread.join(timeout=5)
219
+
220
+ def _loop(self):
221
+ while True:
222
+ with self._lock:
223
+ if self._closed:
224
+ return
225
+ self._lock.wait(self.interval)
226
+ if self._closed:
227
+ return
228
+ try:
229
+ self.poll()
230
+ except Exception: # noqa: BLE001 -- one bad watch never stops the others
231
+ log.exception("inbound: a wait for a run failed")
232
+
233
+ def remote(self):
234
+ if self._remote is not None:
235
+ return self._remote
236
+ try:
237
+ from ..remote import service as remote_service
238
+ except ImportError:
239
+ return None
240
+ return remote_service.service_for(self.runtime)
241
+
242
+ # -- what the app does -------------------------------------------------------------------------------------------
243
+
244
+ def state(self):
245
+ return {"on": on(), "channels": [
246
+ {"channel": channel, "name": NAMES[channel], "senders": self.store.senders(channel),
247
+ "workflows": self.enabled(channel)} for channel in PLATFORMS]}
248
+
249
+ def _channel(self, channel):
250
+ if channel not in PLATFORMS:
251
+ raise ValueError(f"channel is one of {', '.join(PLATFORMS)}")
252
+ return channel
253
+
254
+ def new_code(self, channel):
255
+ """A link code to type in Slack or Discord: {"code", "expiresAt"}. The relay learns its hash only."""
256
+ self._channel(channel)
257
+ remote = self.remote()
258
+ if not on():
259
+ raise APIError(OFF, 409, "off")
260
+ if remote is None or not remote.address("hooks"):
261
+ raise APIError("Turn on Mobster Remote first, in Settings › Remote. Commands reach this Mac through it.",
262
+ 409, "remote_off")
263
+ now = self.clock()
264
+ code = "".join(secrets.choice(CODE_CHARS) for _ in range(8))
265
+ digest = code_hash(code)
266
+ with self._lock:
267
+ for old in [h for h, v in self._codes.items() if v[1] <= now]:
268
+ del self._codes[old]
269
+ if len(self._codes) >= MAX_CODES:
270
+ del self._codes[min(self._codes, key=lambda h: self._codes[h][1])]
271
+ self._codes[digest] = (channel, now + CODE_SECONDS)
272
+ remote.inbound_code(channel, digest)
273
+ return {"code": f"{code[:4]}-{code[4:]}", "expiresAt": round((now + CODE_SECONDS) * 1000)}
274
+
275
+ def forget(self, channel, sender):
276
+ self._channel(channel)
277
+ if self.store.sender(channel, sender) is None:
278
+ raise APIError("That account isn't linked.", 404, "not_found")
279
+ self.store.remove_sender(channel, sender)
280
+ remote = self.remote()
281
+ if remote is not None:
282
+ remote.inbound_unlink(channel, sender)
283
+ return sender
284
+
285
+ def set_public(self, channel, sender, public):
286
+ self._channel(channel)
287
+ if type(public) is not bool:
288
+ raise ValueError("public is true or false")
289
+ if self.store.sender(channel, sender) is None:
290
+ raise APIError("That account isn't linked.", 404, "not_found")
291
+ self.store.set_public(channel, sender, public)
292
+ return self.store.sender(channel, sender)
293
+
294
+ def enable(self, channel, workflow_id):
295
+ self._channel(channel)
296
+ if not isinstance(workflow_id, str) or not WORKFLOW_ID.fullmatch(workflow_id):
297
+ raise ValueError("workflowId is a saved workflow's id")
298
+ self.runtime.workflows.get(workflow_id)
299
+ self.store.enable(channel, workflow_id, self.clock())
300
+ return workflow_id
301
+
302
+ def disable(self, channel, workflow_id):
303
+ self._channel(channel)
304
+ self.store.disable(channel, workflow_id)
305
+ return workflow_id
306
+
307
+ def enabled(self, channel):
308
+ """The published workflows turned on for ``channel``: [{"id", "name", "blanks"}]. One removed or unpublished
309
+ since drops out here, and a removed one is forgotten."""
310
+ wanted, found = set(self.store.workflow_ids(channel)), []
311
+ for workflow in self.runtime.workflows.list():
312
+ if workflow["id"] in wanted:
313
+ wanted.discard(workflow["id"])
314
+ if workflow.get("published"):
315
+ found.append({"id": workflow["id"], "name": words(workflow["name"]),
316
+ "blanks": [blank["name"] for blank in starters.blanks(workflow.get("goal"))]})
317
+ for gone in wanted:
318
+ self.store.disable(channel, gone)
319
+ return found
320
+
321
+ # -- a command ---------------------------------------------------------------------------------------------------
322
+
323
+ def handle(self, message):
324
+ """One command from the relay: {"status", "message", "public"}. 202 a workflow started, 200 done, else refused.
325
+ Never raises."""
326
+ try:
327
+ return self._handle(message if isinstance(message, dict) else {})
328
+ except Exception: # noqa: BLE001 -- a broken command answers 500 and starts nothing
329
+ log.exception("inbound: a command failed")
330
+ return refuse(500, "Mobster hit an error. Nothing started.")
331
+
332
+ def _handle(self, m):
333
+ channel, sender, verb = m.get("channel"), m.get("sender"), m.get("verb")
334
+ text, key = m.get("text"), m.get("key")
335
+ if not on():
336
+ return refuse(503, OFF)
337
+ if (channel not in PLATFORMS or not isinstance(sender, str) or not SENDER[channel].fullmatch(sender)
338
+ or verb not in VERBS or not isinstance(text, str) or len(text) > TEXT_MAX
339
+ or not isinstance(key, str) or not 3 <= len(key) <= 100):
340
+ return refuse(400, "Mobster didn't understand that command. Nothing started.")
341
+ label = words(m.get("label") or sender)[:40]
342
+ now = self.clock()
343
+ if verb == "link":
344
+ return self._link(channel, sender, label, text, now)
345
+ person = self.store.sender(channel, sender)
346
+ if person is None:
347
+ return refuse(403, STRANGER)
348
+ if self.store.seen(channel, key, now):
349
+ return refuse(409, "Mobster already took that command, so nothing started again.")
350
+ self.store.used(channel, sender, now)
351
+ if verb == "list":
352
+ listed = self.enabled(channel)
353
+ if not listed:
354
+ return refuse(200, f"No workflow is turned on for {NAMES[channel]} yet. "
355
+ "Turn one on in Mobster for Mac.")
356
+ lines = [item["name"] + (f" ({', '.join(item['blanks'])})" if item["blanks"] else "") for item in listed]
357
+ return {"status": 200, "message": "You can start: " + "; ".join(lines) + ".", "public": False}
358
+ return self._run(channel, person, key, words(text), m.get("reply"), now)
359
+
360
+ def _link(self, channel, sender, label, text, now):
361
+ digest = code_hash(normalize(text))
362
+ with self._lock:
363
+ entry = self._codes.get(digest)
364
+ if entry is None or entry[0] != channel or entry[1] <= now:
365
+ return refuse(403, "That code didn't match, or it ran out. Make a new one in Mobster for Mac.")
366
+ del self._codes[digest]
367
+ self.store.add_sender(channel, sender, label, now)
368
+ return {"status": 200, "message": f"Linked {label}. Send `list` to see what you can start.", "public": False}
369
+
370
+ def _run(self, channel, person, key, text, reply, now):
371
+ listed = self.enabled(channel)
372
+ match = next((item for item in sorted(listed, key=lambda w: -len(w["name"]))
373
+ if text[:len(item["name"])].casefold() == item["name"].casefold()
374
+ and text[len(item["name"]):len(item["name"]) + 1] in ("", " ")), None)
375
+ if match is None:
376
+ return refuse(404, f"No workflow like that is turned on for {NAMES[channel]}. Send `list` to see yours.")
377
+ if sum(1 for item in listed if item["name"].casefold() == match["name"].casefold()) > 1:
378
+ return refuse(409, f"Several workflows are called {match['name']}. Rename one in Mobster for Mac.")
379
+ workflow = self.runtime.workflows.get(match["id"])
380
+ values, why = read_values(workflow, text[len(match["name"]):].strip())
381
+ if values is None:
382
+ return refuse(422, why)
383
+ filled, ignored = hooks.fill(workflow, values)
384
+ if ignored:
385
+ return refuse(422, f"{workflow['name']} has no blank called {ignored[0]}.")
386
+ body = {"via": channel, "from": person["label"], "text": secret_filter.redact(text)[:TEXT_MAX]}
387
+ try:
388
+ started = self.runtime.workflows.start(workflow["id"], f"{channel}-{code_hash(key)[:40]}", filled or None,
389
+ origin="hook", body=body)
390
+ except APIError as error:
391
+ return refuse(error.status if 400 <= error.status < 600 else 409, str(error))
392
+ except ValueError as error:
393
+ return refuse(422, str(error))
394
+ sentence = (f"Started {workflow['name']}. Anything it sends, posts, buys or deletes waits for your OK in "
395
+ "Mobster "
396
+ "on your Mac.")
397
+ if reply_to.address(channel, reply) and (started.get("batch") or started.get("run")):
398
+ self._watch(Watch(channel, reply, person["public"], workflow["name"], lambda: self._result(started),
399
+ now + reply_to.WINDOW[channel]))
400
+ return {"status": 202, "message": sentence, "public": person["public"]}
401
+
402
+ # -- the answer --------------------------------------------------------------------------------------------------
403
+
404
+ def _watch(self, watch):
405
+ """Wait for a run to end: the thread starts with the first wait, so a Mac that takes no commands runs none."""
406
+ with self._lock:
407
+ self._watches.append(watch)
408
+ if self._thread is None and not self._closed:
409
+ self._thread = threading.Thread(target=self._loop, name="mobster-inbound", daemon=True)
410
+ self._thread.start()
411
+ self._lock.notify_all()
412
+
413
+ def poll(self):
414
+ """Post the result of each run that ended, once, and drop a wait whose address ran out. The thread calls it
415
+ every ``interval`` seconds; a test calls it."""
416
+ with self._lock:
417
+ watches = list(self._watches)
418
+ for watch in watches:
419
+ expired = self.clock() >= watch.deadline
420
+ text = None if expired else watch.check()
421
+ if text is None and not expired:
422
+ continue
423
+ with self._lock:
424
+ if watch in self._watches:
425
+ self._watches.remove(watch)
426
+ if text is not None:
427
+ reply_to.post(watch.channel, watch.reply, f"{watch.name}: {text}", watch.public, self.call)
428
+
429
+ def _result(self, started):
430
+ """How the run ended, as one masked line (and its last answer), or None while it goes."""
431
+ workflows = self.runtime.workflows
432
+ if started.get("batch"):
433
+ record = workflows.batches.public(workflows.batches.get(started["batch"]["id"]))
434
+ if record is not None and record["status"] == "running":
435
+ return None
436
+ status, reason = (record or {}).get("status") or "ended", (record or {}).get("reason")
437
+ answer = next((r["answer"] for r in reversed((record or {}).get("rounds") or []) if r.get("answer")), None)
438
+ else:
439
+ with self.runtime.lock:
440
+ run = self.runtime.runs.get((started.get("run") or {}).get("id"))
441
+ if run is None:
442
+ return None
443
+ if run.finished_at is None:
444
+ return None
445
+ summary = run.summary if isinstance(run.summary, dict) else {}
446
+ status, reason, answer = run.status, summary.get("reason"), summary.get("answer")
447
+ head = "finished" if status == "completed" else f"ended ({str(status).replace('_', ' ')})"
448
+ said = (reason if status != "completed" else None, answer)
449
+ rest = [item for item in said if isinstance(item, str) and item.strip()]
450
+ return secret_filter.redact(" ".join([head + "."] + [words(item) for item in rest]))[:ANSWER_CHARS]
451
+
452
+
453
+ _services = {}
454
+ _services_lock = threading.Lock()
455
+
456
+
457
+ def service_for(runtime):
458
+ with _services_lock:
459
+ return _services.get(id(runtime))
460
+
461
+
462
+ def start(runtime):
463
+ service = InboundService(runtime)
464
+ with _services_lock:
465
+ _services[id(runtime)] = service
466
+
467
+
468
+ def close(runtime):
469
+ with _services_lock:
470
+ service = _services.pop(id(runtime), None)
471
+ if service is not None:
472
+ service.close()
@@ -0,0 +1,57 @@
1
+ """Where a platform lets Mobster answer a command later: Slack's ``response_url`` and Discord's interaction token.
2
+
3
+ Slack and Discord hand the relay a one-use address with the command. Mobster posts the run's result there when the run
4
+ ends, as a new message in the place the command was typed (in the thread, when it was typed in one). Only
5
+ hooks.slack.com and discord.com are called, whatever the relay passes on; an address that isn't one of those is dropped.
6
+ One try each: a post whose outcome is unknown is never repeated.
7
+ """
8
+
9
+ import json
10
+ import re
11
+
12
+ from . import net
13
+ from .webhook import USER_AGENT
14
+
15
+ SLACK = re.compile(r"https://hooks\.slack\.com/(?:commands|actions)/[A-Za-z0-9/_-]{1,200}")
16
+ # Joined here so no tracked file spells a Discord webhook address (scripts/check_no_webhook.py).
17
+ DISCORD_API = "https://discord.com" + "/api/v10/" + "webhooks"
18
+ DISCORD_APP = re.compile(r"\d{15,25}")
19
+ DISCORD_TOKEN = re.compile(r"[A-Za-z0-9._-]{20,300}")
20
+ # How long the platform keeps the address good (Slack: 30 minutes, Discord: 15), less a minute.
21
+ WINDOW = {"slack": 29 * 60, "discord": 14 * 60}
22
+
23
+
24
+ def address(channel, reply):
25
+ """The URL to post to, or None when ``reply`` isn't an address this platform gave."""
26
+ if not isinstance(reply, dict):
27
+ return None
28
+ if channel == "slack":
29
+ url = reply.get("url")
30
+ return url if isinstance(url, str) and SLACK.fullmatch(url) else None
31
+ app, token = reply.get("app"), reply.get("token")
32
+ if (isinstance(app, str) and isinstance(token, str)
33
+ and DISCORD_APP.fullmatch(app) and DISCORD_TOKEN.fullmatch(token)):
34
+ return f"{DISCORD_API}/{app}/{token}"
35
+ return None
36
+
37
+
38
+ def body(channel, text, public):
39
+ """The post's JSON. Slack's ``&``, ``<`` and ``>`` are escaped as it asks and Discord pings nobody, so text from a
40
+ screen can't mention a channel or a person or make a link."""
41
+ if channel == "slack":
42
+ escaped = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
43
+ return {"response_type": "in_channel" if public else "ephemeral", "text": escaped[:3000]}
44
+ return {"content": text[:1900], "flags": 0 if public else 64, "allowed_mentions": {"parse": []}}
45
+
46
+
47
+ def post(channel, reply, text, public, call=net.call):
48
+ """Post ``text`` once. True when the platform took it."""
49
+ url = address(channel, reply)
50
+ if url is None:
51
+ return False
52
+ data = json.dumps(body(channel, text, public)).encode()
53
+ try:
54
+ answer = call("POST", url, data, {"Content-Type": "application/json", "User-Agent": USER_AGENT})
55
+ except net.NetError:
56
+ return False
57
+ return answer.status in (200, 204)
@@ -0,0 +1,81 @@
1
+ """The local API for commands from Slack and Discord, under /api/outlets/inbound (api_routes, owner "outlets").
2
+
3
+ | Route | Auth | Body | Answer |
4
+ |---|---|---|---|
5
+ | GET /api/outlets/inbound | token | | {on, channels: [{channel, name, senders, workflows}]} |
6
+ | POST /api/outlets/inbound/codes | app | {channel} | 201 {code, expiresAt}: the code to type in Slack or Discord |
7
+ | POST /api/outlets/inbound/senders/{channel}/{sender} | app | {public} | {sender} |
8
+ | DELETE /api/outlets/inbound/senders/{channel}/{sender} | app | | {sender} |
9
+ | POST /api/outlets/inbound/workflows | app | {channel, workflowId} | 201 {workflowId} |
10
+ | DELETE /api/outlets/inbound/workflows/{channel}/{id} | app | | {workflowId} |
11
+
12
+ ``channel`` is slack or discord. A sender is {channel, sender, label, public, createdAt, lastAt, uses}; ``public`` is
13
+ whether the run's result is posted for the whole channel to read. 409 off while ``MOBSTER_EXPERIMENTAL_INBOUND``
14
+ isn't 1, and 409 remote_off while Mobster Remote isn't set up here.
15
+ """
16
+
17
+ from ..api_errors import APIError
18
+ from ..api_routes import Response, Route, register
19
+ from . import inbound
20
+ from .routes import OFF, needs_app
21
+
22
+
23
+ def service_of(request):
24
+ service = inbound.service_for(request.runtime)
25
+ if service is None:
26
+ raise APIError(OFF, 503, "unavailable")
27
+ return service
28
+
29
+
30
+ def body_of(request):
31
+ return request.body if isinstance(request.body, dict) else {}
32
+
33
+
34
+ def state(request):
35
+ return Response.json(service_of(request).state())
36
+
37
+
38
+ def code(request):
39
+ service = service_of(request)
40
+ needs_app(request)
41
+ return Response.json(service.new_code(body_of(request).get("channel")), 201)
42
+
43
+
44
+ def sender_update(request):
45
+ service = service_of(request)
46
+ needs_app(request)
47
+ row = service.set_public(request.params["channel"], request.params["sender"], body_of(request).get("public"))
48
+ return Response.json({"sender": row})
49
+
50
+
51
+ def sender_remove(request):
52
+ service = service_of(request)
53
+ needs_app(request)
54
+ return Response.json({"sender": service.forget(request.params["channel"], request.params["sender"])})
55
+
56
+
57
+ def workflow_add(request):
58
+ service = service_of(request)
59
+ needs_app(request)
60
+ body = body_of(request)
61
+ return Response.json({"workflowId": service.enable(body.get("channel"), body.get("workflowId"))}, 201)
62
+
63
+
64
+ def workflow_remove(request):
65
+ service = service_of(request)
66
+ needs_app(request)
67
+ return Response.json({"workflowId": service.disable(request.params["channel"], request.params["id"])})
68
+
69
+
70
+ def register_routes():
71
+ where = r"(?P<channel>slack|discord)"
72
+ for route in (Route("outlets", "GET", "/api/outlets/inbound", state, body="none"),
73
+ Route("outlets", "POST", "/api/outlets/inbound/codes", code),
74
+ Route("outlets", "POST", f"/api/outlets/inbound/senders/{where}/(?P<sender>[A-Za-z0-9:]{{3,60}})",
75
+ sender_update),
76
+ Route("outlets", "DELETE", f"/api/outlets/inbound/senders/{where}/(?P<sender>[A-Za-z0-9:]{{3,60}})",
77
+ sender_remove, body="none"),
78
+ Route("outlets", "POST", "/api/outlets/inbound/workflows", workflow_add),
79
+ Route("outlets", "DELETE", f"/api/outlets/inbound/workflows/{where}/{{id}}", workflow_remove,
80
+ body="none")):
81
+ register(route)