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,326 @@
1
+ """The files track's HTTP routes (seam S3; documented in docs/files.mdx)."""
2
+
3
+ import tempfile
4
+ import uuid
5
+ from pathlib import Path
6
+
7
+ from ..api_errors import APIError
8
+ from ..api_routes import Response, Route
9
+ from ..phone_io import PhoneIOError
10
+ from . import service
11
+ from .store import FILE_MAX, ID, StoreError
12
+
13
+ DESTINATION_APP = "app:"
14
+
15
+
16
+ class _Copied(Exception):
17
+ def __init__(self, result):
18
+ self.result = result
19
+
20
+
21
+ def _store(request):
22
+ store = service.store_for(request.runtime)
23
+ if store is None:
24
+ raise APIError("Attaching files needs Mobster's data folder. Run Mobster with its task history on.", 503,
25
+ "files_unavailable")
26
+ return store
27
+
28
+
29
+ def _found(store, attachment_id):
30
+ found = store.get(attachment_id)
31
+ if found is None:
32
+ raise APIError("That file isn't here any more. Attach it again.", 404, "attachment_not_found")
33
+ return found
34
+
35
+
36
+ def _refuse(error):
37
+ if isinstance(error, StoreError):
38
+ return Response.error(str(error), error.status, error.code)
39
+ raise error
40
+
41
+
42
+ def _query(request, name):
43
+ values = request.query.get(name) or []
44
+ if len(values) > 1:
45
+ raise ValueError(f"Supply {name} once")
46
+ return values[0] if values else None
47
+
48
+
49
+ def upload(request):
50
+ store = _store(request)
51
+ name = (_query(request, "name") or "").strip()
52
+ if len(name) > 1024:
53
+ raise ValueError("That file name is too long")
54
+ thread = _query(request, "thread")
55
+ if thread is not None and not ID.fullmatch(thread):
56
+ raise ValueError("thread must be a conversation id")
57
+ try:
58
+ attachment = store.create(request.body or b"", name or "file", thread_id=thread)
59
+ except StoreError as error:
60
+ return _refuse(error)
61
+ return Response.json({"attachment": attachment.public()}, 201)
62
+
63
+
64
+ def metadata(request):
65
+ store = _store(request)
66
+ return Response.json({"attachment": _found(store, request.params["id"]).public()})
67
+
68
+
69
+ def content(request):
70
+ store = _store(request)
71
+ attachment = _found(store, request.params["id"])
72
+ path = store.path(attachment, "original")
73
+ if path is None:
74
+ raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
75
+ mime = attachment.data.get("mime") or "application/octet-stream"
76
+ if attachment.kind in ("text", "csv"):
77
+ mime = "text/plain; charset=utf-8" # never served as HTML, JSON or a script
78
+ return Response.bytes(path.read_bytes(), mime)
79
+
80
+
81
+ def thumb(request):
82
+ store = _store(request)
83
+ attachment = _found(store, request.params["id"])
84
+ path = store.path(attachment, "thumb")
85
+ if path is None:
86
+ raise APIError("This file has no thumbnail.", 404, "no_thumbnail")
87
+ return Response.bytes(path.read_bytes(), "image/jpeg")
88
+
89
+
90
+ def delete(request):
91
+ store = _store(request)
92
+ if not store.delete(request.params["id"]):
93
+ raise APIError("That file isn't here any more.", 404, "attachment_not_found")
94
+ return Response.json({"deleted": True})
95
+
96
+
97
+ def _phone(request, device):
98
+ """(fleet phone record, PhoneFiles) for ``device`` (None: the default device)."""
99
+ if device is not None and (not isinstance(device, str) or not 0 < len(device) <= 128):
100
+ raise ValueError("device must be a device id or name")
101
+ record = service.device_record(request.runtime, device)
102
+ try:
103
+ return record, service.phone_files(record)
104
+ except PhoneIOError as error:
105
+ raise _phone_error(error) from None
106
+
107
+
108
+ def _phone_error(error):
109
+ status = {"needs_cable": 409, "no_file_sharing": 409, "no_device": 409, "locked": 409, "not_found": 404,
110
+ "tools": 503, "usage": 400}.get(error.code, 502)
111
+ message = str(error) + (f" {error.fix}" if error.fix else "")
112
+ return APIError(message, status, error.code)
113
+
114
+
115
+ def phone_list(request):
116
+ from .agent import file_sharing_apps
117
+ record, phone = _phone(request, _query(request, "device"))
118
+ app = _query(request, "app")
119
+ try:
120
+ out = {"device": {"id": record["id"], "name": record["name"], "kind": record["kind"]},
121
+ "apps": file_sharing_apps(phone)}
122
+ if app:
123
+ out["files"] = phone.ls(app)
124
+ except PhoneIOError as error:
125
+ raise _phone_error(error) from None
126
+ return Response.json(out)
127
+
128
+
129
+ def phone_put(request):
130
+ # The user's own click in the Mac app: api_http.APP_ONLY_ROUTES needs its session, so a process holding only the
131
+ # token file can't put files on the phone.
132
+ body = request.body or {}
133
+ unknown = set(body) - {"device", "attachmentId", "destination"}
134
+ if unknown:
135
+ raise ValueError("Unsupported field: " + ", ".join(sorted(unknown)))
136
+ key = request.headers.get("Idempotency-Key")
137
+ if key is not None and not 0 < len(key) <= 200:
138
+ raise ValueError("Idempotency-Key is 1 to 200 characters")
139
+ store = _store(request)
140
+ attachment = _found(store, body.get("attachmentId") if isinstance(body.get("attachmentId"), str) else "")
141
+ destination = body.get("destination")
142
+ if not isinstance(destination, str) or not (destination in ("clipboard", "photos")
143
+ or destination.startswith(DESTINATION_APP)):
144
+ raise ValueError("destination is clipboard, photos or app:<bundle id>")
145
+ device = body.get("device")
146
+ if device is not None and (not isinstance(device, str) or not 0 < len(device) <= 128):
147
+ raise ValueError("device must be a device id or name")
148
+ runtime = request.runtime
149
+ phone = runtime.resolve_device(device)
150
+ key = key or uuid.uuid4().hex
151
+ fingerprint = {"device": phone.id, "attachmentId": attachment.id, "destination": destination}
152
+ earlier = store.copy_result(key, fingerprint)
153
+ if earlier is not None:
154
+ return Response.json(earlier)
155
+ reserved = False
156
+ def before_copy():
157
+ nonlocal reserved
158
+ if reserved:
159
+ return
160
+ previous = store.reserve_copy(key, fingerprint)
161
+ if previous is not None:
162
+ raise _Copied(previous)
163
+ reserved = True
164
+
165
+ from ..journal import JournalError, Lease
166
+ with runtime.lock:
167
+ if getattr(runtime, "closing", False):
168
+ raise APIError("The service is shutting down", 503, "shutting_down")
169
+ if runtime.run_on(phone) is not None:
170
+ raise APIError(f"{phone.name or 'The iPhone'} is busy with a task. Try again when it ends.", 409,
171
+ "device_busy")
172
+ url = (runtime.config.wda_url if getattr(phone, "primary", False) else phone.wda_url) or ""
173
+ if not url:
174
+ raise APIError(f"{phone.name or 'The device'} isn't set up yet.", 409, "no_device")
175
+ try:
176
+ lease = Lease.device(url.rstrip("/"))
177
+ except JournalError:
178
+ raise APIError("Your iPhone is busy. Wait for it to finish, then try again.", 409, "device_busy") from None
179
+ try:
180
+ earlier = store.copy_result(key, fingerprint)
181
+ if earlier is not None:
182
+ return Response.json(earlier)
183
+ if destination == "clipboard":
184
+ result = _put_clipboard(request, store, attachment, phone.id, before_copy)
185
+ elif destination == "photos":
186
+ result = _put_photos(request, store, attachment, phone.id, before_copy)
187
+ else:
188
+ bundle = destination[len(DESTINATION_APP):]
189
+ record, files = _phone(request, phone.id)
190
+ original = store.path(attachment, "original")
191
+ if original is None:
192
+ raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
193
+ try:
194
+ copied = files.put(bundle, original, name=attachment.name, before_copy=before_copy)
195
+ except PhoneIOError as error:
196
+ raise _phone_error(error) from None
197
+ result = {"status": "copied", "name": copied["name"], "path": copied["path"], "destination": destination,
198
+ "device": record["id"]}
199
+ store.finish_copy(key, result)
200
+ return Response.json(result)
201
+ except _Copied as previous:
202
+ return Response.json(previous.result)
203
+ finally:
204
+ lease.close()
205
+
206
+
207
+ def _wda(request, device):
208
+ """(fleet phone, call(method, path, body, timeout)) for a device's WebDriverAgent session, refused while a task runs
209
+ on it."""
210
+ import json
211
+ import urllib.request
212
+
213
+ from ..phone_io import checked_request
214
+ runtime = request.runtime
215
+ phone = runtime.resolve_device(device)
216
+ if runtime.device_busy(phone.id):
217
+ raise APIError(f"{phone.name or 'The iPhone'} is busy with a task. Try again when it ends.", 409,
218
+ "device_busy")
219
+ url = (phone.wda_url or "").rstrip("/")
220
+ if not url:
221
+ raise APIError(f"{phone.name or 'The device'} isn't set up yet.", 409, "no_device")
222
+
223
+ def send(method, path, body, timeout):
224
+ session = phone.wda_session()
225
+ data = json.dumps(body).encode() if body is not None else None
226
+ req = urllib.request.Request(f"{url}/session/{session}{path}", data=data, method=method,
227
+ headers={"Content-Type": "application/json"})
228
+ with urllib.request.urlopen(req, timeout=timeout) as response:
229
+ return json.load(response)
230
+ return phone, checked_request(send)
231
+
232
+
233
+ def _put_clipboard(request, store, attachment, device, before_copy):
234
+ """A text file onto the phone's clipboard through WebDriverAgent (any device), never while a task runs on it."""
235
+ from ..phone_io.clipboard import set_text
236
+ if attachment.kind not in ("text", "csv"):
237
+ raise APIError("Only a text file goes on the clipboard.", 400, "not_text")
238
+ phone, call = _wda(request, device)
239
+ text = store.whole_text(attachment) # the file itself, never the excerpt the agent reads
240
+ if text is None:
241
+ raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
242
+ try:
243
+ set_text(_copy_call(call, before_copy), text)
244
+ except PhoneIOError as error:
245
+ raise _phone_error(error) from None
246
+ except OSError:
247
+ raise APIError(f"{phone.name or 'The iPhone'} didn't answer. Keep it unlocked and connected, then try again.",
248
+ 502, "phone_unreachable") from None
249
+ return {"status": "copied", "name": attachment.name, "path": None, "destination": "clipboard",
250
+ "device": phone.id}
251
+
252
+
253
+ def _put_photos(request, store, attachment, device, before_copy):
254
+ """A picture or video into the phone's Photos library (phone_io.photos.send: Mobster's runner saves it), never while
255
+ a task runs on the phone."""
256
+ from ..phone_io.photos import send
257
+ if attachment.kind != "image":
258
+ raise APIError("Only a photo or a video goes into Photos.", 400, "not_media")
259
+ original = store.path(attachment, "original")
260
+ if original is None:
261
+ raise APIError("That file's content is gone. Attach it again.", 404, "attachment_not_found")
262
+ phone, call = _wda(request, device)
263
+ try:
264
+ added = send(call, original, name=phone.name or "the iPhone",
265
+ activate=lambda bundle: call("POST", "/wda/apps/activate", {"bundleId": bundle}, 15),
266
+ before_send=before_copy)
267
+ except PhoneIOError as error:
268
+ status = {"needs_rebuild": 409, "photos_denied": 409, "photos_restricted": 409,
269
+ "photos_not_determined": 409, "usage": 400}.get(error.code, 502)
270
+ raise APIError(str(error) + (f" {error.fix}" if error.fix else ""), status, error.code) from None
271
+ except OSError:
272
+ raise APIError(f"{phone.name or 'The iPhone'} didn't answer. Keep it unlocked and connected, then try again.",
273
+ 502, "phone_unreachable") from None
274
+ return {"status": "copied", "name": attachment.name, "path": None, "destination": "photos", "device": phone.id,
275
+ "kind": added["kind"]}
276
+
277
+
278
+ def _copy_call(call, before_copy):
279
+ """Journal intent after local validation and read-only preflight, before the first WDA mutation."""
280
+ def dispatch(method, path, body=None, timeout=15):
281
+ if method != "GET":
282
+ before_copy()
283
+ return call(method, path, body, timeout)
284
+ return dispatch
285
+
286
+
287
+ def phone_get(request):
288
+ from ..harness_api import post_thread_item
289
+ body = request.body or {}
290
+ unknown = set(body) - {"device", "app", "name", "threadId"}
291
+ if unknown:
292
+ raise ValueError("Unsupported field: " + ", ".join(sorted(unknown)))
293
+ app, name, thread = body.get("app"), body.get("name"), body.get("threadId")
294
+ if not isinstance(app, str) or not isinstance(name, str) or not name:
295
+ raise ValueError("Name the app (its bundle ID) and the file")
296
+ if thread is not None and (not isinstance(thread, str) or not ID.fullmatch(thread)):
297
+ raise ValueError("threadId must be a conversation id")
298
+ store = _store(request)
299
+ record, phone = _phone(request, body.get("device"))
300
+ with tempfile.TemporaryDirectory(prefix="mobster-phone-file-") as folder:
301
+ try:
302
+ saved = phone.get(app, name, Path(folder))
303
+ except PhoneIOError as error:
304
+ raise _phone_error(error) from None
305
+ if saved.stat().st_size > FILE_MAX:
306
+ raise APIError("Mobster attaches files of 25 MB or less.", 413, "too_large")
307
+ try:
308
+ attachment = store.create(saved.read_bytes(), saved.name, thread_id=thread, origin="phone")
309
+ except StoreError as error:
310
+ return _refuse(error)
311
+ if thread:
312
+ post_thread_item(thread, {"kind": "file_saved", "attachment": attachment.ref(), "app": app,
313
+ "device": record["name"]})
314
+ return Response.json({"attachment": attachment.public()}, 201)
315
+
316
+
317
+ ROUTES = (
318
+ Route("files", "POST", "/api/attachments", upload, body="raw", max_bytes=FILE_MAX),
319
+ Route("files", "GET", "/api/attachments/{id}", metadata, body="none"),
320
+ Route("files", "GET", "/api/attachments/{id}/content", content, body="none"),
321
+ Route("files", "GET", "/api/attachments/{id}/thumb", thumb, body="none"),
322
+ Route("files", "DELETE", "/api/attachments/{id}", delete, body="none"),
323
+ Route("files", "GET", "/api/phone/files", phone_list, body="none"),
324
+ Route("files", "POST", "/api/phone/files/put", phone_put, idempotency=True),
325
+ Route("files", "POST", "/api/phone/files/get", phone_get),
326
+ )
@@ -0,0 +1,170 @@
1
+ """The attachments service per Runtime: its store, the run listener that records which task used which file, and the
2
+ clean-up thread."""
3
+
4
+ import logging
5
+ import threading
6
+ import weakref
7
+ from pathlib import Path
8
+
9
+ log = logging.getLogger("mobster.attachments")
10
+
11
+ GC_FIRST, GC_EVERY, BUS_WAIT = 60.0, 1800.0, 5.0
12
+ _stores = weakref.WeakKeyDictionary()
13
+ _lock = threading.Lock()
14
+
15
+
16
+ def store_for(runtime):
17
+ """The runtime's AttachmentStore, or None when it keeps no journal on disk."""
18
+ from .store import AttachmentStore
19
+ with _lock:
20
+ if getattr(runtime, "closing", False):
21
+ return None
22
+ found = _stores.get(runtime)
23
+ if found is not None:
24
+ return found
25
+ state_db = getattr(getattr(runtime, "config", None), "state_db", None)
26
+ journal = getattr(runtime, "journal", None)
27
+ if not state_db or journal is None:
28
+ return None
29
+
30
+ def publish(event):
31
+ post = getattr(runtime, "publish", None)
32
+ if callable(post):
33
+ post("attachments", event)
34
+
35
+ store = AttachmentStore(journal, Path(state_db).parent / "attachments", publish=publish,
36
+ referenced=lambda: referenced(runtime))
37
+ try:
38
+ # Called under the runtime's lock (delete_run, create's retention): only note the ids here; the
39
+ # clean-up thread deletes what they leave unused.
40
+ journal.add_expiry_listener(lambda run_ids: expire_later(runtime, run_ids))
41
+ except Exception: # noqa: BLE001
42
+ log.exception("could not listen for expired tasks")
43
+ _stores[runtime] = store
44
+ return store
45
+
46
+
47
+ def device_record(runtime, device=None):
48
+ """A devices.py-shaped record of the device ``device`` names (an id, UDID or name; None: the default device), for
49
+ phone_io.files."""
50
+ phone = runtime.resolve_device(device)
51
+ return {"id": phone.id, "kind": phone.kind, "udid": phone.udid, "name": phone.name or phone.id,
52
+ "transport": getattr(phone, "transport", None), "wdaUrl": phone.wda_url}
53
+
54
+
55
+ def phone_files(record):
56
+ """phone_io.files.PhoneFiles for ``record`` (tests replace this)."""
57
+ from ..phone_io.files import PhoneFiles
58
+ return PhoneFiles(record)
59
+
60
+
61
+ def referenced(runtime):
62
+ """{attachment id: [run ids]} the tasks in memory name in their ``attachmentIds``."""
63
+ out = {}
64
+ lock = getattr(runtime, "lock", None)
65
+ if lock is None:
66
+ return out
67
+ with lock:
68
+ runs = list(getattr(runtime, "runs", {}).values())
69
+ for run in runs:
70
+ for identifier in (getattr(run, "extras", None) or {}).get("attachmentIds") or ():
71
+ out.setdefault(identifier, []).append(run.id)
72
+ return out
73
+
74
+
75
+ def expire_later(runtime, run_ids):
76
+ """The journal's expiry listener: hand the removed tasks to the clean-up thread (there is one whenever this runtime
77
+ keeps any file)."""
78
+ cleaner = _cleaners.get(runtime)
79
+ if cleaner is not None and run_ids:
80
+ with cleaner.lock:
81
+ cleaner.expired.update(run_ids)
82
+
83
+
84
+ class Cleaner:
85
+ def __init__(self, runtime):
86
+ self.runtime = runtime
87
+ self.stop = threading.Event()
88
+ self.lock = threading.Lock()
89
+ self.expired = set()
90
+ self.thread = threading.Thread(target=self._loop, name="mobster-attachments", daemon=True)
91
+
92
+ def take_expired(self):
93
+ with self.lock:
94
+ ids, self.expired = self.expired, set()
95
+ return ids
96
+
97
+ def _loop(self):
98
+ import time
99
+
100
+ from ..eventbus import bus as default_bus
101
+ bus = getattr(self.runtime, "bus", None) or default_bus
102
+ cursor = bus.position()
103
+ due = time.monotonic() + GC_FIRST
104
+ while not self.stop.is_set():
105
+ try:
106
+ events = bus.wait({"threads"}, cursor, BUS_WAIT)
107
+ except Exception: # noqa: BLE001
108
+ events = []
109
+ self.stop.wait(BUS_WAIT)
110
+ if events and isinstance(events[-1].get("id"), str):
111
+ cursor = events[-1]["id"]
112
+ store = store_for(self.runtime) if not self.stop.is_set() else None
113
+ if store is None:
114
+ continue
115
+ expired = self.take_expired()
116
+ if expired:
117
+ try:
118
+ store.expired(sorted(expired))
119
+ except Exception: # noqa: BLE001
120
+ log.exception("could not clean up the files of tasks that left history")
121
+ for event in events:
122
+ thread = event.get("thread") if isinstance(event.get("thread"), dict) else {}
123
+ if event.get("event") == "thread_deleted" and thread.get("id"):
124
+ try:
125
+ store.delete_thread(thread["id"])
126
+ except Exception: # noqa: BLE001
127
+ log.exception("could not delete a conversation's files")
128
+ if time.monotonic() >= due:
129
+ due = time.monotonic() + GC_EVERY
130
+ try:
131
+ store.gc()
132
+ except Exception: # noqa: BLE001
133
+ log.exception("attachment clean-up failed")
134
+
135
+
136
+ _cleaners = weakref.WeakKeyDictionary()
137
+
138
+
139
+ def start(runtime):
140
+ """register_service start: the store, and its clean-up thread once there is anything to clean (now, or at the first
141
+ upload)."""
142
+ store = store_for(runtime)
143
+ if store is None:
144
+ return
145
+ store.on_first_write = lambda: ensure_cleaner(runtime)
146
+ try:
147
+ kept = store.count()
148
+ except Exception: # noqa: BLE001 -- a journal that can't answer starts no thread; the first upload will
149
+ kept = 0
150
+ if kept:
151
+ ensure_cleaner(runtime)
152
+
153
+
154
+ def ensure_cleaner(runtime):
155
+ with _lock:
156
+ if runtime in _cleaners or getattr(runtime, "closing", False):
157
+ return
158
+ cleaner = Cleaner(runtime)
159
+ _cleaners[runtime] = cleaner
160
+ cleaner.thread.start()
161
+
162
+
163
+ def close(runtime):
164
+ with _lock:
165
+ store = _stores.pop(runtime, None)
166
+ cleaner = _cleaners.pop(runtime, None)
167
+ if store is not None:
168
+ store.on_first_write = None
169
+ if cleaner is not None:
170
+ cleaner.stop.set()
@@ -0,0 +1,152 @@
1
+ """What a file is, from its first bytes, never from its name: the attachment allowlist."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import unicodedata
7
+ from dataclasses import dataclass
8
+
9
+ KINDS = ("image", "pdf", "text", "csv")
10
+ NAME_LIMIT = 120
11
+ # Characters a name never keeps: C0/C1 controls, and the invisible marks that make a name read differently.
12
+ _UNSAFE = re.compile(r"[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]")
13
+ _HEIF_BRANDS = {b"heic", b"heix", b"hevc", b"hevx", b"heim", b"heis", b"hevm", b"hevs", b"mif1", b"msf1", b"avif"}
14
+
15
+
16
+ class Unsupported(ValueError):
17
+ """The file isn't one Mobster reads. ``code``: unsupported_type."""
18
+
19
+ code = "unsupported_type"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class FileType:
24
+ mime: str
25
+ kind: str # image | pdf | text | csv
26
+ ext: str # the extension Mobster stores it under (no dot)
27
+ label: str # a word for people: "PDF", "PNG image", "CSV"
28
+
29
+
30
+ IMAGE_TYPES = {
31
+ "image/png": FileType("image/png", "image", "png", "PNG image"),
32
+ "image/jpeg": FileType("image/jpeg", "image", "jpg", "JPEG image"),
33
+ "image/gif": FileType("image/gif", "image", "gif", "GIF image"),
34
+ "image/webp": FileType("image/webp", "image", "webp", "WebP image"),
35
+ "image/heic": FileType("image/heic", "image", "heic", "HEIC photo"),
36
+ }
37
+ PDF = FileType("application/pdf", "pdf", "pdf", "PDF")
38
+ PLAIN = FileType("text/plain", "text", "txt", "Text")
39
+ MARKDOWN = FileType("text/markdown", "text", "md", "Markdown")
40
+ JSON_TYPE = FileType("application/json", "text", "json", "JSON")
41
+ CSV = FileType("text/csv", "csv", "csv", "CSV")
42
+ TSV = FileType("text/tab-separated-values", "csv", "tsv", "TSV")
43
+
44
+ UNSUPPORTED = "Mobster reads images, PDFs, text and CSV files. This file is none of those."
45
+
46
+
47
+ def image_type(head):
48
+ """The image type ``head`` (a file's first bytes) starts with, or None."""
49
+ if head.startswith(b"\x89PNG\r\n\x1a\n"):
50
+ return IMAGE_TYPES["image/png"]
51
+ if head.startswith(b"\xff\xd8\xff"):
52
+ return IMAGE_TYPES["image/jpeg"]
53
+ if head.startswith((b"GIF87a", b"GIF89a")):
54
+ return IMAGE_TYPES["image/gif"]
55
+ if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
56
+ return IMAGE_TYPES["image/webp"]
57
+ if head[4:8] == b"ftyp" and head[8:12] in _HEIF_BRANDS:
58
+ return IMAGE_TYPES["image/heic"]
59
+ return None
60
+
61
+
62
+ def is_pdf(head):
63
+ """A PDF's header ``%PDF-`` within its first 1 KB (some writers put junk before it, as readers allow)."""
64
+ return b"%PDF-" in head[:1024]
65
+
66
+
67
+ def decode_text(data):
68
+ """``data`` as text, or None when it isn't UTF-8 text: it must decode, hold no NUL, and at most 1 in 1,000 of its
69
+ characters may be a control character other than tab, newline, carriage return or form feed."""
70
+ if b"\x00" in data:
71
+ return None
72
+ try:
73
+ text = data.decode("utf-8")
74
+ except UnicodeDecodeError:
75
+ return None
76
+ if text.startswith("\ufeff"):
77
+ text = text[1:]
78
+ controls = sum(1 for ch in text if (ord(ch) < 32 and ch not in "\t\n\r\f") or ord(ch) == 127)
79
+ if controls > max(1, len(text) // 1000):
80
+ return None
81
+ return text
82
+
83
+
84
+ def sniff(data, name=""):
85
+ """The FileType of ``data`` (the whole file), or Unsupported. The name only says which text a text file is."""
86
+ head = bytes(data[:4096])
87
+ found = image_type(head)
88
+ if found is not None:
89
+ return found
90
+ if is_pdf(head):
91
+ return PDF
92
+ text = decode_text(bytes(data))
93
+ if text is None:
94
+ raise Unsupported(UNSUPPORTED)
95
+ ext = os.path.splitext(str(name or "").lower())[1]
96
+ if ext == ".csv":
97
+ return CSV
98
+ if ext == ".tsv":
99
+ return TSV
100
+ if ext == ".json":
101
+ try:
102
+ json.loads(text)
103
+ return JSON_TYPE
104
+ except ValueError:
105
+ return PLAIN
106
+ if ext in (".md", ".markdown"):
107
+ return MARKDOWN
108
+ return PLAIN
109
+
110
+
111
+ def clean_name(name, fallback="file"):
112
+ """A file name that is safe to show and to write anywhere: the last path part only, Unicode NFC, without control or
113
+ direction-override characters, spaces collapsed, never "", "." or "..", never starting with "-" or ".", at most
114
+ NAME_LIMIT characters with its extension kept."""
115
+ text = unicodedata.normalize("NFC", str(name or ""))
116
+ text = text.replace("\\", "/").rsplit("/", 1)[-1]
117
+ text = _UNSAFE.sub("", text)
118
+ text = " ".join(text.split()).strip(" .")
119
+ text = text.lstrip("-").strip()
120
+ if not text or text in (".", ".."):
121
+ text = fallback
122
+ if len(text) > NAME_LIMIT:
123
+ stem, ext = os.path.splitext(text)
124
+ ext = ext if len(ext) <= 12 else ""
125
+ text = stem[:NAME_LIMIT - len(ext)].rstrip(" .") + ext
126
+ return text
127
+
128
+
129
+ def with_extension(name, file_type):
130
+ """``name`` ending in an extension that matches ``file_type`` (``scan`` + PDF -> ``scan.pdf``); a name that already
131
+ has a fitting one keeps it (``photo.jpeg`` stays)."""
132
+ stem, ext = os.path.splitext(name)
133
+ ext = ext.lower().lstrip(".")
134
+ fitting = {"jpg": {"jpg", "jpeg"}, "heic": {"heic", "heif"}, "txt": {"txt", "text", "log", ""},
135
+ "md": {"md", "markdown"}, "csv": {"csv"}, "tsv": {"tsv"}, "json": {"json"}}
136
+ if ext == file_type.ext or ext in fitting.get(file_type.ext, set()):
137
+ return name if ext or file_type.kind not in ("image", "pdf") else f"{name}.{file_type.ext}"
138
+ if file_type.kind == "text" and file_type.ext == "txt":
139
+ return name # a .log or .yaml file keeps its name; it is read as plain text
140
+ return clean_name(f"{name}.{file_type.ext}")
141
+
142
+
143
+ def size_words(count):
144
+ """``412 KB``, ``3.4 MB``: a file's size for people (1 KB = 1,000 bytes, as Finder counts)."""
145
+ count = int(count or 0)
146
+ if count < 1000:
147
+ return f"{count} byte" + ("" if count == 1 else "s")
148
+ for unit, step in (("KB", 1e3), ("MB", 1e6), ("GB", 1e9)):
149
+ value = count / step
150
+ if value < 999.5 or unit == "GB":
151
+ return f"{value:.0f} {unit}" if value >= 10 or unit == "KB" else f"{value:.1f} {unit}"
152
+ return f"{count} bytes"