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,364 @@
1
+ """Heals as reviewable diffs: ``<check>.heal.yaml`` next to the check, Accept and Keep failing.
2
+
3
+ A heal is written the moment a run heals, never applied silently:
4
+
5
+ - the run's results.json lists it in ``healed[]`` (step, from, to, why, frames before and after);
6
+ - ``<check>.heal.yaml`` holds every heal not yet reviewed, beside the check, so it shows in a pull request;
7
+ - the approved cached path stays unchanged until Accept; the full proposal stays private in the replay cache.
8
+
9
+ Review:
10
+
11
+ - **Accept** (``mobster test heal --accept``): the proposed steps become approved; the .heal.yaml file goes.
12
+ - **Keep failing** (``--reject``): the approved steps stay and become strict at the changed positions: when the
13
+ screen differs there again, the check fails instead of healing. Use it when the change is a bug.
14
+
15
+ A heal never touches the check file: its steps and its expectations stay as you wrote them.
16
+ """
17
+
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import re
22
+ import stat
23
+ import time
24
+ from pathlib import Path
25
+
26
+ from .path import PathError, validate
27
+ from .store import NAME, Cache, atomic_write, file_name, revision, sync_directory
28
+
29
+ VERSION = 2
30
+ SUFFIX = ".heal.yaml"
31
+ PROPOSAL = re.compile(r"[0-9a-f]{64}")
32
+
33
+
34
+ class HealError(Exception):
35
+ """A heal file or its cached path that can't be reviewed: one plain sentence."""
36
+
37
+
38
+ class InputChanged(HealError):
39
+ """A check's input differs from what the check was admitted with, as opposed to one that can't be read."""
40
+
41
+
42
+ def heal_file(check_file):
43
+ """``<checks>/paywall.heal.yaml`` for ``<checks>/paywall.yaml``."""
44
+ check_file = Path(check_file)
45
+ return check_file.with_name(check_file.name.rsplit(".", 1)[0] + SUFFIX)
46
+
47
+
48
+ def _header(check_name):
49
+ return ("# Mobster heal diff: steps of this check changed on screen and Smart healed them. Review them.\n"
50
+ "# A heal never changes an expectation. The check file itself is untouched.\n"
51
+ f"# Accept: mobster test heal --accept {check_name}\n"
52
+ f"# Keep failing: mobster test heal --reject {check_name}\n")
53
+
54
+
55
+ def read(check_file):
56
+ """The heal document for ``check_file``, or None when nothing waits for review. Raises HealError when the file
57
+ exists but can't be read."""
58
+ path = heal_file(check_file)
59
+ if not path.is_file():
60
+ return None
61
+ import yaml
62
+ try:
63
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
64
+ except (OSError, UnicodeDecodeError, yaml.YAMLError) as error:
65
+ raise HealError(f"{path.name} can't be read ({type(error).__name__})") from None
66
+ if not isinstance(data, dict) or not isinstance(data.get("heals"), list):
67
+ raise HealError(f"{path.name} isn't a heal diff Mobster wrote")
68
+ return data
69
+
70
+
71
+ def _write(path, document):
72
+ import yaml
73
+ text = _header(Path(document.get("check") or path.name).name) + yaml.safe_dump(
74
+ document, sort_keys=False, allow_unicode=True, default_flow_style=False, width=110)
75
+ atomic_write(path, text)
76
+
77
+
78
+ def _digest(value):
79
+ return hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False,
80
+ separators=(",", ":")).encode()).hexdigest()
81
+
82
+
83
+ def effective_digest(check):
84
+ """Bind fixture-flattened execution and assertions without storing their text in the proposal."""
85
+ from .checks import _proof
86
+ return _digest(_proof(check))
87
+
88
+
89
+ def _proposal_file(cache_dir, cache_name, proposal):
90
+ if not isinstance(cache_name, str) or not NAME.fullmatch(cache_name) or not isinstance(proposal, str) \
91
+ or not PROPOSAL.fullmatch(proposal):
92
+ raise HealError("the heal file doesn't name a valid recorded proposal")
93
+ return Path(cache_dir) / f"{cache_name[:-5]}-{proposal}.proposal.json"
94
+
95
+
96
+ def _check_digest(check_file):
97
+ try:
98
+ return hashlib.sha256(Path(check_file).read_bytes()).hexdigest()
99
+ except OSError as error:
100
+ raise HealError(f"the check file can't be read ({type(error).__name__})") from None
101
+
102
+
103
+ def _file_digest(path, limit):
104
+ """Hash one regular asset without following its final symlink or accepting an in-place rewrite."""
105
+ try:
106
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
107
+ try:
108
+ before = os.fstat(fd)
109
+ if not stat.S_ISREG(before.st_mode) or before.st_size > limit:
110
+ raise HealError("an external check asset is not a bounded regular file")
111
+ digest = hashlib.sha256()
112
+ size = 0
113
+ while chunk := os.read(fd, min(65536, limit + 1 - size)):
114
+ size += len(chunk)
115
+ if size > limit:
116
+ raise HealError("an external check asset exceeds its proof limit")
117
+ digest.update(chunk)
118
+ after = os.fstat(fd)
119
+ identity = lambda item: (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns, item.st_ctime_ns)
120
+ if size != before.st_size or identity(after) != identity(before):
121
+ raise HealError("an external check asset changed while it was read")
122
+ return size, digest.hexdigest()
123
+ finally:
124
+ os.close(fd)
125
+ except OSError as error:
126
+ raise HealError(f"an external check asset can't be read ({type(error).__name__})") from None
127
+
128
+
129
+ def _assets(files, *, limit, count_limit):
130
+ rows, total = [], 0
131
+ for label, path in files:
132
+ size, digest = _file_digest(path, limit - total)
133
+ total += size
134
+ rows.append((label, size, digest))
135
+ if len(rows) > count_limit:
136
+ raise HealError("the check has too many external assets to freeze")
137
+ return _digest(rows)
138
+
139
+
140
+ def proof_assets(check, *, include_a11y=False):
141
+ """Digests of external evidence the parsed check uses to judge a run."""
142
+ from ..verify import conditions, fixtures
143
+ seed = getattr(check, "seed", None)
144
+ seed_files = []
145
+ for source, destination in getattr(seed, "files", ()):
146
+ source = Path(source)
147
+ if source.is_symlink():
148
+ raise HealError("a seed asset is a symlink; the approved path was kept")
149
+ if source.is_file():
150
+ seed_files.append((destination, source))
151
+ elif source.is_dir():
152
+ paths = []
153
+ for path in source.rglob("*"):
154
+ paths.append(path)
155
+ if len(paths) > fixtures.FILES_LIMIT * 8:
156
+ raise HealError("the seed directory has too many entries to freeze")
157
+ for path in sorted(paths):
158
+ if path.is_symlink():
159
+ raise HealError("a seed asset is a symlink; the approved path was kept")
160
+ if path.is_file():
161
+ seed_files.append((str(Path(destination) / path.relative_to(source)), path))
162
+ elif not path.is_dir():
163
+ raise HealError("a seed asset is not a regular file")
164
+ else:
165
+ raise HealError("a seed asset is unavailable; the approved path was kept")
166
+ seed_sha = _assets(seed_files, limit=fixtures.BYTES_LIMIT, count_limit=fixtures.FILES_LIMIT * 4)
167
+ condition = getattr(check, "conditions", None) or {}
168
+ media = condition.get("media") or ()
169
+ media_sha = _assets(((str(index), Path(path)) for index, path in enumerate(media)),
170
+ limit=128 * 1024 * 1024, count_limit=conditions.MEDIA_LIMIT)
171
+ push = condition.get("push")
172
+ push_sha = _file_digest(push, conditions.PAYLOAD_LIMIT)[1] if push else None
173
+ a11y_sha = None
174
+ if include_a11y or getattr(check, "a11y", None) or "a11y" in getattr(check, "audit", ()) \
175
+ or getattr(check, "a11y_refresh", False):
176
+ if getattr(check, "a11y_refresh", False):
177
+ if check.a11y_baseline:
178
+ raise HealError("the accessibility refresh did not apply an empty baseline")
179
+ baseline = check.a11y_baseline # the applied empty set; the old disk baseline is being replaced
180
+ else:
181
+ from ..testkit import a11y_baseline
182
+ from ..verify.checks import project_root
183
+ try:
184
+ baseline = a11y_baseline.load(project_root(check.source))
185
+ except a11y_baseline.BaselineError as error:
186
+ raise HealError(f"the accessibility baseline can't be read ({type(error).__name__})") from None
187
+ if getattr(check, "a11y_scope", None) and baseline != check.a11y_baseline:
188
+ raise InputChanged("the accessibility baseline changed since check admission")
189
+ a11y_sha = _digest(sorted(baseline))
190
+ business_sha = None
191
+ if getattr(check, "business", None) is not None:
192
+ from ..verify.assertions import CheckError
193
+ from ..verify.business import load_business_binding
194
+ from ..verify.checks import project_root
195
+ try:
196
+ business_sha = load_business_binding(check, project_root(check.source)).digest
197
+ except (CheckError, OSError) as error:
198
+ raise HealError(f"the app-state fixture can't be read ({type(error).__name__})") from None
199
+ return {"visualSha256": None, "businessSha256": business_sha, "seedSha256": seed_sha,
200
+ "mediaSha256": media_sha, "pushSha256": push_sha, "a11ySha256": a11y_sha}
201
+
202
+
203
+ def discard(check_file, cache_dir, cache_name):
204
+ """An explicit fresh recording supersedes a proposal for this cache key."""
205
+ document = read(check_file)
206
+ if document is None or document.get("cache") != cache_name:
207
+ return False
208
+ private = None
209
+ if document.get("version") == VERSION:
210
+ private = _proposal_file(cache_dir, cache_name, document.get("proposal"))
211
+ heal_file(check_file).unlink()
212
+ sync_directory(heal_file(check_file).parent)
213
+ if private is not None:
214
+ private.unlink(missing_ok=True)
215
+ sync_directory(private.parent)
216
+ return True
217
+
218
+
219
+ def record(check_file, heals, *, run_id, cache_file, candidate, base_revision, check_sha256,
220
+ effective_sha256, proof,
221
+ app=None, ios=None, device=None):
222
+ """Stage a private candidate, then publish its review diff. The approved path is untouched."""
223
+ path = heal_file(check_file)
224
+ if path.exists():
225
+ raise HealError(f"{path.name} already waits for review")
226
+ validate(candidate)
227
+ cache_file = Path(cache_file)
228
+ if file_name(candidate["identity"], candidate["env"]) != cache_file.name \
229
+ or not isinstance(base_revision, str) or not PROPOSAL.fullmatch(base_revision):
230
+ raise HealError("the healed path doesn't match its approved recording")
231
+ from ..verify.checks import project_root
232
+ root = project_root(check_file)
233
+ at = time.strftime("%Y-%m-%dT%H:%M:%S")
234
+ entries = []
235
+ for heal in heals:
236
+ entry = {key_: heal.get(key_) for key_ in ("step", "from", "to", "why", "old", "new")}
237
+ entry["frames"] = {name: _from_root(value, root) for name, value in (heal.get("frames") or {}).items()}
238
+ entries.append(entry | {"run": run_id, "at": at})
239
+ check_sha = _check_digest(check_file)
240
+ if check_sha != check_sha256:
241
+ raise HealError("the check changed during healing; the approved path was kept")
242
+ from .checks import _saved_check
243
+ try:
244
+ current_check = _saved_check(check_file)
245
+ current_proof = proof_assets(current_check, include_a11y=proof["a11ySha256"] is not None)
246
+ except (OSError, ValueError) as error:
247
+ raise HealError(f"the check can't be read ({type(error).__name__})") from None
248
+ if current_proof != proof or effective_digest(current_check) != effective_sha256:
249
+ raise HealError("the check's evidence changed during healing; the approved path was kept")
250
+ payload = {"candidate": candidate, "baseRevision": base_revision, "checkSha256": check_sha,
251
+ "effectiveSha256": effective_sha256, "proof": proof, "heals": entries}
252
+ proposal = _digest(payload)
253
+ private = _proposal_file(cache_file.parent, cache_file.name, proposal)
254
+ atomic_write(private, json.dumps(payload, ensure_ascii=False))
255
+ document = {"version": VERSION, "check": Path(check_file).name, "run": run_id,
256
+ "cache": cache_file.name, "proposal": proposal,
257
+ "baseRevision": base_revision, "candidateRevision": revision(candidate),
258
+ "checkSha256": check_sha, "app": app, "ios": ios, "device": device, "heals": entries}
259
+ _write(path, document)
260
+ return path
261
+
262
+
263
+ def _from_root(value, root):
264
+ """A frame's path relative to the project (``.mobster/runs/<run>/frames/...``), else as it is."""
265
+ if not value:
266
+ return None
267
+ try:
268
+ return Path(value).resolve().relative_to(Path(root).resolve()).as_posix()
269
+ except (ValueError, OSError):
270
+ return str(value)
271
+
272
+
273
+ def _review(check_file, cache_dir, decision):
274
+ cache = Cache(cache_dir)
275
+ with cache.locked():
276
+ return _review_locked(check_file, cache, decision)
277
+
278
+
279
+ def _review_locked(check_file, cache, decision):
280
+ document = read(check_file)
281
+ if document is None:
282
+ return 0
283
+ if document.get("version") != VERSION:
284
+ raise HealError("this older heal has no approved baseline; record the check again with --fresh")
285
+ name, proposal = document.get("cache"), document.get("proposal")
286
+ private = _proposal_file(cache.directory, name, proposal)
287
+ if (not isinstance(document.get("baseRevision"), str)
288
+ or not PROPOSAL.fullmatch(document["baseRevision"])
289
+ or not isinstance(document.get("checkSha256"), str)
290
+ or not PROPOSAL.fullmatch(document["checkSha256"])
291
+ or not isinstance(document.get("candidateRevision"), str)
292
+ or not PROPOSAL.fullmatch(document["candidateRevision"])):
293
+ raise HealError("the heal diff is invalid; review evidence was kept")
294
+ try:
295
+ payload = json.loads(private.read_text(encoding="utf-8"))
296
+ candidate = validate(payload["candidate"])
297
+ bound_name = file_name(candidate["identity"], candidate["env"])
298
+ except (OSError, ValueError, KeyError, TypeError, PathError):
299
+ raise HealError("the heal proposal is missing or invalid; review evidence was kept") from None
300
+ if (_digest(payload) != proposal or payload.get("baseRevision") != document.get("baseRevision")
301
+ or payload.get("checkSha256") != document.get("checkSha256")
302
+ or payload.get("heals") != document.get("heals")
303
+ or revision(candidate) != document.get("candidateRevision")
304
+ or bound_name != name or _check_digest(check_file) != document["checkSha256"]):
305
+ raise HealError("the heal proposal or check changed; review evidence was kept")
306
+ from .checks import _saved_check
307
+ try:
308
+ current_check = _saved_check(check_file)
309
+ expected_proof = payload.get("proof")
310
+ if not isinstance(expected_proof, dict):
311
+ raise HealError("the heal proposal has no evidence proof; review evidence was kept")
312
+ current_proof = proof_assets(current_check, include_a11y=expected_proof.get("a11ySha256") is not None)
313
+ except (OSError, ValueError) as error:
314
+ raise HealError(f"the check can't be read ({type(error).__name__}); review evidence was kept") from None
315
+ if current_proof != expected_proof or effective_digest(current_check) != payload.get("effectiveSha256"):
316
+ raise HealError("the check's evidence changed; review evidence was kept")
317
+ approved = cache.load(candidate["identity"], candidate["env"])
318
+ if approved is None:
319
+ raise HealError("the approved recording is missing or invalid; review evidence was kept")
320
+ receipt = approved.get("review")
321
+ if receipt is not None and not isinstance(receipt, dict):
322
+ raise HealError("the approved recording has an invalid review receipt; review evidence was kept")
323
+ receipt = receipt or {}
324
+ if receipt.get("proposal") == proposal:
325
+ if receipt.get("decision") != decision:
326
+ raise HealError("this heal already received the opposite decision")
327
+ else:
328
+ if revision(approved) != document["baseRevision"]:
329
+ raise HealError("the approved recording changed; review evidence was kept")
330
+ for heal in document["heals"]:
331
+ if (not isinstance(heal, dict) or not isinstance(heal.get("step"), int)
332
+ or isinstance(heal["step"], bool) or not 1 <= heal["step"] <= len(approved["steps"])
333
+ or not isinstance(heal.get("old"), list)):
334
+ raise HealError("the heal diff has an invalid step; review evidence was kept")
335
+ if decision == "accept":
336
+ chosen = dict(candidate)
337
+ else:
338
+ chosen = dict(approved)
339
+ strict = set(chosen.get("strict") or ())
340
+ for heal in document["heals"]:
341
+ start, old = heal["step"], heal["old"]
342
+ count = max(1, len(old))
343
+ strict.update(range(start, min(start + count, len(chosen["steps"]) + 1)))
344
+ chosen["strict"] = sorted(strict)
345
+ chosen["review"] = {"proposal": proposal, "decision": decision}
346
+ cache.save(chosen)
347
+ heal_file(check_file).unlink()
348
+ sync_directory(heal_file(check_file).parent)
349
+ private.unlink(missing_ok=True)
350
+ sync_directory(private.parent)
351
+ return len(document["heals"])
352
+
353
+
354
+ def accept(check_file, cache_dir):
355
+ """Make the reviewed proposal the active recording, after verifying its approved base."""
356
+ return _review(check_file, cache_dir, "accept")
357
+
358
+
359
+ def reject(check_file, cache_dir):
360
+ """Keep the approved steps and protect the replaced interval from later healing."""
361
+ return _review(check_file, cache_dir, "reject")
362
+
363
+
364
+ __all__ = ["HealError", "heal_file", "read", "record", "accept", "reject", "SUFFIX"]
@@ -0,0 +1,240 @@
1
+ """A recorded action path: what a passing run did, step by step, with what each step needs to find on the screen.
2
+
3
+ A step is a plain dict, so it goes to JSON and YAML as it is::
4
+
5
+ {"op": "TAP", "app": "com.example.app",
6
+ "target": {"label": "Continue", "role": "Button", "id": "/0/3/1", "frame": [.1, .8, .8, .06]},
7
+ "screen": {"hash": "9f2c...", "signature": [["Button", "Continue"], ["StaticText", "Welcome"]]},
8
+ "text": null, "param": null, "commit": null, "point": null, "live": false}
9
+
10
+ - ``target``: the element acted on (label, role, the tree path as ``id``, its frame in screen fractions).
11
+ - ``screen``: the screen before the action: state.py's structural signature (role/label pairs) and its hash.
12
+ Coordinate steps also hold its exact content fingerprint, including geometry.
13
+ - ``text``: typed text a replay may type again (a literal from the check's own steps), or None.
14
+ - ``param``: for a learned workflow, the name of the blank the typed text fills; ``text`` is then None.
15
+ - ``commit``: the act (harness_api.commit_act) when the step sends, posts, deletes, buys or submits.
16
+ - ``point``: (x, y) in screen fractions for TAP_XY and DISMISS, which have no element.
17
+ - ``live``: the step can't be replayed (text the model wrote, a macro read): the model does it each run.
18
+ """
19
+
20
+ # A screen's role/label pairs as short hashes: the frontier's StepRecord.shape uses the same rule.
21
+ from ..state import SHAPE_LIMIT, SIGNATURE_LIMIT, screen_shape, signature_hash # noqa: F401
22
+
23
+ VERSION = 1
24
+ MAX_STEPS = 60
25
+ MAX_ALTS = 4
26
+
27
+ ELEMENT_OPS = frozenset({"TAP", "LONG_PRESS", "TYPE", "TYPE_SUBMIT", "SET_TEXT", "SUBMIT"})
28
+ TEXT_OPS = frozenset({"TYPE", "TYPE_SUBMIT", "SET_TEXT"})
29
+ SWIPES = frozenset({"SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT"})
30
+ POINT_OPS = frozenset({"TAP_XY", "DISMISS"})
31
+ OPS = ELEMENT_OPS | SWIPES | POINT_OPS | frozenset({"LAUNCH_APP", "HOME", "WAIT"})
32
+ LIVE_OPS = frozenset({"READ_LIST", "SCROLL_TO"})
33
+
34
+ VERBS = {"TAP": "Tap", "LONG_PRESS": "Press and hold", "TYPE": "Type in", "TYPE_SUBMIT": "Type and submit in",
35
+ "SET_TEXT": "Set the text of", "SUBMIT": "Submit", "SWIPE_UP": "Swipe up", "SWIPE_DOWN": "Swipe down",
36
+ "SWIPE_LEFT": "Swipe left", "SWIPE_RIGHT": "Swipe right", "LAUNCH_APP": "Open",
37
+ "HOME": "Go to the Home Screen",
38
+ "TAP_XY": "Tap a point", "DISMISS": "Dismiss", "WAIT": "Wait", "READ_LIST": "Read the list",
39
+ "SCROLL_TO": "Scroll to"}
40
+
41
+
42
+ class PathError(ValueError):
43
+ """A stored path that can't be used: it is ignored, and the next run records a new one."""
44
+
45
+
46
+ def screen_hashes(snapshot):
47
+ """{"hash", "shape", "app"} for ``snapshot`` as hashes, with no screen text; None when there's no screen."""
48
+ from ..state import signature
49
+ if snapshot is None:
50
+ return None
51
+ try:
52
+ pairs = signature(snapshot)
53
+ except Exception:
54
+ return None
55
+ return {"hash": signature_hash(pairs), "shape": screen_shape(pairs),
56
+ "app": getattr(snapshot, "bundle_id", None) or None}
57
+
58
+
59
+ def target_of(element):
60
+ """A step's ``target`` from a state.Element."""
61
+ if element is None:
62
+ return None
63
+ return {"label": element.label or "", "role": element.role or "", "id": element.locator or None,
64
+ "frame": [round(float(value), 4) for value in element.rect]}
65
+
66
+
67
+ def describe(step):
68
+ if not isinstance(step, dict):
69
+ return "an unknown step"
70
+ if isinstance(step.get("purpose"), str) and step["purpose"].strip():
71
+ return step["purpose"].strip()
72
+ op = step.get("op") or "?"
73
+ verb = VERBS.get(op, op.title())
74
+ target = step.get("target") or {}
75
+ label = target.get("label") if isinstance(target, dict) else None
76
+ if op == "LAUNCH_APP":
77
+ return f"{verb} {step.get('app') or step.get('bundle') or 'the app'}"
78
+ if label:
79
+ return f'{verb} "{label}"'
80
+ if op in ELEMENT_OPS and isinstance(target, dict) and target.get("role"):
81
+ return f"{verb} the {target['role']}"
82
+ return verb
83
+
84
+
85
+ def anchor(step):
86
+ """What a heal diff shows for a step: op, label and role (no tree path, no frame)."""
87
+ target = step.get("target") if isinstance(step.get("target"), dict) else {}
88
+ out = {"op": step.get("op")}
89
+ if target.get("label"):
90
+ out["label"] = target["label"]
91
+ if target.get("role"):
92
+ out["role"] = target["role"]
93
+ if step.get("op") == "LAUNCH_APP" and step.get("app"):
94
+ out["app"] = step["app"]
95
+ if step.get("param"):
96
+ out["text"] = "{" + step["param"] + "}"
97
+ elif step.get("text") is not None:
98
+ out["text"] = step["text"]
99
+ return out
100
+
101
+
102
+ def _string(value, limit=500):
103
+ return isinstance(value, str) and len(value) <= limit
104
+
105
+
106
+ def valid_step(step):
107
+ """True for a step a replay can trust. A path with any invalid step is dropped as a whole."""
108
+ if not isinstance(step, dict):
109
+ return False
110
+ op = step.get("op")
111
+ if not isinstance(op, str):
112
+ return False
113
+ # A saved program's scroll to a blank's value (learned/programs.py) is kept, not live: the frontier runs it as its
114
+ # SCROLL_TO with the request's value. Nothing else replays it (Replayer.act refuses an op it can't run).
115
+ scroll = op == "SCROLL_TO" and _string(step.get("param"), 16) and step.get("target") is None
116
+ if op not in OPS and not scroll and not (step.get("live") is True and _string(op, 40) and op.isupper()):
117
+ return False
118
+ if step.get("app") is not None and not _string(step["app"], 255):
119
+ return False
120
+ target = step.get("target")
121
+ if target is not None:
122
+ if not isinstance(target, dict) or not _string(target.get("label", ""), 500) \
123
+ or not _string(target.get("role", ""), 500):
124
+ return False
125
+ if target.get("id") is not None and not _string(target["id"], 16000):
126
+ return False
127
+ frame = target.get("frame")
128
+ if frame is not None and (not isinstance(frame, list) or len(frame) != 4
129
+ or not all(isinstance(v, (int, float)) and 0 <= v <= 1.0001 for v in frame)):
130
+ return False
131
+ if op in ELEMENT_OPS and not step.get("live") and target is None:
132
+ return False
133
+ if op == "LAUNCH_APP" and not (_string(step.get("app"), 255) and "." in step["app"]):
134
+ return False
135
+ screen = step.get("screen")
136
+ if screen is not None:
137
+ if not isinstance(screen, dict) or not _string(screen.get("hash", ""), 64):
138
+ return False
139
+ signature = screen.get("signature")
140
+ if signature is not None and (not isinstance(signature, list) or len(signature) > SIGNATURE_LIMIT or not all(
141
+ isinstance(pair, list) and len(pair) == 2 and all(_string(v) for v in pair) for pair in signature)):
142
+ return False
143
+ shape = screen.get("shape")
144
+ if shape is not None and (not isinstance(shape, list) or len(shape) > SHAPE_LIMIT
145
+ or not all(_string(v, 16) for v in shape)):
146
+ return False
147
+ fingerprint = screen.get("fingerprint")
148
+ if fingerprint is not None and (not isinstance(fingerprint, str) or len(fingerprint) != 64
149
+ or any(char not in "0123456789abcdef" for char in fingerprint)):
150
+ return False
151
+ if step.get("text") is not None and not _string(step["text"], 4000):
152
+ return False
153
+ if step.get("param") is not None and not _string(step["param"], 40):
154
+ return False
155
+ if op in TEXT_OPS and not step.get("live") and step.get("text") is None and step.get("param") is None:
156
+ return False
157
+ point = step.get("point")
158
+ if point is not None and (not isinstance(point, list) or len(point) != 2
159
+ or not all(isinstance(v, (int, float)) and 0 <= v <= 1 for v in point)):
160
+ return False
161
+ if op in POINT_OPS and not step.get("live") and point is None:
162
+ return False
163
+ if step.get("commit") is not None and not _string(step["commit"], 40):
164
+ return False
165
+ # A taught step (learned/teach.py): what it is for, the person's rules for it, and other screens it was shown on.
166
+ if step.get("purpose") is not None and not _string(step["purpose"], 200):
167
+ return False
168
+ rules = step.get("rules")
169
+ if rules is not None and (not isinstance(rules, list) or len(rules) > 3 or not all(_string(r, 200) for r in rules)):
170
+ return False
171
+ # ``decide``: the person asked Mobster to choose this step each run (the model does it, by its purpose and rules).
172
+ if step.get("decide") is not None and not isinstance(step["decide"], bool):
173
+ return False
174
+ alts = step.get("alts")
175
+ if alts is not None and (not isinstance(alts, list) or len(alts) > MAX_ALTS or not all(
176
+ isinstance(alt, dict) and _alt_ok(step, alt) for alt in alts)):
177
+ return False
178
+ return True
179
+
180
+
181
+ def _alt_ok(step, alt):
182
+ op = alt.get("op")
183
+ if op in ("TAP", "TAP_XY", "LONG_PRESS"):
184
+ return valid_step({**step, **alt, "alts": None, "param": None, "text": None})
185
+ if op == step.get("op") and (op in SWIPES or op in TEXT_OPS):
186
+ return valid_step({**step, **alt, "alts": None})
187
+ return False
188
+
189
+
190
+ def validate(data):
191
+ """A stored path dict, checked; raises PathError. Returns it."""
192
+ if not isinstance(data, dict) or data.get("version") != VERSION:
193
+ raise PathError("not a replay path of this version")
194
+ steps = data.get("steps")
195
+ if not isinstance(steps, list) or not 0 < len(steps) <= MAX_STEPS:
196
+ raise PathError(f"a path has 1 to {MAX_STEPS} steps")
197
+ for index, step in enumerate(steps):
198
+ if not valid_step(step):
199
+ raise PathError(f"step {index + 1} is not a step a replay can trust")
200
+ final = data.get("final")
201
+ if final is not None and (not isinstance(final, dict) or not _string(final.get("hash", ""), 64)
202
+ or final.get("app") is not None and not _string(final["app"], 255)
203
+ or final.get("shape") is not None and (
204
+ not isinstance(final["shape"], list) or len(final["shape"]) > SHAPE_LIMIT
205
+ or not all(_string(v, 16) for v in final["shape"]))):
206
+ raise PathError("the final screen is not valid")
207
+ strict = data.get("strict", [])
208
+ if not isinstance(strict, list) or not all(isinstance(i, int) and not isinstance(i, bool) for i in strict):
209
+ raise PathError("strict must be a list of step numbers")
210
+ return data
211
+
212
+
213
+ def new_path(steps, *, kind, identity, env, final=None, source_run=None, created_at=None):
214
+ """A path dict from recorded ``steps``."""
215
+ import time
216
+ return {"version": VERSION, "kind": kind, "identity": identity, "env": dict(env), "steps": list(steps),
217
+ "final": final, "source_run": source_run, "created_at": created_at or round(time.time(), 3),
218
+ "strict": [], "replays": 0}
219
+
220
+
221
+ def first_run(run_id, seconds, cost_usd, at=None):
222
+ """What the run a path was first learned from took; ``costUsd`` is None when unreported, never a guess."""
223
+ import time
224
+ if not isinstance(seconds, (int, float)) or isinstance(seconds, bool) or not 0 <= seconds < 86_400:
225
+ return None
226
+ cost = cost_usd if isinstance(cost_usd, (int, float)) and not isinstance(cost_usd, bool) and cost_usd >= 0 else None
227
+ return {"runId": run_id if isinstance(run_id, str) else None, "seconds": round(float(seconds), 2),
228
+ "costUsd": round(float(cost), 5) if cost is not None else None, "at": round(at or time.time(), 3)}
229
+
230
+
231
+ def first_of(path, given=None):
232
+ kept = (path or {}).get("first") if isinstance(path, dict) else None
233
+ for value in (kept, given):
234
+ if isinstance(value, dict) and isinstance(value.get("seconds"), (int, float)):
235
+ return first_run(value.get("runId"), value["seconds"], value.get("costUsd"), value.get("at"))
236
+ return None
237
+
238
+
239
+ def replayable_count(path):
240
+ return sum(1 for step in path.get("steps") or () if not step.get("live"))