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,169 @@
1
+ """A Google Sheets destination: each result is a row in "Mobster results",
2
+ a sheet Mobster makes in your Google Drive."""
3
+
4
+ import json
5
+ import re
6
+ from urllib.parse import quote, urlencode
7
+
8
+ from . import connect, net
9
+ from .kinds import Kind, again, careful, connection, flat
10
+ from .webhook import USER_AGENT, Outcome
11
+
12
+ SHEETS = "https://sheets.googleapis.com/v4/spreadsheets"
13
+ DRIVE = "https://www.googleapis.com/drive/v3/files"
14
+ TITLE = "Mobster results"
15
+ LABEL = "Google Sheets"
16
+ SHEET_ID = re.compile(r"[A-Za-z0-9_-]{20,200}")
17
+ CELL_MAX = 50_000 # Google's limit for one cell's text
18
+
19
+
20
+ def headers(token, body=False):
21
+ out = {"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT}
22
+ if body:
23
+ out["Content-Type"] = "application/json"
24
+ return out
25
+
26
+
27
+ def refusal(status):
28
+ """Google's refusal as a sentence: what happened, then what to do."""
29
+ if status == 401:
30
+ return f"{LABEL} signed Mobster out. " + again(LABEL)
31
+ if status in (403, 404):
32
+ return (f"Mobster can't open {TITLE} any more: it was deleted, or Google took back Mobster's access. "
33
+ f"Connect {LABEL} again to make a new one.")
34
+ return f"Google answered {status}."
35
+
36
+
37
+ def find_or_make(call, token):
38
+ """{"id", "title"}: the "Mobster results" sheet Mobster made before, else a new one. NetError, or ValueError."""
39
+ query = f"name = '{TITLE}' and mimeType = 'application/vnd.google-apps.spreadsheet' and trashed = false"
40
+ reply = call("GET", f"{DRIVE}?" + urlencode({"q": query, "fields": "files(id,name)", "orderBy": "createdTime",
41
+ "pageSize": "1", "spaces": "drive"}), None, headers(token))
42
+ answer = reply.json_object()
43
+ if reply.status != 200 or answer is None:
44
+ raise ValueError(refusal(reply.status))
45
+ files = [item for item in answer.get("files") or () if isinstance(item, dict)
46
+ and isinstance(item.get("id"), str) and SHEET_ID.fullmatch(item["id"])]
47
+ if files:
48
+ return {"id": files[0]["id"], "title": TITLE}
49
+ sheet = {"properties": {"title": TITLE},
50
+ "sheets": [{"properties": {"title": "Results", "gridProperties": {"frozenRowCount": 1}}}]}
51
+ reply = call("POST", SHEETS, json.dumps(sheet).encode(), headers(token, body=True))
52
+ answer = reply.json_object()
53
+ made = (answer or {}).get("spreadsheetId")
54
+ if reply.status != 200 or not isinstance(made, str) or not SHEET_ID.fullmatch(made):
55
+ raise ValueError(f"Google didn't make the sheet ({reply.status}). Connect again.")
56
+ return {"id": made, "title": TITLE}
57
+
58
+
59
+ def value(item):
60
+ """A cell: numbers and true/false as they are, anything else as text Google keeps as written (RAW)."""
61
+ if item is None:
62
+ return ""
63
+ if isinstance(item, bool) or (isinstance(item, (int, float)) and item == item and abs(item) != float("inf")):
64
+ return item
65
+ text = item if isinstance(item, str) else json.dumps(item, ensure_ascii=False)
66
+ return text[:CELL_MAX]
67
+
68
+
69
+ class Stop(Exception):
70
+ """One try ends here, with ``outcome``."""
71
+
72
+ def __init__(self, outcome):
73
+ super().__init__(outcome.message)
74
+ self.outcome = outcome
75
+
76
+
77
+ class Sheets(Kind):
78
+ id, label = "sheets", LABEL
79
+ example = "Sales sheet"
80
+ fields = ("connect",)
81
+ replay_safe = False
82
+ provider = "google-sheets"
83
+
84
+ def connected(self, call, tokens):
85
+ sheet = find_or_make(call, tokens["access_token"])
86
+ oauth = {key: item for key, item in tokens.items() if key != "kept"}
87
+ return {"credentials": {"oauth": oauth, "sheet": sheet}, "account": sheet["title"], "targets": None}
88
+
89
+ def setup(self, body, current=None):
90
+ if current is not None and "connect" not in body:
91
+ return {}
92
+ found = connection(body, self)["credentials"]
93
+ return {"hint": f"{LABEL} › {found['sheet']['title']}", "has_secret": True,
94
+ "config": {"spreadsheet": found["sheet"]["id"], "title": found["sheet"]["title"]},
95
+ "vault": {"url": None, "secret": None, "oauth": found["oauth"]}, "last_test": None}
96
+
97
+ @staticmethod
98
+ def _sheet(ctx):
99
+ sheet = ctx.destination["config"].get("spreadsheet")
100
+ if not sheet:
101
+ # A sheet saved before 15 Oct 2026 took rows through an Apps Script web app.
102
+ raise Stop(Outcome(False, False, None, "Mobster now adds rows itself, with no script. " + again(LABEL),
103
+ unsent=True))
104
+ return f"{SHEETS}/{quote(sheet, safe='')}"
105
+
106
+ @staticmethod
107
+ def _call(ctx, method, url, data, token):
108
+ """One request that adds nothing (a read, or the header, written the same each time it's tried), with a token
109
+ that works: a 401 renews it once."""
110
+ for refresh in (False, True):
111
+ if refresh:
112
+ token = connect.access(ctx, LABEL, refresh=True)
113
+ try:
114
+ reply = ctx.call(method, url, data, headers(token, body=data is not None))
115
+ except net.NetError as error:
116
+ # Nothing this sends can add a row, so another try is safe even when this one may have arrived.
117
+ raise Stop(Outcome(False, error.retry, None, error.message, unsent=True)) from None
118
+ if reply.status == 401 and not refresh:
119
+ continue
120
+ if reply.status == 200:
121
+ return reply, token
122
+ raise Stop(Outcome(False, reply.status == 429 or reply.status >= 500, reply.status,
123
+ refusal(reply.status), unsent=True))
124
+ raise Stop(Outcome(False, False, 401, refusal(401), unsent=True))
125
+
126
+ def _header(self, ctx, base, token):
127
+ reply, token = self._call(ctx, "GET", f"{base}/values/1:1?majorDimension=ROWS", None, token)
128
+ rows = (reply.json_object() or {}).get("values") or [[]]
129
+ return [str(name) for name in rows[0]], token
130
+
131
+ def send(self, ctx, body):
132
+ try:
133
+ base = self._sheet(ctx)
134
+ token = connect.access(ctx, LABEL)
135
+ header, token = self._header(ctx, base, token)
136
+ row = flat(body)
137
+ known = {name.casefold() for name in header}
138
+ missing = [name for name in row if name.casefold() not in known]
139
+ if missing:
140
+ header += missing
141
+ data = json.dumps({"values": [header]}, ensure_ascii=False).encode()
142
+ _reply, token = self._call(ctx, "PUT", f"{base}/values/A1?valueInputOption=RAW", data, token)
143
+ except (Stop, connect.NoToken) as error:
144
+ return error.outcome
145
+ by_name = {name.casefold(): item for name, item in row.items()}
146
+ data = json.dumps({"values": [[value(by_name.get(name.casefold())) for name in header]]},
147
+ ensure_ascii=False).encode()
148
+ # RAW input, so text from a screen is never read as a formula.
149
+ url = f"{base}/values/A1:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS"
150
+ reply, failed = careful(lambda: ctx.call("POST", url, data, headers(token, body=True)), TITLE)
151
+ if failed:
152
+ return failed
153
+ if reply.status == 200:
154
+ return Outcome(True, False, 200)
155
+ if reply.status == 429:
156
+ return Outcome(False, True, 429, "Google asked Mobster to wait.", unsent=True)
157
+ if reply.status >= 500:
158
+ return Outcome(False, False, reply.status, f"Google answered {reply.status}. Check {TITLE}, then resend "
159
+ "if the row is missing.")
160
+ return Outcome(False, False, reply.status, refusal(reply.status), unsent=True)
161
+
162
+ def test(self, ctx):
163
+ """Reads the header: the sign-in and the sheet work. No row is added."""
164
+ try:
165
+ self._header(ctx, self._sheet(ctx), connect.access(ctx, LABEL))
166
+ except (Stop, connect.NoToken) as error:
167
+ return Outcome(False, False, error.outcome.status, error.outcome.message)
168
+ title = ctx.destination["config"].get("title") or TITLE
169
+ return Outcome(True, False, 200, f"Connected to {title}. Each result adds a row.")
@@ -0,0 +1,177 @@
1
+ """A Slack destination: each result is a message in one channel you picked, posted as you."""
2
+
3
+ import json
4
+ import re
5
+ from urllib.parse import urlencode
6
+
7
+ from ..api_errors import APIError
8
+ from . import webhook
9
+ from .kinds import Kind, again, careful, connection, target
10
+ from .webhook import USER_AGENT, Outcome
11
+
12
+ API = "https://slack.com/api"
13
+ LABEL = "Slack"
14
+ CHANNEL = re.compile(r"[CG][A-Z0-9]{2,30}")
15
+ PAGES = 5 # users.conversations pages of 200 read at Connect
16
+ SAFE_LINK = re.compile(r"https://[A-Za-z0-9._~:/?#\[\]@!$'()*+,;=%&-]{1,500}")
17
+ SECTION = 3000 # Block Kit's limit for a section's text
18
+ STATUS = {"run.started": "Started", "needs_you": "Needs you in Mobster", "test": "Test"}
19
+ RENEW = ("invalid_auth", "token_expired", "not_authed")
20
+ SIGNED_OUT = ("token_revoked", "account_inactive", "user_removed_from_team", "team_disabled")
21
+ NOT_HERE = {"channel_not_found": "the channel is gone", "is_archived": "the channel was archived",
22
+ "not_in_channel": "you're no longer in the channel", "restricted_action": "a workspace admin turned "
23
+ "posting there off", "cant_post": "you can't post in that channel"}
24
+ TEST = "Mobster: this is a test. Results from your tasks will arrive here."
25
+
26
+
27
+ def headers(token, body=False):
28
+ out = {"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT}
29
+ if body:
30
+ out["Content-Type"] = "application/json; charset=utf-8"
31
+ return out
32
+
33
+
34
+ def refusal(error):
35
+ """Slack's ``error`` as a sentence: what happened, then what to do."""
36
+ if error in RENEW or error in SIGNED_OUT:
37
+ return "Slack signed Mobster out. Sign in to Slack again under Use in tasks in Settings › Connectors."
38
+ if error == "missing_scope":
39
+ return ("Your Slack sign-in can't do this: it needs chat:write, channels:read and groups:read. Sign in to "
40
+ "Slack again under Use in tasks.")
41
+ if error in NOT_HERE:
42
+ return f"Slack refused: {NOT_HERE[error]}. Pick another channel: " + again(LABEL)
43
+ return f"Slack refused ({str(error)[:60]})."
44
+
45
+
46
+ def read(call, method, token, query=None):
47
+ """One Web API read: Slack's answer when ``ok``. NetError, or ValueError with Slack's refusal."""
48
+ reply = call("GET", f"{API}/{method}" + (f"?{urlencode(query)}" if query else ""), None, headers(token))
49
+ answer = reply.json_object()
50
+ if reply.status != 200 or answer is None:
51
+ raise ValueError(f"Slack answered {reply.status}. Try again.")
52
+ if answer.get("ok") is not True:
53
+ raise ValueError(refusal(answer.get("error")))
54
+ return answer
55
+
56
+
57
+ def channels(call, token):
58
+ """The channels you're in, by name: [{"id", "name"}]."""
59
+ found, cursor = [], None
60
+ for _ in range(PAGES):
61
+ query = {"types": "public_channel,private_channel", "exclude_archived": "true", "limit": "200"}
62
+ if cursor:
63
+ query["cursor"] = cursor
64
+ answer = read(call, "users.conversations", token, query)
65
+ for item in answer.get("channels") or ():
66
+ if isinstance(item, dict) and isinstance(item.get("id"), str) and CHANNEL.fullmatch(item["id"]) \
67
+ and isinstance(item.get("name"), str):
68
+ found.append({"id": item["id"], "name": "#" + item["name"][:80]})
69
+ cursor = (answer.get("response_metadata") or {}).get("next_cursor")
70
+ if not cursor:
71
+ break
72
+ return sorted(found, key=lambda item: item["name"])
73
+
74
+
75
+ class Slack(Kind):
76
+ id, label = "slack", LABEL
77
+ example = "Team channel"
78
+ fields = ("connect", "target")
79
+ replay_safe = False
80
+ keychain = False
81
+
82
+ def connected(self, call, tokens):
83
+ token = tokens["access_token"]
84
+ team = read(call, "auth.test", token).get("team")
85
+ return {"credentials": {"connector": tokens["connector"]},
86
+ "account": team[:100] if isinstance(team, str) and team.strip() else None,
87
+ "targets": channels(call, token)}
88
+
89
+ def setup(self, body, current=None):
90
+ if current is not None and not ({"connect", "target"} & set(body)):
91
+ return {}
92
+ found = connection(body, self)
93
+ channel = target(found, body.get("target"), "a channel")
94
+ config = {"connector": found["credentials"]["connector"], "channel": channel["id"],
95
+ "channelName": channel["name"]}
96
+ if found.get("account"):
97
+ config["workspace"] = found["account"]
98
+ return {"hint": f"Slack › {channel['name']}", "config": config, "last_test": None}
99
+
100
+ @staticmethod
101
+ def suite_message(body):
102
+ """A suite's message (testkit/notify.py): the title bold, one section of its lines, and a context line."""
103
+ report = body["report"]
104
+ title = webhook.slack_text(" ".join(str(report["title"]).split()))[:SECTION]
105
+ link = report.get("link")
106
+ safe = isinstance(link, str) and SAFE_LINK.fullmatch(link)
107
+ lines = [f"<{link.replace('&', '&amp;')}|Report>" if safe and line == f"Report: {link}"
108
+ else webhook.slack_text(" ".join(str(line).split())) for line in report["lines"]]
109
+ text = "\n".join(lines)
110
+ budget = SECTION - len(title) - 3 # bold title and newline share this section
111
+ text = text if len(text) <= budget else text[:budget - 1] + "…"
112
+ return {"text": title, "unfurl_links": False, "unfurl_media": False, "link_names": False,
113
+ "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": f"*{title}*\n{text}"}},
114
+ {"type": "context", "elements": [{"type": "mrkdwn", "text": "Checks run by Mobster"}]}]}
115
+
116
+ @staticmethod
117
+ def message(body):
118
+ """Block Kit: the line as one section, and how the task ended as a quiet context line. &, < and > are escaped:
119
+ text from a screen can't mention @channel or a person, or make a link."""
120
+ text = webhook.slack_text(" ".join(str(body.get("text") or "Mobster").split()))
121
+ text = text if len(text) <= SECTION else text[:SECTION - 1] + "…"
122
+ kind = body.get("type")
123
+ if kind == "tests.finished":
124
+ return Slack.suite_message(body)
125
+ status = STATUS.get(kind) or ("Done" if body.get("status") in webhook.DONE else "Couldn't finish")
126
+ flow = (body.get("workflow") or {}).get("name")
127
+ context = webhook.slack_text(f"{status} in Mobster" + (f", from {flow}" if flow else ""))
128
+ return {"text": text, "unfurl_links": False, "unfurl_media": False, "link_names": False,
129
+ "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": text}},
130
+ {"type": "context", "elements": [{"type": "mrkdwn", "text": context}]}]}
131
+
132
+ def _post(self, ctx, message):
133
+ config = ctx.destination["config"]
134
+ if not config.get("channel") or not config.get("connector"):
135
+ # A Slack destination saved before 15 Oct 2026 posted through an incoming webhook.
136
+ return Outcome(False, False, None, "Mobster now posts as you, in a channel you pick. " + again(LABEL),
137
+ unsent=True)
138
+ connectors = ctx.connectors()
139
+ if connectors is None:
140
+ return Outcome(False, True, None, "Mobster's connectors aren't running. Restart Mobster, then resend.",
141
+ unsent=True)
142
+ from ..connectors import client as mcp
143
+ data = json.dumps({**message, "channel": config["channel"]}, ensure_ascii=False).encode()
144
+ for renew in (False, True):
145
+ try:
146
+ token = connectors.bearer(config["connector"], refresh=renew)
147
+ except APIError:
148
+ return Outcome(False, False, None, "Slack was disconnected from Mobster. Connect Slack under Use in "
149
+ "tasks, then connect this destination again.", unsent=True)
150
+ except mcp.AuthNeeded:
151
+ return Outcome(False, False, None, refusal("token_revoked"), unsent=True)
152
+ except mcp.NotSent as error:
153
+ return Outcome(False, True, None, error.message, unsent=True)
154
+ reply, failed = careful(lambda: ctx.call("POST", f"{API}/chat.postMessage", data,
155
+ headers(token, body=True)), "the channel")
156
+ if failed:
157
+ return failed
158
+ if reply.status == 429:
159
+ return Outcome(False, True, 429, "Slack asked Mobster to wait.", unsent=True)
160
+ if reply.status >= 500:
161
+ return Outcome(False, False, reply.status, f"Slack answered {reply.status}. Check the channel, then "
162
+ "resend if the message is missing.")
163
+ answer = reply.json_object() or {}
164
+ if reply.status == 200 and answer.get("ok") is True:
165
+ return Outcome(True, False, 200)
166
+ error = answer.get("error")
167
+ if error in RENEW and not renew:
168
+ continue
169
+ # Slack answered ok: false, so it posted nothing.
170
+ return Outcome(False, error == "ratelimited", reply.status, refusal(error), unsent=True)
171
+ return Outcome(False, False, 401, refusal("invalid_auth"), unsent=True)
172
+
173
+ def send(self, ctx, body):
174
+ return self._post(ctx, self.message(body))
175
+
176
+ def test(self, ctx):
177
+ return self._post(ctx, self.message({"type": "test", "text": TEST}))
@@ -0,0 +1,194 @@
1
+ """Outlets' state in the run journal: schema ``outlets`` v2, in the same private SQLite file (0600)."""
2
+
3
+ import json
4
+ import sqlite3
5
+
6
+ from ..journal import JournalError
7
+
8
+ SCHEMA = "outlets"
9
+ VERSION = 3
10
+ MAX_DESTINATIONS = 50
11
+ # Deliveries kept: the newest, for the runs History still has.
12
+ MAX_DELIVERIES = 2000
13
+ STATUSES = ("sending", "sent", "failed")
14
+ EVENTS = ("run.finished", "run.started", "needs_you")
15
+ LOG_MAX = 50 # deliveries one destination's log shows
16
+
17
+
18
+ def migrate(connection, from_version):
19
+ """journal.register_schema's migration: v0 -> v1 creates the tables; v1 -> v2 adds the kinds."""
20
+ if from_version < 1:
21
+ connection.execute("""CREATE TABLE IF NOT EXISTS outlet_destinations (
22
+ id TEXT PRIMARY KEY,
23
+ name TEXT NOT NULL,
24
+ hint TEXT NOT NULL,
25
+ has_secret INTEGER NOT NULL DEFAULT 0,
26
+ screenshot INTEGER NOT NULL DEFAULT 0,
27
+ last_test TEXT,
28
+ created_at REAL NOT NULL,
29
+ updated_at REAL NOT NULL)""")
30
+ connection.execute("""CREATE TABLE IF NOT EXISTS outlet_deliveries (
31
+ id TEXT PRIMARY KEY,
32
+ run_id TEXT NOT NULL,
33
+ destination_id TEXT NOT NULL,
34
+ destination_name TEXT NOT NULL,
35
+ status TEXT NOT NULL CHECK (status IN ('sending', 'sent', 'failed')),
36
+ attempts INTEGER NOT NULL DEFAULT 0,
37
+ http_status INTEGER,
38
+ error TEXT,
39
+ next_at REAL,
40
+ created_at REAL NOT NULL,
41
+ updated_at REAL NOT NULL)""")
42
+ connection.execute("CREATE INDEX IF NOT EXISTS outlet_deliveries_run ON outlet_deliveries(run_id)")
43
+ if from_version < 2:
44
+ columns = {row[1] for row in connection.execute("PRAGMA table_info(outlet_destinations)")}
45
+ if "kind" not in columns:
46
+ connection.execute("ALTER TABLE outlet_destinations ADD COLUMN kind TEXT NOT NULL DEFAULT 'webhook'")
47
+ if "config" not in columns:
48
+ connection.execute("ALTER TABLE outlet_destinations ADD COLUMN config TEXT")
49
+ if "inflight_at" not in {row[1] for row in connection.execute("PRAGMA table_info(outlet_deliveries)")}:
50
+ connection.execute("ALTER TABLE outlet_deliveries ADD COLUMN inflight_at REAL")
51
+ if from_version < 3:
52
+ columns = {row[1] for row in connection.execute("PRAGMA table_info(outlet_deliveries)")}
53
+ if "event" not in columns:
54
+ connection.execute("ALTER TABLE outlet_deliveries ADD COLUMN event TEXT NOT NULL DEFAULT 'run.finished'")
55
+ if "payload" not in columns:
56
+ connection.execute("ALTER TABLE outlet_deliveries ADD COLUMN payload TEXT")
57
+ connection.execute("CREATE INDEX IF NOT EXISTS outlet_deliveries_destination "
58
+ "ON outlet_deliveries(destination_id, created_at)")
59
+
60
+
61
+ DESTINATION_COLUMNS = ("id", "name", "hint", "has_secret", "screenshot", "last_test", "created_at", "updated_at",
62
+ "kind", "config")
63
+ DELIVERY_COLUMNS = ("id", "run_id", "destination_id", "destination_name", "status", "attempts", "http_status", "error",
64
+ "next_at", "created_at", "updated_at", "inflight_at", "event", "payload")
65
+
66
+
67
+ def _destination(row):
68
+ if row is None:
69
+ return None
70
+ value = dict(zip(DESTINATION_COLUMNS, row))
71
+ value["has_secret"], value["screenshot"] = bool(value["has_secret"]), bool(value["screenshot"])
72
+ for key in ("last_test", "config"):
73
+ try:
74
+ value[key] = json.loads(value[key]) if value[key] else None
75
+ except ValueError:
76
+ value[key] = None
77
+ value["kind"] = value["kind"] or "webhook"
78
+ value["config"] = value["config"] if isinstance(value["config"], dict) else {}
79
+ return value
80
+
81
+
82
+ def _delivery(row):
83
+ if row is None:
84
+ return None
85
+ value = dict(zip(DELIVERY_COLUMNS, row))
86
+ value["event"] = value["event"] or "run.finished"
87
+ try:
88
+ value["payload"] = json.loads(value["payload"]) if value["payload"] else None
89
+ except ValueError:
90
+ value["payload"] = None
91
+ return value
92
+
93
+
94
+ class OutletStore:
95
+ def __init__(self, journal):
96
+ self.journal = journal
97
+
98
+ def _read(self, sql, args=()):
99
+ with self.journal.lock:
100
+ if self.journal.connection is None:
101
+ raise JournalError("The run journal is closed")
102
+ try:
103
+ return self.journal.connection.execute(sql, args).fetchall()
104
+ except sqlite3.Error:
105
+ raise JournalError("Mobster could not read its saved destinations") from None
106
+
107
+ def destinations(self):
108
+ cols = ", ".join(DESTINATION_COLUMNS)
109
+ return [_destination(row) for row in self._read(f"SELECT {cols} FROM outlet_destinations "
110
+ "ORDER BY created_at, rowid")]
111
+
112
+ def destination(self, destination_id):
113
+ cols = ", ".join(DESTINATION_COLUMNS)
114
+ rows = self._read(f"SELECT {cols} FROM outlet_destinations WHERE id=?", (destination_id,))
115
+ return _destination(rows[0]) if rows else None
116
+
117
+ def count(self):
118
+ return self._read("SELECT count(*) FROM outlet_destinations")[0][0]
119
+
120
+ def add_destination(self, row):
121
+ with self.journal.transaction() as connection:
122
+ connection.execute(f"INSERT INTO outlet_destinations({', '.join(DESTINATION_COLUMNS)}) "
123
+ f"VALUES ({', '.join('?' * len(DESTINATION_COLUMNS))})",
124
+ [row["id"], row["name"], row["hint"], int(bool(row["has_secret"])),
125
+ int(bool(row["screenshot"])), None, row["created_at"], row["updated_at"],
126
+ row.get("kind") or "webhook", json.dumps(row.get("config") or {})])
127
+ return self.destination(row["id"])
128
+
129
+ def update_destination(self, destination_id, **changes):
130
+ allowed = {"name", "hint", "has_secret", "screenshot", "last_test", "updated_at", "config"}
131
+ if set(changes) - allowed:
132
+ raise ValueError("Unknown destination field")
133
+ values = []
134
+ for key, value in changes.items():
135
+ if key in ("has_secret", "screenshot"):
136
+ value = int(bool(value))
137
+ elif key in ("last_test", "config"):
138
+ value = json.dumps(value) if value is not None else None
139
+ values.append(value)
140
+ if values:
141
+ with self.journal.transaction() as connection:
142
+ connection.execute(f"UPDATE outlet_destinations SET {', '.join(f'{k}=?' for k in changes)} WHERE id=?",
143
+ [*values, destination_id])
144
+ return self.destination(destination_id)
145
+
146
+ def delete_destination(self, destination_id):
147
+ with self.journal.transaction() as connection:
148
+ connection.execute("DELETE FROM outlet_destinations WHERE id=?", (destination_id,))
149
+
150
+ def add_delivery(self, row):
151
+ with self.journal.transaction() as connection:
152
+ connection.execute(f"INSERT INTO outlet_deliveries({', '.join(DELIVERY_COLUMNS)}) "
153
+ f"VALUES ({', '.join('?' * len(DELIVERY_COLUMNS))})",
154
+ [json.dumps(row["payload"], ensure_ascii=False) if column == "payload" and
155
+ row.get("payload") is not None else
156
+ (row.get(column) or "run.finished") if column == "event" else row.get(column)
157
+ for column in DELIVERY_COLUMNS])
158
+ excess = connection.execute("SELECT count(*) FROM outlet_deliveries").fetchone()[0] - MAX_DELIVERIES
159
+ if excess > 0:
160
+ connection.execute("DELETE FROM outlet_deliveries WHERE rowid IN (SELECT rowid FROM outlet_deliveries "
161
+ "WHERE status != 'sending' ORDER BY created_at LIMIT ?)", (excess,))
162
+ return self.delivery(row["id"])
163
+
164
+ def delivery(self, delivery_id):
165
+ rows = self._read(f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE id=?", (delivery_id,))
166
+ return _delivery(rows[0]) if rows else None
167
+
168
+ def deliveries_for(self, run_id):
169
+ return [_delivery(row) for row in self._read(
170
+ f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE run_id=? ORDER BY created_at, rowid",
171
+ (run_id,))]
172
+
173
+ def deliveries_to(self, destination_id, limit=LOG_MAX):
174
+ """A destination's newest deliveries first: its delivery log."""
175
+ return [_delivery(row) for row in self._read(
176
+ f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE destination_id=? "
177
+ "ORDER BY created_at DESC, rowid DESC LIMIT ?", (destination_id, max(1, min(int(limit), LOG_MAX))))]
178
+
179
+ def pending(self):
180
+ return [_delivery(row) for row in self._read(
181
+ f"SELECT {', '.join(DELIVERY_COLUMNS)} FROM outlet_deliveries WHERE status='sending' ORDER BY next_at")]
182
+
183
+ def update_delivery(self, delivery_id, **changes):
184
+ allowed = {"status", "attempts", "http_status", "error", "next_at", "updated_at", "inflight_at"}
185
+ if set(changes) - allowed:
186
+ raise ValueError("Unknown delivery field")
187
+ with self.journal.transaction() as connection:
188
+ connection.execute(f"UPDATE outlet_deliveries SET {', '.join(f'{k}=?' for k in changes)} WHERE id=?",
189
+ [*changes.values(), delivery_id])
190
+ return self.delivery(delivery_id)
191
+
192
+ def forget_run(self, run_id):
193
+ with self.journal.transaction() as connection:
194
+ connection.execute("DELETE FROM outlet_deliveries WHERE run_id=?", (run_id,))
@@ -0,0 +1,80 @@
1
+ """Where a destination's address, secret and token live: one generic password per destination in the login Keychain,
2
+ readable only by Mobster's own programs (keychain_acl.py), as remote/keychain.py keeps Mobster Remote's device key."""
3
+
4
+ import base64
5
+ import json
6
+ import re
7
+
8
+ from ..connectors import keystore
9
+
10
+ SERVICE = "app.mobster.desktop.outlets"
11
+ ENV = "MOBSTER_OUTLETS_KEYCHAIN"
12
+ ACCOUNT = re.compile(r"destination:[0-9a-f]{12}")
13
+ WORD = re.compile(r"[A-Za-z0-9_-]{1,8000}")
14
+ VaultError = keystore.VaultError
15
+
16
+
17
+ def account_for(destination_id):
18
+ account = f"destination:{destination_id}"
19
+ if not ACCOUNT.fullmatch(account):
20
+ raise ValueError("A destination's id is 12 hex characters")
21
+ return account
22
+
23
+
24
+ EXTRA = {"token": str, "to": str, "oauth": dict}
25
+
26
+
27
+ def encode(url, secret, extra=None):
28
+ item = {"url": url, "secret": secret or None}
29
+ for key, kind in EXTRA.items():
30
+ if isinstance((extra or {}).get(key), kind) and extra[key]:
31
+ item[key] = extra[key]
32
+ raw = json.dumps(item, separators=(",", ":")).encode()
33
+ return base64.urlsafe_b64encode(raw).decode().rstrip("=")
34
+
35
+
36
+ def decode(word):
37
+ """{"url", "secret", "token"?, "to"?, "oauth"?}: ``url`` and ``secret`` may be None (a Notion, Sheets or email
38
+ destination)."""
39
+ try:
40
+ raw = base64.urlsafe_b64decode(word + "=" * (-len(word) % 4))
41
+ value = json.loads(raw)
42
+ except (ValueError, TypeError):
43
+ raise VaultError("Mobster couldn't read a saved destination") from None
44
+ kinds = {"url": str, **EXTRA}
45
+ if not isinstance(value, dict) or not any(isinstance(value.get(key), kind) and value[key]
46
+ for key, kind in kinds.items()):
47
+ raise VaultError("Mobster couldn't read a saved destination")
48
+ out = {"url": None, "secret": None}
49
+ for key, kind in {"url": str, "secret": str, **EXTRA}.items():
50
+ found = value.get(key)
51
+ if isinstance(found, kind) and found:
52
+ out[key] = found
53
+ return out
54
+
55
+
56
+ class Vault:
57
+ """``put(id, url, secret, extra)``, ``get(id) -> {"url", "secret", ...} | None`` and ``delete(id)`` over
58
+ a raw store."""
59
+
60
+ def put(self, destination_id, url, secret, extra=None):
61
+ self._set(account_for(destination_id), encode(url, secret, extra))
62
+
63
+ def get(self, destination_id):
64
+ word = self._get(account_for(destination_id))
65
+ return decode(word) if word else None
66
+
67
+ def delete(self, destination_id):
68
+ self._delete(account_for(destination_id))
69
+
70
+
71
+ class FileVault(keystore.FileStore, Vault):
72
+ files = "destinations"
73
+
74
+
75
+ class KeychainVault(keystore.KeychainStore, Vault):
76
+ service, noun, word = SERVICE, "destination", WORD
77
+
78
+
79
+ def backend():
80
+ return keystore.backend(ENV, FileVault, KeychainVault)