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,580 @@
1
+ """Destinations and the delivery worker: one OutletService per Runtime (harness_api.register_service)."""
2
+
3
+ import logging
4
+ import random
5
+ import re
6
+ import threading
7
+ import time
8
+ import uuid
9
+ from types import SimpleNamespace
10
+
11
+ from .. import harness_api
12
+ from ..api_errors import APIError
13
+ from ..connectors import oauth
14
+ from ..service_support import ServiceRegistry, ms
15
+ from . import connect as outlet_connect
16
+ from . import kinds as outlet_kinds
17
+ from . import net, webhook
18
+ from . import store as outlet_store
19
+ from . import vault as outlet_vault
20
+
21
+ log = logging.getLogger("mobster.outlets")
22
+
23
+ ATTEMPTS = 5
24
+ BACKOFF = (10.0, 60.0, 300.0, 900.0) # seconds before the 2nd, 3rd, 4th and 5th tries
25
+ JITTER = 0.2 # each pause is BACKOFF ± 20%, so a receiver that fell over isn't hit at once
26
+ ID = re.compile(r"[0-9a-f]{12}")
27
+ REMOVED = "That destination was removed. Choose another one."
28
+ GONE_RUN = "That task is no longer in History."
29
+ NO_ADDRESS = "Mobster couldn't find this destination's address or sign-in. Add it again in Settings › Connectors."
30
+ KEYCHAIN = "Mobster couldn't read this destination from the Keychain. Unlock your Mac's Keychain, then resend."
31
+ UNKNOWN = "Mobster quit while sending this, so it may have arrived. Check {name}, then resend if it's missing."
32
+
33
+ _services = ServiceRegistry()
34
+
35
+
36
+ def service_for(runtime):
37
+ return _services.get(runtime)
38
+
39
+
40
+ def services():
41
+ return _services.all()
42
+
43
+
44
+ def start(runtime):
45
+ """register_service's start: an OutletService for the runtime; picks up deliveries an earlier launch left."""
46
+ service = OutletService(runtime)
47
+ _services.add(runtime, service)
48
+ service.start()
49
+
50
+
51
+ def close(runtime):
52
+ service = _services.pop(runtime)
53
+ if service is not None:
54
+ service.close()
55
+
56
+
57
+ class OutletService:
58
+ def __init__(self, runtime, *, vault=None, post=None, call=None, clock=time.time, backoff=BACKOFF,
59
+ attempts=ATTEMPTS, jitter=JITTER, rand=random.random, connectors=None, refresh=oauth.refresh,
60
+ apps=None, sign_in=oauth.SignIn):
61
+ self.runtime = runtime
62
+ self.store = outlet_store.OutletStore(runtime.journal)
63
+ self.vault = vault if vault is not None else outlet_vault.backend()
64
+ self.post = post or webhook.post
65
+ self.call = call or net.call
66
+ self.clock = clock
67
+ self.refresh = refresh # renews a Notion or Google sign-in through the license API
68
+ self.connectors = connectors or self._connectors
69
+ self.connects = outlet_connect.Connects(call=self.call, connectors=self.connectors, apps=apps, sign_in=sign_in)
70
+ self.backoff = tuple(backoff)
71
+ self.attempts = attempts
72
+ self.jitter, self.rand = jitter, rand
73
+ self._lock = threading.Condition()
74
+ self._due = {} # delivery id -> when to try it (seconds)
75
+ self._thread = None
76
+ self._closed = False
77
+ self._busy = set() # destination ids with a test in flight
78
+ self._trying = {} # destination id -> threading.Lock: one try (a test or a delivery) at a time
79
+
80
+ def start(self):
81
+ try:
82
+ pending = self.store.pending()
83
+ except Exception: # noqa: BLE001 -- a journal that can't be read leaves nothing to pick up
84
+ pending = []
85
+ now = self.clock()
86
+ # A try under way when Mobster quit, to a kind that can't take a result twice: its outcome is unknown, so it
87
+ # fails and says to check.
88
+ for row in [r for r in pending if r.get("inflight_at")]:
89
+ pending.remove(row)
90
+ try:
91
+ self.store.update_delivery(row["id"], status="failed", inflight_at=None, next_at=None, updated_at=now,
92
+ error=UNKNOWN.format(name=row["destination_name"]))
93
+ except Exception: # noqa: BLE001
94
+ log.warning("outlets: couldn't close a delivery whose try was under way")
95
+ with self._lock:
96
+ for row in pending:
97
+ self._due[row["id"]] = min(row["next_at"] or now, now)
98
+ journal = getattr(self.runtime, "journal", None)
99
+ if journal is not None and callable(getattr(journal, "add_expiry_listener", None)):
100
+ journal.add_expiry_listener(self._forget_runs)
101
+ if pending:
102
+ self._ensure()
103
+
104
+ def close(self, timeout=1.5):
105
+ self.connects.close()
106
+ with self._lock:
107
+ self._closed = True
108
+ self._lock.notify_all()
109
+ thread = self._thread
110
+ if thread is not None and thread is not threading.current_thread():
111
+ thread.join(timeout)
112
+
113
+ def _ensure(self):
114
+ with self._lock:
115
+ if self._closed or (self._thread is not None and self._thread.is_alive()):
116
+ return
117
+ self._thread = threading.Thread(target=self._loop, name="mobster-outlets", daemon=True)
118
+ self._thread.start()
119
+
120
+ def _forget_runs(self, run_ids):
121
+ for run_id in run_ids:
122
+ try:
123
+ self.store.forget_run(run_id)
124
+ except Exception: # noqa: BLE001
125
+ log.warning("outlets: couldn't forget a removed run's deliveries")
126
+
127
+ @staticmethod
128
+ def public(row):
129
+ test = row.get("last_test")
130
+ kind = row.get("kind") or "webhook"
131
+ config = row.get("config") or {}
132
+ shown = {key: config[key] for key in ("folder", "file", "format", "database", "title", "preset", "workspace",
133
+ "channel", "channelName", "server", "spreadsheet") if key in config}
134
+ return {"id": row["id"], "name": row["name"], "kind": kind, "config": shown, "hint": row["hint"],
135
+ "events": config.get("events") or ["run.finished"], "scope": config.get("scope") or "named",
136
+ "hasSecret": row["has_secret"],
137
+ "screenshot": row["screenshot"], "createdAt": ms(row["created_at"]),
138
+ "lastTest": {"ok": bool(test.get("ok")), "at": ms(test.get("at")), "status": test.get("status"),
139
+ "message": test.get("message")} if isinstance(test, dict) else None}
140
+
141
+ def list(self):
142
+ used = {}
143
+ for flow in self._workflows():
144
+ if flow.get("sendTo"):
145
+ used[flow["sendTo"]] = used.get(flow["sendTo"], 0) + 1
146
+ return [{**self.public(row), "workflows": used.get(row["id"], 0)} for row in self.store.destinations()]
147
+
148
+ def kinds(self):
149
+ """The kinds the app may offer to add now, in kinds.py's order (Connects.offered)."""
150
+ return [kind_id for kind_id, kind in outlet_kinds.all_kinds().items() if self.connects.offered(kind)]
151
+
152
+ def _workflows(self):
153
+ workflows = getattr(self.runtime, "workflows", None)
154
+ try:
155
+ return list(workflows.list()) if workflows is not None else []
156
+ except Exception: # noqa: BLE001 -- the list stands without its counts
157
+ return []
158
+
159
+ def find(self, wanted):
160
+ """A destination by its id, else by its name (case and spacing ignored). ValueError when none or two match."""
161
+ if not isinstance(wanted, str) or not wanted.strip():
162
+ raise ValueError("Name a saved destination, or give its id")
163
+ wanted = wanted.strip()
164
+ rows = self.store.destinations()
165
+ found = [r for r in rows if r["id"] == wanted.lower()] or [
166
+ r for r in rows if " ".join(r["name"].split()).casefold() == " ".join(wanted.split()).casefold()]
167
+ if not found:
168
+ names = ", ".join(r["name"] for r in rows[:8])
169
+ raise ValueError(f"No destination is called {wanted[:60]}. "
170
+ + (f"You have: {names}." if names else "Add one in Settings › Connectors."))
171
+ if len(found) > 1:
172
+ raise ValueError(f"Several destinations are called {wanted[:60]}. Use its id: "
173
+ + ", ".join(r["id"] for r in found))
174
+ return found[0]
175
+
176
+ def _get(self, destination_id):
177
+ row = self.store.destination(destination_id) if isinstance(destination_id, str) else None
178
+ if row is None:
179
+ raise APIError("That destination was removed.", 404, "not_found")
180
+ return row
181
+
182
+ def _unique(self, name, but=None):
183
+ for row in self.store.destinations():
184
+ if row["id"] != but and row["name"].casefold() == name.casefold():
185
+ raise ValueError(f"You already have a destination called {name}. Give this one another name.")
186
+
187
+ def _connectors(self):
188
+ """The ConnectorService a Slack destination posts through, or None (connectors/service.py)."""
189
+ try:
190
+ from ..connectors import service as connector_service
191
+ except ImportError:
192
+ return None
193
+ return connector_service.service_for(self.runtime)
194
+
195
+ def connect(self, body):
196
+ """Start connecting a destination: {"connect", "authorizeUrl"?} (connect.py)."""
197
+ if not isinstance(body, dict) or set(body) != {"kind"}:
198
+ raise ValueError('Say which kind to connect: {"kind": "slack" | "discord" | "notion" | "sheets"}')
199
+ return self.connects.begin(self._kind(body["kind"]))
200
+
201
+ def _resolved(self, kind, body):
202
+ """``body`` with its ``connect`` id replaced by what that connect found."""
203
+ if "connect" not in body:
204
+ return body
205
+ return {**body, "connect": self.connects.found(body["connect"], kind.id)}
206
+
207
+ @staticmethod
208
+ def _kind(value):
209
+ if value is not None and not isinstance(value, str):
210
+ raise ValueError("A destination's kind is a word, like webhook")
211
+ return outlet_kinds.get(value)
212
+
213
+ def _keychain_put(self, destination_id, item, then):
214
+ try:
215
+ self.vault.put(destination_id, item.get("url"), item.get("secret"),
216
+ {key: item[key] for key in outlet_vault.EXTRA if item.get(key)})
217
+ except outlet_vault.VaultError as error:
218
+ raise APIError(f"{error}. {then}", 503, "keychain_unavailable") from None
219
+
220
+ def add(self, body):
221
+ if not isinstance(body, dict):
222
+ raise ValueError("A destination has a name and where it sends")
223
+ kind = self._kind(body.get("kind"))
224
+ if set(body) - set(outlet_kinds.FIELDS) - set(kind.fields):
225
+ raise ValueError("A destination has a name, its kind, and " + ", ".join(kind.fields))
226
+ name = webhook.validate_name(body.get("name"), kind.example)
227
+ self._unique(name)
228
+ if self.store.count() >= outlet_store.MAX_DESTINATIONS:
229
+ raise APIError(f"You have {outlet_store.MAX_DESTINATIONS} destinations. Remove one first.", 409,
230
+ "destination_limit")
231
+ subscribed = outlet_kinds.subscription(body, kind.id)
232
+ setup = kind.setup(self._resolved(kind, {k: v for k, v in body.items() if k not in ("events", "scope")}))
233
+ destination_id = uuid.uuid4().hex[:12]
234
+ if setup.get("vault"):
235
+ self._keychain_put(destination_id, setup["vault"], "Nothing was saved.")
236
+ now = self.clock()
237
+ try:
238
+ row = self.store.add_destination({"id": destination_id, "name": name, "kind": kind.id,
239
+ "hint": setup["hint"],
240
+ "config": {**(setup.get("config") or {}), **subscribed},
241
+ "has_secret": setup.get("has_secret", False),
242
+ "screenshot": setup.get("screenshot", False),
243
+ "created_at": now, "updated_at": now})
244
+ except Exception:
245
+ self._drop_secret(destination_id)
246
+ raise
247
+ self.connects.drop(body.get("connect"))
248
+ return self.public(row)
249
+
250
+ def update(self, destination_id, body):
251
+ row = self._get(destination_id)
252
+ kind = outlet_kinds.get(row["kind"])
253
+ if not isinstance(body, dict) or not body or set(body) - set(outlet_kinds.FIELDS) - set(kind.fields):
254
+ raise ValueError("Change the name or " + ", ".join(kind.fields))
255
+ if "kind" in body and body["kind"] != kind.id:
256
+ raise ValueError("A destination keeps its kind. Add a new one instead.")
257
+ changes = {}
258
+ if "name" in body:
259
+ changes["name"] = webhook.validate_name(body["name"], kind.example)
260
+ self._unique(changes["name"], but=row["id"])
261
+
262
+ def saved():
263
+ try:
264
+ return self.vault.get(row["id"])
265
+ except outlet_vault.VaultError as error:
266
+ raise APIError(f"{error}. Nothing was changed.", 503, "keychain_unavailable") from None
267
+ fields = {k: v for k, v in body.items() if k not in outlet_kinds.FIELDS}
268
+ setup = kind.setup(self._resolved(kind, fields), {"row": row, "saved": saved})
269
+ if setup.get("vault"):
270
+ self._keychain_put(row["id"], setup["vault"], "Nothing was changed.")
271
+ changes.update({key: value for key, value in setup.items() if key != "vault"})
272
+ if "config" in changes or "events" in body or "scope" in body:
273
+ # A new config from setup keeps what the destination takes and for which tasks.
274
+ base = changes.get("config", row["config"])
275
+ changes["config"] = {**base, **outlet_kinds.subscription(body, kind.id, row["config"])}
276
+ changes["updated_at"] = self.clock()
277
+ updated = self.public(self.store.update_destination(row["id"], **changes))
278
+ self.connects.drop(body.get("connect"))
279
+ return updated
280
+
281
+ def delete(self, destination_id):
282
+ """Remove a destination: its Keychain item goes, workflows that sent there stop sending, and deliveries on their
283
+ way to it fail."""
284
+ row = self._get(destination_id)
285
+ self.store.delete_destination(row["id"])
286
+ self._drop_secret(row["id"])
287
+ workflows = getattr(self.runtime, "workflows", None)
288
+ for flow in self._workflows():
289
+ if flow.get("sendTo") == row["id"]:
290
+ try:
291
+ workflows.update(flow["id"], {"sendTo": None, "revision": flow["revision"]})
292
+ except Exception: # noqa: BLE001 -- its next run finds the destination gone and sends nothing
293
+ log.warning("outlets: a workflow kept a removed destination")
294
+ return row["id"]
295
+
296
+ def _drop_secret(self, destination_id):
297
+ try:
298
+ self.vault.delete(destination_id)
299
+ except outlet_vault.VaultError:
300
+ log.warning("outlets: the Keychain kept a removed destination's item")
301
+
302
+ def _saved(self, row):
303
+ """{"url", "secret", ...} of a destination; APIError when the Keychain can't give it."""
304
+ if not outlet_kinds.get(row.get("kind")).keychain:
305
+ return {"url": None, "secret": None}
306
+ try:
307
+ saved = self.vault.get(row["id"])
308
+ except outlet_vault.VaultError:
309
+ raise APIError(KEYCHAIN, 503, "keychain_unavailable") from None
310
+ if saved is None:
311
+ raise APIError(NO_ADDRESS, 409, "address_missing")
312
+ return saved
313
+
314
+ def _context(self, row, saved, delivery_id, event):
315
+ def keep(tokens):
316
+ """A renewed sign-in, saved before it's used: a vendor that rotates refresh tokens takes each once."""
317
+ saved["oauth"] = tokens
318
+ try:
319
+ self.vault.put(row["id"], saved.get("url"), saved.get("secret"),
320
+ {key: saved[key] for key in outlet_vault.EXTRA if saved.get(key)})
321
+ except outlet_vault.VaultError:
322
+ log.warning("outlets: the Keychain didn't keep a renewed sign-in")
323
+ return SimpleNamespace(destination=row, saved=saved, delivery_id=delivery_id, event=event, now=self.clock(),
324
+ wall=self.clock, post=self.post, call=self.call, refresh=self.refresh, keep=keep,
325
+ connectors=self.connectors)
326
+
327
+ def _one_try(self, destination_id):
328
+ """One try at a time per destination, so a test and a delivery never renew the same token at once."""
329
+ with self._lock:
330
+ return self._trying.setdefault(destination_id, threading.Lock())
331
+
332
+ def test(self, destination_id):
333
+ """One test, now, the way the destination's kind tests (a webhook gets the test body; a folder is checked
334
+ writable; Notion's database is read), kept on the destination."""
335
+ row = self._get(destination_id)
336
+ kind = outlet_kinds.get(row["kind"])
337
+ with self._lock:
338
+ if row["id"] in self._busy:
339
+ raise APIError("A test is already on its way to this destination.", 409, "test_running")
340
+ self._busy.add(row["id"])
341
+ try:
342
+ with self._one_try(row["id"]):
343
+ saved = self._saved(row)
344
+ outcome = kind.test(self._context(row, saved, uuid.uuid4().hex[:12], "test"))
345
+ finally:
346
+ with self._lock:
347
+ self._busy.discard(row["id"])
348
+ result = {"ok": outcome.ok, "status": outcome.status, "message": outcome.message, "at": self.clock()}
349
+ self.store.update_destination(row["id"], last_test=result)
350
+ return {"ok": outcome.ok, "status": outcome.status, "message": outcome.message}
351
+
352
+ def send_event(self, destination_id, body, event="notification.matched"):
353
+ """One try, now, of a body that isn't a finished run's result (a notification trigger's match), to a saved
354
+ destination, the way its kind sends."""
355
+ row = self._get(destination_id)
356
+ kind = outlet_kinds.get(row["kind"])
357
+ saved = self._saved(row)
358
+ return kind.send(self._context(row, saved, uuid.uuid4().hex[:12], event), body)
359
+
360
+ @staticmethod
361
+ def public_delivery(row):
362
+ return {"id": row["id"], "runId": row["run_id"], "destinationId": row["destination_id"],
363
+ "destinationName": row["destination_name"], "status": row["status"], "attempts": row["attempts"],
364
+ "httpStatus": row["http_status"], "error": row["error"], "nextAt": ms(row["next_at"]),
365
+ "createdAt": ms(row["created_at"]), "updatedAt": ms(row["updated_at"]),
366
+ "event": row.get("event") or "run.finished"}
367
+
368
+ def deliveries(self, run_id):
369
+ return [self.public_delivery(row) for row in self.store.deliveries_for(run_id)
370
+ if (row.get("event") or "run.finished") == "run.finished"]
371
+
372
+ def history(self, destination_id, limit=outlet_store.LOG_MAX):
373
+ """The destination's delivery log, newest first."""
374
+ row = self._get(destination_id)
375
+ return [self.public_delivery(item) for item in self.store.deliveries_to(row["id"], limit)]
376
+
377
+ def enqueue(self, run_id, destination_id, event="run.finished", payload=None):
378
+ """A delivery of ``run_id``'s result (or, with ``payload``, another event) to the destination; its row."""
379
+ row = self.store.destination(destination_id)
380
+ now = self.clock()
381
+ delivery = {"id": uuid.uuid4().hex[:12], "run_id": run_id, "destination_id": destination_id,
382
+ "destination_name": row["name"] if row else "A removed destination",
383
+ "status": "sending" if row else "failed", "attempts": 0, "http_status": None,
384
+ "error": None if row else REMOVED, "next_at": now if row else None, "created_at": now,
385
+ "updated_at": now, "event": event, "payload": payload}
386
+ saved = self.store.add_delivery(delivery)
387
+ if row is not None:
388
+ self._schedule(saved["id"], now)
389
+ return saved
390
+
391
+ def resend(self, delivery_id):
392
+ """The same result again, as a new delivery to the same destination."""
393
+ earlier = self.store.delivery(delivery_id) if isinstance(delivery_id, str) else None
394
+ if earlier is None:
395
+ raise APIError("Mobster no longer has that delivery.", 404, "not_found")
396
+ if earlier["status"] == "sending":
397
+ raise APIError("It's still on its way.", 409, "still_sending")
398
+ self._get(earlier["destination_id"])
399
+ if earlier.get("payload") is not None:
400
+ return self.public_delivery(self.enqueue(earlier["run_id"], earlier["destination_id"], earlier["event"],
401
+ earlier["payload"]))
402
+ with self.runtime.lock:
403
+ run = self.runtime.runs.get(earlier["run_id"])
404
+ if run is None or run.finished_at is None:
405
+ raise APIError(GONE_RUN, 409, "run_gone")
406
+ return self.public_delivery(self.enqueue(run.id, earlier["destination_id"]))
407
+
408
+ def _schedule(self, delivery_id, when):
409
+ with self._lock:
410
+ self._due[delivery_id] = when
411
+ self._lock.notify_all()
412
+ self._ensure()
413
+
414
+ def _loop(self):
415
+ while True:
416
+ with self._lock:
417
+ while not self._closed:
418
+ now = self.clock()
419
+ due = [(when, key) for key, when in self._due.items() if when <= now]
420
+ if due:
421
+ break
422
+ wait = min((when - now for when in self._due.values()), default=None)
423
+ self._lock.wait(timeout=1.0 if wait is None else max(0.01, min(wait, 1.0)))
424
+ if self._closed:
425
+ return
426
+ due.sort()
427
+ delivery_id = due[0][1]
428
+ self._due.pop(delivery_id, None)
429
+ try:
430
+ self._attempt(delivery_id)
431
+ except Exception: # noqa: BLE001 -- one bad delivery never stops the others
432
+ log.exception("outlets: a delivery failed unexpectedly")
433
+ try:
434
+ self.store.update_delivery(delivery_id, status="failed", error="Mobster hit an error sending it.",
435
+ next_at=None, updated_at=self.clock())
436
+ except Exception: # noqa: BLE001
437
+ pass
438
+
439
+ def _fail(self, delivery_id, message, attempts=None, status=None):
440
+ changes = {"status": "failed", "error": message, "next_at": None, "updated_at": self.clock()}
441
+ if attempts is not None:
442
+ changes.update(attempts=attempts, http_status=status)
443
+ self.store.update_delivery(delivery_id, **changes)
444
+ self._published(delivery_id)
445
+
446
+ def _attempt(self, delivery_id):
447
+ row = self.store.delivery(delivery_id)
448
+ if row is None or row["status"] != "sending":
449
+ return
450
+ destination = self.store.destination(row["destination_id"])
451
+ if destination is None:
452
+ return self._fail(delivery_id, REMOVED)
453
+ try:
454
+ kind = outlet_kinds.get(destination["kind"])
455
+ except ValueError:
456
+ return self._fail(delivery_id, "This Mobster can't send to that kind of destination. Update Mobster.")
457
+ with self._one_try(destination["id"]):
458
+ return self._try(delivery_id, row, destination, kind)
459
+
460
+ def _try(self, delivery_id, row, destination, kind):
461
+ try:
462
+ saved = self._saved(destination)
463
+ except APIError as error:
464
+ return self._fail(delivery_id, str(error))
465
+ event = row.get("event") or "run.finished"
466
+ if row.get("payload") is not None:
467
+ body = row["payload"] # made and masked when the event happened
468
+ else:
469
+ with self.runtime.lock:
470
+ run = self.runtime.runs.get(row["run_id"])
471
+ if run is None:
472
+ return self._fail(delivery_id, GONE_RUN)
473
+ workflow = workflow_of(run)
474
+ screenshot = webhook.screenshot_of(run, getattr(self.runtime, "frames", None)) \
475
+ if destination["screenshot"] and kind.id == "webhook" else None
476
+ body = webhook.payload(run, workflow, screenshot)
477
+ if not kind.replay_safe:
478
+ # Journaled before it is sent: a crash from here on leaves an outcome nobody knows, never a second try.
479
+ self.store.update_delivery(delivery_id, inflight_at=self.clock())
480
+ outcome = kind.send(self._context(destination, saved, delivery_id, event), body)
481
+ attempts = row["attempts"] + 1
482
+ now = self.clock()
483
+ if outcome.ok:
484
+ self.store.update_delivery(delivery_id, status="sent", attempts=attempts, http_status=outcome.status,
485
+ error=None, next_at=None, inflight_at=None, updated_at=now)
486
+ elif outcome.retry and attempts < self.attempts and (kind.replay_safe or outcome.unsent):
487
+ pause = self.backoff[min(attempts - 1, len(self.backoff) - 1)] if self.backoff else 0.0
488
+ pause *= 1 + self.jitter * (2 * self.rand() - 1)
489
+ self.store.update_delivery(delivery_id, attempts=attempts, http_status=outcome.status,
490
+ error=outcome.message, next_at=now + pause, inflight_at=None, updated_at=now)
491
+ self._schedule(delivery_id, now + pause)
492
+ else:
493
+ tried = f" Tried {attempts} times." if attempts > 1 else ""
494
+ self.store.update_delivery(delivery_id, inflight_at=None)
495
+ return self._fail(delivery_id, (outcome.message or "It didn't arrive.") + tried, attempts, outcome.status)
496
+ self._published(delivery_id)
497
+
498
+ def _published(self, delivery_id):
499
+ publish = getattr(self.runtime, "publish", None)
500
+ row = self.store.delivery(delivery_id)
501
+ if callable(publish) and row is not None:
502
+ publish("outlets", {"event": "delivery", "runId": row["run_id"], "deliveryId": row["id"],
503
+ "status": row["status"]})
504
+
505
+
506
+ def named(run):
507
+ """The destination id a run's sendTo names, or None."""
508
+ send_to = (getattr(run, "extras", None) or {}).get("sendTo")
509
+ destination = send_to.get("destination") if isinstance(send_to, dict) else None
510
+ return destination if isinstance(destination, str) else None
511
+
512
+
513
+ def workflow_of(run):
514
+ send_to = (getattr(run, "extras", None) or {}).get("sendTo")
515
+ return send_to.get("workflow") if isinstance(send_to, dict) else None
516
+
517
+
518
+ def subscribers(service, run, event):
519
+ """The destinations that take ``event`` for ``run``: the one it names, when that one takes the event, and each that
520
+ takes it for every run."""
521
+ if harness_api.stays_on_mac(getattr(run, "extras", None)):
522
+ return [] # a result that stays on this Mac goes to no destination
523
+ chosen = named(run)
524
+ out = []
525
+ try:
526
+ rows = service.store.destinations()
527
+ except Exception: # noqa: BLE001 -- a journal that can't be read sends nothing
528
+ return []
529
+ for row in sorted(rows, key=lambda r: r["id"] != chosen):
530
+ config = row.get("config") or {}
531
+ events = config.get("events") or ["run.finished"]
532
+ if event in events and (row["id"] == chosen or config.get("scope") == "all"):
533
+ out.append(row["id"])
534
+ if event == "run.finished" and chosen and chosen not in out:
535
+ out.insert(0, chosen) # a named destination always gets the result (an old row, or a removed one)
536
+ return out
537
+
538
+
539
+ class Listener:
540
+ """Deliveries for a run's events, made on the listeners' thread, never the run's: its result to the destination it
541
+ names and to each that takes every run's results; run.started and needs_you to those that take them."""
542
+
543
+ @staticmethod
544
+ def _service(run, runtime=None):
545
+ for service in services():
546
+ if runtime is not None and service.runtime is runtime:
547
+ return service
548
+ runs = getattr(service.runtime, "runs", None) or {}
549
+ if runs.get(run.id) is run:
550
+ return service
551
+ return None
552
+
553
+ def on_created(self, run, runtime):
554
+ service = self._service(run, runtime)
555
+ if service is None:
556
+ return
557
+ targets = subscribers(service, run, "run.started")
558
+ if targets:
559
+ payload = webhook.started_payload(run, workflow_of(run))
560
+ for destination in targets:
561
+ service.enqueue(run.id, destination, "run.started", payload)
562
+
563
+ def on_event(self, run, event):
564
+ if not isinstance(event, dict) or event.get("event") != "approval_requested" or event.get("preapproved"):
565
+ return
566
+ service = self._service(run)
567
+ if service is None:
568
+ return
569
+ targets = subscribers(service, run, "needs_you")
570
+ if targets:
571
+ payload = webhook.needs_payload(run, event, workflow_of(run), service.clock())
572
+ for destination in targets:
573
+ service.enqueue(run.id, destination, "needs_you", payload)
574
+
575
+ def on_finished(self, run, summary, private):
576
+ service = self._service(run)
577
+ if service is None:
578
+ return
579
+ for destination in subscribers(service, run, "run.finished"):
580
+ service.enqueue(run.id, destination)