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,582 @@
1
+ """A stdlib-only MCP client: stdio (a command Mobster starts) and streamable HTTP (a URL), as mcp_server/protocol.py is
2
+ the server side."""
3
+
4
+ import http.client
5
+ import json
6
+ import os
7
+ import queue
8
+ import socket
9
+ import ssl
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ from urllib.parse import urlsplit
14
+
15
+ from .. import __version__
16
+
17
+ PROTOCOL_VERSION = "2025-06-18"
18
+ LIST_TIMEOUT = 20.0
19
+ CALL_TIMEOUT = 60.0
20
+ PING_TIMEOUT = 5.0
21
+ CONNECT_TIMEOUT = 30.0 # starting a command (npx may download its package the first time) and initialize
22
+ MAX_TOOLS = 500 # tools/list stops after this many
23
+ MAX_PAGES = 10
24
+ MAX_MESSAGE_BYTES = 8 * 1024 * 1024
25
+ STDERR_KEEP = 2000 # the last characters a stdio server wrote to stderr, for "it didn't start" messages
26
+
27
+
28
+ class ConnectorError(Exception):
29
+ """A failed MCP request. ``message`` is safe to show and journal: never a token, an env value or a URL's query."""
30
+
31
+ sent = None # False: nothing happened; True: the server answered; None: unknown
32
+
33
+ def __init__(self, message):
34
+ super().__init__(message)
35
+ self.message = message
36
+
37
+
38
+ class NotSent(ConnectorError):
39
+ """The request never reached the server (the command didn't start, the connection was refused): nothing happened."""
40
+
41
+ sent = False
42
+
43
+
44
+ class AuthNeeded(NotSent):
45
+ """The server answered 401 or 403 before it did anything: nothing happened; sign in again."""
46
+
47
+
48
+ class SlowToAnswer(NotSent):
49
+ """A server that was started and has not answered the handshake yet: a first `npx` can take a minute to download."""
50
+
51
+
52
+ class ServerError(ConnectorError):
53
+ sent = True
54
+
55
+ def __init__(self, message, code=None):
56
+ super().__init__(message)
57
+ self.code = code
58
+
59
+
60
+ class OutcomeUnknown(ConnectorError):
61
+ """The request went out and no answer came in time: it may have happened, and the caller must not send it again."""
62
+
63
+ sent = None
64
+
65
+
66
+ def _jsonrpc(method, params, ident=None):
67
+ message = {"jsonrpc": "2.0", "method": method}
68
+ if params is not None:
69
+ message["params"] = params
70
+ if ident is not None:
71
+ message["id"] = ident
72
+ return message
73
+
74
+
75
+ def _error_of(message, name):
76
+ error = message.get("error")
77
+ if not isinstance(error, dict):
78
+ return None
79
+ text = " ".join(str(error.get("message") or "an error").split())[:300]
80
+ return ServerError(f"{name} answered with an error: {text}", error.get("code"))
81
+
82
+
83
+ class Session:
84
+ """The MCP conversation over one transport."""
85
+
86
+ name = "The server"
87
+
88
+ def __init__(self):
89
+ self._ids = 0
90
+ self._lock = threading.Lock()
91
+ self.server_info = {}
92
+ self.instructions = ""
93
+ self.ready = False
94
+ # The server said its tools changed (notifications/tools/list_changed): the service lists them again before
95
+ # the next call.
96
+ self.tools_changed = False
97
+
98
+ def _next_id(self):
99
+ with self._lock:
100
+ self._ids += 1
101
+ return self._ids
102
+
103
+ def initialize(self, timeout=CONNECT_TIMEOUT):
104
+ deadline = time.monotonic() + timeout
105
+ answer = self._ask(_jsonrpc("initialize", {
106
+ "protocolVersion": PROTOCOL_VERSION, "capabilities": {},
107
+ "clientInfo": {"name": "Mobster", "version": __version__}}, self._next_id()), deadline, phase="connect")
108
+ result = answer.get("result")
109
+ if not isinstance(result, dict):
110
+ raise NotSent(f"{self.name} didn't answer like an MCP server.")
111
+ self.server_info = result.get("serverInfo") if isinstance(result.get("serverInfo"), dict) else {}
112
+ self.instructions = str(result.get("instructions") or "")[:2000]
113
+ self.protocol = str(result.get("protocolVersion") or PROTOCOL_VERSION)
114
+ self._send(_jsonrpc("notifications/initialized", None), deadline)
115
+ self.ready = True
116
+ return result
117
+
118
+ def list_tools(self, timeout=LIST_TIMEOUT):
119
+ """Every tool the server lists, as {"name", "description", "inputSchema", "annotations"} dicts."""
120
+ deadline = time.monotonic() + timeout
121
+ tools, cursor = [], None
122
+ for _ in range(MAX_PAGES):
123
+ answer = self._ask(_jsonrpc("tools/list", {"cursor": cursor} if cursor else {}, self._next_id()),
124
+ deadline, phase="list")
125
+ result = answer.get("result") or {}
126
+ for tool in result.get("tools") or ():
127
+ if isinstance(tool, dict) and isinstance(tool.get("name"), str) and tool["name"]:
128
+ tools.append(tool)
129
+ cursor = result.get("nextCursor")
130
+ if not cursor or len(tools) >= MAX_TOOLS:
131
+ break
132
+ return tools[:MAX_TOOLS]
133
+
134
+ def ping(self, timeout=PING_TIMEOUT):
135
+ """MCP ``ping``: the cheapest proof the session still answers."""
136
+ if not self.ready:
137
+ raise NotSent(f"{self.name} isn't connected.")
138
+ deadline = time.monotonic() + timeout
139
+ try:
140
+ self._ask(_jsonrpc("ping", None, self._next_id()), deadline, phase="list")
141
+ except ServerError:
142
+ pass # a server without ping still answered
143
+ return True
144
+
145
+ def _notified(self, message):
146
+ if message.get("method") == "notifications/tools/list_changed":
147
+ self.tools_changed = True
148
+
149
+ def call_tool(self, name, arguments, timeout=CALL_TIMEOUT):
150
+ """The tool's result ({"content", "structuredContent"?, "isError"?})."""
151
+ if not self.ready:
152
+ raise NotSent(f"{self.name} isn't connected.")
153
+ deadline = time.monotonic() + timeout
154
+ ident = self._next_id()
155
+ try:
156
+ answer = self._ask(_jsonrpc("tools/call", {"name": name, "arguments": arguments}, ident), deadline,
157
+ phase="call")
158
+ except OutcomeUnknown:
159
+ self._cancel(ident)
160
+ raise
161
+ result = answer.get("result")
162
+ if not isinstance(result, dict):
163
+ raise ServerError(f"{self.name} answered without a result.")
164
+ return result
165
+
166
+ def _cancel(self, ident):
167
+ try:
168
+ self._send(_jsonrpc("notifications/cancelled", {"requestId": ident, "reason": "timed out"}),
169
+ time.monotonic() + 2)
170
+ except ConnectorError:
171
+ pass
172
+
173
+ def _send(self, message, deadline):
174
+ raise NotImplementedError
175
+
176
+ def _ask(self, message, deadline, phase):
177
+ raise NotImplementedError
178
+
179
+ def close(self):
180
+ self.ready = False
181
+
182
+
183
+ class StdioSession(Session):
184
+ """A command Mobster starts and talks to over its stdin and stdout, newline-delimited JSON-RPC."""
185
+
186
+ def __init__(self, argv, env=None, cwd=None, name="The server"):
187
+ super().__init__()
188
+ self.argv, self.env, self.cwd, self.name = list(argv), env, cwd, name
189
+ self.process = None
190
+ self._answers = {}
191
+ self._waiters = {}
192
+ self._stderr = ""
193
+ self._closed = threading.Event()
194
+ self._write_lock = threading.Lock()
195
+
196
+ def start(self):
197
+ try:
198
+ self.process = subprocess.Popen(
199
+ self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env,
200
+ cwd=self.cwd, start_new_session=True)
201
+ except FileNotFoundError:
202
+ raise NotSent(f"Mobster couldn't find {os.path.basename(self.argv[0])}. Install it, or check the "
203
+ "command.") from None
204
+ except (OSError, ValueError) as error:
205
+ raise NotSent(f"Mobster couldn't start {os.path.basename(self.argv[0])} "
206
+ f"({type(error).__name__}).") from None
207
+ threading.Thread(target=self._read, name="mobster-connector-out", daemon=True).start()
208
+ threading.Thread(target=self._drain, name="mobster-connector-err", daemon=True).start()
209
+ return self
210
+
211
+ def _read(self):
212
+ stream = self.process.stdout
213
+ try:
214
+ for line in stream:
215
+ if len(line) > MAX_MESSAGE_BYTES:
216
+ continue
217
+ try:
218
+ message = json.loads(line)
219
+ except ValueError:
220
+ continue # a server's stray output line is skipped, as protocol.py skips a client's
221
+ if not isinstance(message, dict):
222
+ continue
223
+ ident = message.get("id")
224
+ if "method" in message:
225
+ if ident is None:
226
+ self._notified(message)
227
+ else:
228
+ # A request from the server (sampling, roots, elicitation): Mobster offers none of them.
229
+ self._write(_jsonrpc_error(ident, -32601, "Mobster doesn't offer this"))
230
+ continue
231
+ with self._lock:
232
+ waiter = self._waiters.pop(ident, None)
233
+ if waiter is not None:
234
+ waiter.put(message)
235
+ except (OSError, ValueError):
236
+ pass
237
+ finally:
238
+ self._closed.set()
239
+ with self._lock:
240
+ waiters, self._waiters = list(self._waiters.values()), {}
241
+ for waiter in waiters:
242
+ waiter.put(None)
243
+
244
+ def _drain(self):
245
+ try:
246
+ for line in self.process.stderr:
247
+ self._stderr = (self._stderr + line.decode(errors="replace"))[-STDERR_KEEP:]
248
+ except (OSError, ValueError):
249
+ pass
250
+
251
+ def stderr_tail(self):
252
+ from ..secret_filter import redact
253
+ return redact(" ".join(self._stderr.split())[-300:])
254
+
255
+ def _write(self, message):
256
+ data = (json.dumps(message, separators=(",", ":")) + "\n").encode()
257
+ with self._write_lock:
258
+ self.process.stdin.write(data)
259
+ self.process.stdin.flush()
260
+
261
+ def _send(self, message, deadline):
262
+ if self.process is None or self._closed.is_set():
263
+ raise NotSent(f"{self.name} isn't running.")
264
+ try:
265
+ self._write(message)
266
+ except (OSError, ValueError):
267
+ raise NotSent(f"{self.name} stopped.") from None
268
+
269
+ def _ask(self, message, deadline, phase):
270
+ if self.process is None or self._closed.is_set():
271
+ raise NotSent(self._stopped_message())
272
+ waiter = queue.Queue(maxsize=1)
273
+ with self._lock:
274
+ self._waiters[message["id"]] = waiter
275
+ try:
276
+ self._write(message)
277
+ except (OSError, ValueError):
278
+ with self._lock:
279
+ self._waiters.pop(message["id"], None)
280
+ raise NotSent(self._stopped_message()) from None
281
+ try:
282
+ answer = waiter.get(timeout=max(0.0, deadline - time.monotonic()))
283
+ except queue.Empty:
284
+ with self._lock:
285
+ self._waiters.pop(message["id"], None)
286
+ if phase == "call":
287
+ raise OutcomeUnknown(f"{self.name} didn't answer in time.") from None
288
+ raise SlowToAnswer(f"{self.name} didn't answer in time.") from None
289
+ if answer is None:
290
+ if phase == "call":
291
+ raise OutcomeUnknown(f"{self.name} stopped before it answered.")
292
+ raise NotSent(self._stopped_message())
293
+ error = _error_of(answer, self.name)
294
+ if error is not None:
295
+ if phase == "connect":
296
+ raise NotSent(error.message)
297
+ raise error
298
+ return answer
299
+
300
+ def _stopped_message(self):
301
+ tail = self.stderr_tail()
302
+ return f"{self.name} stopped." + (f" It said: {tail}" if tail else "")
303
+
304
+ def close(self):
305
+ super().close()
306
+ process, self.process = self.process, None
307
+ if process is None:
308
+ return
309
+ try:
310
+ process.stdin.close()
311
+ except OSError:
312
+ pass
313
+ try:
314
+ process.wait(timeout=1.0)
315
+ except subprocess.TimeoutExpired:
316
+ try:
317
+ os.killpg(process.pid, 15)
318
+ process.wait(timeout=1.0)
319
+ except (OSError, subprocess.TimeoutExpired):
320
+ try:
321
+ os.killpg(process.pid, 9)
322
+ process.wait(timeout=1.0)
323
+ except (OSError, subprocess.TimeoutExpired):
324
+ pass
325
+ for stream in (process.stdout, process.stderr):
326
+ try:
327
+ stream.close()
328
+ except OSError:
329
+ pass
330
+
331
+
332
+ def _jsonrpc_error(ident, code, message):
333
+ return {"jsonrpc": "2.0", "id": ident, "error": {"code": code, "message": message}}
334
+
335
+
336
+ def validate_url(url):
337
+ """``url`` when it is an http(s) URL without credentials; plain http only on this Mac."""
338
+ if not isinstance(url, str):
339
+ raise ValueError("A server's URL is text")
340
+ url = url.strip()
341
+ parts = urlsplit(url)
342
+ if parts.scheme not in ("https", "http") or not parts.hostname:
343
+ raise ValueError("A server's URL starts with https://")
344
+ if parts.username or parts.password:
345
+ raise ValueError("Put the sign-in in the token field, not in the URL")
346
+ if parts.scheme == "http" and parts.hostname not in ("localhost", "127.0.0.1", "::1"):
347
+ raise ValueError("A server on the internet needs an https:// URL")
348
+ if len(url) > 2000 or any(c in url for c in "\r\n\t "):
349
+ raise ValueError("That URL isn't valid")
350
+ return url
351
+
352
+
353
+ def unreachable(name, url, error):
354
+ """Why a connection to ``url`` didn't open, in words that say what to fix (P05-3: only "couldn't reach")."""
355
+ parts = urlsplit(url)
356
+ where = f"{parts.hostname}:{parts.port}" if parts.port else parts.hostname
357
+ if isinstance(error, socket.gaierror):
358
+ return f"Mobster couldn't find {parts.hostname}. Check the address."
359
+ if isinstance(error, ConnectionRefusedError):
360
+ return f"Nothing answered at {where}. Check the address, and that the server is running."
361
+ if isinstance(error, ssl.SSLCertVerificationError):
362
+ return f"{parts.hostname}'s certificate isn't one this Mac trusts, so Mobster didn't connect."
363
+ if isinstance(error, TimeoutError):
364
+ return f"{where} didn't answer. Check the address, and that the server is running."
365
+ return f"Mobster couldn't reach {name}."
366
+
367
+
368
+ def hint(url):
369
+ """The scheme, host and path of ``url`` (no query): what the API and the UI show."""
370
+ parts = urlsplit(url)
371
+ host = parts.hostname or ""
372
+ port = f":{parts.port}" if parts.port else ""
373
+ return f"{parts.scheme}://{host}{port}{parts.path or ''}"[:200]
374
+
375
+
376
+ class HttpSession(Session):
377
+ """Streamable HTTP: each message is a POST to the URL; an answer is JSON or an SSE stream."""
378
+
379
+ def __init__(self, url, token=None, name="The server", headers=None):
380
+ super().__init__()
381
+ self.url, self.token, self.name = validate_url(url), token, name
382
+ self.extra_headers = dict(headers or {})
383
+ self.session_id = None
384
+ self.protocol = None
385
+ self.resource_metadata = None # from a 401's WWW-Authenticate: where its OAuth metadata is
386
+
387
+ def start(self):
388
+ return self
389
+
390
+ def _headers(self):
391
+ headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
392
+ "User-Agent": f"Mobster/{__version__}", **self.extra_headers}
393
+ if self.token:
394
+ headers["Authorization"] = "Bearer " + self.token
395
+ if self.session_id:
396
+ headers["Mcp-Session-Id"] = self.session_id
397
+ if self.protocol:
398
+ headers["MCP-Protocol-Version"] = self.protocol
399
+ return headers
400
+
401
+ def _connection(self, deadline):
402
+ parts = urlsplit(self.url)
403
+ cls = http.client.HTTPSConnection if parts.scheme == "https" else http.client.HTTPConnection
404
+ left = deadline - time.monotonic()
405
+ if left <= 0:
406
+ raise NotSent(f"{self.name} didn't answer in time.")
407
+ return cls(parts.hostname, parts.port, timeout=left), (parts.path or "/") + (
408
+ f"?{parts.query}" if parts.query else "")
409
+
410
+ def _post(self, message, deadline, phase, expect_answer):
411
+ connection, path = self._connection(deadline)
412
+ body = json.dumps(message, separators=(",", ":")).encode()
413
+ finished = threading.Event()
414
+
415
+ def expire():
416
+ if finished.is_set():
417
+ return
418
+ sock = connection.sock
419
+ if sock is not None:
420
+ try:
421
+ sock.shutdown(socket.SHUT_RDWR)
422
+ except OSError:
423
+ pass
424
+ connection.close()
425
+
426
+ timer = threading.Timer(max(0.0, deadline - time.monotonic()), expire)
427
+ timer.daemon = True
428
+ timer.start()
429
+ sent = False
430
+ try:
431
+ try:
432
+ connection.connect()
433
+ except (OSError, http.client.HTTPException) as error:
434
+ raise NotSent(unreachable(self.name, self.url, error)) from None
435
+ try:
436
+ connection.request("POST", path, body, self._headers())
437
+ sent = True
438
+ response = connection.getresponse()
439
+ except (OSError, http.client.HTTPException):
440
+ if sent and phase == "call":
441
+ raise OutcomeUnknown(f"{self.name} didn't answer in time.") from None
442
+ raise NotSent(f"{self.name} didn't answer.") from None
443
+ status = response.status
444
+ if status in (401, 403):
445
+ challenge = response.getheader("WWW-Authenticate") or ""
446
+ self.resource_metadata = _resource_metadata(challenge)
447
+ response.read(64_000)
448
+ raise AuthNeeded(f"{self.name} needs you to sign in again." if self.token else
449
+ f"{self.name} needs you to sign in.")
450
+ if status == 404 and self.session_id and phase != "connect":
451
+ response.read(64_000)
452
+ self.ready = False
453
+ raise NotSent(f"{self.name} ended the session. Try again.")
454
+ session = response.getheader("Mcp-Session-Id")
455
+ if session and phase == "connect":
456
+ self.session_id = session.strip()[:200]
457
+ if not expect_answer:
458
+ response.read(64_000)
459
+ if status >= 400:
460
+ raise NotSent(f"{self.name} answered HTTP {status}.")
461
+ return None
462
+ if status >= 400:
463
+ response.read(64_000)
464
+ # 4xx: the server refused it before doing anything; 5xx after it was sent may be either.
465
+ if status >= 500 and phase == "call":
466
+ raise OutcomeUnknown(f"{self.name} answered HTTP {status}.")
467
+ raise NotSent(f"{self.name} answered HTTP {status}.")
468
+ kind = (response.getheader("Content-Type") or "").split(";")[0].strip().lower()
469
+ try:
470
+ if kind == "text/event-stream":
471
+ answer = _from_events(response, message.get("id"), self._notified)
472
+ else:
473
+ raw = response.read(MAX_MESSAGE_BYTES + 1)
474
+ if len(raw) > MAX_MESSAGE_BYTES:
475
+ raise ValueError("too large")
476
+ answer = json.loads(raw)
477
+ except (OSError, http.client.HTTPException, ValueError):
478
+ if phase == "call":
479
+ raise OutcomeUnknown(f"{self.name}'s answer was cut off.") from None
480
+ raise NotSent(f"{self.name}'s answer was cut off.") from None
481
+ if isinstance(answer, list):
482
+ answer = next((m for m in answer if isinstance(m, dict) and m.get("id") == message.get("id")), None)
483
+ if not isinstance(answer, dict):
484
+ if phase == "call":
485
+ raise OutcomeUnknown(f"{self.name} didn't answer like an MCP server.")
486
+ raise NotSent(f"{self.name} didn't answer like an MCP server.")
487
+ return answer
488
+ finally:
489
+ finished.set()
490
+ timer.cancel()
491
+ connection.close()
492
+
493
+ def _send(self, message, deadline):
494
+ self._post(message, deadline, "notify", expect_answer=False)
495
+
496
+ def _ask(self, message, deadline, phase):
497
+ answer = self._post(message, deadline, phase, expect_answer=True)
498
+ error = _error_of(answer, self.name)
499
+ if error is not None:
500
+ if phase == "connect":
501
+ raise NotSent(error.message)
502
+ raise error
503
+ return answer
504
+
505
+ def close(self):
506
+ if self.session_id and self.ready:
507
+ try:
508
+ connection, path = self._connection(time.monotonic() + 3)
509
+ connection.request("DELETE", path, headers=self._headers())
510
+ connection.getresponse().read(1000)
511
+ connection.close()
512
+ except (OSError, http.client.HTTPException, ConnectorError):
513
+ pass
514
+ super().close()
515
+
516
+
517
+ def _resource_metadata(challenge):
518
+ """The ``resource_metadata`` URL of a WWW-Authenticate: Bearer challenge, or None."""
519
+ for part in challenge.split(","):
520
+ key, _, value = part.strip().partition("=")
521
+ if key.strip().lower().endswith("resource_metadata"):
522
+ value = value.strip().strip('"')
523
+ if value.startswith("https://") or value.startswith("http://127.0.0.1") or \
524
+ value.startswith("http://localhost"):
525
+ return value[:2000]
526
+ return None
527
+
528
+
529
+ def _from_events(response, ident, notified=None):
530
+ """The JSON-RPC answer with ``ident`` from an SSE stream."""
531
+ data, size = [], 0
532
+ while True:
533
+ line = response.readline(MAX_MESSAGE_BYTES)
534
+ if not line:
535
+ raise ValueError("stream ended")
536
+ size += len(line)
537
+ if size > MAX_MESSAGE_BYTES:
538
+ raise ValueError("too large")
539
+ text = line.decode(errors="replace").rstrip("\r\n")
540
+ if text.startswith("data:"):
541
+ data.append(text[5:].lstrip(" "))
542
+ continue
543
+ if text == "" and data:
544
+ payload, data = "\n".join(data), []
545
+ try:
546
+ message = json.loads(payload)
547
+ except ValueError:
548
+ continue
549
+ if isinstance(message, dict) and message.get("id") == ident and "method" not in message:
550
+ return message
551
+ if isinstance(message, dict) and "method" in message and message.get("id") is None and notified:
552
+ notified(message)
553
+
554
+
555
+ # A server whose tools Mobster answers itself with the vendor's web API, because the vendor's MCP server refuses
556
+ # Mobster's app: Slack's takes only Marketplace and internal apps (slack_api.py).
557
+ SLACK = "https://mcp.slack.com/mcp"
558
+ # Microsoft 365 has no MCP server a Mobster app can sign in to: its tools are Microsoft Graph's (graph_api.py).
559
+ GRAPH = "https://graph.microsoft.com/v1.0"
560
+
561
+
562
+ def connect(spec, *, env=None, token=None, timeout=CONNECT_TIMEOUT, name="The server"):
563
+ """A started, initialized session for ``spec`` ({"transport": "stdio", "command", "args"} or {"transport": "http",
564
+ "url"})."""
565
+ if spec.get("transport") != "stdio" and spec.get("url") == SLACK:
566
+ from . import slack_api # here, not at the top: slack_api imports this module
567
+ return slack_api.connect(token=token, timeout=timeout, name=name)
568
+ if spec.get("transport") != "stdio" and spec.get("url") == GRAPH:
569
+ from . import graph_api # here, not at the top: graph_api imports this module
570
+ return graph_api.connect(token=token, timeout=timeout, name=name)
571
+ if spec.get("transport") == "stdio":
572
+ session = StdioSession([spec["command"], *spec.get("args", ())], env=env, name=name)
573
+ else:
574
+ session = HttpSession(spec["url"], token=token, name=name)
575
+ session.start()
576
+ try:
577
+ session.initialize(timeout=timeout)
578
+ except BaseException:
579
+ session.close()
580
+ raise
581
+ return session
582
+
@@ -0,0 +1,55 @@
1
+ """Mobster Checks, Mobster's GitHub App for pull requests' checks (docs/ci/github-app.mdx): the agent's half."""
2
+
3
+ import re
4
+
5
+ from .. import plan_gate, share
6
+ from ..api_errors import APIError
7
+
8
+ PRESET = "github"
9
+ INSTALL_URL = "https://github.com/apps/mobster-checks/installations/new"
10
+ REPO = re.compile(r"[A-Za-z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}")
11
+ TOKEN = re.compile(r"[A-Za-z0-9_]{20,255}")
12
+ EXPIRES = re.compile(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ")
13
+ NO_GITHUB = ("Connect GitHub in Mobster (Connectors), so Mobster Checks can tell you can push to this repository. "
14
+ "Then run the checks again.")
15
+ SIGN_IN = "Sign in to Mobster, then run the checks again."
16
+ CODES = ("not_installed", "no_access", "not_allowed", "plan_required", "github_sign_in", "sign_in", "unconfigured")
17
+
18
+
19
+ def account_file(runtime):
20
+ """The account.json this agent's plan check reads, else the Mac app's."""
21
+ gate = getattr(runtime, "plan_gate", None)
22
+ return gate.account_file if gate is not None else plan_gate.app_account_file()
23
+
24
+
25
+ def token(service, repo, account, confirm=None):
26
+ """{token, expiresAt, repo} for ``repo`` (owner/name), or APIError with the reason and what to do. ``confirm(repo)``
27
+ runs before Mobster's server is asked: it raises to refuse."""
28
+ if not isinstance(repo, str) or not REPO.fullmatch(repo) or repo.split("/")[1] in (".", ".."):
29
+ raise APIError("repo must be a GitHub repository as owner/name.", 400, "invalid_repo")
30
+ stored = plan_gate.stored_account(account)
31
+ if stored is None:
32
+ raise APIError(SIGN_IN, 401, "sign_in")
33
+ github = service.preset_bearer(PRESET)
34
+ if not github:
35
+ raise APIError(NO_GITHUB, 409, "no_github")
36
+ if confirm is not None:
37
+ confirm(repo)
38
+ try:
39
+ status, value = share.call("POST", "/api/github/token", stored["entitlement"]["token"],
40
+ {"repo": repo, "github": github})
41
+ except share.ShareError as error:
42
+ raise APIError(str(error), error.status, error.code) from None
43
+ finally:
44
+ github = None # noqa: F841 -- the sign-in's only other reference goes with this frame
45
+ if status == 200:
46
+ minted, expires, named = value.get("token"), value.get("expires_at"), value.get("repo")
47
+ if not (isinstance(minted, str) and TOKEN.fullmatch(minted) and isinstance(expires, str)
48
+ and EXPIRES.fullmatch(expires) and isinstance(named, str) and REPO.fullmatch(named)):
49
+ raise APIError("Mobster's server answered without a token. Run the checks again.", 502, "error")
50
+ return {"token": minted, "expiresAt": expires, "repo": named}
51
+ code = value.get("error") if value.get("error") in CODES else "error"
52
+ message = value.get("message") if isinstance(value.get("message"), str) else \
53
+ "Mobster's server couldn't get a GitHub token. Run the checks again in a minute."
54
+ extra = {"installUrl": INSTALL_URL} if code in ("not_installed", "not_allowed") else {}
55
+ raise APIError(message[:400], status if 400 <= status < 600 else 502, code, **extra)