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,798 @@
1
+ """A run's tape: every call the agent made to the phone and to its model, in order, with what came back, so the run
2
+ can be replayed on today's code with no phone and no model.
3
+
4
+ The rule is DeepSeek Harness's, applied to a phone: everything the agent saw is on the tape. Recording wraps the
5
+ driver (``TapeDriver``) and the model client (``TapeClient``) at the two boundaries the loop already has. ``replay``
6
+ serves the tape back (``ReplayDriver``, ``ReplayClient``) on a virtual clock and runs FrontierAgent on the same
7
+ request. Code that would act differently on a recorded run stops the replay at the first action the recording didn't
8
+ send (a ``Divergence``), with the turn it happened in. Code that acts the same replays the whole run in seconds, for
9
+ free. That makes a recorded run a regression test for the loop, its gates and its target resolution.
10
+
11
+ Format: ``tape.jsonl``, one JSON object a line.
12
+
13
+ - ``{"kind": "header", "version", "request", "apps", "model", "frontier", "env", "capabilities", "t", "hooks"?}``
14
+ (``hooks``: what the recorded run had that a replay doesn't: approvals, steering, tools)
15
+ - ``{"kind": "call", "on", "name", "args", "t0", "t1", "result" | "error"}``: a driver method (``on`` is ``driver``, or
16
+ ``driver.frame_clock`` and so on for an object the driver hands out)
17
+ - ``{"kind": "value", "on", "name", "value"}``: a driver attribute the agent read
18
+ - ``{"kind": "model", "purpose", "prompt", "t0", "t1", "reply" | "error", "usage"}``: ``prompt`` is a hash of the
19
+ prompt's text (images left out), so a replay also says whether the model would have been asked the same thing
20
+ - ``{"kind": "end", "status", "answer", "steps", "actions"}``
21
+
22
+ What it holds: the request and the screens as data (labels, values, frames), the model's replies. A picture is kept
23
+ as its size only: a replayed model doesn't look. Benchmarks on simulators write it (bench/iosworld.py). A run in the
24
+ app or the terminal writes it too, when traces are on (traces.py): masked by the secret filter as it is written, in
25
+ a private file beside the run's frames.
26
+ """
27
+
28
+ import dataclasses
29
+ import gzip
30
+ import hashlib
31
+ import importlib
32
+ import inspect
33
+ import json
34
+ import os
35
+ import shutil
36
+ import sys
37
+ import threading
38
+ import time
39
+ import zipfile
40
+ from concurrent.futures import ThreadPoolExecutor
41
+ from contextlib import contextmanager
42
+ from pathlib import Path
43
+ from unittest import mock
44
+
45
+ from .. import state as state_module
46
+ from ..drivers import Driver
47
+ from ..transport import TransportError, error_name
48
+ from ..wda.guards import DriverRejection
49
+
50
+ VERSION = 1
51
+ NAME = "tape.jsonl" # gzipped to tape.jsonl.gz when the run ends
52
+ # Calls that change the phone: a replay sends these only in the recorded order, with the same arguments. Every other
53
+ # call (a read, a settle, tune, recover) is served from the tape by name.
54
+ ACTS = frozenset({"execute", "tap_point", "long_press", "set_value", "write_text", "replace_text", "clear_text",
55
+ "relaunch_app", "drag_next", "dismiss_at", "open_url", "launch"})
56
+ BASE = ("observe", "execute", "close", "can_type", "last_image")
57
+ _PIXEL = []
58
+
59
+
60
+ def pixel():
61
+ """A small grey JPEG: what a replay hands over for any picture the recording took."""
62
+ if not _PIXEL:
63
+ import io
64
+
65
+ from PIL import Image
66
+ out = io.BytesIO()
67
+ Image.new("RGB", (8, 8), (128, 128, 128)).save(out, "JPEG")
68
+ _PIXEL.append(out.getvalue())
69
+ return _PIXEL[0]
70
+
71
+
72
+ DATACLASSES = {cls.__name__: cls for cls in (state_module.Element, state_module.Snapshot)}
73
+ # Environment kept with the header: the switches that steer the loop, never a key.
74
+ SECRET_WORDS = ("KEY", "TOKEN", "SECRET", "PASSWORD")
75
+
76
+
77
+ class Divergence(BaseException):
78
+ """The replay asked for something the tape doesn't have: an action the recording didn't send, or a read past its
79
+ end. A BaseException, so the loop's own error handling can't turn it into a run that "failed"."""
80
+
81
+ def __init__(self, why, expected=None, got=None, index=None):
82
+ self.why, self.expected, self.got, self.index = why, expected, got, index
83
+ super().__init__(why)
84
+
85
+
86
+ def encode(value, depth=0):
87
+ """``value`` as JSON: dataclasses by name, bytes by size, an exception by type and message."""
88
+ if depth > 12:
89
+ return None
90
+ if isinstance(value, str) and value.startswith("data:image/"):
91
+ return {"__t": "bytes", "n": len(value)} # a picture as a data URL: its size only, like bytes
92
+ if value is None or isinstance(value, (bool, int, str)):
93
+ return value
94
+ if isinstance(value, float):
95
+ return value if value == value and abs(value) != float("inf") else None
96
+ if isinstance(value, (bytes, bytearray)):
97
+ return {"__t": "bytes", "n": len(value)}
98
+ if dataclasses.is_dataclass(value) and not isinstance(value, type) and type(value).__name__ in DATACLASSES:
99
+ return {"__t": type(value).__name__,
100
+ **{f.name: encode(getattr(value, f.name), depth + 1) for f in dataclasses.fields(value)}}
101
+ if isinstance(value, dict):
102
+ return {str(k): encode(v, depth + 1) for k, v in value.items()}
103
+ if isinstance(value, (list, tuple, set, frozenset)):
104
+ return [encode(v, depth + 1) for v in value]
105
+ return {"__t": "object", "type": type(value).__name__}
106
+
107
+
108
+ def _tupled(value):
109
+ return tuple(_tupled(v) for v in value) if isinstance(value, list) else value
110
+
111
+
112
+ def decode(value):
113
+ """What ``encode`` wrote, back as the objects the loop reads (lists stay lists outside dataclasses)."""
114
+ if isinstance(value, list):
115
+ return [decode(v) for v in value]
116
+ if not isinstance(value, dict):
117
+ return value
118
+ kind = value.get("__t")
119
+ if kind == "bytes":
120
+ return pixel()
121
+ if kind == "object":
122
+ return None
123
+ if kind in DATACLASSES:
124
+ cls = DATACLASSES[kind]
125
+ fields = {}
126
+ for f in dataclasses.fields(cls):
127
+ if f.name not in value:
128
+ continue
129
+ item = decode(value[f.name])
130
+ fields[f.name] = item if f.name == "elements" or not isinstance(item, list) else _tupled(item)
131
+ return cls(**fields)
132
+ return {k: decode(v) for k, v in value.items()}
133
+
134
+
135
+ def encode_error(error):
136
+ out = {"type": type(error).__name__, "module": type(error).__module__, "message": str(error)[:500]}
137
+ if isinstance(error, DriverRejection):
138
+ out["code"] = error.code
139
+ elif isinstance(error, TransportError) and (error.status is not None or error.wda_error is not None):
140
+ out.update(status=error.status, wda_error=error.wda_error)
141
+ return out
142
+
143
+
144
+ def decode_error(data):
145
+ """The recorded exception, as close to its class as can be rebuilt; else a RuntimeError naming it."""
146
+ if data.get("type") == "DriverRejection":
147
+ return DriverRejection(data.get("code"))
148
+ module, name, message = data.get("module") or "", data.get("type") or "", data.get("message") or ""
149
+ if module == "builtins" or module.startswith("mobile_agent."):
150
+ try:
151
+ cls = getattr(importlib.import_module(module), name)
152
+ if isinstance(cls, type) and issubclass(cls, Exception):
153
+ error = cls(message)
154
+ if isinstance(error, TransportError):
155
+ # Set after building, since a subclass's own __init__ (MirroringError, CableBtError) takes
156
+ # neither. A legacy record has neither, and reads as unproven.
157
+ status = data.get("status")
158
+ error.status = status if type(status) is int else None
159
+ error.wda_error = error_name({"error": data.get("wda_error")})
160
+ return error
161
+ except Exception:
162
+ pass
163
+ return RuntimeError(f"{name}: {message}")
164
+
165
+
166
+ def _element_key(value):
167
+ return {"label": value.label, "role": value.role} if isinstance(value, state_module.Element) else value
168
+
169
+
170
+ def act_key(name, args, kwargs):
171
+ """What makes two calls the same action: the method, its operation and target (by label and role), its text and
172
+ flags. Snapshots and timeouts are left out: the loop's own reads decide those."""
173
+ parts = [name]
174
+ for value in list(args) + [kwargs[k] for k in sorted(kwargs) if k not in ("timeout", "purpose")]:
175
+ if isinstance(value, state_module.Snapshot):
176
+ continue
177
+ if isinstance(value, float):
178
+ value = round(value, 3)
179
+ parts.append(encode(_element_key(value)))
180
+ return parts
181
+
182
+
183
+ def prompt_hash(messages):
184
+ """A hash of the prompt's text parts, in order (images left out)."""
185
+ digest = hashlib.sha256()
186
+ for message in messages or ():
187
+ content = message.get("content")
188
+ parts = [content] if isinstance(content, str) else content or ()
189
+ for part in parts:
190
+ text = part if isinstance(part, str) else part.get("text") if isinstance(part, dict) else None
191
+ if isinstance(text, str):
192
+ digest.update(text.encode("utf-8", "replace"))
193
+ return digest.hexdigest()[:16]
194
+
195
+
196
+ def purpose_of(schema):
197
+ """A model call's purpose: its reply schema's property names (a decision has ``actions``, a contract ``items``)."""
198
+ return ",".join(sorted((schema or {}).get("properties") or {}))
199
+
200
+
201
+ def switches(env=None):
202
+ env = os.environ if env is None else env
203
+ return {k: v for k, v in sorted(env.items())
204
+ if k.startswith("MOBSTER_") and not any(word in k for word in SECRET_WORDS)}
205
+
206
+
207
+ class Tape:
208
+ """The writer: one JSONL file, appended under a lock (the verify read and the contract compile run on their own
209
+ threads). A write that fails stops the tape, never the run. The file is private (0600) and stops growing at
210
+ MAX_BYTES.
211
+
212
+ ``scrub(entry)`` rewrites each entry before it is written (traces.py passes the secret filter, which costs tens of
213
+ milliseconds on a screen). With one, the entries go to a single worker thread in order, so the agent never waits
214
+ for it, and ``close`` waits for the queue; without one, each write is made before the call returns, as a
215
+ benchmark's tape needs to survive a crash."""
216
+
217
+ MAX_BYTES = 32_000_000
218
+
219
+ def __init__(self, path, scrub=None):
220
+ self.path = Path(path)
221
+ self.scrub = scrub
222
+ self._worker = ThreadPoolExecutor(max_workers=1, thread_name_prefix="mobster-tape") if scrub else None
223
+ self._lock = threading.Lock()
224
+ self._file = None
225
+ self._screens = set()
226
+ self._size = 0
227
+ self.broken = None
228
+
229
+ def _shared(self, result):
230
+ """A screen read again unchanged is written once: later reads refer to it by hash, with their own times."""
231
+ if not (isinstance(result, dict) and result.get("__t") == "Snapshot"):
232
+ return result
233
+ times = {k: result.get(k) for k in ("captured_at", "read_started")}
234
+ body = {k: v for k, v in result.items() if k not in times}
235
+ digest = hashlib.sha256(json.dumps(body, sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:20]
236
+ with self._lock:
237
+ seen = digest in self._screens
238
+ self._screens.add(digest)
239
+ return {"__t": "ref", "h": digest, **times} if seen else {**result, "__h": digest}
240
+
241
+ def write(self, entry):
242
+ if self._worker is None:
243
+ self._write(entry)
244
+ return
245
+ try:
246
+ self._worker.submit(self._write, entry)
247
+ except RuntimeError:
248
+ pass # a late call (a hedged one that lost) after the tape ended
249
+
250
+ def _write(self, entry):
251
+ if self.broken:
252
+ return
253
+ try:
254
+ if self.scrub is not None:
255
+ entry = self.scrub(entry)
256
+ if entry.get("kind") == "call" and "result" in entry:
257
+ entry = {**entry, "result": self._shared(entry["result"])}
258
+ line = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
259
+ with self._lock:
260
+ self._size += len(line) + 1
261
+ if self._size > self.MAX_BYTES:
262
+ self.broken = "the tape passed its size limit"
263
+ return
264
+ if self._file is None:
265
+ self.path.parent.mkdir(parents=True, exist_ok=True)
266
+ # a line a write: a crash keeps the tape
267
+ descriptor = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW, 0o600)
268
+ self._file = os.fdopen(descriptor, "a", encoding="utf-8", buffering=1)
269
+ self._file.write(line + "\n")
270
+ except Exception as error: # noqa: BLE001
271
+ self.broken = f"{type(error).__name__}: {error}"
272
+
273
+ def header(self, *, request, apps, model, frontier, driver, env=None, hooks=()):
274
+ kinds = capabilities(driver)
275
+ values = {name: encode(getattr(driver, name, None)) for name, kind in kinds.items() if kind == "value"}
276
+ self.write({"kind": "header", "version": VERSION, "request": request, "apps": dict(apps or {}),
277
+ "model": model, "frontier": encode(frontier), "env": switches(env), "capabilities": kinds,
278
+ "values": values, "t": time.monotonic(), **({"hooks": sorted(hooks)} if hooks else {})})
279
+
280
+ def end(self, outcome):
281
+ """The run's end, then the tape gzipped (tape.jsonl.gz): a tape that never ends stays plain."""
282
+ outcome = outcome or {}
283
+ self.write({"kind": "end", "status": outcome.get("status"), "answer": encode(outcome.get("answer")),
284
+ "steps": outcome.get("steps"), "actions": outcome.get("actions")})
285
+ self.close()
286
+ if self.broken or not self.path.exists():
287
+ return
288
+ try:
289
+ descriptor = os.open(str(self.path) + ".gz", os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600)
290
+ with open(self.path, "rb") as plain, os.fdopen(descriptor, "wb") as raw, \
291
+ gzip.GzipFile(fileobj=raw, mode="wb", compresslevel=6) as packed:
292
+ shutil.copyfileobj(plain, packed)
293
+ self.path.unlink()
294
+ except OSError as error:
295
+ self.broken = f"{type(error).__name__}: {error}"
296
+
297
+ def close(self):
298
+ if self._worker is not None:
299
+ self._worker.shutdown(wait=True)
300
+ with self._lock:
301
+ if self._file is not None:
302
+ self._file.close()
303
+ self._file = None
304
+
305
+
306
+ def capabilities(driver):
307
+ """{name: "call" | "value" | "object"} for the base contract and every Driver.OPTIONAL name ``driver`` has."""
308
+ out = {}
309
+ for name in (*BASE, *Driver.OPTIONAL):
310
+ try:
311
+ value = getattr(driver, name)
312
+ except Exception: # noqa: BLE001
313
+ continue
314
+ # Present means the attribute exists, set or not: settled_by is None until the first settle.
315
+ out[name] = ("call" if callable(value) else "object" if name == "frame_clock" and value is not None
316
+ else "value")
317
+ return out
318
+
319
+
320
+ class _Recorded:
321
+ """A proxy that writes each call and attribute read of ``inner`` to the tape."""
322
+
323
+ _OBJECTS = frozenset({"frame_clock", "video"})
324
+
325
+ def __init__(self, inner, tape, on):
326
+ object.__setattr__(self, "_inner", inner)
327
+ object.__setattr__(self, "_tape", tape)
328
+ object.__setattr__(self, "_on", on)
329
+
330
+ def __getattr__(self, name):
331
+ value = getattr(self._inner, name)
332
+ if name.startswith("__"):
333
+ return value
334
+ if callable(value) and not isinstance(value, type):
335
+ return self._call(name, value)
336
+ if name in self._OBJECTS and value is not None:
337
+ return _Recorded(value, self._tape, f"{self._on}.{name}")
338
+ self._tape.write({"kind": "value", "on": self._on, "name": name, "value": encode(value)})
339
+ return value
340
+
341
+ def __setattr__(self, name, value):
342
+ setattr(self._inner, name, value)
343
+
344
+ def _call(self, name, method):
345
+ tape, on = self._tape, self._on
346
+
347
+ def call(*args, **kwargs):
348
+ entry = {"kind": "call", "on": on, "name": name, "args": act_key(name, args, kwargs),
349
+ "thread": threading.current_thread().name, "t0": time.monotonic()}
350
+ try:
351
+ result = method(*args, **kwargs)
352
+ except BaseException as error:
353
+ entry.update(t1=time.monotonic(), error=encode_error(error))
354
+ tape.write(entry)
355
+ raise
356
+ entry.update(t1=time.monotonic(), result=encode(result))
357
+ tape.write(entry)
358
+ return result
359
+
360
+ return call
361
+
362
+
363
+ class TapeDriver(_Recorded):
364
+ """``driver`` with every call and read on ``tape``."""
365
+
366
+ def __init__(self, driver, tape):
367
+ super().__init__(driver, tape, "driver")
368
+ # Declared on the proxy: reading it must not write a tape entry (the header holds the set).
369
+ object.__setattr__(self, "capabilities", driver.capabilities)
370
+
371
+ # The abstract three, spelled out so the proxy is a complete driver even to code that looks them up on the class.
372
+ def observe(self, timeout=10):
373
+ return self.__getattr__("observe")(timeout=timeout)
374
+
375
+ def execute(self, operation, target, snapshot, text=None, timeout=10):
376
+ return self.__getattr__("execute")(operation, target, snapshot, text=text, timeout=timeout)
377
+
378
+ def close(self):
379
+ return self._inner.close()
380
+
381
+
382
+ class TapeClient:
383
+ """The model client with every call on ``tape``: its purpose, a hash of the prompt's text, the reply or the
384
+ error, and the client's usage after it (hedges included), so a replay's cost is the recording's."""
385
+
386
+ def __init__(self, client, tape):
387
+ object.__setattr__(self, "_inner", client)
388
+ object.__setattr__(self, "_tape", tape)
389
+
390
+ def __getattr__(self, name):
391
+ return getattr(self._inner, name)
392
+
393
+ def __setattr__(self, name, value):
394
+ setattr(self._inner, name, value)
395
+
396
+ def complete(self, messages, schema, **kwargs):
397
+ entry = {"kind": "model", "purpose": purpose_of(schema), "prompt": prompt_hash(messages),
398
+ "t0": time.monotonic()}
399
+ try:
400
+ reply, usage = self._inner.complete(messages, schema, **kwargs)
401
+ except BaseException as error:
402
+ entry.update(t1=time.monotonic(), error=encode_error(error),
403
+ usage=encode(dict(getattr(self._inner, "usage", None) or {})))
404
+ self._tape.write(entry)
405
+ raise
406
+ entry.update(t1=time.monotonic(), reply=encode(reply), call_usage=encode(usage),
407
+ usage=encode(dict(getattr(self._inner, "usage", None) or {})))
408
+ self._tape.write(entry)
409
+ return reply, usage
410
+
411
+
412
+ BUNDLE_TAPE_LIMIT = 200_000_000 # unpacked bytes: a bundle is a person's own, but a zip can lie about its size
413
+
414
+
415
+ def read(path):
416
+ """The tape's text from a tape.jsonl, a tape.jsonl.gz, a folder holding one, or a trace bundle (traces.py: a zip
417
+ with tape.jsonl.gz in it). ValueError when a bundle holds no tape."""
418
+ path = Path(path)
419
+ if path.is_dir():
420
+ path = path / NAME if (path / NAME).exists() else path / (NAME + ".gz")
421
+ if path.suffix == ".zip":
422
+ try:
423
+ with zipfile.ZipFile(path) as archive:
424
+ info = archive.getinfo(NAME + ".gz")
425
+ if info.file_size > BUNDLE_TAPE_LIMIT:
426
+ raise ValueError(f"{path}: the bundle's tape is too large")
427
+ packed = archive.read(info)
428
+ except (KeyError, zipfile.BadZipFile):
429
+ raise ValueError(f"{path} holds no tape (the task ran without MOBSTER_EXPERIMENTAL_TRACES=1)") from None
430
+ return gzip.decompress(packed).decode("utf-8")
431
+ if path.suffix == ".gz":
432
+ return gzip.decompress(path.read_bytes()).decode("utf-8")
433
+ return path.read_text(encoding="utf-8")
434
+
435
+
436
+ def load(path):
437
+ """The tape's entries; ValueError when it has no header."""
438
+ path = Path(path)
439
+ text = read(path)
440
+ entries = [json.loads(line) for line in text.splitlines() if line.strip()]
441
+ if not entries or entries[0].get("kind") != "header":
442
+ raise ValueError(f"{path} is not a tape (no header)")
443
+ screens = {}
444
+ for entry in entries:
445
+ result = entry.get("result")
446
+ if isinstance(result, dict) and result.get("__t") == "Snapshot" and "__h" in result:
447
+ screens[result.pop("__h")] = result
448
+ elif isinstance(result, dict) and result.get("__t") == "ref":
449
+ shared = screens.get(result.get("h"))
450
+ if shared is None:
451
+ raise ValueError(f"{path}: a screen refers to one the tape doesn't hold")
452
+ entry["result"] = {**shared, "captured_at": result.get("captured_at"),
453
+ "read_started": result.get("read_started")}
454
+ return entries
455
+
456
+
457
+ class Clock:
458
+ """The replay's time: the recording's monotonic clock, moved to each served call's end; sleeping moves it."""
459
+
460
+ def __init__(self, start):
461
+ self.now = float(start)
462
+ self._lock = threading.Lock()
463
+
464
+ def reach(self, t):
465
+ if isinstance(t, (int, float)):
466
+ with self._lock:
467
+ self.now = max(self.now, float(t))
468
+
469
+ def monotonic(self):
470
+ return self.now
471
+
472
+ def sleep(self, seconds):
473
+ with self._lock:
474
+ self.now += max(0.0, float(seconds or 0))
475
+
476
+
477
+ class _VirtualTime:
478
+ """The ``time`` module as the loop sees it in a replay: monotonic and sleep from the Clock, the rest real."""
479
+
480
+ def __init__(self, clock):
481
+ self._clock = clock
482
+
483
+ def monotonic(self):
484
+ return self._clock.monotonic()
485
+
486
+ def sleep(self, seconds):
487
+ self._clock.sleep(seconds)
488
+
489
+ def __getattr__(self, name):
490
+ return getattr(time, name)
491
+
492
+
493
+ class Player:
494
+ """The tape served back: acts in order and compared, reads by name in order, values by name (the last one again
495
+ once they run out), model replies by purpose in order."""
496
+
497
+ def __init__(self, entries):
498
+ self.header = entries[0]
499
+ self.clock = Clock(self.header.get("t") or 0.0)
500
+ self.acts, self.reads, self.values, self.models = [], {}, {}, {}
501
+ self.decisions = []
502
+ self.end = next((e for e in entries if e.get("kind") == "end"), None)
503
+ self.recorded_kinds = {}
504
+ for entry in entries[1:]:
505
+ kind = entry.get("kind")
506
+ if kind == "call":
507
+ key = (entry["on"], entry["name"])
508
+ self.recorded_kinds[key] = "call"
509
+ if self._is_act(entry["name"], entry.get("args")):
510
+ self.acts.append(entry)
511
+ else:
512
+ self.reads.setdefault(key, []).append(entry)
513
+ elif kind == "value":
514
+ key = (entry["on"], entry["name"])
515
+ self.recorded_kinds.setdefault(key, "value")
516
+ self.values.setdefault(key, []).append(entry)
517
+ elif kind == "model":
518
+ self.models.setdefault(entry.get("purpose") or "", []).append(entry)
519
+ if "actions" in (entry.get("purpose") or "").split(","):
520
+ self.decisions.append(entry.get("t1") or 0) # when each decision came back, for ``steps_of``
521
+ self.acts_done = 0
522
+ self.prompts_same = self.prompts_seen = 0
523
+ self._served_values = {}
524
+ self._lock = threading.Lock()
525
+
526
+ @staticmethod
527
+ def _is_act(name, args):
528
+ if name in ACTS:
529
+ return True
530
+ # A raw WebDriverAgent request acts unless it's a GET.
531
+ return name == "call" and bool(args) and len(args) > 1 and str(args[1]).upper() != "GET"
532
+
533
+ def _answer(self, entry):
534
+ self.clock.reach(entry.get("t1"))
535
+ if "error" in entry:
536
+ raise decode_error(entry["error"])
537
+ return decode(entry.get("result"))
538
+
539
+ def call(self, on, name, args, kwargs):
540
+ key = act_key(name, args, kwargs)
541
+ with self._lock:
542
+ if self._is_act(name, key):
543
+ index = self.acts_done
544
+ if index >= len(self.acts):
545
+ raise Divergence("an action the recording never sent", None, key, index)
546
+ expected = self.acts[index]
547
+ if expected["on"] != on or expected["args"] != json.loads(json.dumps(key)):
548
+ raise Divergence("a different action", expected["args"], key, index)
549
+ self.acts_done += 1
550
+ entry = expected
551
+ else:
552
+ queue = self.reads.get((on, name))
553
+ if not queue:
554
+ raise Divergence(f"a read the recording didn't make ({on}.{name})", None, key, self.acts_done)
555
+ entry = queue.pop(0)
556
+ return self._answer(entry)
557
+
558
+ def value(self, on, name):
559
+ with self._lock:
560
+ queue = self.values.get((on, name))
561
+ if queue:
562
+ self._served_values[(on, name)] = queue.pop(0)
563
+ entry = self._served_values.get((on, name))
564
+ if entry is None:
565
+ raise AttributeError(name)
566
+ return decode(entry.get("value"))
567
+
568
+ def model(self, messages, schema):
569
+ purpose = purpose_of(schema)
570
+ with self._lock:
571
+ queue = self.models.get(purpose)
572
+ if not queue:
573
+ raise Divergence(f"a model call the recording didn't make ({purpose or 'no schema'})", None,
574
+ purpose, self.acts_done)
575
+ entry = queue.pop(0)
576
+ self.prompts_seen += 1
577
+ self.prompts_same += prompt_hash(messages) == entry.get("prompt")
578
+ self.clock.reach(entry.get("t1"))
579
+ if "error" in entry:
580
+ raise decode_error(entry["error"])
581
+ return decode(entry.get("reply")), decode(entry.get("call_usage") or {}), decode(entry.get("usage") or {})
582
+
583
+
584
+ class _Served:
585
+ """The replayed side of an object the driver handed out (``on`` names it)."""
586
+
587
+ def __init__(self, player, on):
588
+ object.__setattr__(self, "_player", player)
589
+ object.__setattr__(self, "_on", on)
590
+
591
+ def __getattr__(self, name):
592
+ if name.startswith("__"):
593
+ raise AttributeError(name)
594
+ on, player = self._on, self._player
595
+ kind = player.recorded_kinds.get((on, name))
596
+ if kind == "call":
597
+ return lambda *args, **kwargs: player.call(on, name, args, kwargs)
598
+ if (on, name) in player.values or (on, name) in player._served_values:
599
+ return player.value(on, name)
600
+ if any(key[0] == f"{on}.{name}" for key in player.recorded_kinds):
601
+ return _Served(player, f"{on}.{name}")
602
+ raise AttributeError(name)
603
+
604
+ def __setattr__(self, name, value):
605
+ pass # what the loop sets on a driver changes nothing a replay serves
606
+
607
+
608
+ class ReplayDriver(_Served):
609
+ """The recorded driver: the capabilities it had, its reads and its answers."""
610
+
611
+ def __init__(self, player):
612
+ super().__init__(player, "driver")
613
+ kinds = dict(player.header.get("capabilities") or {})
614
+ object.__setattr__(self, "_kinds", kinds)
615
+ object.__setattr__(self, "capabilities", frozenset(kinds).intersection(Driver.OPTIONAL))
616
+
617
+ def __getattr__(self, name):
618
+ kind = self._kinds.get(name)
619
+ if kind is None and not name.startswith("__"):
620
+ raise AttributeError(name)
621
+ if kind == "call":
622
+ player = self._player
623
+ return lambda *args, **kwargs: player.call("driver", name, args, kwargs)
624
+ if kind == "object":
625
+ return _Served(self._player, f"driver.{name}")
626
+ try:
627
+ return self._player.value("driver", name)
628
+ except AttributeError:
629
+ return decode((self._player.header.get("values") or {}).get(name))
630
+
631
+ def observe(self, timeout=10):
632
+ return self._player.call("driver", "observe", (), {"timeout": timeout})
633
+
634
+ def execute(self, operation, target, snapshot, text=None, timeout=10):
635
+ return self._player.call("driver", "execute", (operation, target, snapshot), {"text": text,
636
+ "timeout": timeout})
637
+
638
+ def close(self):
639
+ pass
640
+
641
+
642
+ class ReplayClient:
643
+ """The recorded model: each call answered with the recorded reply for its purpose, usage as it was after it."""
644
+
645
+ per_call_reasoning = True
646
+ cancelled = None
647
+ on_hedge_usage = None
648
+
649
+ def __init__(self, player):
650
+ self.player = player
651
+ self.model = player.header.get("model") or ""
652
+ self.reasoning = "low"
653
+ self.usage = {"prompt_tokens": 0, "completion_tokens": 0, "cached_tokens": 0, "cache_write_tokens": 0,
654
+ "calls": 0}
655
+
656
+ def complete(self, messages, schema, **kwargs):
657
+ reply, call_usage, usage = self.player.model(messages, schema)
658
+ if usage:
659
+ self.usage.clear()
660
+ self.usage.update(usage)
661
+ return reply, call_usage
662
+
663
+
664
+ @dataclasses.dataclass
665
+ class Report:
666
+ tape: str
667
+ request: str
668
+ recorded_acts: int
669
+ replayed_acts: int
670
+ divergence: dict = None # {"why", "expected", "got", "index", "step"} or None
671
+ recorded_status: str = None
672
+ replayed_status: str = None
673
+ same_answer: bool = None
674
+ prompts_same: int = 0
675
+ prompts_seen: int = 0
676
+ seconds: float = 0.0
677
+ error: str = None
678
+ steps: list = dataclasses.field(default_factory=list) # [{"index", "turn", "act", "now"}], see ``steps_of``
679
+ hooks: list = dataclasses.field(default_factory=list) # what the live run had that a replay doesn't
680
+
681
+ @property
682
+ def identical(self):
683
+ return (self.divergence is None and self.error is None and self.replayed_acts == self.recorded_acts
684
+ and self.recorded_status == self.replayed_status)
685
+
686
+ def public(self):
687
+ return {**dataclasses.asdict(self), "identical": self.identical}
688
+
689
+
690
+ # Modules whose ``time`` a replay swaps for the virtual one: the loop and the code it calls between two driver calls.
691
+ # A package stands for every module in it, so a new frontier module is on the clock without being listed.
692
+ # wda.guards holds the stream-still check (``unchanged_since``) the refresh before an action compares with a
693
+ # read's start: on the host's clock it reads now as hours after the recording and makes reads the recording didn't.
694
+ TIMED = ("frontier", "contract", "effect_ledger", "wda.system_dialogs", "wda.guards")
695
+
696
+
697
+ def _timed_modules():
698
+ for name in TIMED:
699
+ try:
700
+ importlib.import_module(f"mobile_agent.{name}")
701
+ except Exception: # noqa: BLE001
702
+ continue
703
+ names = tuple(f"mobile_agent.{name}" for name in TIMED)
704
+ return [module for key, module in sorted(sys.modules.items())
705
+ if module is not None and (key in names or key.startswith(tuple(n + "." for n in names)))]
706
+
707
+
708
+ @contextmanager
709
+ def _virtual_time(clock):
710
+ shim = _VirtualTime(clock)
711
+ patches = []
712
+ for module in _timed_modules():
713
+ if getattr(module, "time", None) is time:
714
+ patches.append(mock.patch.object(module, "time", shim))
715
+ for patch in patches:
716
+ patch.start()
717
+ try:
718
+ yield
719
+ finally:
720
+ for patch in reversed(patches):
721
+ patch.stop()
722
+
723
+
724
+ def act_words(args):
725
+ """A recorded action's key in words: the method, then its target's label and its text."""
726
+ parts = [str(args[0])] if args else []
727
+ for item in args[1:]:
728
+ if isinstance(item, dict) and "label" in item:
729
+ parts.append(repr(item["label"]))
730
+ elif isinstance(item, (str, int, float)) and not isinstance(item, bool):
731
+ parts.append(str(item)[:60])
732
+ return " ".join(parts)
733
+
734
+
735
+ def steps_of(player, divergence):
736
+ """The recorded actions one by one against the replay: ``now`` is "same" for each the replay sent as recorded,
737
+ "differs: ..." at the one it didn't (the replay stops there; a read or a model call the recording never made says
738
+ so), "never asked" for an action the replay ended before, and "not reached" for the rest. An action the recording
739
+ never sent comes last, as a row of its own. ``turn`` counts the decisions the recording took before the action."""
740
+ differs = None
741
+ if divergence is not None:
742
+ asked = divergence["got"] and divergence["why"] in ("a different action", "an action the recording never sent")
743
+ differs = "differs: " + (act_words(divergence["got"]) if asked else divergence["why"])
744
+ out = []
745
+ for index, act in enumerate(player.acts):
746
+ if index < player.acts_done:
747
+ now = "same"
748
+ elif divergence is not None and index == divergence["index"]:
749
+ now = differs
750
+ else:
751
+ now = "not reached" if divergence is not None else "never asked"
752
+ turn = sum(1 for t1 in player.decisions if t1 <= (act.get("t0") or 0)) or None
753
+ out.append({"index": index, "turn": turn, "act": act_words(act.get("args") or ()), "now": now})
754
+ if divergence is not None and divergence["index"] >= len(player.acts):
755
+ step = divergence.get("step")
756
+ out.append({"index": len(player.acts), "turn": None if step is None else step + 1,
757
+ "act": "(the recording sent none)", "now": differs})
758
+ return out
759
+
760
+
761
+ def build_agent(driver, client, header, emit):
762
+ """FrontierAgent as the recording built it: the same apps and limits, the engine's switches as they were."""
763
+ from ..frontier import FrontierAgent
764
+ frontier = dict(decode(header.get("frontier")) or {})
765
+ accepted = set(inspect.signature(FrontierAgent.__init__).parameters)
766
+ return FrontierAgent(driver, client, apps=dict(header.get("apps") or {}), emit=emit,
767
+ **{k: v for k, v in frontier.items() if k in accepted})
768
+
769
+
770
+ def replay(path, *, build=build_agent):
771
+ """Replay the tape at ``path`` (a file, or a folder holding tape.jsonl) on today's code; a ``Report``."""
772
+ entries = load(path)
773
+ player = Player(entries)
774
+ header = player.header
775
+ events = []
776
+ report = Report(str(path), header.get("request") or "", recorded_acts=len(player.acts), replayed_acts=0,
777
+ recorded_status=(player.end or {}).get("status"))
778
+ started = time.monotonic()
779
+ env = {**{k: v for k, v in os.environ.items() if not k.startswith("MOBSTER_")}, **(header.get("env") or {})}
780
+ with mock.patch.dict(os.environ, env, clear=True), _virtual_time(player.clock):
781
+ agent = build(ReplayDriver(player), ReplayClient(player), header, events.append)
782
+ try:
783
+ outcome = agent.run(header.get("request") or "")
784
+ report.replayed_status = outcome.get("status")
785
+ recorded_answer = decode((player.end or {}).get("answer"))
786
+ report.same_answer = outcome.get("answer") == recorded_answer
787
+ except Divergence as divergence:
788
+ step = next((e.get("step") for e in reversed(events) if e.get("event") == "frontier_decision"), None)
789
+ report.divergence = {"why": divergence.why, "expected": divergence.expected, "got": divergence.got,
790
+ "index": divergence.index, "step": step}
791
+ except Exception as error: # noqa: BLE001
792
+ report.error = f"{type(error).__name__}: {error}"[:300]
793
+ report.replayed_acts = player.acts_done
794
+ report.steps = steps_of(player, report.divergence)
795
+ report.hooks = list(header.get("hooks") or ())
796
+ report.prompts_same, report.prompts_seen = player.prompts_same, player.prompts_seen
797
+ report.seconds = round(time.monotonic() - started, 3)
798
+ return report