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,271 @@
1
+ """Bounded HTTP with one deadline per operation and no automatic retry: an interrupted request's outcome is unknown."""
2
+
3
+ import http.client
4
+ import json
5
+ import math
6
+ import select
7
+ import socket
8
+ import threading
9
+ import time
10
+ from urllib.parse import urlsplit
11
+
12
+ from . import __version__
13
+ from .errors import MobsterError
14
+
15
+
16
+ def user_agent():
17
+ """The shared client identity used when calling Mobster services."""
18
+ import platform
19
+ return f"Mobster/{__version__} (macOS {platform.mac_ver()[0] or 'unknown'})"
20
+
21
+
22
+ class TransportError(MobsterError):
23
+ """``status`` and ``wda_error``: a failed answer's HTTP status and WDA's error name, when there was an answer.
24
+ Not its message, which can hold app text."""
25
+
26
+ def __init__(self, *args, status=None, wda_error=None):
27
+ super().__init__(*args)
28
+ self.status, self.wda_error = status, wda_error
29
+
30
+
31
+ # The request never left, so its outcome is known: nothing happened.
32
+ class NotSent(TransportError):
33
+ pass
34
+
35
+
36
+ # WDA checks the session before it runs anything, so nothing ran.
37
+ class StaleSession(NotSent):
38
+ pass
39
+
40
+
41
+ # Every error name WebDriverAgent v16.12.10 answers with, and the one HTTP status it pairs with each
42
+ # (WebDriverAgentLib/Routing/FBCommandStatus.m). Anything else isn't WDA's name and is never kept: it could be app text.
43
+ WDA_ERRORS = {
44
+ "unknown error": 500, "unable to capture screen": 500, "no such element": 404, "invalid element state": 400,
45
+ "invalid argument": 400, "stale element reference": 404, "invalid selector": 400, "no such alert": 404,
46
+ "unexpected alert open": 500, "unknown method": 405, "session not created": 500, "invalid coordinates": 400,
47
+ "unsupported operation": 500, "unknown command": 404, "timeout": 408, "element not visible": 400,
48
+ "invalid session id": 404,
49
+ }
50
+
51
+
52
+ def error_name(value):
53
+ """WDA's error name from an answer's ``value``, or None: only one of WDA_ERRORS' names, exactly."""
54
+ name = value.get("error") if isinstance(value, dict) else None
55
+ return name if isinstance(name, str) and name in WDA_ERRORS else None
56
+
57
+
58
+ def answer_error(raw):
59
+ """Strictly decoded: a body with two ``error`` fields, say, names nothing."""
60
+ try:
61
+ return error_name(decode_json(raw).get("value"))
62
+ except (ValueError, AttributeError, UnicodeDecodeError, RecursionError):
63
+ return None
64
+
65
+
66
+ class Deadline:
67
+ def __init__(self, timeout):
68
+ if type(timeout) not in (int, float) or not math.isfinite(timeout) or timeout <= 0:
69
+ raise ValueError("Timeout must be a finite positive number")
70
+ self.end = time.monotonic() + timeout
71
+
72
+ def remaining(self):
73
+ left = self.end - time.monotonic()
74
+ if left <= 0:
75
+ raise TimeoutError("Operation deadline exceeded")
76
+ return left
77
+
78
+
79
+ def _unique_object(pairs):
80
+ # dict(pairs) then a length check: 4-6% faster than a Python loop on a 66 KB WDA tree (~400 us), 2026-09-24.
81
+ result = dict(pairs)
82
+ if len(result) != len(pairs):
83
+ raise ValueError("Duplicate JSON field")
84
+ return result
85
+
86
+
87
+ def _invalid_constant(value):
88
+ raise ValueError("Nonfinite JSON value")
89
+
90
+
91
+ def _finite_float(value):
92
+ result = float(value)
93
+ if not math.isfinite(result):
94
+ raise ValueError("Nonfinite JSON value")
95
+ return result
96
+
97
+
98
+ def decode_json(raw):
99
+ return json.loads(raw, parse_constant=_invalid_constant, parse_float=_finite_float,
100
+ object_pairs_hook=_unique_object)
101
+
102
+
103
+ class HTTP:
104
+ def __init__(self, base_url, key="", timeout=20):
105
+ url = urlsplit(base_url)
106
+ if (url.scheme not in {"http", "https"} or not url.hostname or url.username or url.password
107
+ or url.query or url.fragment):
108
+ raise ValueError("Expected HTTP(S) base URL without embedded credentials")
109
+ if key and url.scheme != "https" and url.hostname not in {"localhost", "127.0.0.1", "::1"}:
110
+ raise ValueError("Remote credentials require HTTPS")
111
+ cls = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
112
+ self.connection = cls(url.hostname, url.port, timeout=timeout)
113
+ self.prefix, self.key = url.path.rstrip("/"), key
114
+ self._inflight = threading.Lock()
115
+
116
+ def request(self, method, path, body=None, timeout=20):
117
+ deadline = Deadline(timeout)
118
+ if not isinstance(path, str) or not path.startswith("/") or any(c in path for c in "\r\n"):
119
+ raise ValueError("Expected an absolute HTTP request path")
120
+ # Encoding/validation precedes dispatch; invalid input cannot poison a live connection.
121
+ payload = None if body is None else json.dumps(body, allow_nan=False).encode()
122
+ if not self._inflight.acquire(blocking=False):
123
+ raise TransportError("HTTP client already has a request in flight; not retried")
124
+ headers = {"Content-Type": "application/json"}
125
+ if self.key:
126
+ headers["Authorization"] = "Bearer " + self.key
127
+ guard, finished = threading.Lock(), threading.Event()
128
+ active_socket = {"value": None}
129
+ answered_in_full = False
130
+
131
+ def expire():
132
+ # Socket timeouts restart on each chunk: cut a slow drip at the absolute deadline.
133
+ with guard:
134
+ if finished.is_set():
135
+ return
136
+ sock = active_socket["value"] or self.connection.sock
137
+ if sock:
138
+ try:
139
+ sock.shutdown(socket.SHUT_RDWR)
140
+ except OSError:
141
+ pass
142
+ self.connection.close()
143
+
144
+ timer = threading.Timer(max(0, deadline.end - time.monotonic()), expire)
145
+ timer.daemon = True
146
+ timer.start()
147
+ try:
148
+ if self.connection.sock is not None and peer_closed(self.connection.sock):
149
+ # A kept connection the server has closed since, as WDA's failClient does after a header-level 400
150
+ # with no Connection header. Nothing of this request is sent yet, so a new connection resends nothing.
151
+ self.connection.close()
152
+ self.connection.timeout = deadline.remaining()
153
+ if self.connection.sock:
154
+ self.connection.sock.settimeout(deadline.remaining())
155
+ self.connection.request(method, self.prefix + path, payload, headers)
156
+ active_socket["value"] = self.connection.sock
157
+ deadline.remaining()
158
+ response = self.connection.getresponse()
159
+ raw = response.read(16_000_001)
160
+ deadline.remaining()
161
+ if len(raw) > 16_000_000:
162
+ raise TransportError("HTTP response exceeds limit; outcome unknown; not retried")
163
+ if isinstance(getattr(response, "length", None), int) and response.length != 0:
164
+ raise TransportError("Incomplete HTTP response; outcome unknown; not retried")
165
+ if response.status >= 300:
166
+ # Read to its end, so the connection is ready for the next request. WebDriverAgent keeps a descriptor
167
+ # per connection until it restarts: routine 404s would each cost one.
168
+ answered_in_full = response.isclosed()
169
+ name = answer_error(raw)
170
+ if response.status == 404 and name == "invalid session id":
171
+ raise StaleSession("HTTP 404 StaleSession: invalid session id; nothing sent; not retried")
172
+ raise TransportError(f"HTTP {response.status}; request not retried",
173
+ status=response.status, wda_error=name)
174
+ result = decode_json(raw)
175
+ deadline.remaining()
176
+ if not isinstance(result, dict):
177
+ raise TransportError("HTTP response must be an object; outcome unknown; not retried")
178
+ return result
179
+ except TransportError:
180
+ if not answered_in_full:
181
+ self.connection.close()
182
+ raise
183
+ except (OSError, http.client.HTTPException, ValueError, RecursionError) as exc:
184
+ self.connection.close()
185
+ if isinstance(exc, ConnectionRefusedError):
186
+ raise NotSent(f"HTTP {type(exc).__name__}; nothing sent; not retried") from None
187
+ raise TransportError(f"HTTP {type(exc).__name__}; request outcome unknown; not retried") from None
188
+ finally:
189
+ with guard:
190
+ finished.set()
191
+ timer.cancel()
192
+ self._inflight.release()
193
+
194
+ def close(self):
195
+ self.connection.close()
196
+
197
+
198
+ def peer_closed(sock):
199
+ """Whether an idle kept socket has anything to read, which on an idle HTTP connection means the server closed it
200
+ (or sent something unasked). Either way it can't carry the next request. A socket that can't be asked is closed."""
201
+ try:
202
+ if getattr(sock, "pending", lambda: 0)():
203
+ return True
204
+ # poll, not select: select refuses a descriptor numbered 1024 or more, which would read every socket as closed
205
+ # and bring back the per-request connections #2058 removed.
206
+ probe = select.poll()
207
+ probe.register(sock, select.POLLIN)
208
+ return bool(probe.poll(0))
209
+ except (OSError, ValueError):
210
+ return True
211
+
212
+
213
+ def request_once(base_url, method, path, body=None, timeout=20):
214
+ client = HTTP(base_url)
215
+ try:
216
+ return client.request(method, path, body, timeout=timeout)
217
+ finally:
218
+ client.close()
219
+
220
+
221
+ # A new connection per repeated read ran WebDriverAgent out of file descriptors in about 14 minutes (17 Oct).
222
+ KEPT_MAX = 16
223
+ CLOSED_WHILE_KEPT = ("RemoteDisconnected", "BrokenPipeError", "ConnectionResetError", "ConnectionAbortedError",
224
+ "BadStatusLine")
225
+ _kept = {} # base url -> (HTTP, lock), oldest first
226
+ _kept_lock = threading.Lock()
227
+
228
+
229
+ def read_kept(base_url, path, timeout=2.0):
230
+ # Reads only: a kept connection the helper closed while idle is asked once more, which is safe for a GET.
231
+ base = base_url.rstrip("/")
232
+ deadline = Deadline(timeout)
233
+ with _kept_lock:
234
+ entry = _kept.get(base)
235
+ while entry is None and len(_kept) >= KEPT_MAX:
236
+ for address, (old_client, old_lock) in _kept.items():
237
+ if old_lock.acquire(blocking=False):
238
+ del _kept[address]
239
+ old_client.close()
240
+ old_lock.release()
241
+ break
242
+ else:
243
+ break
244
+ if entry is None and len(_kept) < KEPT_MAX:
245
+ entry = (HTTP(base), threading.Lock())
246
+ _kept[base] = entry
247
+ acquired = entry is not None and entry[1].acquire(blocking=False)
248
+ if acquired:
249
+ _kept.pop(base)
250
+ _kept[base] = entry
251
+ if not acquired:
252
+ return request_once(base, "GET", path, timeout=deadline.remaining())
253
+ client, lock = entry
254
+ try:
255
+ reused = client.connection.sock is not None
256
+ try:
257
+ return client.request("GET", path, timeout=deadline.remaining())
258
+ except TransportError as error:
259
+ if not reused or not any(name in str(error) for name in CLOSED_WHILE_KEPT):
260
+ raise
261
+ return client.request("GET", path, timeout=deadline.remaining())
262
+ finally:
263
+ lock.release()
264
+
265
+
266
+ def wda_screenshot(wda_url, timeout=2):
267
+ try:
268
+ value = request_once(wda_url, "GET", "/screenshot", timeout=timeout).get("value")
269
+ return "data:image/png;base64," + value if isinstance(value, str) and value else None
270
+ except Exception:
271
+ return None
@@ -0,0 +1 @@
1
+ """Notification triggers: a notification sends its text to a destination, runs a workflow or starts a task."""
@@ -0,0 +1,204 @@
1
+ """What one firing does: send to a destination, run a workflow or start a task, once and never retried."""
2
+
3
+ import hashlib
4
+ import logging
5
+ import time
6
+ import uuid
7
+
8
+ from .. import harness_api, secret_filter
9
+ from ..api_errors import APIError
10
+ from ..notify_triggers.sources import iso
11
+ from . import model
12
+
13
+ log = logging.getLogger("mobster.triggers")
14
+
15
+ TOPIC = "triggers"
16
+ NOTIFICATION_MAX = 1200
17
+ TEXT_MAX = 3000
18
+ DESTINATION_GONE = ("The destination was removed, so nothing was sent. Choose another destination for this trigger "
19
+ "in Connectors.")
20
+ WORKFLOW_GONE = "The workflow was deleted, so it didn't run. Choose another workflow for this trigger."
21
+ TRIGGER_GONE = "The trigger was removed before it could run."
22
+ BUSY = "Another task was running, so this one didn't start. Mobster doesn't queue a notification's run."
23
+ DUPLICATE = "Mobster already started a run for this notification."
24
+ NO_OUTLETS = "Sending isn't available right now. Restart Mobster, then the next match sends."
25
+ NO_WORKFLOWS = "Workflows aren't available right now. Restart Mobster, then the next match runs."
26
+ WORKFLOW_SKIPS = {"skipped_busy": "The workflow was running and one run already waits, so this one didn't start.",
27
+ "skipped_rate_limit": "The workflow already started 6 times in the last hour, "
28
+ "so this one didn't start."}
29
+ SKIP_CODES = {"run_active", "device_busy"}
30
+ OFFLINE_CODES = {"device_unavailable", "shutting_down", "device_not_found"}
31
+ LABELS = {"destination": "Send to a destination", "workflow": "Run a workflow", "task": "Start a task"}
32
+
33
+
34
+ def request_key(trigger_id, notification):
35
+ """The Idempotency-Key for this trigger and notification: [A-Za-z0-9_.:-], at most 128 characters."""
36
+ digest = hashlib.sha256(f"{trigger_id}\n{notification.key}".encode()).hexdigest()[:32]
37
+ return f"trigger:{trigger_id}:{digest}"
38
+
39
+
40
+ def event_body(trigger, notification, masked):
41
+ """What a destination receives."""
42
+ head = notification.title or notification.app
43
+ said = " ".join((notification.body or notification.subtitle or "").split())
44
+ text = f"{trigger['name']}: {head}" + (f": {said}" if said else "")
45
+ when = iso(notification.date)
46
+ fields = {"trigger": trigger["name"], "app": notification.app, "title": notification.title,
47
+ "body": notification.body or notification.subtitle, "date": when}
48
+ body = {"type": "notification.matched", **fields, "masked": bool(masked),
49
+ "runId": None, "status": "completed", "answer": text[:TEXT_MAX], "data": dict(fields), "reason": None,
50
+ "startedAt": when, "finishedAt": when,
51
+ "workflow": {"id": trigger["id"], "name": trigger["name"], "blanks": {}}, "text": text[:TEXT_MAX]}
52
+ from ..outlets import webhook
53
+ return webhook.masked(body)
54
+
55
+
56
+ def fenced(notification):
57
+ """The notification as data, for the end of a workflow's steps or a task's words. Never longer than a run takes."""
58
+ lines = [f"App: {notification.app}", f"From: {'your iPhone' if notification.source == 'iphone' else 'this Mac'}"]
59
+ for label, value in (("Title", notification.title), ("Subtitle", notification.subtitle),
60
+ ("Body", notification.body)):
61
+ if value:
62
+ lines.append(f"{label}: {value}")
63
+ text = secret_filter.redact("\n".join(lines))[:NOTIFICATION_MAX]
64
+ return harness_api.fence("The notification that started this", text)
65
+
66
+
67
+ def who(trigger):
68
+ """The run field ``trigger``: who started it. The run view says "Started by trigger: <name>"."""
69
+ return {"id": trigger["id"], "name": trigger["name"]}
70
+
71
+
72
+ class Firer:
73
+ def __init__(self, runtime, store, lookup, *, clock=time.time, publish=None):
74
+ self.runtime = runtime
75
+ self.store = store
76
+ self.lookup = lookup # trigger id -> the saved trigger (dict) or None
77
+ self.clock = clock
78
+ self.publish = publish or getattr(runtime, "publish", None)
79
+
80
+ def act(self, rule, notification, masked=False):
81
+ trigger = self.lookup(rule.name)
82
+ if trigger is None:
83
+ return {"ok": False, "outcome": "skipped", "message": TRIGGER_GONE}
84
+ kind = trigger["action"]["kind"]
85
+ firing = {"id": uuid.uuid4().hex[:12], "trigger_id": trigger["id"], "trigger_name": trigger["name"],
86
+ "app": trigger["app"], "at": self.clock(), "kind": kind, "outcome": "sending", "message": None,
87
+ "run_id": None}
88
+ self.store.add_firing(firing) # before anything is sent
89
+ try:
90
+ outcome, message, run_id = self._do(trigger, notification, masked)
91
+ except Exception as error: # noqa: BLE001 -- one firing's failure never stops the watcher
92
+ log.exception("triggers: a firing failed unexpectedly")
93
+ outcome, message, run_id = "failed", f"The action failed unexpectedly ({type(error).__name__}).", None
94
+ self.store.update_firing(firing["id"], outcome=outcome, message=message, run_id=run_id)
95
+ self.announce(firing["id"], trigger, outcome, message, run_id)
96
+ return {"ok": outcome == "done", "outcome": outcome, "message": message, "runId": run_id}
97
+
98
+ def announce(self, firing_id, trigger, outcome, message, run_id=None, reason=None):
99
+ event = {"event": "trigger_fired" if outcome in ("done", "failed") else "trigger_skipped",
100
+ "firingId": firing_id, "triggerId": trigger["id"], "trigger": trigger["name"], "app": trigger["app"],
101
+ "action": trigger["action"]["kind"], "outcome": outcome, "message": message, "runId": run_id}
102
+ if reason:
103
+ event["reason"] = reason
104
+ if callable(self.publish):
105
+ self.publish(TOPIC, event)
106
+
107
+ def skip(self, rule_name, name, app, reason, message):
108
+ """A refusal before any action (rate limit, loop guard, duplicate, access lost): journaled and announced."""
109
+ trigger = self.lookup(rule_name) or {"id": rule_name, "name": name or "A removed trigger", "app": app or "",
110
+ "action": {"kind": "destination"}}
111
+ firing = {"id": uuid.uuid4().hex[:12], "trigger_id": trigger["id"], "trigger_name": trigger["name"],
112
+ "app": trigger["app"], "at": self.clock(), "kind": trigger["action"]["kind"], "outcome": "skipped",
113
+ "message": message, "run_id": None}
114
+ try:
115
+ self.store.add_firing(firing)
116
+ except Exception: # noqa: BLE001 -- the event still goes out
117
+ log.warning("triggers: couldn't journal a skipped firing")
118
+ self.announce(firing["id"], trigger, "skipped", message, reason=reason)
119
+
120
+ def _do(self, trigger, notification, masked):
121
+ action = trigger["action"]
122
+ if action["kind"] == "destination":
123
+ return self._destination(trigger, notification, masked)
124
+ if action["kind"] == "workflow":
125
+ return self._workflow(trigger, notification)
126
+ return self._task(trigger, notification)
127
+
128
+ def _destination(self, trigger, notification, masked):
129
+ from ..outlets import service as outlet_service
130
+ service = outlet_service.service_for(self.runtime)
131
+ if service is None:
132
+ return "skipped", NO_OUTLETS, None
133
+ try:
134
+ row = service.store.destination(trigger["action"]["outletId"])
135
+ if row is None:
136
+ return "skipped", DESTINATION_GONE, None
137
+ outcome = service.send_event(row["id"], event_body(trigger, notification, masked))
138
+ except APIError as error:
139
+ if error.code == "not_found":
140
+ return "skipped", DESTINATION_GONE, None
141
+ return "failed", str(error), None
142
+ if outcome.ok:
143
+ return "done", f"Sent to {row['name']}.", None
144
+ return "failed", (outcome.message or "It didn't arrive.") + " Mobster doesn't send it again.", None
145
+
146
+ def _workflow(self, trigger, notification):
147
+ workflows = getattr(self.runtime, "workflows", None)
148
+ if workflows is None:
149
+ return "skipped", NO_WORKFLOWS, None
150
+ workflow_id = trigger["action"]["workflowId"]
151
+ try:
152
+ workflows.get(workflow_id)
153
+ except APIError as error:
154
+ if error.status == 404:
155
+ return "skipped", WORKFLOW_GONE, None
156
+ return "failed", str(error), None
157
+ data = {"app": notification.app, "title": notification.title,
158
+ "body": notification.body or notification.subtitle,
159
+ "receivedAt": round((notification.date or self.clock()) * 1000)}
160
+ try:
161
+ # The engine masks, cuts and fences the fields; each task it starts says "Started by trigger: <name>".
162
+ outcome = workflows.triggers.fire(workflow_id, data, started_by=who(trigger))
163
+ except APIError as error:
164
+ if error.code == "not_published":
165
+ return "skipped", str(error), None
166
+ return self._refused(error)
167
+ except ValueError as error:
168
+ return "skipped", str(error), None
169
+ if outcome is None:
170
+ return "skipped", DUPLICATE, None
171
+ if outcome == "started":
172
+ return "done", "Started the workflow.", None
173
+ if outcome == "queued":
174
+ return "done", "The workflow was running, so it starts when that run ends.", None
175
+ if outcome in WORKFLOW_SKIPS:
176
+ return "skipped", WORKFLOW_SKIPS[outcome], None
177
+ return self._refused(APIError("The workflow didn't start.", 409, outcome))
178
+
179
+ def _task(self, trigger, notification):
180
+ action = trigger["action"]
181
+ if not action.get("allowed"):
182
+ return "skipped", model.TASK_OFF, None
183
+ goal = action["text"] + "\n\n" + fenced(notification)
184
+ kwargs = {"extras": {"trigger": who(trigger)}, "origin": "trigger"}
185
+ if getattr(self.runtime, "fleet", None) is not None:
186
+ kwargs["default_device"] = "primary"
187
+ try:
188
+ created = self.runtime.create(None, goal, "live", request_key(trigger["id"], notification), **kwargs)
189
+ except APIError as error:
190
+ return self._refused(error)
191
+ except ValueError as error:
192
+ return "skipped", str(error), None
193
+ run, replayed = created
194
+ if replayed:
195
+ return "skipped", DUPLICATE, getattr(run, "id", None)
196
+ return "done", "Started a task. It asks before it acts.", getattr(run, "id", None)
197
+
198
+ @staticmethod
199
+ def _refused(error):
200
+ if error.code in SKIP_CODES:
201
+ return "skipped", BUSY, None
202
+ if error.code in OFFLINE_CODES:
203
+ return "skipped", f"{error} Nothing was started.", None
204
+ return "failed", str(error), None
@@ -0,0 +1,146 @@
1
+ """A trigger's fields, checked. Every refusal is a sentence that says what to change."""
2
+
3
+ import re
4
+
5
+ from ..notify_triggers.rules import own_app
6
+
7
+ NAME_MAX = 60
8
+ WORDS_MAX = 10
9
+ WORD_MAX = 40
10
+ TASK_MAX = 1000
11
+ MAX_TRIGGERS = 50
12
+ SOURCES = ("iphone", "mac", "any")
13
+ ACTIONS = ("destination", "workflow", "task")
14
+ BUNDLE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,158}[A-Za-z0-9_]")
15
+ ID = re.compile(r"[0-9a-f]{12}")
16
+ FIELDS = {"name", "enabled", "app", "appName", "words", "source", "action"}
17
+ ACTION_FIELDS = {"destination": {"kind", "outletId"}, "workflow": {"kind", "workflowId"},
18
+ "task": {"kind", "text", "allowed"}}
19
+ TASK_OFF = ("Starting a task from a notification is off for this trigger. Turn it on in the trigger's settings, "
20
+ "then it runs and asks before acting.")
21
+
22
+
23
+ def name_of(value):
24
+ text = " ".join(value.split()) if isinstance(value, str) else ""
25
+ if not 1 <= len(text) <= NAME_MAX:
26
+ raise ValueError(f"Give the trigger a name of 1 to {NAME_MAX} characters.")
27
+ return text
28
+
29
+
30
+ def app_of(value):
31
+ app = value.strip() if isinstance(value, str) else ""
32
+ if not BUNDLE.fullmatch(app):
33
+ raise ValueError("Choose the iPhone app this trigger watches.")
34
+ if own_app(app) or app.lower().startswith("app.mobster"):
35
+ raise ValueError("A trigger can't watch Mobster's own notifications, because the notification it sends "
36
+ "would start it again.")
37
+ return app
38
+
39
+
40
+ def words_of(value):
41
+ """The words, as a list of 0 to 10 short phrases. Text with commas or new lines splits into words."""
42
+ if value is None:
43
+ return []
44
+ if isinstance(value, str):
45
+ value = re.split(r"[,\n]", value)
46
+ if not isinstance(value, list) or not all(isinstance(word, str) for word in value):
47
+ raise ValueError("Words is a list of words or phrases to look for in the notification.")
48
+ words = []
49
+ for word in value:
50
+ word = " ".join(word.split())
51
+ if not word:
52
+ continue
53
+ if len(word) > WORD_MAX:
54
+ raise ValueError(f"A word or phrase is at most {WORD_MAX} characters.")
55
+ if word.casefold() not in [w.casefold() for w in words]:
56
+ words.append(word)
57
+ if len(words) > WORDS_MAX:
58
+ raise ValueError(f"Use at most {WORDS_MAX} words or phrases.")
59
+ return words
60
+
61
+
62
+ def source_of(value):
63
+ if value is None:
64
+ return "iphone"
65
+ if value not in SOURCES:
66
+ raise ValueError("Source is iphone, mac or any.")
67
+ return value
68
+
69
+
70
+ def enabled_of(value):
71
+ if value is None:
72
+ return False
73
+ if type(value) is not bool:
74
+ raise ValueError("Enabled is true or false.")
75
+ return value
76
+
77
+
78
+ def action_of(value):
79
+ """The action, cleaned. It names its destination or workflow by id;
80
+ whether that exists is the service's to check."""
81
+ if not isinstance(value, dict) or value.get("kind") not in ACTIONS:
82
+ raise ValueError("Choose what the trigger does: send to a destination, run a workflow or start a task.")
83
+ kind = value["kind"]
84
+ extra = set(value) - ACTION_FIELDS[kind]
85
+ if extra:
86
+ raise ValueError(f"A {kind} action doesn't take {', '.join(sorted(extra))}.")
87
+ if kind == "destination":
88
+ if not isinstance(value.get("outletId"), str) or not ID.fullmatch(value["outletId"]):
89
+ raise ValueError("Choose a destination you saved in Connectors.")
90
+ return {"kind": kind, "outletId": value["outletId"]}
91
+ if kind == "workflow":
92
+ if not isinstance(value.get("workflowId"), str) or not ID.fullmatch(value["workflowId"]):
93
+ raise ValueError("Choose a saved workflow.")
94
+ return {"kind": kind, "workflowId": value["workflowId"]}
95
+ text = value.get("text")
96
+ if not isinstance(text, str) or not 1 <= len(text.strip()) <= TASK_MAX:
97
+ raise ValueError(f"Write the task in 1 to {TASK_MAX} characters.")
98
+ allowed = value.get("allowed", False)
99
+ if type(allowed) is not bool:
100
+ raise ValueError("Allowed is true or false.")
101
+ return {"kind": kind, "text": text.strip(), "allowed": allowed}
102
+
103
+
104
+ def clean(body, current=None):
105
+ """The fields to save from a create or update body."""
106
+ if not isinstance(body, dict):
107
+ raise ValueError("A trigger has a name, an app and an action.")
108
+ extra = set(body) - FIELDS
109
+ if extra:
110
+ raise ValueError(f"A trigger doesn't take {', '.join(sorted(extra))}.")
111
+ out = dict(current or {})
112
+ if current is None or "name" in body:
113
+ out["name"] = name_of(body.get("name"))
114
+ if current is None or "app" in body:
115
+ out["app"] = app_of(body.get("app"))
116
+ if "appName" in body or current is None:
117
+ name = body.get("appName")
118
+ if name is not None and (not isinstance(name, str) or len(name.strip()) > 80):
119
+ raise ValueError("The app's name is at most 80 characters.")
120
+ out["appName"] = " ".join(name.split()) if isinstance(name, str) and name.strip() else None
121
+ if current is None or "words" in body:
122
+ out["words"] = words_of(body.get("words"))
123
+ if current is None or "source" in body:
124
+ out["source"] = source_of(body.get("source"))
125
+ if current is None or "enabled" in body:
126
+ out["enabled"] = enabled_of(body.get("enabled"))
127
+ if current is None or "action" in body:
128
+ action = action_of(body.get("action"))
129
+ # "Start a task" is off until the person turns it on for this trigger. A change that leaves "allowed" out keeps
130
+ # what was saved, unless the task's words changed: new words are off until they are turned on again.
131
+ before = (current or {}).get("action") or {}
132
+ if action["kind"] == "task" and "allowed" not in body["action"]:
133
+ same = before.get("kind") == "task" and before.get("text") == action["text"]
134
+ action["allowed"] = bool(before.get("allowed")) if same else False
135
+ out["action"] = action
136
+ return out
137
+
138
+
139
+ def word_pattern(words, regex=None):
140
+ """The compiled search for a trigger"""
141
+ if regex:
142
+ return re.compile(regex, re.IGNORECASE | re.MULTILINE)
143
+ if not words:
144
+ return None
145
+ alternatives = "|".join(re.escape(" ".join(word.split())).replace(r"\ ", r"\s+") for word in words)
146
+ return re.compile(r"(?<!\w)(?:" + alternatives + r")(?!\w)", re.IGNORECASE)
@@ -0,0 +1,29 @@
1
+ """Track ``triggers``' registrations (tracks.load calls ``register(api)`` once per process, staged)."""
2
+
3
+ import re
4
+
5
+ from . import routes, service
6
+
7
+ ID = re.compile(r"[0-9a-f]{12}")
8
+
9
+
10
+ def validate_trigger(value, runtime):
11
+ """The run field: {"id": 12 hex characters, "name": 1 to 60 characters}."""
12
+ if not isinstance(value, dict) or set(value) != {"id", "name"}:
13
+ raise ValueError('trigger is {"id", "name"}')
14
+ if not isinstance(value["id"], str) or not ID.fullmatch(value["id"]):
15
+ raise ValueError("A trigger's id is 12 hex characters")
16
+ if not isinstance(value["name"], str) or not 1 <= len(value["name"].strip()) <= 60:
17
+ raise ValueError("A trigger's name is 1 to 60 characters")
18
+ return {"id": value["id"], "name": " ".join(value["name"].split())}
19
+
20
+
21
+ def register(api):
22
+ """``api`` is mobile_agent.harness_api."""
23
+ from .. import full_disk_access, journal
24
+ from . import store
25
+ journal.register_schema(store.SCHEMA, store.VERSION, store.migrate)
26
+ api.register_run_field("trigger", validate_trigger, public=True)
27
+ api.register_service(service.start, service.close)
28
+ routes.register_routes()
29
+ full_disk_access.register_route()