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,892 @@
1
+ """The iPhone tools an outside agent gets over MCP while it runs a Mobster task (ai_plans)."""
2
+
3
+ import base64
4
+ import json
5
+ import math
6
+ import re
7
+ import threading
8
+ import time
9
+ from types import SimpleNamespace
10
+
11
+ from .. import offers
12
+ from ..mcp_server.protocol import ACTS, READ_ONLY, ToolResult
13
+ from ..wda.point_gestures import DRAG_SCHEMA, FLICK_SCHEMA, GESTURE_OPS, HOLD_SCHEMA, validate_gesture_request
14
+
15
+ NAMES = ("screen", "act", "tap", "tap_point", "drag_point", "hold_point", "flick_point", "type_text", "swipe",
16
+ "long_press", "open_app", "home", "read_list", "scroll_to",
17
+ "dismiss", "open_url", "ask_user", "wait")
18
+ CHAINABLE = ("tap", "type_text", "swipe", "long_press", "open_app", "home", "scroll_to", "dismiss", "wait")
19
+ MAX_CHAIN = 8
20
+ COMMIT_OPS = frozenset({"TAP", "LONG_PRESS", "SUBMIT", "TYPE_SUBMIT"})
21
+ SWIPES = {"up": "SWIPE_UP", "down": "SWIPE_DOWN", "left": "SWIPE_LEFT", "right": "SWIPE_RIGHT"}
22
+ ELEMENT_ID = re.compile(r"e[0-9]{1,4}")
23
+ WAIT_MAX = 10.0
24
+ QUESTION_CHARS, CHOICE_CHARS = 300, 120
25
+ LIST_ROWS_SHOWN = 80
26
+ # Facts a screen can show word for word: amount, version, time, percentage, code, number.
27
+ FACT = re.compile(r"\$\s?\d[\d,]*(?:\.\d+)?|\b\d+(?:[.:]\d+)+\b|\b\d+(?:\.\d+)?%|\b[A-Z]{2,}-?\d{3,}\b|\b\d[\d,]*\b")
28
+ PROOF_ITEMS = 12
29
+
30
+
31
+ def step_name(step):
32
+ """A chain step as the agent wrote it: the tool and what it names. Never the text a step types."""
33
+ tool = step.get("tool") if isinstance(step, dict) else None
34
+ if not isinstance(tool, str) or not tool:
35
+ return "a step with no tool" # an unrun step is never checked, so it can be anything the agent sent
36
+ keys = ("label", "element", "app", "direction") + (("text",) if tool == "scroll_to" else ())
37
+ target = next((step[k] for k in keys if isinstance(step.get(k), str) and step[k]), None)
38
+ return f"{tool} {json.dumps(target)}" if target else str(tool)
39
+
40
+
41
+ class ToolProblem(Exception):
42
+ """A tool error: one plain sentence telling the agent what to do next (the result's isError)."""
43
+
44
+
45
+ class RunStopped(ToolProblem):
46
+ """The person stopped the task, or the phone guard ended it: no more actions."""
47
+
48
+
49
+ def frame_bytes(driver):
50
+ """The screen as JPEG bytes for the step list."""
51
+ from ..frontier.screen import video_frame
52
+ data = video_frame(driver)
53
+ if data is None:
54
+ capture = getattr(driver, "capture_preview", None)
55
+ if callable(capture) and not getattr(driver, "ax_only_steps", 0):
56
+ try:
57
+ data = capture(timeout=5)
58
+ except Exception:
59
+ data = None
60
+ if isinstance(data, str):
61
+ try:
62
+ data = base64.b64decode(data.split(",", 1)[1] if data.startswith("data:") else data)
63
+ except (ValueError, TypeError):
64
+ return None
65
+ return bytes(data) if isinstance(data, (bytes, bytearray)) and data else None
66
+
67
+
68
+ def has_text(element):
69
+ """Whether a field holds text of its own (not only its placeholder)."""
70
+ value = " ".join(str(element.value or "").split())
71
+ return bool(value) and value not in (" ".join(str(element.placeholder or "").split()),
72
+ " ".join(str(element.label or "").split()))
73
+
74
+
75
+ class PhoneTools:
76
+ """The tools of one run."""
77
+
78
+ def __init__(self, driver, *, apps, goal, emit, approve=None, ask=None, cancelled=None, guard=None,
79
+ build=None, remembered=None, permissions):
80
+ from .. import engines
81
+ self.driver, self.apps, self.goal = driver, dict(apps or {}), goal or ""
82
+ self.out = emit or (lambda event: None)
83
+ self.approve, self.ask = approve, ask
84
+ self.permissions = permissions
85
+ # the person's "Don't ask again" list (permissions.exceptions.Exceptions.contains), or None
86
+ self.remembered = remembered
87
+ self.cancelled = cancelled or (lambda: False)
88
+ build = build or engines.build_frontier
89
+ kwargs = {"guard": guard} if guard is not None else {}
90
+ # No model: only the frontier's reads, actuation, macros, masking and guard are used.
91
+ self.agent = build(driver, None, apps=self.apps, emit=self._trace, cancelled=self.cancelled, **kwargs)
92
+ self.lock = threading.RLock()
93
+ self.snapshot, self.aliases = None, {}
94
+ self.point_image = None # the screenshot the agent last saw: a point it names belongs to this picture
95
+ self.steps = 0
96
+ self.actions = 0
97
+ self.stopped = None # the GuardVerdict that ended the run, once one did
98
+ self.typed = None # (app, text) last typed into a composer: what a Send tap sends, for its card
99
+ # Every screen shown to the agent, as the contract records them,
100
+ # and how many actions may have changed something.
101
+ from ..contract import Contract
102
+ self.ledger = Contract([], self.goal)
103
+ self.changes = 0
104
+ self.approved_send = None # an approved Return that may not have sent: its Send tap isn't asked again
105
+ self.tuned = False
106
+
107
+ def proof(self, answer):
108
+ """The result's proof (smart_events.final_proof's shape) for a run that changed nothing."""
109
+ from ..contract_screen import plain, quoted
110
+ if self.changes or not answer:
111
+ return []
112
+ facts = list(dict.fromkeys([plain(fact) for fact in FACT.findall(answer)] +
113
+ [plain(name) for name in quoted(answer)]))
114
+ if not facts:
115
+ return []
116
+ proof, rows_used = [], set()
117
+ for fact in facts:
118
+ pattern = re.compile(r"(?<![\w.:$])" + re.escape(fact) + r"(?![\w]|[.:]\d)")
119
+ found = next(((step, app, title, row, original)
120
+ for step, app, title, originals in reversed(self.ledger.shown)
121
+ for row, original in originals.items() if pattern.search(row)), None)
122
+ if found is None:
123
+ return []
124
+ step, app, title, row, original = found
125
+ if (step, row) not in rows_used:
126
+ rows_used.add((step, row))
127
+ proof.append({"quote": original, "app": app, "screen": title or app, "step": step})
128
+ return proof[:PROOF_ITEMS]
129
+
130
+ @staticmethod
131
+ def definitions():
132
+ return [{"name": name, "description": DESCRIPTIONS[name], "inputSchema": SCHEMAS[name],
133
+ "annotations": ANNOTATIONS[name]} for name in NAMES]
134
+
135
+ def call(self, name, arguments):
136
+ """Run tool ``name``. Returns a ToolResult; a problem is one too, with is_error set."""
137
+ if name == "act":
138
+ return self._act_call(arguments if isinstance(arguments, dict) else {})
139
+ handler = getattr(self, f"tool_{name}", None) if name in NAMES else None
140
+ if handler is None:
141
+ return ToolResult(f"Mobster has no tool named {str(name)[:60]}.", is_error=True)
142
+ arguments = arguments if isinstance(arguments, dict) else {}
143
+ from ..frontier import GuardStop
144
+ try:
145
+ with self.lock:
146
+ self._check_running()
147
+ return handler(arguments)
148
+ except GuardStop as error: # a BaseException: past the handler below
149
+ self.stopped = error.verdict
150
+ return ToolResult(error.verdict.message or "The phone stopped the task.", is_error=True)
151
+ except ToolProblem as problem:
152
+ return ToolResult(str(problem), is_error=True)
153
+ except Exception as error: # noqa: BLE001 -- a tool never takes the run down
154
+ return ToolResult(f"That didn't work ({type(error).__name__}: {str(error)[:160]}). Call screen to "
155
+ "see the phone now.", is_error=True)
156
+
157
+ def _check_running(self):
158
+ if self.stopped is not None:
159
+ raise RunStopped(self.stopped.message or "The task has ended.")
160
+ if self.cancelled():
161
+ raise RunStopped("The person stopped this task. Stop now and say nothing more.")
162
+
163
+ def _act_call(self, arguments):
164
+ """act through call's rules: a step that fails ends the chain with what was done before it."""
165
+ done = []
166
+ steps = arguments.get("steps") if isinstance(arguments.get("steps"), list) else []
167
+ if not steps:
168
+ return ToolResult("steps is a list of actions, such as [{\"tool\": \"tap\", \"label\": \"Alex\"}].",
169
+ is_error=True)
170
+ last, unrun = None, []
171
+ chain = steps[:MAX_CHAIN]
172
+ for index, step in enumerate(chain):
173
+ name = step.get("tool") if isinstance(step, dict) else None
174
+ if name not in CHAINABLE:
175
+ last = ToolResult(f"Step {index + 1}: tool is one of {', '.join(CHAINABLE)}.", is_error=True)
176
+ break
177
+ last = self.call(name, {k: v for k, v in step.items() if k != "tool" and v is not None})
178
+ if last.is_error:
179
+ break
180
+ done.append(last.text.split("\n", 1)[0])
181
+ if last.no_effect:
182
+ unrun = chain[index + 1:]
183
+ break
184
+ if last is None:
185
+ return ToolResult("Nothing to do.", is_error=True)
186
+ if last.is_error:
187
+ before = ("Done first: " + " ".join(done) + "\n") if done else ""
188
+ failed = len(done) + 1
189
+ return ToolResult(f"{before}Step {failed} didn't work: {last.text}", last.structured, last.images,
190
+ is_error=True)
191
+ rest = last.text.split("\n\n", 1)[1] if "\n\n" in last.text else last.text
192
+ stopped = (f" Step {len(done)} showed no effect ({last.no_effect}), so these didn't run: "
193
+ + ", ".join(step_name(step) for step in unrun) + ". Look at the screen before you send them again."
194
+ if unrun else "")
195
+ return ToolResult("Done: " + " ".join(done) + stopped + "\n\n" + rest, last.structured, last.images,
196
+ no_effect=last.no_effect)
197
+
198
+ def _trace(self, event):
199
+ """The frontier's own trace events: the phone guard's stops are kept; the rest is noise here."""
200
+ if isinstance(event, dict) and event.get("event") == "phone_guard":
201
+ self.out(event)
202
+
203
+ def _read(self):
204
+ if not self.tuned:
205
+ self.tuned = True
206
+ self.agent._tune()
207
+ self.agent._guard_check("resume")
208
+ snapshot = self.agent._observe()
209
+ self.agent._check_app(snapshot)
210
+ return snapshot
211
+
212
+ def _show(self, snapshot, image=False, head=""):
213
+ """The screen as text the agent reads (ids e1 ...), remembered for the next action."""
214
+ from ..frontier import screen_rows
215
+ masked = self.agent._masked(snapshot)
216
+ self.snapshot = snapshot
217
+ self.aliases, lines = screen_rows(masked, self.agent.run_state.receipts)
218
+ app = self.agent._app_name(snapshot) or "Home Screen"
219
+ if snapshot.bundle_id == "com.apple.springboard":
220
+ app = "Home Screen"
221
+ self.ledger.observe(masked, app, self.steps)
222
+ text = (head + "\n\n" if head else "") + f"Screen: {app}\n" + ("\n".join(lines) or "(nothing readable)")
223
+ images = []
224
+ self.point_image = None
225
+ if image:
226
+ part = self.agent._screenshot(snapshot)
227
+ url = ((part or {}).get("image_url") or {}).get("url") or ""
228
+ if url.startswith("data:image/jpeg;base64,"):
229
+ images.append(url.split(",", 1)[1])
230
+ self.point_image = part
231
+ structured = {"app": app, "bundle_id": snapshot.bundle_id or None, "elements": len(self.aliases)}
232
+ return ToolResult(text, structured, images)
233
+
234
+ def _element(self, arguments, required=True):
235
+ """The element ``element`` (an id from the latest screen) or ``label`` names."""
236
+ from ..frontier import FrontierAgent
237
+ ref, label = arguments.get("element"), arguments.get("label")
238
+ snapshot = self.snapshot
239
+ if snapshot is None or not self._still(snapshot):
240
+ snapshot = self._read()
241
+ fresh = True
242
+ else:
243
+ fresh = False
244
+ if not ref and not label:
245
+ if required:
246
+ raise ToolProblem("Say which element: its id from the latest screen (element: \"e12\") or its "
247
+ "visible label (label).")
248
+ return None, snapshot
249
+ known = self.aliases.get(ref) if ref else None
250
+ if ref and known is None and not label:
251
+ raise ToolProblem(f"{ref} isn't on the latest screen. Call screen for current ids.")
252
+ if known is not None:
253
+ found = known if not fresh else FrontierAgent._find(snapshot, known.label, known.role, known.locator)
254
+ else:
255
+ found = FrontierAgent._find(snapshot, label)
256
+ if found is None:
257
+ what = f"{ref} ({json.dumps(known.label)})" if known is not None else json.dumps(label)
258
+ self._show(snapshot)
259
+ raise ToolProblem(f"{what} isn't on the screen now. Call screen to see what is.")
260
+ return found, snapshot
261
+
262
+ def _still(self, snapshot):
263
+ """Whether ``snapshot`` is still the screen: nothing moved since it was read (one fast check)."""
264
+ from ..wda.guards import unchanged_since
265
+ try:
266
+ return unchanged_since(getattr(self.driver, "frame_clock", None), getattr(snapshot, "read_started", 0.0))
267
+ except Exception:
268
+ return False
269
+
270
+ def _approval(self, op, element, snapshot, text, declared):
271
+ """Ask the person before ``op`` on ``element`` when it commits something, raising ToolProblem unless approved;
272
+ returns the target to act on, read again after a card, where a change from what the card showed refuses, its
273
+ screen, and whether the gate read it as a commit or asked."""
274
+ from .. import task_policy as policy
275
+ gate = self.permissions
276
+ subject = policy.approval_subject(snapshot.elements, element) if op in ("TAP", "LONG_PRESS") else element
277
+ label, role = subject.label or "", subject.role
278
+ # A hold on a control is read as a tap on it (Smart asks for both alike): holding Send can send.
279
+ reading = "TAP" if op == "LONG_PRESS" else op
280
+ commits = False
281
+ if op in COMMIT_OPS:
282
+ tier = policy.action_risk_tier(reading, label, self.goal)
283
+ kind = policy.approval_kind(reading, label, risk_tier=getattr(tier, "value", tier), role=role)
284
+ from ..permissions.consequence import FOLDERS, clears_query_field, keypad_key
285
+ from ..permissions.rules import norm
286
+
287
+ # A calculator's Clear key and a mailbox row named Trash are no act.
288
+ from ..state import pixel_read
289
+ pixels = pixel_read(snapshot)
290
+ folder = (role == "Cell" or pixels) and norm(label) in FOLDERS and subject.rect[2] >= .6
291
+ # A search bar's Clear empties a query, not a draft or a note: no act.
292
+ query = clears_query_field(label, role, subject, snapshot.elements,
293
+ getattr(snapshot, "bundle_id", "") or "", pixels)
294
+ commits = (kind == "commit" and not folder and not query
295
+ and not keypad_key(label, role, snapshot.elements, pixels)) or declared
296
+ if commits or op not in ("TAP", "LONG_PRESS"):
297
+ self.changes += 1 # a commit, or typing: the result needs the screen to show it, which proof doesn't check
298
+ act = policy.commit_act(label, self.goal, role, reading) if commits else None
299
+ if op in ("TYPE_SUBMIT", "SUBMIT") and act == "submit" and self._field(element) == "composer":
300
+ act = "send" # Return in a message composer sends it, whatever the field is called ("iMessage")
301
+ app_name = self.agent._app_name(snapshot) or None
302
+ from ..permissions import action_from
303
+ typed = text if op in ("TYPE", "SET_TEXT", "TYPE_SUBMIT") else None
304
+ decision = gate.decide(action_from(op, subject, self.agent._masked(snapshot), app_name=app_name or "",
305
+ text=typed), existing_act=act)
306
+ if decision.decision == "allow":
307
+ return element, snapshot, commits
308
+ # It asks: with Ask before acting off too, since a one-time code asks in every mode.
309
+ asker, reason, title = self.approve or self.ask, decision.reason or None, None
310
+ if act is None:
311
+ title, act = decision.title, decision.act or "other"
312
+ remember = gate.remember(decision, getattr(snapshot, "bundle_id", "") or "", app_name or "")
313
+ if asker is None:
314
+ raise ToolProblem(f"{json.dumps(label or 'That')} needs the person's OK, and nobody is watching this "
315
+ "task to give it, so Mobster didn't do it. Finish and say what you need.")
316
+ # The card shows the message as it will go and who it goes to.
317
+ payload = text if op in ("TYPE_SUBMIT", "SUBMIT") and text else None
318
+ if payload is None and act in policy.MESSAGE_ACTS and self.typed and self.typed[0] == app_name:
319
+ payload = self.typed[1]
320
+ from ..contract_screen import thread_title, visible_recipient
321
+ recipient = (visible_recipient(self.agent._masked(snapshot), app_name, element)
322
+ if act in policy.MESSAGE_ACTS else None)
323
+ # One message, one approval: a Return that didn't send isn't asked again for the Send tap after it.
324
+ held = self.approved_send
325
+ if (held and op in ("TAP", "LONG_PRESS") and act in policy.MESSAGE_ACTS and payload == held["text"]
326
+ and held["app"] == app_name and held["thread"] == recipient):
327
+ self.approved_send = None
328
+ return element, snapshot, True
329
+ x, y, w, h = element.rect
330
+ request = {
331
+ "kind": "commit", "step": self.steps + 1, "operation": op, "label": label[:200], "role": role,
332
+ "text": self.agent._mask(payload) if payload else None,
333
+ "title": title or policy.approval_title(act, recipient=recipient), "act": act, "app_name": app_name,
334
+ "target": {"x": round(x, 4), "y": round(y, 4), "w": round(w, 4), "h": round(h, 4)}}
335
+ if act in offers.MONEY_ACTS:
336
+ reason = offers.reason(self.agent._masked(snapshot), label, reason) # what it buys, as the screen shows it
337
+ if reason:
338
+ request["reason"] = reason
339
+ if remember:
340
+ request["remember"] = remember
341
+ from ..frontier import CHANGED_WHILE_ASKING, FrontierAgent, binding, same_binding
342
+ bound = binding(snapshot, element)
343
+ answer = asker(request)
344
+ answer = str(answer or "denied")
345
+ if answer == "approved":
346
+ # The approval is bound to what the card showed: a fresh read must still show it.
347
+ fresh = self._read()
348
+ found = FrontierAgent._find(fresh, element.label, element.role, element.locator)
349
+ if found is None or not same_binding(bound, binding(fresh, found)):
350
+ self._show(fresh)
351
+ raise ToolProblem(CHANGED_WHILE_ASKING + " Call screen, then make the call again.")
352
+ # Return in a composer is a message whatever its label calls it ("Text Alex" names no send).
353
+ thread = recipient or thread_title(self.agent._masked(snapshot), app_name)
354
+ self.approved_send = ({"text": payload, "app": app_name, "thread": thread}
355
+ if op in ("TYPE_SUBMIT", "SUBMIT") and payload else None)
356
+ return found, fresh, True
357
+ self._declined(answer, label)
358
+
359
+ def _link_approval(self, decision):
360
+ """A link deeplinks.decide says asks."""
361
+ ask = self.approve or self.ask
362
+ if ask is None:
363
+ raise ToolProblem("Nobody is watching this task to approve that link, so Mobster didn't open it. Open "
364
+ "the app and use its screens instead, or finish and say what you need.")
365
+ snapshot = self.snapshot if self.snapshot is not None else self._read()
366
+ url = decision.url
367
+ request = {"kind": "commit", "step": self.steps + 1, "operation": "OPEN_URL", "label": f"Open {url[:180]}",
368
+ "role": "Link", "text": self.agent._mask(url[:500]), "title": decision.title or "Open this link?",
369
+ "act": "open link", "app_name": decision.app_name or self.agent._app_name(snapshot) or None}
370
+ if decision.remember() and self.remembered is not None: # never for a run an outside agent started
371
+ request["remember"] = decision.remember()
372
+ self._declined(str(ask(request) or "denied"), url[:200])
373
+
374
+ @staticmethod
375
+ def _declined(answer, label):
376
+ """Raises for any answer but approved: what the agent is told when the person said no, or nobody did."""
377
+ if answer == "approved":
378
+ return
379
+ if answer == "stopped":
380
+ raise RunStopped("The person stopped this task. Stop now and say nothing more.")
381
+ if answer.startswith("redirected:"):
382
+ raise ToolProblem(f"The person declined that and said what to do instead: {answer.split(':', 1)[1]}")
383
+ if answer == "timeout":
384
+ raise ToolProblem("Nobody answered the approval in time, so Mobster didn't do it. Finish, and say it "
385
+ "is still waiting for their OK.")
386
+ raise ToolProblem(f"The person declined {json.dumps(label or 'that')}, so Mobster didn't do it. Don't try "
387
+ "another way: finish and say so, or ask them with ask_user.")
388
+
389
+ @staticmethod
390
+ def _refused(snapshot):
391
+ """The iPhone's own passcode keypad is never touched: Mobster doesn't unlock phones."""
392
+ from ..frontier import KEYPAD_REFUSAL
393
+ from ..task_policy import springboard_keypad
394
+ if springboard_keypad(snapshot):
395
+ raise ToolProblem(KEYPAD_REFUSAL.replace("Refused:", "Mobster refused this:", 1))
396
+
397
+ def _once(self, kind, execute, finish):
398
+ """Send one action that must not be sent twice, then ``finish`` it; once its first byte may have left, any
399
+ failure ends the run with the outcome unknown, and nothing here retries it."""
400
+ from ..agent_hooks import GuardVerdict
401
+ from ..wda.guards import DriverRejection
402
+ sent = False
403
+ try:
404
+ before = execute()
405
+ sent = True
406
+ self.actions += 1
407
+ return finish(before)
408
+ except RunStopped:
409
+ raise
410
+ except Exception as error: # noqa: BLE001 -- anything after dispatch is an unknown outcome
411
+ if not sent and isinstance(error, DriverRejection) and error.code in DriverRejection.PRE_DISPATCH:
412
+ raise
413
+ self.stopped = GuardVerdict("stop", "action_unknown", f"The {kind} outcome is unknown. Inspect the phone "
414
+ "before starting another task; it was not retried.")
415
+ raise RunStopped(self.stopped.message) from None
416
+
417
+ def _do(self, op, element, snapshot, *, text=None, app=None, step_label=None, field=None, where=None,
418
+ image=False, point=None, asks=False):
419
+ """One action through the frontier's _execute, then the step (with its frame) and the screen after."""
420
+ from ..frontier import UNKNOWN_BLOCKS
421
+ from ..frontier.stages.after import may_commit
422
+ from ..smart_events import PRESSES, step_text
423
+ self._check_running()
424
+ label = step_label if step_label is not None else (element.label if element is not None else "")
425
+ self.out({"event": "action_started", "operation": op, "target": label})
426
+
427
+ def finish(before):
428
+ if op in GESTURE_OPS:
429
+ self.out({"event": "action_acknowledged", "step": self.steps + 1, "operation": op})
430
+ after = self.agent._settle(before, last=True)
431
+ self.agent._check_app(after)
432
+ self.agent.last_screen = after
433
+ words = step_text(op, label, self.apps, text=text, field=field, where=where)
434
+ changed = getattr(before, "content_fingerprint", None) != getattr(after, "content_fingerprint", None)
435
+ # A press commits by the permission gate's reading or by the loop's own test, where a test that fails counts
436
+ # as a commit (may_commit), so a tap that worked never ends the run as an unknown outcome.
437
+ self._step(words, commit=asks or may_commit(op, element, snapshot) if op in PRESSES else None,
438
+ changed=changed)
439
+ feedback, self.agent.run_state.macro_feedback = self.agent.run_state.macro_feedback, None
440
+ head = words + "." + ("" if changed or op in ("READ_LIST",) else " The screen didn't change.")
441
+ if feedback:
442
+ head += " " + feedback
443
+ result = self._show(after, image=image, head=head)
444
+ result.no_effect = self._no_effect(op, app, text, after, changed) or ""
445
+ return result
446
+
447
+ def execute():
448
+ self.agent.run_state.last_receipt = None
449
+ return self.agent._execute(op, element, app, text, snapshot, point=point)
450
+
451
+ if op in UNKNOWN_BLOCKS or op in GESTURE_OPS:
452
+ return self._once(GESTURE_OPS.get(op) or ("tap" if op in ("TAP", "TAP_XY", "LONG_PRESS") else "action"),
453
+ execute, finish)
454
+ before = execute()
455
+ self.actions += 1
456
+ return finish(before)
457
+
458
+ def _no_effect(self, op, app, text, after, changed):
459
+ """Why the read after an action shows no effect, for act's chain, or None when it shows one. The screen moving
460
+ is the plain case. An app in front, an item on screen and a write whose field reads back are effects without a
461
+ move, and a scroll that moved without reaching its item isn't one. A moved screen isn't proof of the right
462
+ screen: this only stops a chain from running on past a step that did nothing it can see."""
463
+ if op == "LAUNCH_APP":
464
+ return None if after.bundle_id == app else "the app isn't in front"
465
+ if op == "HOME":
466
+ return None if after.bundle_id == "com.apple.springboard" else "the Home Screen isn't showing"
467
+ if op == "SCROLL_TO":
468
+ return None if (text or "").casefold() in (after.text or "").casefold() else "the item isn't on screen"
469
+ receipt = self.agent.run_state.last_receipt
470
+ if receipt is not None and receipt.get("value") is not None:
471
+ return None if receipt.get("matches") else "the field doesn't hold what was written"
472
+ return None if changed else "the screen didn't change"
473
+
474
+ def _step(self, words, commit=None, changed=None):
475
+ self.steps += 1
476
+ event = {"event": "step", "n": self.steps, "step": self.steps, "text": words}
477
+ # Left out when unknown (smart_events.SmartEvents._step).
478
+ if isinstance(commit, bool):
479
+ event["commit"] = commit
480
+ if isinstance(changed, bool):
481
+ event["changed"] = changed
482
+ data = frame_bytes(self.driver)
483
+ if data:
484
+ event["_frame"] = data
485
+ self.out(event)
486
+
487
+ def _field(self, element):
488
+ from ..contract_screen import COMPOSER, SEARCHY
489
+ words = f"{element.label} {element.placeholder}"
490
+ if element.role == "SearchField" or SEARCHY.search(words):
491
+ return "search"
492
+ return "composer" if COMPOSER.search(words) else "field"
493
+
494
+ def tool_screen(self, a):
495
+ return self._show(self._read(), image=a.get("image", True))
496
+
497
+ def tool_tap(self, a):
498
+ element, snapshot = self._element(a)
499
+ self._refused(snapshot)
500
+ element, snapshot, asks = self._approval("TAP", element, snapshot, None, bool(a.get("irreversible")))
501
+ return self._do("TAP", element, snapshot, image=a.get("image", False), asks=asks)
502
+
503
+ def _point_approval(self, operation, points, label, snapshot):
504
+ """Ask before a tap at a point or a gesture, as the gate reads it; a block at any point refuses the whole
505
+ action, and what asks is refused with nobody to answer."""
506
+ from ..contract_asks import point_spot
507
+ from ..permissions import action_from
508
+ masked = self.agent._masked(snapshot)
509
+ app_name = self.agent._app_name(snapshot) or ""
510
+ decision = None
511
+ for location in points:
512
+ _, under = point_spot(snapshot, *location)
513
+ spot = under or SimpleNamespace(label="", role="Point", value="", locator="", placeholder="",
514
+ identifier="", rect=(*location, .001, .001))
515
+ decision = self.permissions.decide(action_from("TAP_XY", spot, masked, app_name=app_name),
516
+ existing_act="other" if operation != "TAP_XY" else None)
517
+ if decision.decision == "block":
518
+ raise ToolProblem(f"Refused: {decision.reason or 'not allowed'}. Nothing was done.")
519
+ if operation != "TAP_XY" or decision.act:
520
+ self.changes += 1 # a gesture, or a point on something that acts
521
+ if decision.decision == "allow":
522
+ return False
523
+ asker = self.approve or self.ask
524
+ if asker is None:
525
+ raise ToolProblem(f"{json.dumps(label or 'That')} needs the person's OK, and nobody is watching this task "
526
+ "to give it, so Mobster didn't do it. Finish and say what you need.")
527
+ x, y = points[0]
528
+ request = {"kind": "commit", "step": self.steps + 1, "operation": operation, "label": label[:200],
529
+ "role": "Point", "text": None, "act": decision.act or "other", "app_name": app_name or None,
530
+ "title": (decision.title or f"{label[:60]}?" if operation == "TAP_XY"
531
+ else f"{operation[:-3].capitalize()} {label[:60]}? This may change something."),
532
+ "target": {"x": round(x, 4), "y": round(y, 4), "w": .001, "h": .001}}
533
+ if decision.reason:
534
+ request["reason"] = decision.reason
535
+ self._declined(str(asker(request) or "denied"), label)
536
+ return True
537
+
538
+ def tool_tap_point(self, a):
539
+ x, y, label = a.get("x"), a.get("y"), a.get("label")
540
+ if (any(isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or not 0 < v < 100
541
+ for v in (x, y)) or not isinstance(label, str) or not label.strip()):
542
+ raise ToolProblem("Name the visible control in label and give x and y between 0 and 100 percent.")
543
+ self._point_image_required()
544
+
545
+ def grounded():
546
+ try:
547
+ fresh = self.agent.point_screen(self.snapshot, self.point_image, [(x / 100, y / 100)])
548
+ except ValueError as error:
549
+ raise ToolProblem(str(error)) from None
550
+ self._refused(fresh)
551
+ return fresh
552
+
553
+ from ..frontier import Action
554
+ snapshot = grounded()
555
+ point, refused = self.agent._tap_xy(self.goal, Action("TAP_XY", target=f"{x}%,{y}%", text=label), self.aliases,
556
+ snapshot)
557
+ if refused:
558
+ raise ToolProblem(refused)
559
+ if self._point_approval("TAP_XY", [point], label, snapshot):
560
+ snapshot = grounded() # the person approved the spot on the screen as it was
561
+ self._check_running()
562
+ self.point_image = None
563
+ return self._do("TAP_XY", None, snapshot, point=point, step_label=label, image=True)
564
+
565
+ def _point_image_required(self):
566
+ if self.snapshot is None or self.point_image is None:
567
+ raise ToolProblem("Call screen with image true before choosing a point on the screenshot.")
568
+
569
+ def _point_gesture(self, kind, arguments):
570
+ self._point_image_required()
571
+ try:
572
+ gesture = validate_gesture_request(kind, arguments)
573
+ snapshot = self.agent.gesture_screen(self.snapshot, self.point_image, gesture, self.goal)
574
+ except ValueError as error:
575
+ raise ToolProblem(str(error)) from None
576
+ self._refused(snapshot)
577
+ if self._point_approval(gesture.operation, gesture.points, gesture.label, snapshot):
578
+ try:
579
+ snapshot = self.agent.gesture_screen(self.snapshot, self.point_image, gesture, self.goal)
580
+ except ValueError as error:
581
+ raise ToolProblem(str(error)) from None
582
+ self._check_running()
583
+ self.point_image = None # a gesture consumes the picture even if reading its result fails
584
+ return self._do(gesture.operation, None, snapshot, point=gesture, step_label=gesture.label, image=True)
585
+
586
+ def tool_drag_point(self, a):
587
+ return self._point_gesture("drag", a)
588
+
589
+ def tool_hold_point(self, a):
590
+ return self._point_gesture("hold", a)
591
+
592
+ def tool_flick_point(self, a):
593
+ return self._point_gesture("flick", a)
594
+
595
+ def tool_long_press(self, a):
596
+ element, snapshot = self._element(a)
597
+ self._refused(snapshot)
598
+ element, snapshot, asks = self._approval("LONG_PRESS", element, snapshot, None, bool(a.get("irreversible")))
599
+ return self._do("LONG_PRESS", element, snapshot, image=a.get("image", False), asks=asks)
600
+
601
+ def tool_type_text(self, a):
602
+ text = a.get("text")
603
+ if not isinstance(text, str) or not text:
604
+ raise ToolProblem("Say what to type in text.")
605
+ element, snapshot = self._element(a)
606
+ self._refused(snapshot)
607
+ if not element.editable:
608
+ raise ToolProblem(f"{json.dumps(element.label or 'That element')} isn't a text field. Tap the field "
609
+ "first, or name the field itself.")
610
+ from ..state import validate_input_text
611
+ try: # refused here, where it can be corrected: past this point a failed send ends the run
612
+ validate_input_text(text, multiline=element.role == "TextView")
613
+ except ValueError as error:
614
+ raise ToolProblem(f"{error}. Shorten or correct the text and type it again.") from None
615
+ submit, append = bool(a.get("submit")), bool(a.get("append"))
616
+ field = self._field(element)
617
+ if field == "composer":
618
+ current = (element.value or "") if append and has_text(element) else ""
619
+ self.typed = (self.agent._app_name(snapshot) or None, current + text)
620
+ # The permission gate reads what is typed (a password is never typed); Return asks as a commit.
621
+ element, snapshot, _ = self._approval("TYPE_SUBMIT" if submit else "TYPE", element, snapshot, text,
622
+ bool(a.get("irreversible")))
623
+ if submit and (append or not has_text(element)):
624
+ return self._do("TYPE_SUBMIT", element, snapshot, text=text, field=field, image=a.get("image", False))
625
+ # An empty field takes the text as it is: no clear first (a clear and a read cost a round trip).
626
+ op = "TYPE" if append or not has_text(element) else "SET_TEXT"
627
+ result = self._do(op, element, snapshot, text=text, field=field)
628
+ if not submit:
629
+ return result
630
+ from ..frontier import FrontierAgent
631
+ current = self.snapshot
632
+ again = FrontierAgent._find(current, element.label, element.role, element.locator) or element
633
+ return self._do("SUBMIT", again, current, step_label=element.label, image=a.get("image", False))
634
+
635
+ def tool_swipe(self, a):
636
+ direction = a.get("direction")
637
+ if direction not in SWIPES:
638
+ raise ToolProblem("direction is up, down, left or right (up shows what is below).")
639
+ element, snapshot = self._element(a, required=False)
640
+ op = SWIPES[direction]
641
+ if op in ("SWIPE_UP", "SWIPE_DOWN"):
642
+ element = None # the frontier scrolls the screen's scroll view
643
+ elif element is not None:
644
+ # A row's swipe archives or deletes in some apps (Gmail, Outlook): the gate reads it like a tap.
645
+ element, snapshot, _ = self._approval(op, element, snapshot, None, bool(a.get("irreversible")))
646
+ return self._do(op, element, snapshot, image=a.get("image", False))
647
+
648
+ def tool_open_app(self, a):
649
+ wanted = " ".join(str(a.get("app") or "").split())
650
+ if not wanted:
651
+ raise ToolProblem("Say which app: its name as on the Home Screen, or its bundle id.")
652
+ bundle = wanted if wanted in self.apps else next(
653
+ (b for b, name in self.apps.items() if name.casefold() == wanted.casefold()), None)
654
+ if bundle is None:
655
+ near = [name for name in self.apps.values() if wanted.casefold() in name.casefold()][:5]
656
+ raise ToolProblem(f"There's no app called {json.dumps(wanted)} on this iPhone."
657
+ + (" Did you mean " + ", ".join(near) + "?" if near else
658
+ " The apps are listed in your instructions."))
659
+ snapshot = self.snapshot if self.snapshot is not None else self._read()
660
+ self._allowed_app(bundle) # asks before the app opens, the first time
661
+ return self._do("LAUNCH_APP", None, snapshot, app=bundle, step_label=bundle, image=a.get("image", False))
662
+
663
+ def _allowed_app(self, bundle):
664
+ """Raises ToolProblem unless the person lets Mobster use ``bundle`` (the run's guard asks the first time)."""
665
+ from ..agent_hooks import safe_allows
666
+ guard = getattr(self.agent, "guard", None)
667
+ if not bundle or bundle == "com.apple.springboard" or guard is None or safe_allows(guard, bundle):
668
+ return
669
+ said = getattr(guard, "blocked_message", None)
670
+ try:
671
+ why = said(bundle, self.apps.get(bundle)) if callable(said) else None
672
+ except Exception:
673
+ why = None
674
+ raise ToolProblem((why or f"Mobster may not use {self.apps.get(bundle, bundle)}.") + " Do the task another "
675
+ "way, or finish and say what is left for the person.")
676
+
677
+ def tool_home(self, a):
678
+ snapshot = self.snapshot if self.snapshot is not None else self._read()
679
+ return self._do("HOME", None, snapshot, image=a.get("image", False))
680
+
681
+ def tool_dismiss(self, a):
682
+ snapshot = self._read()
683
+ return self._do("DISMISS", None, snapshot)
684
+
685
+ def tool_read_list(self, a):
686
+ snapshot = self._read()
687
+ stop_at = a.get("stop_at") or None
688
+ app = self.agent._app_name(snapshot) or None
689
+ self.out({"event": "action_started", "operation": "READ_LIST", "target": ""})
690
+ self.agent._execute("READ_LIST", None, None, stop_at, snapshot)
691
+ rows, self.agent.run_state.listing = list(self.agent.run_state.listing or ()), None
692
+ from ..smart_events import step_text
693
+ self.actions += 1
694
+ self._step(step_text("READ_LIST", "", self.apps, where=app))
695
+ after = self.agent._read_once()
696
+ feedback, self.agent.run_state.macro_feedback = self.agent.run_state.macro_feedback, None
697
+ shown = rows[:LIST_ROWS_SHOWN]
698
+ head = (f"Read {len(rows)} row{'s' if len(rows) != 1 else ''} of the list, top to bottom"
699
+ + (f" (showing the first {LIST_ROWS_SHOWN})" if len(rows) > LIST_ROWS_SHOWN else "") + ":\n"
700
+ + "\n".join(f"- {self.agent._mask(row)}" for row in shown) + ("\n" + feedback if feedback else ""))
701
+ return self._show(after, head=head)
702
+
703
+ def tool_scroll_to(self, a):
704
+ text = " ".join(str(a.get("text") or "").split())
705
+ if not text:
706
+ raise ToolProblem("Say the text of the item to scroll to.")
707
+ snapshot = self._read()
708
+ return self._do("SCROLL_TO", None, snapshot, text=text, step_label="")
709
+
710
+ def tool_open_url(self, a):
711
+ url = str(a.get("url") or "").strip()
712
+ if not re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:", url) or len(url) > 2000:
713
+ raise ToolProblem("The link needs a scheme, such as https://example.com or maps://?q=coffee.")
714
+ self._refused(self.snapshot if self.snapshot is not None else self._read())
715
+ from ..deeplinks import LinkError, app_for, decide, open_link
716
+ try:
717
+ decision = decide(url, self.remembered)
718
+ except LinkError as error:
719
+ raise ToolProblem(str(error)) from None
720
+ self._allowed_app(app_for(url)) # the app the link opens, before it opens
721
+ if decision.action == "ask":
722
+ self.changes += 1
723
+ self._link_approval(decision)
724
+ self.out({"event": "action_started", "operation": "OPEN_URL", "target": url[:80]})
725
+ opener = getattr(self.driver, "open_url", None)
726
+ if callable(opener):
727
+ opener(url)
728
+ else:
729
+ open_link(self.driver, url, bundle=app_for(url) or None)
730
+ self.actions += 1
731
+ words = self._link_words(url)
732
+ self._step(words)
733
+ return self._show(self._read(), image=a.get("image", False), head=f"Opened {url[:200]}.")
734
+
735
+ def _link_words(self, url):
736
+ """A link's step in plain words, never its scheme or query: "Opened example.com", "Opened Settings", else
737
+ "Opened a link"."""
738
+ from urllib.parse import urlsplit
739
+
740
+ from ..catalog import APPS
741
+ from ..deeplinks import app_for
742
+ host = urlsplit(url).hostname if url.casefold().startswith(("http://", "https://")) else None
743
+ if host:
744
+ return f"Opened {host.removeprefix('www.')[:60]}"
745
+ bundle = app_for(url)
746
+ name = self.apps.get(bundle) or next((app["name"] for app in APPS if app["bundleId"] == bundle), None)
747
+ return f"Opened {name}" if name else "Opened a link"
748
+
749
+ def tool_wait(self, a):
750
+ try:
751
+ seconds = min(WAIT_MAX, max(0.5, float(a.get("seconds", 2))))
752
+ except (TypeError, ValueError):
753
+ seconds = 2.0
754
+ deadline = time.monotonic() + seconds
755
+ while time.monotonic() < deadline:
756
+ if self.cancelled():
757
+ raise RunStopped("The person stopped this task. Stop now and say nothing more.")
758
+ time.sleep(min(.25, max(0.0, deadline - time.monotonic())))
759
+ return self._show(self._read(), head=f"Waited {seconds:g} s.")
760
+
761
+ def tool_ask_user(self, a):
762
+ question = " ".join(str(a.get("question") or "").split())[:QUESTION_CHARS]
763
+ if not question:
764
+ raise ToolProblem("Say what to ask in question.")
765
+ if self.ask is None:
766
+ raise ToolProblem("Nobody is watching this task to answer. Decide from the request, or finish and say "
767
+ "what you need to know.")
768
+ choices = [" ".join(str(c).split())[:CHOICE_CHARS] for c in (a.get("choices") or ()) if str(c).strip()][:6]
769
+ request = {"kind": "clarify", "operation": "ASK_USER", "label": question[:80], "question": question,
770
+ "choices": [{"id": f"c{i + 1}", "label": c} for i, c in enumerate(choices)],
771
+ "allow_text": True, "step": self.steps + 1}
772
+ answer = str(self.ask(request) or "timeout")
773
+ if answer == "stopped":
774
+ raise RunStopped("The person stopped this task. Stop now and say nothing more.")
775
+ if answer.startswith("answer:"):
776
+ return ToolResult("They answered: " + answer.split(":", 1)[1], {"answer": answer.split(":", 1)[1]})
777
+ if answer.startswith("choice:"):
778
+ picked = answer.split(":", 1)[1]
779
+ label = next((c for i, c in enumerate(choices) if f"c{i + 1}" == picked), picked)
780
+ return ToolResult("They chose: " + label, {"answer": label})
781
+ if answer == "approved" and choices:
782
+ return ToolResult("They chose: " + choices[0], {"answer": choices[0]})
783
+ raise ToolProblem("Nobody answered in time. Finish, and say what you need to know.")
784
+
785
+
786
+ def _schema(properties, required=()):
787
+ return {"type": "object", "properties": properties, "required": list(required), "additionalProperties": False}
788
+
789
+
790
+ ELEMENT = {"type": "string", "pattern": ELEMENT_ID.pattern,
791
+ "description": "the element's id from the latest screen, such as e12"}
792
+ LABEL = {"type": "string", "minLength": 1, "maxLength": 200,
793
+ "description": "or the element's visible label, exactly as the screen lists it"}
794
+ IMAGE = {"type": "boolean",
795
+ "description": "also return a screenshot (default false: the text screen is usually enough)"}
796
+ IRREVERSIBLE = {"type": "boolean",
797
+ "description": "true when this sends, posts, buys, books, deletes or can't be undone: "
798
+ "Mobster asks the person first"}
799
+ DESCRIPTIONS = {
800
+ "hold_point": "Hold a named visible screenshot point for 0.1 to 10 seconds (default 1.5), then release. Uses the "
801
+ "screenshot from your last screen call, existing approvals and one bounded gesture. Returns a fresh "
802
+ "screenshot; cannot be chained or interrupted during contact. An unknown outcome stops the run.",
803
+ "flick_point": "Flick between named visible screenshot points in 0.08 to 0.3 seconds (default 0.12), without a "
804
+ "pickup hold. Existing approvals and screenshot and path checks apply. Returns a fresh screenshot; "
805
+ "cannot be chained. An unknown outcome stops the run.",
806
+ "drag_point": "Drag between two visible screenshot points with named endpoints and screen percentages. Optionally "
807
+ "hold before moving with hold_seconds (0 to 2, default 0.1). Uses one gesture, returns a fresh "
808
+ "screenshot and cannot be chained. Existing approval rules apply; a changed screen, or a sensitive "
809
+ "control along the path, is refused. An unknown outcome stops the run.",
810
+ "tap_point": "Tap a visible control that has no element id, such as an icon the screen listing leaves out. x and y "
811
+ "are screen percentages from your last screenshot, label names the control. Returns a fresh "
812
+ "screenshot; choose each point from it. Existing approval rules still apply.",
813
+ "act": "Do several actions in a row in one call when you can predict the screens, such as opening a chat, typing "
814
+ "and tapping Send: faster than one call per action. Name elements after the first by label, since the "
815
+ "screen changes. Stops at the first step that fails, isn't approved or shows no effect, "
816
+ "and returns the screen after the last.",
817
+ "screen": "Read the iPhone's current screen: the app in front and every element with an id (e1, e2 ...), its role, "
818
+ "label, value and position, plus a screenshot. Call it first, and whenever you are unsure what is shown.",
819
+ "tap": "Tap an element, named by its id from the latest screen or by its label. Returns the screen after the tap. "
820
+ "Mobster asks the person before a tap that sends, posts, buys, books or deletes; "
821
+ "mark such a tap irreversible.",
822
+ "type_text": "Type into a text field (replacing its text unless append is true); submit presses Return after "
823
+ "typing. Returns the screen after. Return that sends a message waits for the person's OK.",
824
+ "swipe": "Swipe the screen: up shows what is below, down what is above; left and right on an element swipe that "
825
+ "row or carousel. Returns the screen after.",
826
+ "long_press": ("Press and hold an element (opens its menu). Returns the screen after. Mobster asks the person "
827
+ "first when the hold commits something, as for tap."),
828
+ "open_app": "Open an app by its name as on the Home Screen (Messages, Notes) or its bundle id. Faster and surer "
829
+ "than finding its icon. Returns the screen after.",
830
+ "home": "Go to the Home Screen.",
831
+ "read_list": "Read the whole list or feed on screen from here down in one call, scrolling as it goes (up to about "
832
+ "60 rows): faster than swiping and reading screen by screen. stop_at stops at the first row "
833
+ "containing that text.",
834
+ "scroll_to": "Scroll down until an item containing this text is on screen. Returns the screen after.",
835
+ "dismiss": "Close a pop-up, sheet or banner that covers the screen, by tapping outside it.",
836
+ "open_url": ("Open a link or deep link on the iPhone (https://..., maps://?q=..., tel:, sms:). A web page, Maps or "
837
+ "the App Store opens at once; a call, FaceTime, a message, an email or a shortcut waits for the "
838
+ "person's OK each time, and another app's link the first time."),
839
+ "ask_user": "Ask the person a question (with up to 6 choices) and wait for their answer. Use it when the "
840
+ "request is "
841
+ "ambiguous or you need something only they know; never for a password or passcode. A choice that names "
842
+ "a person or a place says what tells it apart after \" \u2014 \" (\"Sam Lee \u2014 texted Tuesday\").",
843
+ "wait": "Wait a few seconds (at most 10) for something to load, then read the screen.",
844
+ }
845
+ SCHEMAS = {
846
+ "drag_point": DRAG_SCHEMA,
847
+ "hold_point": HOLD_SCHEMA,
848
+ "flick_point": FLICK_SCHEMA,
849
+ "tap_point": _schema({"x": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100},
850
+ "y": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 100},
851
+ "label": LABEL}, ("x", "y", "label")),
852
+ "screen": _schema({"image": {"type": "boolean", "description": "include a screenshot (default true)"}}),
853
+ "act": _schema({"steps": {"type": "array", "minItems": 1, "maxItems": MAX_CHAIN, "items": {
854
+ "type": "object", "required": ["tool"], "additionalProperties": True,
855
+ "properties": {"tool": {"type": "string", "enum": list(CHAINABLE)},
856
+ "element": ELEMENT, "label": LABEL,
857
+ "text": {"type": "string", "maxLength": 4000},
858
+ "submit": {"type": "boolean"}, "append": {"type": "boolean"},
859
+ "irreversible": IRREVERSIBLE,
860
+ "direction": {"type": "string", "enum": ["up", "down", "left", "right"]},
861
+ "app": {"type": "string", "maxLength": 200},
862
+ "seconds": {"type": "number", "minimum": 0.5, "maximum": WAIT_MAX}}},
863
+ "description": "the actions in order, each with its tool and that tool's arguments"}}, ("steps",)),
864
+ "tap": _schema({"element": ELEMENT, "label": LABEL, "irreversible": IRREVERSIBLE, "image": IMAGE}),
865
+ "type_text": _schema({"element": ELEMENT, "label": LABEL,
866
+ "text": {"type": "string", "minLength": 1, "maxLength": 4000, "description": "what to type"},
867
+ "submit": {"type": "boolean", "description": "press Return after typing"},
868
+ "append": {"type": "boolean",
869
+ "description": "add to the field's text instead of replacing it"},
870
+ "irreversible": IRREVERSIBLE, "image": IMAGE}, ("text",)),
871
+ "swipe": _schema({"direction": {"type": "string", "enum": ["up", "down", "left", "right"]},
872
+ "element": ELEMENT, "label": LABEL, "image": IMAGE}, ("direction",)),
873
+ "long_press": _schema({"element": ELEMENT, "label": LABEL, "irreversible": IRREVERSIBLE, "image": IMAGE}),
874
+ "open_app": _schema({"app": {"type": "string", "minLength": 1, "maxLength": 200,
875
+ "description": "the app's name or bundle id"}, "image": IMAGE}, ("app",)),
876
+ "home": _schema({"image": IMAGE}),
877
+ "read_list": _schema({"stop_at": {"type": "string", "maxLength": 200,
878
+ "description": "stop at the first row containing this text"}}),
879
+ "scroll_to": _schema({"text": {"type": "string", "minLength": 1, "maxLength": 200}}, ("text",)),
880
+ "dismiss": _schema({}),
881
+ "open_url": _schema({"url": {"type": "string", "minLength": 3, "maxLength": 2000}, "image": IMAGE}, ("url",)),
882
+ "ask_user": _schema({"question": {"type": "string", "minLength": 1, "maxLength": QUESTION_CHARS},
883
+ "choices": {"type": "array", "maxItems": 6,
884
+ "items": {"type": "string", "minLength": 1, "maxLength": CHOICE_CHARS}}},
885
+ ("question",)),
886
+ "wait": _schema({"seconds": {"type": "number", "minimum": 0.5, "maximum": WAIT_MAX}}),
887
+ }
888
+ # The same risk hints as mobster mcp's tools (mcp_server/protocol.py): a tool that changes the phone is destructive.
889
+ READS = {**READ_ONLY, "idempotentHint": True}
890
+ ANNOTATIONS = {name: dict(READS if name in ("screen", "read_list", "wait", "ask_user") else ACTS) for name in NAMES}
891
+ for _name in NAMES:
892
+ ANNOTATIONS[_name]["title"] = _name.replace("_", " ").capitalize()