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,807 @@
1
+ """The Mac app's plan check, in the agent's own run path: a signed token checked offline,
2
+ and anything unreadable refuses."""
3
+
4
+ import base64
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import sys
12
+ import threading
13
+ import time
14
+ from pathlib import Path
15
+
16
+ from . import build_identity, genuine, premium_model
17
+ from .api_errors import APIError
18
+
19
+ ACCOUNT_FILE = "account.json"
20
+ # The sentence names no amount: the site owns the prices, and a coding agent passes it to the person as it is.
21
+ BUY_URL = "mobster.dev/pricing"
22
+ TRIAL_ENDED = ("Your 7-day free trial on this Mac has ended, so Mobster did nothing: choose a plan at "
23
+ f"{BUY_URL}, then ask again.")
24
+ NO_PLAN = ("This Mac has no active Mobster plan or free trial, so Mobster did nothing: open Mobster to start your free "
25
+ f"trial or sign in, or choose a plan at {BUY_URL}, then ask again.")
26
+ # A team's station whose Team plan doesn't license it now; `{team}` is a name, never an instruction.
27
+ NO_TEAM_PLAN = ("{team}'s Team plan isn't active on this Mac, so Mobster did nothing. An Owner can choose a plan in "
28
+ "your account, then ask again.")
29
+ # One Mac at a time: another Mac took this seat; `{mac}` is a name, never an instruction.
30
+ MOVED = ("Mobster moved to {mac}, so it did nothing on this Mac. Mobster runs on one Mac at a time: to use it here, "
31
+ "open Mobster on this Mac and choose Move it back, then ask again.")
32
+ # Every plan's token ends 7 days after the check that signed it (services/license-api CHECK_IN_S);
33
+ # the plan hasn't ended.
34
+ CHECK_IN = "Mobster couldn't check your plan for 7 days, so it did nothing. Connect to the internet, then try again."
35
+ # desktop/src-tauri/license-public.pem; MACHINE_SALT and INACTIVE_STATUSES match entitlement.rs.
36
+ PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
37
+ MCowBQYDK2VwAyEAPykA81VTckj/R8OOQ2CFhAGhlQvkFSZYr6+t5ipU4KY=
38
+ -----END PUBLIC KEY-----
39
+ """
40
+ MACHINE_SALT = "mobster-license-machine-v1"
41
+ INACTIVE_STATUSES = frozenset({"canceled", "cancelled", "revoked", "refunded", "disputed", "unpaid",
42
+ "incomplete_expired", "inactive"})
43
+ PLANS = frozenset({"lifetime", "monthly", "yearly", "trial"})
44
+ TEAM_PLANS = frozenset({"team", "team_trial"})
45
+ GATE_OFF_ENV = "MOBSTER_ACCOUNT_GATE"
46
+
47
+
48
+ # Ed25519 verification (RFC 8032 §5.1.7) in plain Python: the agent ships no crypto library.
49
+ _P = 2 ** 255 - 19
50
+ _Q = 2 ** 252 + 27742317777372353535851937790883648493
51
+ _D = -121665 * pow(121666, _P - 2, _P) % _P
52
+ _SQRT_M1 = pow(2, (_P - 1) // 4, _P)
53
+
54
+
55
+ def _add(a, b):
56
+ x1, y1, z1, t1 = a
57
+ x2, y2, z2, t2 = b
58
+ first, second = (y1 - x1) * (y2 - x2) % _P, (y1 + x1) * (y2 + x2) % _P
59
+ third, fourth = 2 * t1 * t2 * _D % _P, 2 * z1 * z2 % _P
60
+ e, f, g, h = second - first, fourth - third, fourth + third, second + first
61
+ return e * f % _P, g * h % _P, f * g % _P, e * h % _P
62
+
63
+
64
+ def _mul(scalar, point):
65
+ result = (0, 1, 1, 0)
66
+ while scalar > 0:
67
+ if scalar & 1:
68
+ result = _add(result, point)
69
+ point = _add(point, point)
70
+ scalar >>= 1
71
+ return result
72
+
73
+
74
+ def _same(a, b):
75
+ return (a[0] * b[2] - b[0] * a[2]) % _P == 0 and (a[1] * b[2] - b[1] * a[2]) % _P == 0
76
+
77
+
78
+ def _x_for(y, sign):
79
+ if y >= _P:
80
+ return None
81
+ x2 = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
82
+ if x2 == 0:
83
+ return None if sign else 0
84
+ x = pow(x2, (_P + 3) // 8, _P)
85
+ if (x * x - x2) % _P:
86
+ x = x * _SQRT_M1 % _P
87
+ if (x * x - x2) % _P:
88
+ return None
89
+ return _P - x if (x & 1) != sign else x
90
+
91
+
92
+ def _point(data):
93
+ if len(data) != 32:
94
+ return None
95
+ y = int.from_bytes(data, "little")
96
+ sign, y = y >> 255, y & ((1 << 255) - 1)
97
+ x = _x_for(y, sign)
98
+ return None if x is None else (x, y, 1, x * y % _P)
99
+
100
+
101
+ _BASE = (_x_for(4 * pow(5, _P - 2, _P) % _P, 0), 4 * pow(5, _P - 2, _P) % _P, 1, 0)
102
+ _BASE = (_BASE[0], _BASE[1], 1, _BASE[0] * _BASE[1] % _P)
103
+
104
+
105
+ def _hash(data):
106
+ return int.from_bytes(hashlib.sha512(data).digest(), "little") % _Q
107
+
108
+
109
+ def key_id(key):
110
+ """The license signing key's ID: the first 16 hex characters of its raw bytes' SHA-256."""
111
+ return hashlib.sha256(key).hexdigest()[:16]
112
+
113
+
114
+ def ed25519_verify(public, message, signature):
115
+ """Whether ``signature`` (64 bytes) is ``public``'s (32 bytes) over ``message``."""
116
+ if len(public) != 32 or len(signature) != 64:
117
+ return False
118
+ a, r = _point(public), _point(signature[:32])
119
+ s = int.from_bytes(signature[32:], "little")
120
+ if a is None or r is None or s >= _Q:
121
+ return False
122
+ return _same(_mul(s, _BASE), _add(r, _mul(_hash(signature[:32] + public + message), a)))
123
+
124
+
125
+ def public_key_from_pem(pem):
126
+ """The 32 key bytes of an Ed25519 SPKI PEM, or None."""
127
+ body = "".join(line.strip() for line in pem.splitlines() if not line.startswith("-----"))
128
+ try:
129
+ der = base64.b64decode(body, validate=True)
130
+ except ValueError:
131
+ return None
132
+ prefix = bytes.fromhex("302a300506032b6570032100")
133
+ return der[12:] if len(der) == 44 and der[:12] == prefix else None
134
+
135
+
136
+ EMBEDDED_KEYS = tuple(key for key in (public_key_from_pem(PUBLIC_KEY_PEM),) if key)
137
+ DEV_KEY_ENV = "MOBSTER_LICENSE_PUBLIC_KEY"
138
+ DEV_MACHINE_ENV = "MOBSTER_DEV_MACHINE"
139
+
140
+
141
+ def local_account_api(environ):
142
+ """Whether MOBSTER_ACCOUNT_API is a license server on this Mac's loopback (http://127.0.0.1)."""
143
+ from urllib.parse import urlsplit
144
+ parts = urlsplit((environ.get("MOBSTER_ACCOUNT_API") or "").strip())
145
+ return parts.scheme == "http" and parts.hostname == "127.0.0.1"
146
+
147
+
148
+ def trusted_keys(environ=None):
149
+ """The license keys this Mac trusts."""
150
+ environ = os.environ if environ is None else environ
151
+ raw = (environ.get(DEV_KEY_ENV) or "").strip()
152
+ if not raw or getattr(sys, "frozen", False) or not local_account_api(environ):
153
+ return EMBEDDED_KEYS
154
+ try:
155
+ key = base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4))
156
+ except ValueError:
157
+ return EMBEDDED_KEYS
158
+ return (key,) if len(key) == 32 else EMBEDDED_KEYS
159
+
160
+
161
+ def _b64url(text):
162
+ text = text.strip()
163
+ if not text or any(c not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=" for c in text):
164
+ raise ValueError("not base64url")
165
+ return base64.urlsafe_b64decode(text.rstrip("=") + "=" * (-len(text.rstrip("=")) % 4))
166
+
167
+
168
+ def seconds(value):
169
+ """Unix seconds; a value in milliseconds is scaled down (entitlement.rs › seconds)."""
170
+ return value // 1000 if value > 100_000_000_000 else value
171
+
172
+
173
+ class TokenProblem(Exception):
174
+ """Why a token doesn't license this Mac."""
175
+
176
+ def __init__(self, problem, plan=None):
177
+ super().__init__(problem)
178
+ self.problem = problem
179
+ self.plan = plan
180
+
181
+
182
+ def verified_payload(token, keys=EMBEDDED_KEYS):
183
+ """The payload a token's signature covers, once it verifies with one of ``keys``."""
184
+ if not isinstance(token, str) or token.count(".") != 1:
185
+ raise TokenProblem("malformed")
186
+ segment, signature = token.strip().split(".")
187
+ try:
188
+ payload, signature = _b64url(segment), _b64url(signature)
189
+ except (ValueError, TypeError):
190
+ raise TokenProblem("malformed") from None
191
+ if len(signature) != 64:
192
+ raise TokenProblem("malformed")
193
+ if not any(ed25519_verify(key, payload, signature) or ed25519_verify(key, segment.encode(), signature)
194
+ for key in keys):
195
+ raise TokenProblem("bad_signature")
196
+ return payload
197
+
198
+
199
+ def check_token(token, sub, machine, now, keys=EMBEDDED_KEYS, payload=None):
200
+ """The claims of a version 2 token for account ``sub`` on ``machine`` at ``now`` (Unix seconds)."""
201
+ if payload is None:
202
+ payload = verified_payload(token, keys)
203
+ try:
204
+ claims = json.loads(payload)
205
+ except (ValueError, UnicodeDecodeError):
206
+ raise TokenProblem("malformed") from None
207
+ if not isinstance(claims, dict) or type(claims.get("v")) is not int or claims["v"] != 2:
208
+ raise TokenProblem("unsupported_version")
209
+ exp = claims.get("exp")
210
+ if (not all(isinstance(claims.get(name), str) for name in ("sub", "status", "machine"))
211
+ or claims.get("plan") not in PLANS or type(claims.get("iat")) is not int
212
+ or (exp is not None and type(exp) is not int)):
213
+ raise TokenProblem("malformed")
214
+ if not isinstance(machine, str) or not machine or claims["machine"].lower() != machine.lower():
215
+ raise TokenProblem("wrong_machine")
216
+ if not isinstance(sub, str) or claims["sub"].lower() != sub.lower():
217
+ raise TokenProblem("wrong_account")
218
+ if claims["status"].lower() in INACTIVE_STATUSES:
219
+ raise TokenProblem("inactive", claims["plan"])
220
+ if exp is not None and now >= seconds(exp):
221
+ raise TokenProblem("expired", claims["plan"])
222
+ return claims
223
+
224
+
225
+ def check_station_token(token, org, station, machine, now, keys=EMBEDDED_KEYS, payload=None):
226
+ """The claims of a version 3 (team station) token for station ``station`` of team ``org`` on ``machine`` at ``now``
227
+ (Unix seconds); TokenProblem otherwise (entitlement.rs › check_station_token)."""
228
+ if payload is None:
229
+ payload = verified_payload(token, keys)
230
+ try:
231
+ claims = json.loads(payload)
232
+ except (ValueError, UnicodeDecodeError):
233
+ raise TokenProblem("malformed") from None
234
+ if not isinstance(claims, dict) or type(claims.get("v")) is not int or claims["v"] != 3:
235
+ raise TokenProblem("unsupported_version")
236
+ if (not all(isinstance(claims.get(name), str) and claims[name]
237
+ for name in ("sub", "org", "station", "status", "machine"))
238
+ or claims.get("plan") not in TEAM_PLANS or type(claims.get("iat")) is not int
239
+ or type(claims.get("exp")) is not int or claims["sub"].lower() != claims["org"].lower()):
240
+ raise TokenProblem("malformed")
241
+ if not isinstance(machine, str) or not machine or claims["machine"].lower() != machine.lower():
242
+ raise TokenProblem("wrong_machine")
243
+ if not isinstance(org, str) or claims["org"].lower() != org.lower():
244
+ raise TokenProblem("wrong_org", claims["plan"])
245
+ if not isinstance(station, str) or claims["station"].lower() != station.lower():
246
+ raise TokenProblem("wrong_station", claims["plan"])
247
+ if claims["status"].lower() in INACTIVE_STATUSES:
248
+ raise TokenProblem("inactive", claims["plan"])
249
+ if now >= seconds(claims["exp"]):
250
+ raise TokenProblem("expired", claims["plan"])
251
+ return claims
252
+
253
+
254
+ # A CI token (version 4) licenses simulator runs of `mobster ci run` and `mobster test` on a machine with no account,
255
+ # such as a hosted CI runner. It names no machine, always expires, and never licenses a phone.
256
+ CI_TOKEN_ENV = "MOBSTER_CI_TOKEN"
257
+ CI_TOKENS_ENV = "MOBSTER_CI_TOKENS"
258
+ # Dark until the owner turns CI tokens on: they join the flat plan only once he confirms it.
259
+ CI_TOKENS_ENABLED = False
260
+ CI_COMMANDS = frozenset({"ci", "test"})
261
+
262
+
263
+ def ci_tokens_enabled(environ=None):
264
+ environ = os.environ if environ is None else environ
265
+ return CI_TOKENS_ENABLED or environ.get(CI_TOKENS_ENV) == "on"
266
+
267
+
268
+ def check_ci_token(token, now, keys=EMBEDDED_KEYS, payload=None):
269
+ """The claims of a version 4 CI token at ``now`` (Unix seconds); TokenProblem otherwise."""
270
+ if payload is None:
271
+ payload = verified_payload(token, keys)
272
+ try:
273
+ claims = json.loads(payload)
274
+ except (ValueError, UnicodeDecodeError):
275
+ raise TokenProblem("malformed") from None
276
+ if not isinstance(claims, dict) or type(claims.get("v")) is not int or claims["v"] != 4:
277
+ raise TokenProblem("unsupported_version")
278
+ if (claims.get("kind") != "ci" or claims.get("scope") != "simulator"
279
+ or not all(isinstance(claims.get(name), str) and claims[name] for name in ("sub", "status"))
280
+ or claims.get("plan") not in PLANS | TEAM_PLANS or type(claims.get("iat")) is not int
281
+ or type(claims.get("exp")) is not int):
282
+ raise TokenProblem("malformed")
283
+ if claims["status"].lower() in INACTIVE_STATUSES:
284
+ raise TokenProblem("inactive", claims["plan"])
285
+ if now >= seconds(claims["exp"]):
286
+ raise TokenProblem("expired", claims["plan"])
287
+ return claims
288
+
289
+
290
+ CI_CHECK_PATH = "/api/ci-tokens/check"
291
+ CI_CHECK_SECONDS = 5.0
292
+ CI_REFUSALS = frozenset({"revoked", "expired", "plan_required"})
293
+
294
+
295
+ def ci_token_answer(token):
296
+ """What the license API says of ``token``: "ok", why it refuses (any 401 or 403 it doesn't name reads as revoked),
297
+ or None when it can't be asked (no network, a pin mismatch, a busy or failing server)."""
298
+ from . import share
299
+ try:
300
+ status, value = share.call("POST", CI_CHECK_PATH, token, None, timeout=CI_CHECK_SECONDS, limit=4096)
301
+ except share.ShareError:
302
+ return None
303
+ if status == 200:
304
+ return "ok"
305
+ if status in (401, 403):
306
+ error = value.get("error")
307
+ return error if error in CI_REFUSALS else "revoked"
308
+ return None
309
+
310
+
311
+ def ci_token_online(token):
312
+ """None when the license API vouches for ``token``, else why it refuses. None too when it can't be asked: with
313
+ ``premiumModel`` off the signature and expiry, checked offline, stand then (ci_token_problem)."""
314
+ answer = ci_token_answer(token)
315
+ return None if answer == "ok" else answer
316
+
317
+
318
+ def ci_token_problem(environ=None, now=None, keys=None, online=None):
319
+ """None when ``MOBSTER_CI_TOKEN`` is a live CI token and CI tokens are on, else why not.
320
+
321
+ The signature and expiry are checked here, offline. Then the license API is asked whether the token was revoked
322
+ (``online``, `ci_token_online` by default) and its answer stands when there is one."""
323
+ environ = os.environ if environ is None else environ
324
+ if not ci_tokens_enabled(environ):
325
+ return "off"
326
+ token = (environ.get(CI_TOKEN_ENV) or "").strip()
327
+ if not token:
328
+ return "no_token"
329
+ try:
330
+ check_ci_token(token, int(time.time()) if now is None else now,
331
+ trusted_keys(environ) if keys is None else keys)
332
+ except TokenProblem as problem:
333
+ return problem.problem
334
+ return (online or ci_token_online)(token)
335
+
336
+
337
+ # With premiumModel on no run needs a CI token, but everything under one that's there refuses unless the license API
338
+ # vouched for it: each verdict's sentence (ci_token_verdict).
339
+ CI_TOKEN_REFUSALS = {
340
+ "revoked": f"This CI token was revoked, so Mobster did nothing. Ask whoever made it, or take {CI_TOKEN_ENV} out of "
341
+ "this job.",
342
+ "expired": f"This CI token has ended, so Mobster did nothing. Create a new one and replace the secret, or take "
343
+ f"{CI_TOKEN_ENV} out of this job.",
344
+ "plan_required": f"The plan behind this CI token isn't active, so Mobster did nothing. Check the plan, or take "
345
+ f"{CI_TOKEN_ENV} out of this job.",
346
+ "unsendable": f"This CI token has characters no request can carry, so Mobster couldn't check it and did nothing. "
347
+ f"Copy the token into the secret again, or take {CI_TOKEN_ENV} out of this job.",
348
+ "unchecked": f"Mobster couldn't check this CI token with its license service, so it did nothing. Check this "
349
+ f"runner's network, or take {CI_TOKEN_ENV} out of this job.",
350
+ }
351
+ # How long a verdict stands, in seconds: one the license API vouched for 5 minutes, and an unanswered check 30 s, so a
352
+ # long-lived MCP server doesn't wait on the API at every tool call. A refusal stands for the life of the process.
353
+ CI_TOKEN_STANDS = {"ok": 300.0, "unchecked": 30.0}
354
+ # What still runs under a token that refuses, beside the plain doctor: `ci doctor` says what's wrong (none of its flags
355
+ # acts: --hosted picks the rows, --json the format), and `ci init` writes a workflow file. Neither touches a phone.
356
+ CI_TOKEN_EXEMPT_CI = frozenset({"doctor", "init"})
357
+ _ci_token_verdicts = {} # token: (when it was asked, its verdict), for a long-lived process's many tool calls
358
+ _ci_token_lock = threading.Lock()
359
+
360
+
361
+ def ci_token_verdict(environ=None, ask=None, clock=time.monotonic):
362
+ """With ``premiumModel`` on, what this job's CI token lets run: None with no token, so a free path asks nothing and
363
+ being offline costs it nothing; "ok" once the license API vouched for it; else a CI_TOKEN_REFUSALS key, and
364
+ everything under it refuses. None with the flag off, where ci_token_problem decides."""
365
+ environ = os.environ if environ is None else environ
366
+ if not premium_model.enabled(environ):
367
+ return None
368
+ token = (environ.get(CI_TOKEN_ENV) or "").strip()
369
+ if not token:
370
+ return None
371
+ with _ci_token_lock:
372
+ asked, verdict = _ci_token_verdicts.get(token, (None, None))
373
+ if verdict is not None and clock() - asked < CI_TOKEN_STANDS.get(verdict, math.inf):
374
+ return verdict
375
+ try:
376
+ verdict = (ask or ci_token_answer)(token) or "unchecked"
377
+ except ValueError: # http.client refuses a header with a line break in it: such a token can't be verified
378
+ verdict = "unsendable"
379
+ with _ci_token_lock:
380
+ kept = _ci_token_verdicts.get(token, (None, None))[1]
381
+ if kept is not None and kept not in CI_TOKEN_STANDS: # a refusal another call heard first stands
382
+ return kept
383
+ _ci_token_verdicts[token] = (clock(), verdict)
384
+ return verdict
385
+
386
+
387
+ def ci_token_stop(command, args=None, environ=None, ask=None):
388
+ """The sentence that stops ``command`` under this job's CI token, else None: every command but the exempt ones."""
389
+ if plain_doctor(command, args) or (command == "ci" and getattr(args, "ci_command", None) in CI_TOKEN_EXEMPT_CI):
390
+ return None
391
+ return ci_token_refusal(ci_token_verdict(environ, ask))
392
+
393
+
394
+ def ci_token_refusal(verdict):
395
+ """None for no token or one the license API vouched for ("ok"); else the sentence that refuses, and a verdict no
396
+ sentence names reads as unchecked."""
397
+ if verdict is None or verdict == "ok":
398
+ return None
399
+ return CI_TOKEN_REFUSALS.get(verdict, CI_TOKEN_REFUSALS["unchecked"])
400
+
401
+
402
+ def plain_doctor(command, args=None):
403
+ """Whether ``command`` is `mobster doctor` with neither --fix, which starts (and for an expired helper rebuilds and
404
+ installs) the helper on the phone and stops processes holding Mobster's ports, nor --apps, which opens every app on
405
+ the phone and presses Home. It only says what's wrong, so it runs in a copy that fails its seal and under a CI token
406
+ that refuses. A flag it can't read counts as given."""
407
+ return command == "doctor" and getattr(args, "fix", True) is False and getattr(args, "apps", True) is False
408
+
409
+
410
+ MODIFIED = ("This copy of Mobster was changed after it was signed, so Mobster did nothing. Download Mobster again from "
411
+ "mobster.dev/download and replace the one in Applications, then ask again.")
412
+ UNCHECKED_COPY = ("Mobster couldn't check that this copy is the one it signed, so it did nothing. Download Mobster "
413
+ "again from mobster.dev/download and replace the one in Applications, then ask again.")
414
+ # The sentence for each way a copy fails its seal (genuine.failure); any other reads as unchecked.
415
+ SEAL_REFUSALS = {genuine.CHANGED: MODIFIED, genuine.UNCHECKED: UNCHECKED_COPY}
416
+
417
+
418
+ def seal_refusal():
419
+ """None when this copy passed its seal check or none runs (genuine.failure), else the sentence that says why not."""
420
+ failed = genuine.failure()
421
+ return None if failed is None else SEAL_REFUSALS.get(failed, UNCHECKED_COPY)
422
+
423
+
424
+ # `mobster mcp` starts, so an MCP client gets the sentence on each tool it calls (``TOOL``) rather than a server that
425
+ # never answers; every tool but `stop` refuses. Equally strict, and the person sees why.
426
+ SERVER_COMMANDS = frozenset({"mcp"})
427
+ TOOL = "tool"
428
+
429
+
430
+ def stopped(command, args=None):
431
+ """The sentence that stops ``command`` whatever it does, else None: a copy that fails its seal check (genuine.py)
432
+ or a CI token that refuses, both with ``premiumModel`` on. The MCP server asks with ``TOOL`` for every tool but
433
+ ``stop``."""
434
+ if command in SERVER_COMMANDS:
435
+ return None
436
+ # A failed seal stops all but the plain doctor and the version (help is answered before this).
437
+ sealed = None if command == "version" or plain_doctor(command, args) else seal_refusal()
438
+ return sealed if sealed is not None else ci_token_stop(command, args)
439
+
440
+
441
+ _ci_scope = [] # the token the CLI let in for this process (grant_ci_token)
442
+
443
+
444
+ def grant_ci_token(command, args, environ=None, named_device=None, online=None):
445
+ """Whether a live CI token licenses ``command``: a simulator run of `ci` or `test`, never a phone.
446
+
447
+ On success this process's gates accept the token. Anything else refuses."""
448
+ _ci_scope.clear()
449
+ if command not in CI_COMMANDS or ci_token_problem(environ, online=online) is not None:
450
+ return False
451
+ if may_drive_real_phone(args, named_device):
452
+ return False
453
+ environ = os.environ if environ is None else environ
454
+ _ci_scope.append(environ[CI_TOKEN_ENV].strip())
455
+ return True
456
+
457
+
458
+ def machine_id_from(platform_uuid):
459
+ return hashlib.sha256(f"{platform_uuid.strip()}{MACHINE_SALT}".encode()).hexdigest()
460
+
461
+
462
+ def platform_uuid_from_ioreg(text):
463
+ for line in text.splitlines():
464
+ if '"IOPlatformUUID"' in line and "=" in line:
465
+ value = line.split("=", 1)[1].strip().strip('"').strip()
466
+ return value or None
467
+ return None
468
+
469
+
470
+ def dev_machine_tag(environ=None):
471
+ """MOBSTER_DEV_MACHINE in a source checkout beside a loopback license server, else None."""
472
+ environ = os.environ if environ is None else environ
473
+ tag = (environ.get(DEV_MACHINE_ENV) or "").strip()
474
+ if not tag or getattr(sys, "frozen", False) or not re.fullmatch(r"[A-Za-z0-9_-]{1,32}", tag) \
475
+ or not local_account_api(environ):
476
+ return None
477
+ return tag
478
+
479
+
480
+ def machine_id():
481
+ """This Mac's id, or None where the platform UUID can't be read."""
482
+ try:
483
+ text = subprocess.run(["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], capture_output=True,
484
+ text=True, timeout=10, check=True).stdout
485
+ except (OSError, subprocess.SubprocessError):
486
+ return None
487
+ uuid = platform_uuid_from_ioreg(text)
488
+ if not uuid:
489
+ return None
490
+ tag = dev_machine_tag()
491
+ return machine_id_from(f"{uuid}:{tag}" if tag else uuid)
492
+
493
+
494
+ def bundled():
495
+ """Whether this frozen process enforces Mobster.app's checks (never the standalone CLI)."""
496
+ return build_identity.app_bundle()
497
+
498
+
499
+ def app_account_file():
500
+ """The Mac app's account.json (its data folder, paths.user_data_dir)."""
501
+ from .paths import user_data_dir
502
+ return user_data_dir() / ACCOUNT_FILE
503
+
504
+
505
+ def for_runtime(config, data_dir):
506
+ """The gate a Runtime keeps, or None: the bundled agent always; a source checkout's only for the Mac app."""
507
+ if bundled():
508
+ return PlanGate(app_account_file())
509
+ if getattr(config, "require_plan", False):
510
+ return PlanGate(Path(data_dir) / ACCOUNT_FILE)
511
+ return None
512
+
513
+
514
+ def bundled_gate():
515
+ """The gate for the bundled agent's commands and MCP tools; None in a source checkout."""
516
+ return PlanGate(app_account_file()) if bundled() else None
517
+
518
+
519
+ PHONE_READS = frozenset({("clipboard", "get"), ("file", "get"), ("file", "ls"), ("photos", "pull")})
520
+
521
+
522
+ def cli_needs_plan(command, args, named_device=None):
523
+ """Whether the bundled agent's ``command`` acts on a phone or runs a task."""
524
+ if command == "run":
525
+ return getattr(args, "demo", False) is not True
526
+ if command in ("verify", "test"):
527
+ return may_drive_real_phone(args, named_device)
528
+ if command == "ci":
529
+ return getattr(args, "ci_command", None) == "run"
530
+ if command == "expo":
531
+ return getattr(args, "expo_command", None) in ("preview", "dev", "reload", "build")
532
+ if command == "asc":
533
+ return getattr(args, "asc_command", None) not in (None, "key")
534
+ if command == "testflight":
535
+ return getattr(args, "testflight_command", None) not in (None, "key")
536
+ if command == "xcode":
537
+ return getattr(args, "xcode_command", None) == "after-build"
538
+ if command == "links":
539
+ return getattr(args, "links_command", None) == "check" and may_drive_real_phone(args, named_device)
540
+ if command == "fixtures":
541
+ return getattr(args, "fixtures_command", None) == "apply"
542
+ if command == "xray":
543
+ # Mapping an app or recording a phone needs the plan; `diff` and `network --from` read files only.
544
+ verb = getattr(args, "xray_command", None)
545
+ return verb == "app" or (verb == "network" and not getattr(args, "from_file", None))
546
+ if command == "phone":
547
+ verb = getattr(args, "phone_command", None)
548
+ return verb is not None and (verb, getattr(args, f"{verb}_command", None)) not in PHONE_READS
549
+ return False
550
+
551
+
552
+ def may_drive_real_phone(args, named_device=None):
553
+ """Whether `mobster verify` or `mobster test` may run on a real iPhone."""
554
+ if getattr(args, "testflight", None):
555
+ return True
556
+ names = getattr(args, "device", None)
557
+ if names is None:
558
+ return False
559
+ if isinstance(names, str):
560
+ names = [names]
561
+ if not isinstance(names, (list, tuple)):
562
+ return True
563
+ if not names:
564
+ return False
565
+ try:
566
+ if named_device is None:
567
+ from .device_targets import named_device
568
+ records = [named_device(name) for name in names]
569
+ except Exception:
570
+ return True
571
+ return any(not isinstance(record, dict) or record.get("kind") != "simulator" for record in records)
572
+
573
+
574
+ def mcp_needs_plan(name, annotations):
575
+ """Whether an MCP tool can need the plan: everything but the read-only tools and ``stop``."""
576
+ return name != "stop" and not (annotations or {}).get("readOnlyHint")
577
+
578
+
579
+ def gate_turned_off(environ=None):
580
+ """``premiumModel`` is on (nothing is walled but the model), or ``MOBSTER_ACCOUNT_GATE=off`` in a source checkout,
581
+ as the shell's development builds read it."""
582
+ environ = os.environ if environ is None else environ
583
+ return premium_model.enabled(environ) or (environ.get(GATE_OFF_ENV) == "off" and not getattr(sys, "frozen", False))
584
+
585
+
586
+ def refusal_text(problem, plan=None, team=None):
587
+ """The sentence a refused task or tool says for ``problem`` (``PlanGate.status``)."""
588
+ if plan in TEAM_PLANS:
589
+ return team_text(team)
590
+ return TRIAL_ENDED if plan == "trial" and problem in ("expired", "inactive") else NO_PLAN
591
+
592
+
593
+ def plain_name(value, limit=64):
594
+ """A name to put in a sentence: control characters out, spaces collapsed, at most ``limit`` characters."""
595
+ text = value if isinstance(value, str) else ""
596
+ return " ".join("".join(ch if ch.isprintable() else " " for ch in text).split())[:limit]
597
+
598
+
599
+ def team_text(team):
600
+ """The refusal on a team's station whose Team plan doesn't license it: ``team`` is the team's name, or None."""
601
+ return NO_TEAM_PLAN.format(team=plain_name(team, 80) or "Your team")
602
+
603
+
604
+ def plan_outlives_its_token(account_file, plan, now):
605
+ """Whether the plan itself still runs at ``now`` although its token ended."""
606
+ stored = stored_account(account_file)
607
+ if stored is None:
608
+ return False
609
+ entitlement = stored["entitlement"]
610
+ if str(entitlement.get("status") or "").lower() in INACTIVE_STATUSES:
611
+ return False
612
+ if plan == "lifetime":
613
+ return True
614
+ end = entitlement.get("exp")
615
+ return type(end) is int and seconds(end) > now
616
+
617
+
618
+ def moved_text(mac):
619
+ """The refusal on a Mac whose seat moved to ``mac``: what happened, where Mobster went, and how to move it back."""
620
+ return MOVED.format(mac=plain_name(mac) or "another Mac")
621
+
622
+
623
+ def moved_to(account_file):
624
+ """The Mac that took this Mac's seat, by name ("" when not known yet), when account.json says the seat moved."""
625
+ try:
626
+ stored = json.loads(Path(account_file).read_bytes())
627
+ except (OSError, ValueError):
628
+ return None
629
+ if not isinstance(stored, dict):
630
+ return None
631
+ user = stored.get("user") if isinstance(stored.get("user"), dict) else {}
632
+ entitlement = stored.get("entitlement") if isinstance(stored.get("entitlement"), dict) else {}
633
+ mac = stored.get("moved_to")
634
+ if not isinstance(user.get("id"), str) or not user["id"] or entitlement.get("token") or not isinstance(mac, str):
635
+ return None
636
+ return mac
637
+
638
+
639
+ def stored_station(account_file):
640
+ """account.json's ``station`` as the shell saved it (account.rs › Stored.station)."""
641
+ try:
642
+ stored = json.loads(Path(account_file).read_bytes())
643
+ except (OSError, ValueError):
644
+ return None
645
+ station = stored.get("station") if isinstance(stored, dict) else None
646
+ if not isinstance(station, dict) or not all(isinstance(station.get(name), str) and station[name]
647
+ for name in ("org", "station", "token")):
648
+ return None
649
+ return {"org": station["org"], "station": station["station"], "token": station["token"],
650
+ "name": station.get("name") if isinstance(station.get("name"), str) else None,
651
+ "max_seen": stored.get("max_seen")}
652
+
653
+
654
+ def stored_account(account_file):
655
+ """account.json as the shell saved it, when it holds a signed-in account and its entitlement token (``user.id`` and
656
+ ``entitlement.token``, non-empty strings); else None."""
657
+ try:
658
+ stored = json.loads(Path(account_file).read_bytes())
659
+ except (OSError, ValueError):
660
+ return None
661
+ if not isinstance(stored, dict):
662
+ return None
663
+ user = stored.get("user") if isinstance(stored.get("user"), dict) else {}
664
+ entitlement = stored.get("entitlement") if isinstance(stored.get("entitlement"), dict) else {}
665
+ token, sub = entitlement.get("token"), user.get("id")
666
+ if not isinstance(token, str) or not token or not isinstance(sub, str) or not sub:
667
+ return None
668
+ return stored
669
+
670
+
671
+ class PlanGate:
672
+ """Whether this Mac's account may run a task now, from ``account_file`` (the shell's account.json)."""
673
+
674
+ def __init__(self, account_file, *, keys=None, machine=None, clock=time.time, turned_off=None, station=None):
675
+ self.account_file = Path(account_file)
676
+ self.keys = trusted_keys() if keys is None else keys
677
+ self.clock = clock
678
+ self.turned_off = gate_turned_off() if turned_off is None else turned_off
679
+ self._machine = machine
680
+ # Answers (team id, station id) while this Mac is a team's station, else None; without it a station token never
681
+ # licenses this Mac.
682
+ self.station = station
683
+ self._lock = threading.Lock()
684
+ self._seen = 0
685
+ self._verified = {}
686
+
687
+ def machine(self):
688
+ with self._lock:
689
+ if self._machine is None:
690
+ self._machine = machine_id() or ""
691
+ return self._machine
692
+
693
+ def status(self):
694
+ """(None, None) when a task may start now, else (why not, the plan the token carried or None)."""
695
+ if genuine.failure() is not None: # premiumModel on, before the gate it turns off: serve and the app's tasks
696
+ return "modified", None
697
+ if self.turned_off or self._ci_licensed():
698
+ return None, None
699
+ stored, station = stored_account(self.account_file), stored_station(self.account_file)
700
+ if stored is None and station is None:
701
+ return "no_account", None
702
+ now = self._now((stored or station).get("max_seen"))
703
+ if station is not None:
704
+ # A machine is a person's Mac or a station, never both,
705
+ # so a personal token beside a station token licenses nothing here.
706
+ problem, plan, _ = self._station_check(station, now)
707
+ return (None, None) if problem is None else (problem, plan)
708
+ token, sub = stored["entitlement"]["token"], stored["user"]["id"]
709
+ try:
710
+ check_token(token, sub, self.machine(), now, payload=self._payload(token))
711
+ except TokenProblem as problem:
712
+ return problem.problem, problem.plan
713
+ return None, None
714
+
715
+ def _ci_licensed(self):
716
+ """Whether the CLI let a live CI token in for this simulator-only run."""
717
+ if not _ci_scope:
718
+ return False
719
+ try:
720
+ check_ci_token(_ci_scope[0], int(self.clock()), self.keys)
721
+ except TokenProblem:
722
+ return False
723
+ return True
724
+
725
+ def station_status(self):
726
+ """The team's entitlement on this Mac, alone."""
727
+ station = stored_station(self.account_file)
728
+ if station is None:
729
+ return "no_token", None, None
730
+ return self._station_check(station, self._now(station.get("max_seen")))
731
+
732
+ def _now(self, seen):
733
+ """The latest time this Mac has shown: its clock, this process's, and account.json's ``max_seen``."""
734
+ with self._lock:
735
+ now = max(int(self.clock()), self._seen, seen if type(seen) is int else 0)
736
+ self._seen = now
737
+ return now
738
+
739
+ def _station_check(self, station, now):
740
+ try:
741
+ payload = self._payload(station["token"])
742
+ ours = self._station_identity()
743
+ if ours is None:
744
+ plan = json.loads(payload).get("plan") if payload else None
745
+ raise TokenProblem("not_a_station", plan if plan in TEAM_PLANS else "team")
746
+ claims = check_station_token(station["token"], ours[0], ours[1], self.machine(), now, payload=payload)
747
+ except TokenProblem as problem:
748
+ return problem.problem, problem.plan or "team", None
749
+ except (ValueError, AttributeError):
750
+ return "malformed", "team", None
751
+ return None, claims["plan"], claims
752
+
753
+ def _payload(self, token):
754
+ """``token``'s verified payload, from the cache or checked now; TokenProblem when its signature fails."""
755
+ with self._lock:
756
+ payload = self._verified.get(token)
757
+ if payload is None:
758
+ payload = verified_payload(token, self.keys)
759
+ with self._lock:
760
+ if len(self._verified) >= 8:
761
+ self._verified.clear()
762
+ self._verified[token] = payload
763
+ return payload
764
+
765
+ def _station_identity(self):
766
+ """(team id, station id) from this Mac's team state, or None: no state, none set, or it can't be read."""
767
+ source = self.station
768
+ if source is None:
769
+ return None
770
+ try:
771
+ identity = source()
772
+ except Exception: # noqa: BLE001 -- unreadable team state refuses (fail closed)
773
+ return None
774
+ if (not isinstance(identity, tuple) or len(identity) != 2
775
+ or not all(isinstance(part, str) and part for part in identity)):
776
+ return None
777
+ return identity
778
+
779
+ def problem(self):
780
+ """None when a task may start now, else why not (a TokenProblem's name, or no_account)."""
781
+ return self.status()[0]
782
+
783
+ def refusal(self):
784
+ """None when a task may start now, else the sentence that says why not and what to do."""
785
+ problem, plan = self.status()
786
+ if problem is None:
787
+ return None
788
+ if problem == "modified":
789
+ return seal_refusal() or UNCHECKED_COPY
790
+ if plan in TEAM_PLANS:
791
+ station = stored_station(self.account_file)
792
+ return team_text(station["name"] if station else None)
793
+ with self._lock:
794
+ now = self._seen
795
+ if problem == "expired" and plan_outlives_its_token(self.account_file, plan, now):
796
+ return CHECK_IN
797
+ mac = moved_to(self.account_file) if problem == "no_account" else None
798
+ return moved_text(mac) if mac is not None else refusal_text(problem, plan)
799
+
800
+ def require(self):
801
+ """APIError 403 plan_required unless a task may start now."""
802
+ message = self.refusal()
803
+ if message is not None:
804
+ raise APIError(message, 403, "plan_required")
805
+
806
+ def allows(self):
807
+ return self.problem() is None