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,101 @@
1
+ """The local API for notification triggers, under /api/triggers; no route returns a notification's text."""
2
+
3
+ import re
4
+
5
+ from .. import full_disk_access
6
+ from ..api_errors import APIError
7
+ from ..api_routes import Response, Route, register
8
+ from . import service as trigger_service
9
+
10
+ APP_ONLY = "Do this in the Mobster app."
11
+
12
+
13
+ def service_of(request):
14
+ service = trigger_service.service_for(request.runtime)
15
+ if service is None:
16
+ raise APIError(trigger_service.GONE, 503, "unavailable")
17
+ return service
18
+
19
+
20
+ def needs_app(request):
21
+ local = getattr(request.runtime, "local_api", None) or {}
22
+ if local.get("app_session") and not request.app_session:
23
+ raise APIError(APP_ONLY, 403, "app_only")
24
+
25
+
26
+ def listed(request):
27
+ service = service_of(request)
28
+ return Response.json({"triggers": service.list(), "status": service.status()})
29
+
30
+
31
+ def add(request):
32
+ service = service_of(request)
33
+ needs_app(request)
34
+ return Response.json({"trigger": service.add(request.body)}, 201)
35
+
36
+
37
+ def update(request):
38
+ service = service_of(request)
39
+ needs_app(request)
40
+ return Response.json({"trigger": service.update(request.params["id"], request.body)})
41
+
42
+
43
+ def delete(request):
44
+ service = service_of(request)
45
+ needs_app(request)
46
+ return Response.json({"id": service.delete(request.params["id"])})
47
+
48
+
49
+ def recent(request):
50
+ return Response.json({"firings": service_of(request).recent(20)})
51
+
52
+
53
+ def pretty(bundle):
54
+ """A name for a bundle id the catalog doesn't know: its last part, in words."""
55
+ last = re.split(r"[.]", bundle)[-1]
56
+ return " ".join(part.capitalize() for part in re.split(r"[-_]+", last) if part) or bundle
57
+
58
+
59
+ def apps(request):
60
+ """iPhone apps Mobster has seen notify"""
61
+ service = service_of(request)
62
+ names = {}
63
+ try:
64
+ for app in request.runtime.apps():
65
+ if app.get("bundleId"):
66
+ names[app["bundleId"].lower()] = app.get("name")
67
+ except Exception: # noqa: BLE001 -- the list stands without names
68
+ pass
69
+ seen = []
70
+ access = full_disk_access.status([full_disk_access.IPHONE_LIBRARY])
71
+ if access["state"] == "granted":
72
+ seen = iphone_apps()
73
+ out, have = [], set()
74
+ for bundle in seen:
75
+ have.add(bundle.lower())
76
+ out.append({"bundleId": bundle, "name": names.get(bundle.lower()) or pretty(bundle), "seen": True})
77
+ for bundle, name in names.items():
78
+ if bundle not in have and name:
79
+ out.append({"bundleId": bundle, "name": name, "seen": False})
80
+ out.sort(key=lambda app: (not app["seen"], app["name"].casefold()))
81
+ return Response.json({"apps": out, "access": {"state": access["state"], "stores": service.stores()}})
82
+
83
+
84
+ def iphone_apps():
85
+ """The bundle ids in the forwarding store's Library.plist, nothing else read."""
86
+ from ..notify_triggers import sources
87
+ try:
88
+ mapping = sources.unarchive((sources.IPHONE_STORE / sources.LIBRARY).read_bytes())
89
+ except Exception: # noqa: BLE001 -- unreadable means no list, not an error
90
+ return []
91
+ return sorted(k for k in (mapping or {}) if isinstance(k, str) and k)
92
+
93
+
94
+ def register_routes():
95
+ for route in (Route("triggers", "GET", "/api/triggers", listed, body="none"),
96
+ Route("triggers", "POST", "/api/triggers", add),
97
+ Route("triggers", "POST", "/api/triggers/{id}", update),
98
+ Route("triggers", "DELETE", "/api/triggers/{id}", delete, body="none"),
99
+ Route("triggers", "GET", "/api/triggers/apps", apps, body="none"),
100
+ Route("triggers", "GET", "/api/triggers/recent", recent, body="none")):
101
+ register(route)
@@ -0,0 +1,387 @@
1
+ """The triggers service: one TriggerService per Runtime (harness_api.register_service)."""
2
+
3
+ import logging
4
+ import threading
5
+ import time
6
+ import uuid
7
+
8
+ from .. import full_disk_access
9
+ from ..notify_triggers import sources as notify_sources
10
+ from ..notify_triggers.rules import GLOBAL_DEFAULT, PER_RULE_DEFAULT, Rule, RuleSet
11
+ from ..notify_triggers.watch import Watcher
12
+ from . import model
13
+ from . import store as trigger_store
14
+ from .actions import Firer
15
+
16
+ log = logging.getLogger("mobster.triggers")
17
+
18
+ POLL_S = 0.5
19
+ ACCESS_S = 2.0
20
+ STORE_PATHS = {"iphone": lambda: full_disk_access.IPHONE_LIBRARY, "mac": lambda: full_disk_access.USERNOTED_DB}
21
+ NEEDS_ACCESS = ("Mobster needs Full Disk Access to read your notifications. Turn it on in System Settings › Privacy & "
22
+ "Security › Full Disk Access.")
23
+ NO_MIRRORING = ("Your iPhone's notifications aren't on this Mac yet. Set up iPhone Mirroring and turn on notifications "
24
+ "from your iPhone, then the trigger starts watching.")
25
+ ACCESS_LOST = "Mobster lost Full Disk Access, so it stopped reading notifications. Turn it back on to start again."
26
+ LIMITED = ("Mobster skipped this notification. A trigger runs at most 6 times an hour, and all triggers together at "
27
+ "most 30.")
28
+ GONE = "Triggers aren't available right now. Restart Mobster, then try again."
29
+
30
+ _services = {}
31
+ _services_lock = threading.Lock()
32
+
33
+
34
+ def service_for(runtime):
35
+ with _services_lock:
36
+ return _services.get(id(runtime))
37
+
38
+
39
+ def start(runtime):
40
+ """register_service's start: a TriggerService for the runtime."""
41
+ service = TriggerService(runtime)
42
+ with _services_lock:
43
+ _services[id(runtime)] = service
44
+ service.start()
45
+
46
+
47
+ def close(runtime):
48
+ with _services_lock:
49
+ service = _services.pop(id(runtime), None)
50
+ if service is not None:
51
+ service.close()
52
+
53
+
54
+ def ms(seconds):
55
+ return round(seconds * 1000) if isinstance(seconds, (int, float)) else None
56
+
57
+
58
+ def rule_of(trigger):
59
+ """The watcher's rule for a saved trigger: the trigger's id is its name, so limits and events say which."""
60
+ return Rule(name=trigger["id"], app=trigger["app"].lower(),
61
+ pattern=model.word_pattern(trigger["words"], trigger.get("regex")), source=trigger["source"],
62
+ per_hour=PER_RULE_DEFAULT, enabled=trigger["enabled"])
63
+
64
+
65
+ def needed_stores(triggers):
66
+ """The stores the enabled triggers read: iphone, mac or both."""
67
+ need = set()
68
+ for trigger in triggers:
69
+ if trigger["enabled"]:
70
+ need |= {"iphone", "mac"} if trigger["source"] == "any" else {trigger["source"]}
71
+ return need
72
+
73
+
74
+ class TriggerService:
75
+ def __init__(self, runtime, *, journal=None, make_source=None, check=None, clock=time.time, autostart=True,
76
+ poll_s=POLL_S, access_s=ACCESS_S, publish=None):
77
+ self.runtime = runtime
78
+ self.store = trigger_store.TriggerStore(journal if journal is not None else runtime.journal)
79
+ self.clock = clock
80
+ self.check = check or full_disk_access.check
81
+ self.make_source = make_source or self._source
82
+ self.poll_s, self.access_s = poll_s, access_s
83
+ self.publish = publish or getattr(runtime, "publish", None)
84
+ self.autostart = autostart
85
+ self.firer = Firer(runtime, self.store, self.lookup, clock=clock, publish=self.publish)
86
+ self._lock = threading.RLock()
87
+ self._triggers = {}
88
+ self._watcher = None
89
+ self._watching = frozenset() # the stores the watcher reads
90
+ self._errors = {} # store -> why its last poll failed
91
+ self._carry = None # the limiter, kept across rebuilds
92
+ self._wake = threading.Event()
93
+ self._stop = threading.Event()
94
+ self._thread = None
95
+ self._checked = 0.0
96
+ self._lost = set() # stores whose access loss was announced
97
+ self._listening = False # a published workflow has a trigger.notification (on_change says)
98
+ self._unsubscribe = None
99
+
100
+ @staticmethod
101
+ def _source(name):
102
+ return notify_sources.IPhoneSource() if name == "iphone" else notify_sources.MacSource()
103
+
104
+ def start(self):
105
+ try:
106
+ self.store.close_unknown()
107
+ except Exception: # noqa: BLE001 -- a journal that can't be read leaves nothing to close
108
+ log.warning("triggers: couldn't close firings left sending")
109
+ graphs = self._graphs()
110
+ if graphs is not None:
111
+ self._unsubscribe = graphs.on_change(self.workflows_changed) # tells it now, then on every change
112
+ self.reload() # starts the thread only if something already listens: see _arm
113
+
114
+ def workflows_changed(self, listening):
115
+ """Workflows.triggers.on_change, under the workflows' lock: take only this service's lock, never theirs."""
116
+ with self._lock:
117
+ self._listening = bool(listening)
118
+ rows = list(self._triggers.values())
119
+ self._wake.set()
120
+ self._arm(rows)
121
+
122
+ def _arm(self, rows):
123
+ """Start the watching thread when something listens and none is going"""
124
+ if not self.autostart:
125
+ return
126
+ with self._lock:
127
+ if (self._stop.is_set() or not self._stores(rows)
128
+ or (self._thread is not None and self._thread.is_alive())):
129
+ return
130
+ self._thread = threading.Thread(target=self._loop, name="mobster-triggers", daemon=True)
131
+ self._thread.start()
132
+
133
+ def close(self, timeout=3.0):
134
+ self._stop.set()
135
+ self._wake.set()
136
+ if self._unsubscribe is not None:
137
+ self._unsubscribe()
138
+ with self._lock: # after _stop is set: _arm starts nothing it didn't already start
139
+ thread = self._thread
140
+ if thread is not None and thread is not threading.current_thread():
141
+ thread.join(timeout)
142
+ self._drop_watcher()
143
+
144
+ def lookup(self, trigger_id):
145
+ with self._lock:
146
+ return self._triggers.get(trigger_id)
147
+
148
+ def reload(self):
149
+ """Read the saved triggers and swap the rules in. Called at start and after every change."""
150
+ rows = self.store.triggers()
151
+ with self._lock:
152
+ self._triggers = {row["id"]: row for row in rows}
153
+ if self._watcher is not None:
154
+ self._watcher.set_rules(self._ruleset(rows))
155
+ self._wake.set()
156
+ self._arm(rows)
157
+
158
+ @staticmethod
159
+ def _ruleset(rows):
160
+ return RuleSet(rules=tuple(rule_of(row) for row in rows if row["enabled"]), per_hour=GLOBAL_DEFAULT)
161
+
162
+ def list(self):
163
+ return [self.public(row) for row in self.store.triggers()]
164
+
165
+ def problem(self, row):
166
+ """Why a trigger can't do its action now, or None: its destination or workflow was removed."""
167
+ action = row["action"]
168
+ try:
169
+ if action["kind"] == "destination":
170
+ from ..outlets import service as outlet_service
171
+ service = outlet_service.service_for(self.runtime)
172
+ if service is not None and service.store.destination(action["outletId"]) is None:
173
+ return "The destination was removed. Choose another one."
174
+ elif action["kind"] == "workflow":
175
+ workflows = getattr(self.runtime, "workflows", None)
176
+ if workflows is not None:
177
+ workflows.get(action["workflowId"])
178
+ elif not action.get("allowed"):
179
+ return "Starting a task is off for this trigger."
180
+ except Exception as error: # noqa: BLE001 -- a lookup that fails says nothing; only a missing target does
181
+ if getattr(error, "status", None) == 404:
182
+ return "The workflow was deleted. Choose another one."
183
+ return None
184
+
185
+ def public(self, row):
186
+ last = self.store.last_firing(row["id"])
187
+ action = dict(row["action"])
188
+ return {"id": row["id"], "name": row["name"], "enabled": row["enabled"], "app": row["app"],
189
+ "appName": row["app_name"], "words": row["words"], "source": row["source"], "action": action,
190
+ "problem": self.problem(row), "createdAt": ms(row["created_at"]),
191
+ "last": ({"at": ms(last["at"]), "outcome": last["outcome"], "message": last["message"]}
192
+ if last else None)}
193
+
194
+ def firing(self, row):
195
+ return {"id": row["id"], "triggerId": row["trigger_id"], "trigger": row["trigger_name"], "app": row["app"],
196
+ "at": ms(row["at"]), "action": row["kind"], "outcome": row["outcome"], "message": row["message"],
197
+ "runId": row["run_id"]}
198
+
199
+ def recent(self, limit=20):
200
+ return [self.firing(row) for row in self.store.recent(limit)]
201
+
202
+ def _check_target(self, action):
203
+ """The action names something that exists now. ValueError otherwise, with a sentence."""
204
+ if action["kind"] == "destination":
205
+ from ..outlets import service as outlet_service
206
+ service = outlet_service.service_for(self.runtime)
207
+ if service is None:
208
+ raise ValueError("Saved destinations aren't available right now. Restart Mobster, then try again.")
209
+ if service.store.destination(action["outletId"]) is None:
210
+ raise ValueError("That destination was removed. Choose another one in Connectors.")
211
+ elif action["kind"] == "workflow":
212
+ workflows = getattr(self.runtime, "workflows", None)
213
+ if workflows is None:
214
+ raise ValueError("Workflows aren't available right now. Restart Mobster, then try again.")
215
+ try:
216
+ workflows.get(action["workflowId"])
217
+ except Exception as error:
218
+ if getattr(error, "status", None) == 404:
219
+ raise ValueError("That workflow was deleted. Choose another one.") from None
220
+ raise
221
+
222
+ def _unique(self, name, but=None):
223
+ for row in self.store.triggers():
224
+ if row["id"] != but and row["name"].casefold() == name.casefold():
225
+ raise ValueError(f"You already have a trigger called {name}. Give this one another name.")
226
+
227
+ def add(self, body):
228
+ fields = model.clean(body)
229
+ self._unique(fields["name"])
230
+ if self.store.count() >= model.MAX_TRIGGERS:
231
+ raise ValueError(f"You have {model.MAX_TRIGGERS} triggers. Remove one first.")
232
+ self._check_target(fields["action"])
233
+ now = self.clock()
234
+ row = self.store.add({"id": uuid.uuid4().hex[:12], "name": fields["name"], "enabled": fields["enabled"],
235
+ "app": fields["app"], "app_name": fields["appName"], "words": fields["words"],
236
+ "source": fields["source"], "action": fields["action"], "created_at": now,
237
+ "updated_at": now})
238
+ self.reload()
239
+ return self.public(row)
240
+
241
+ def update(self, trigger_id, body):
242
+ row = self.store.trigger(trigger_id)
243
+ if row is None:
244
+ from ..api_errors import APIError
245
+ raise APIError("That trigger was removed.", 404, "not_found")
246
+ current = {"name": row["name"], "app": row["app"], "appName": row["app_name"], "words": row["words"],
247
+ "source": row["source"], "enabled": row["enabled"], "action": row["action"]}
248
+ fields = model.clean(body, current)
249
+ if "name" in body:
250
+ self._unique(fields["name"], but=row["id"])
251
+ if "action" in body:
252
+ self._check_target(fields["action"])
253
+ saved = self.store.update(row["id"], name=fields["name"], enabled=fields["enabled"], app=fields["app"],
254
+ app_name=fields["appName"], words=fields["words"], source=fields["source"],
255
+ action=fields["action"], updated_at=self.clock())
256
+ self.reload()
257
+ return self.public(saved)
258
+
259
+ def delete(self, trigger_id):
260
+ row = self.store.trigger(trigger_id)
261
+ if row is None:
262
+ from ..api_errors import APIError
263
+ raise APIError("That trigger was removed.", 404, "not_found")
264
+ self.store.delete(row["id"])
265
+ self.reload()
266
+ return row["id"]
267
+
268
+ def stores(self):
269
+ """Each store's access now: granted, needed or missing. Reads no content."""
270
+ return {name: self.check(path()) for name, path in STORE_PATHS.items()}
271
+
272
+ def status(self):
273
+ with self._lock:
274
+ triggers = list(self._triggers.values())
275
+ watching = sorted(self._watching)
276
+ errors = dict(self._errors)
277
+ need = self._stores(triggers)
278
+ stores = self.stores()
279
+ enabled = sum(1 for t in triggers if t["enabled"])
280
+ if not need:
281
+ state, message = "off", ("Turn on a trigger to start watching." if triggers else "Add a trigger to start.")
282
+ elif any(stores[name] == "needed" for name in need):
283
+ state, message = "needs_access", NEEDS_ACCESS
284
+ elif "iphone" in need and stores["iphone"] == "missing":
285
+ state, message = "missing", NO_MIRRORING
286
+ elif errors:
287
+ state, message = "error", next(iter(errors.values()))
288
+ elif set(watching) >= need:
289
+ state, message = "watching", "Mobster is watching for your notifications."
290
+ else:
291
+ state, message = "starting", "Mobster is about to start watching."
292
+ return {"state": state, "message": message, "stores": stores, "watching": watching, "enabled": enabled,
293
+ "limits": {"perTriggerPerHour": PER_RULE_DEFAULT, "perHour": GLOBAL_DEFAULT}}
294
+
295
+ def _drop_watcher(self):
296
+ with self._lock:
297
+ watcher, self._watcher, self._watching = self._watcher, None, frozenset()
298
+ if watcher is not None:
299
+ self._carry = watcher.limiter
300
+ if watcher is not None:
301
+ try:
302
+ watcher.close()
303
+ except Exception: # noqa: BLE001
304
+ pass
305
+
306
+ def reconcile(self):
307
+ """Make the watch match the triggers and the access: the stores that are needed and granted, from now."""
308
+ with self._lock:
309
+ triggers = list(self._triggers.values())
310
+ need = self._stores(triggers)
311
+ want = {name for name in need if self.check(STORE_PATHS[name]()) == "granted"}
312
+ self._announce_losses(need, want)
313
+ if frozenset(want) == self._watching and (self._watcher is not None or not want):
314
+ return
315
+ self._drop_watcher()
316
+ self._errors.clear()
317
+ if not want:
318
+ return
319
+ sources = [self.make_source(name) for name in sorted(want)]
320
+ watcher = Watcher(sources, self._ruleset(triggers), self.firer, listener=self._heard,
321
+ subscriber=self._subscribed, clock=self.clock)
322
+ if self._carry is not None:
323
+ watcher.limiter = self._carry
324
+ watcher.limiter.per_hour = GLOBAL_DEFAULT
325
+ watcher.start() # primes every source: what is there now never fires
326
+ with self._lock:
327
+ self._watcher, self._watching = watcher, frozenset(want)
328
+
329
+ def _graphs(self):
330
+ """The workflow triggers module, or None when this Runtime has no workflows."""
331
+ return getattr(getattr(self.runtime, "workflows", None), "triggers", None)
332
+
333
+ def _stores(self, triggers):
334
+ """The enabled triggers' stores, or both while a workflow listens; reads only this service's state."""
335
+ return needed_stores(triggers) | ({"iphone", "mac"} if self._listening else set())
336
+
337
+ def _subscribed(self, notification):
338
+ """A new notification, to the workflows that subscribe themselves."""
339
+ graphs = self._graphs()
340
+ if graphs is not None:
341
+ graphs.notify({"app": notification.app, "title": notification.title,
342
+ "body": notification.body or notification.subtitle,
343
+ "receivedAt": round((notification.date or self.clock()) * 1000)})
344
+
345
+ def _announce_losses(self, need, want):
346
+ for name in need - set(want):
347
+ if name in self._watching and name not in self._lost and self.check(STORE_PATHS[name]()) == "needed":
348
+ self._lost.add(name)
349
+ self.firer.skip("", "Triggers", "", "access_lost", ACCESS_LOST)
350
+ self._lost &= (need - set(want))
351
+
352
+ def _heard(self, event, fields):
353
+ """The watcher's lines: its refusals and its stores' errors."""
354
+ if event == "refused" and fields.get("reason") == "rate_limited":
355
+ trigger = self.lookup(fields.get("rule"))
356
+ self.firer.skip(fields.get("rule") or "", trigger and trigger["name"], trigger and trigger["app"],
357
+ "rate_limited", LIMITED)
358
+ elif event == "source_error":
359
+ self._errors[fields.get("source")] = fields.get("message")
360
+ elif event == "source_ok":
361
+ self._errors.pop(fields.get("source"), None)
362
+
363
+ def poll_once(self):
364
+ """One pass: reconcile when due, then poll the stores. The service thread calls it; a test can too."""
365
+ now = time.monotonic()
366
+ if self._wake.is_set() or now - self._checked >= self.access_s:
367
+ self._wake.clear()
368
+ self._checked = now
369
+ self.reconcile()
370
+ watcher = self._watcher
371
+ if watcher is not None:
372
+ watcher.poll()
373
+
374
+ def _loop(self):
375
+ while not self._stop.is_set():
376
+ try:
377
+ self.poll_once()
378
+ except Exception: # noqa: BLE001 -- the loop outlives one bad pass
379
+ log.exception("triggers: a pass failed")
380
+ with self._lock:
381
+ if not self._stores(list(self._triggers.values())):
382
+ # Nothing listens: the pass above dropped the watcher. _arm starts a new thread when something does.
383
+ self._thread = None
384
+ return
385
+ self._wake.wait(self.poll_s if self._watcher is not None else self.access_s)
386
+ if self._stop.is_set():
387
+ break
@@ -0,0 +1,142 @@
1
+ """Triggers' state in the run journal: schema ``triggers`` v1, in the same private SQLite file (0600)."""
2
+
3
+ import json
4
+ import sqlite3
5
+
6
+ from ..journal import JournalError
7
+
8
+ SCHEMA = "triggers"
9
+ VERSION = 1
10
+ MAX_FIRINGS = 500
11
+ OUTCOMES = ("sending", "done", "skipped", "failed", "unknown")
12
+ COLUMNS = ("id", "name", "enabled", "app", "app_name", "words", "source", "action", "regex", "created_at", "updated_at")
13
+ FIRING_COLUMNS = ("id", "trigger_id", "trigger_name", "app", "at", "kind", "outcome", "message", "run_id")
14
+
15
+
16
+ def migrate(connection, from_version):
17
+ """journal.register_schema's migration: v0 -> v1 creates the tables. Runs inside BEGIN IMMEDIATE."""
18
+ if from_version < 1:
19
+ connection.execute("""CREATE TABLE IF NOT EXISTS triggers (
20
+ id TEXT PRIMARY KEY,
21
+ name TEXT NOT NULL,
22
+ enabled INTEGER NOT NULL DEFAULT 0,
23
+ app TEXT NOT NULL,
24
+ app_name TEXT,
25
+ words TEXT NOT NULL DEFAULT '[]',
26
+ source TEXT NOT NULL DEFAULT 'iphone',
27
+ action TEXT NOT NULL,
28
+ regex TEXT,
29
+ created_at REAL NOT NULL,
30
+ updated_at REAL NOT NULL)""")
31
+ connection.execute("""CREATE TABLE IF NOT EXISTS trigger_firings (
32
+ id TEXT PRIMARY KEY,
33
+ trigger_id TEXT NOT NULL,
34
+ trigger_name TEXT NOT NULL,
35
+ app TEXT NOT NULL,
36
+ at REAL NOT NULL,
37
+ kind TEXT NOT NULL,
38
+ outcome TEXT NOT NULL CHECK (outcome IN ('sending', 'done', 'skipped', 'failed', 'unknown')),
39
+ message TEXT,
40
+ run_id TEXT)""")
41
+ connection.execute("CREATE INDEX IF NOT EXISTS trigger_firings_at ON trigger_firings(at)")
42
+
43
+
44
+ def _trigger(row):
45
+ if row is None:
46
+ return None
47
+ value = dict(zip(COLUMNS, row))
48
+ value["enabled"] = bool(value["enabled"])
49
+ for key, default in (("words", []), ("action", {})):
50
+ try:
51
+ value[key] = json.loads(value[key]) if value[key] else default
52
+ except ValueError:
53
+ value[key] = default
54
+ return value
55
+
56
+
57
+ class TriggerStore:
58
+ def __init__(self, journal):
59
+ self.journal = journal
60
+
61
+ def _read(self, sql, args=()):
62
+ with self.journal.lock:
63
+ if self.journal.connection is None:
64
+ raise JournalError("The run journal is closed")
65
+ try:
66
+ return self.journal.connection.execute(sql, args).fetchall()
67
+ except sqlite3.Error:
68
+ raise JournalError("Mobster could not read its saved triggers") from None
69
+
70
+ def triggers(self):
71
+ return [_trigger(row) for row in self._read(
72
+ f"SELECT {', '.join(COLUMNS)} FROM triggers ORDER BY created_at, rowid")]
73
+
74
+ def trigger(self, trigger_id):
75
+ rows = self._read(f"SELECT {', '.join(COLUMNS)} FROM triggers WHERE id=?", (trigger_id,))
76
+ return _trigger(rows[0]) if rows else None
77
+
78
+ def count(self):
79
+ return self._read("SELECT count(*) FROM triggers")[0][0]
80
+
81
+ def add(self, row):
82
+ with self.journal.transaction() as connection:
83
+ connection.execute(
84
+ f"INSERT INTO triggers({', '.join(COLUMNS)}) VALUES ({', '.join('?' * len(COLUMNS))})",
85
+ [row["id"], row["name"], int(bool(row["enabled"])), row["app"], row.get("app_name"),
86
+ json.dumps(row.get("words") or []), row["source"], json.dumps(row["action"]), row.get("regex"),
87
+ row["created_at"], row["updated_at"]])
88
+ return self.trigger(row["id"])
89
+
90
+ def update(self, trigger_id, **changes):
91
+ allowed = {"name", "enabled", "app", "app_name", "words", "source", "action", "updated_at"}
92
+ if set(changes) - allowed:
93
+ raise ValueError("Unknown trigger field")
94
+ values = []
95
+ for key, value in changes.items():
96
+ values.append(int(bool(value)) if key == "enabled" else json.dumps(value) if key in ("words", "action")
97
+ else value)
98
+ with self.journal.transaction() as connection:
99
+ connection.execute(f"UPDATE triggers SET {', '.join(f'{k}=?' for k in changes)} WHERE id=?",
100
+ [*values, trigger_id])
101
+ return self.trigger(trigger_id)
102
+
103
+ def delete(self, trigger_id):
104
+ with self.journal.transaction() as connection:
105
+ connection.execute("DELETE FROM triggers WHERE id=?", (trigger_id,))
106
+
107
+ def add_firing(self, row):
108
+ with self.journal.transaction() as connection:
109
+ connection.execute(
110
+ f"INSERT INTO trigger_firings({', '.join(FIRING_COLUMNS)}) VALUES "
111
+ f"({', '.join('?' * len(FIRING_COLUMNS))})",
112
+ [row.get(column) for column in FIRING_COLUMNS])
113
+ excess = connection.execute("SELECT count(*) FROM trigger_firings").fetchone()[0] - MAX_FIRINGS
114
+ if excess > 0:
115
+ connection.execute("DELETE FROM trigger_firings WHERE rowid IN (SELECT rowid FROM trigger_firings "
116
+ "WHERE outcome != 'sending' ORDER BY at LIMIT ?)", (excess,))
117
+
118
+ def update_firing(self, firing_id, **changes):
119
+ if set(changes) - {"outcome", "message", "run_id"}:
120
+ raise ValueError("Unknown firing field")
121
+ with self.journal.transaction() as connection:
122
+ connection.execute(f"UPDATE trigger_firings SET {', '.join(f'{k}=?' for k in changes)} WHERE id=?",
123
+ [*changes.values(), firing_id])
124
+
125
+ def recent(self, limit=20):
126
+ rows = self._read(f"SELECT {', '.join(FIRING_COLUMNS)} FROM trigger_firings ORDER BY at DESC, rowid DESC "
127
+ "LIMIT ?", (limit,))
128
+ return [dict(zip(FIRING_COLUMNS, row)) for row in rows]
129
+
130
+ def last_firing(self, trigger_id):
131
+ rows = self._read(f"SELECT {', '.join(FIRING_COLUMNS)} FROM trigger_firings WHERE trigger_id=? "
132
+ "ORDER BY at DESC, rowid DESC LIMIT 1", (trigger_id,))
133
+ return dict(zip(FIRING_COLUMNS, rows[0])) if rows else None
134
+
135
+ def close_unknown(self):
136
+ """A firing left ``sending`` when Mobster quit: its outcome is unknown, and it is never sent again."""
137
+ with self.journal.transaction() as connection:
138
+ cursor = connection.execute(
139
+ "UPDATE trigger_firings SET outcome='unknown', message=? WHERE outcome='sending'",
140
+ ("Mobster quit while sending this, so it may have arrived. "
141
+ "Check where it went before you run it again.",))
142
+ return cursor.rowcount