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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""A check (docs/checks.md). Every unknown key is an error, so a typo never silently weakens a check."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .. import links
|
|
13
|
+
from ..state import validate_bundle_id
|
|
14
|
+
from . import conditions, dialogs, netstubs, perf, wire
|
|
15
|
+
from .assertions import CheckError, expand_assertion, reject_unknown_keys
|
|
16
|
+
from .snapshots import Snapshot, parse_snapshot
|
|
17
|
+
from .system_surfaces import spec as surface_spec
|
|
18
|
+
|
|
19
|
+
__all__ = ["CheckError", "Check", "check_from_dict", "load_check", "dump_check", "project_root",
|
|
20
|
+
"RESETS", "LIMITS"]
|
|
21
|
+
|
|
22
|
+
RESETS = ("none", "data", "reinstall")
|
|
23
|
+
LIMITS = {"name": 120, "steps": 20, "step": 500, "expect": 50, "args": 20, "env": 20, "url": 2000,
|
|
24
|
+
"max_usd": 1.00, "max_seconds": 600, "tags": 10}
|
|
25
|
+
DEFAULT_MAX_SECONDS = 180
|
|
26
|
+
DEFAULT_MAX_USD = 0.25
|
|
27
|
+
FILE_LIMIT = 1_000_000
|
|
28
|
+
MAX_NESTING = 100 # lists and objects in a check file; one nests a few deep
|
|
29
|
+
ENV_KEY = re.compile(r"[A-Z_][A-Z0-9_]*")
|
|
30
|
+
URL_SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.\-]*:")
|
|
31
|
+
TAG = re.compile(r"[a-z0-9][a-z0-9_-]{0,39}")
|
|
32
|
+
LAUNCH_ONLY_NAME = "Launch check"
|
|
33
|
+
|
|
34
|
+
FILE_KEYS = ("version", "name", "app", "device", "runtime", "reset", "tags", "launch", "steps", "expect",
|
|
35
|
+
"throughout", "somewhere", "audit", "a11y", "fixture", "business", "conditions", "dialogs", "stubs",
|
|
36
|
+
"budget", "schedule")
|
|
37
|
+
TOOL_KEYS = ("name", "app_path", "bundle_id", "device", "runtime", "reset", "launch_args", "launch_env", "open_url",
|
|
38
|
+
"steps", "expect", "max_usd", "max_seconds")
|
|
39
|
+
TOOL_ONLY = ("image",)
|
|
40
|
+
FILE_SHAPE_KEYS = ("version", "app", "launch", "budget")
|
|
41
|
+
TOOL_SHAPE_KEYS = ("app_path", "bundle_id", "launch_args", "launch_env", "open_url", "max_usd", "max_seconds")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class Check:
|
|
46
|
+
name: str
|
|
47
|
+
steps: tuple # of str
|
|
48
|
+
expect: tuple # of assertions.Assertion
|
|
49
|
+
app_path: Optional[str] # absolute
|
|
50
|
+
bundle_id: Optional[str]
|
|
51
|
+
device: Optional[str]
|
|
52
|
+
runtime: Optional[str]
|
|
53
|
+
reset: str # "auto" | "none" | "data" | "reinstall"
|
|
54
|
+
launch_args: tuple
|
|
55
|
+
launch_env: dict = field(default_factory=dict)
|
|
56
|
+
open_url: Optional[str] = None
|
|
57
|
+
max_seconds: float = DEFAULT_MAX_SECONDS
|
|
58
|
+
max_usd: float = DEFAULT_MAX_USD
|
|
59
|
+
source: Optional[str] = None # the file it came from
|
|
60
|
+
tags: tuple = () # of str: `mobster test --tag` picks checks by them
|
|
61
|
+
# "every 1h" or a five-field cron: Mobster runs this check by itself (testkit/health.py)
|
|
62
|
+
schedule: Optional[str] = None
|
|
63
|
+
throughout: tuple = () # of assertions.Assertion (no_text, absent): must hold on every screen the run reads
|
|
64
|
+
somewhere: tuple = () # of assertions.Assertion: each must hold on at least one screen the run reads
|
|
65
|
+
audit: tuple = () # of str: static audits of the built app (audit.AUDITS)
|
|
66
|
+
fixture: tuple = () # of str: names in .mobster/fixtures/ the check starts from (fixtures.py)
|
|
67
|
+
business: object = None # business.BusinessSpec: locally registered app-state fixture and comparisons
|
|
68
|
+
seed: object = field(default_factory=lambda: _no_seed()) # fixtures.Seed, once the fixtures are flattened in
|
|
69
|
+
fixture_origin: object = None # the check as written, once the fixtures are flattened into it
|
|
70
|
+
sound: tuple = () # of str: sounds (.mobster/sounds/) the iPhone must play during the run (sound.py)
|
|
71
|
+
a11y: Optional[str] = None # "warn" | "strict": the accessibility audit (a11y.py); `audit: a11y` is strict
|
|
72
|
+
a11y_scope: str = "" # the check's file as results.json names it: a baseline entry's first part
|
|
73
|
+
a11y_baseline: frozenset = frozenset() # baseline keys (testkit/a11y_baseline.py) whose findings never fail
|
|
74
|
+
a11y_refresh: bool = False # internal --update-a11y-baseline run; never part of a saved check
|
|
75
|
+
draft_trial: bool = False # internal `mobster test minimize` trial; never part of a saved check
|
|
76
|
+
conditions: dict = field(default_factory=dict) # conditions.parse's block: the device's state for the run
|
|
77
|
+
dialogs: dict = field(default_factory=dict) # dialogs.parse's block: how the run answers iOS's own dialogs
|
|
78
|
+
perf_budget: dict = field(default_factory=dict) # budget.launch_ms / memory_mb / hangs (perf.judge)
|
|
79
|
+
surfaces: tuple = () # of system_surfaces.Surface: widgets, Live Activities and the like (experimental)
|
|
80
|
+
stubs: tuple = () # of dict: netstubs.parse's entries, answered by the run's proxy (simulator only)
|
|
81
|
+
wire: tuple = () # of wire.WireAssertion: expect request, no_request and storage (dark, simulator only)
|
|
82
|
+
l10n: bool = False # `mobster test --locale`: audit the screens for truncation, untranslated text, RTL
|
|
83
|
+
# the `snapshot:` in expect: the final screen against its baseline (snapshots.py)
|
|
84
|
+
snapshot: Optional[Snapshot] = None
|
|
85
|
+
snapshot_update: bool = False # `mobster test --update-snapshots`: write the baseline from this run
|
|
86
|
+
snapshot_record: bool = True # False in an approved CI suite: a missing baseline fails, and none is recorded
|
|
87
|
+
|
|
88
|
+
def reset_level(self):
|
|
89
|
+
"""The reset this run uses: the check's own, else reinstall with a .app and data without. A system app
|
|
90
|
+
(com.apple.*) is never reset."""
|
|
91
|
+
if (self.bundle_id or "").startswith("com.apple."):
|
|
92
|
+
return "none"
|
|
93
|
+
if self.reset != "auto":
|
|
94
|
+
return self.reset
|
|
95
|
+
return "reinstall" if self.app_path else "data"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _text(value, where, limit, *, required=False):
|
|
99
|
+
if value is None:
|
|
100
|
+
if required:
|
|
101
|
+
raise CheckError(f"{where} is required")
|
|
102
|
+
return None
|
|
103
|
+
if isinstance(value, bool) or not isinstance(value, str):
|
|
104
|
+
raise CheckError(f"{where} must be text; quote it if it looks like a number or yes/no")
|
|
105
|
+
value = value.strip()
|
|
106
|
+
if not value:
|
|
107
|
+
if required:
|
|
108
|
+
raise CheckError(f"{where} is empty")
|
|
109
|
+
return None
|
|
110
|
+
if len(value) > limit:
|
|
111
|
+
raise CheckError(f"{where} is longer than {limit:,} characters")
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _number(value, where, low, high, default):
|
|
116
|
+
if value is None:
|
|
117
|
+
return default
|
|
118
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
119
|
+
raise CheckError(f"{where} must be a number")
|
|
120
|
+
if not low < value <= high:
|
|
121
|
+
raise CheckError(f"{where} must be more than {low:g} and at most {high:g}")
|
|
122
|
+
return float(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _strings(value, where, limit, item_limit=1000):
|
|
126
|
+
if value is None:
|
|
127
|
+
return ()
|
|
128
|
+
if not isinstance(value, list):
|
|
129
|
+
raise CheckError(f"{where} must be a list")
|
|
130
|
+
if len(value) > limit:
|
|
131
|
+
raise CheckError(f"{where} has more than {limit} items")
|
|
132
|
+
out = []
|
|
133
|
+
for index, item in enumerate(value):
|
|
134
|
+
if isinstance(item, bool) or not isinstance(item, str):
|
|
135
|
+
raise CheckError(f"{where}[{index}] must be text; quote it, as in \"YES\" or \"3\"")
|
|
136
|
+
if len(item) > item_limit:
|
|
137
|
+
raise CheckError(f"{where}[{index}] is longer than {item_limit:,} characters")
|
|
138
|
+
out.append(item)
|
|
139
|
+
return tuple(out)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _stubs(data, base_dir):
|
|
143
|
+
stubs = netstubs.parse(data.get("stubs"), base_dir)
|
|
144
|
+
if stubs and (data.get("conditions") or {}).get("network") == "offline":
|
|
145
|
+
raise CheckError("stubs answer requests, and conditions.network offline refuses them all. Use slow-3g, lte "
|
|
146
|
+
"or flaky with stubs, or remove the stubs.")
|
|
147
|
+
return stubs
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _env(value, where):
|
|
151
|
+
if value is None:
|
|
152
|
+
return {}
|
|
153
|
+
if not isinstance(value, dict):
|
|
154
|
+
raise CheckError(f"{where} must be an object of KEY: value")
|
|
155
|
+
if len(value) > LIMITS["env"]:
|
|
156
|
+
raise CheckError(f"{where} has more than {LIMITS['env']} entries")
|
|
157
|
+
out = {}
|
|
158
|
+
for key, item in value.items():
|
|
159
|
+
if not isinstance(key, str) or not ENV_KEY.fullmatch(key):
|
|
160
|
+
raise CheckError(f"{where}: {key!r} is not a variable name (A-Z, 0-9 and _, not starting with a digit)")
|
|
161
|
+
if isinstance(item, bool) or not isinstance(item, str):
|
|
162
|
+
raise CheckError(f"{where}.{key} must be text; quote it, as in \"3\" or \"YES\"")
|
|
163
|
+
if len(item) > 1000:
|
|
164
|
+
raise CheckError(f"{where}.{key} is longer than 1,000 characters")
|
|
165
|
+
out[key] = item
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _perf_budget(budget):
|
|
170
|
+
"""``budget.launch_ms``, ``memory_mb`` and ``hangs``: the limits a run's measurements must stay inside."""
|
|
171
|
+
out = {}
|
|
172
|
+
for key, high in (("launch_ms", 600_000), ("memory_mb", 65_536)):
|
|
173
|
+
if budget.get(key) is not None:
|
|
174
|
+
out[key] = _plain_number(_number(budget[key], f"budget.{key}", 0, high, None))
|
|
175
|
+
hangs = budget.get("hangs")
|
|
176
|
+
if hangs is not None:
|
|
177
|
+
if isinstance(hangs, bool) or not isinstance(hangs, int) or not 0 <= hangs <= 1000:
|
|
178
|
+
raise CheckError("budget.hangs must be a whole number from 0 to 1000: the hangs of 250 ms or longer "
|
|
179
|
+
"a run may have")
|
|
180
|
+
out["hangs"] = hangs
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _steps(value):
|
|
185
|
+
if value is None:
|
|
186
|
+
return ()
|
|
187
|
+
if isinstance(value, str):
|
|
188
|
+
value = [value]
|
|
189
|
+
if not isinstance(value, list):
|
|
190
|
+
raise CheckError("steps must be a list of plain-English steps")
|
|
191
|
+
if len(value) > LIMITS["steps"]:
|
|
192
|
+
raise CheckError(f"steps has more than {LIMITS['steps']} steps")
|
|
193
|
+
return tuple(_text(step, f"steps[{index}]", LIMITS["step"], required=True) for index, step in enumerate(value))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _expect_list(value, key, snapshots=None):
|
|
197
|
+
"""The assertions of a list; a ``snapshot`` entry goes to ``snapshots`` (only ``expect`` takes one)."""
|
|
198
|
+
if value is None:
|
|
199
|
+
return ()
|
|
200
|
+
if not isinstance(value, list):
|
|
201
|
+
raise CheckError(f"{key} must be a list of assertions, such as [{{text: Continue}}]")
|
|
202
|
+
out = []
|
|
203
|
+
for index, item in enumerate(value):
|
|
204
|
+
if isinstance(item, dict) and "snapshot" in item:
|
|
205
|
+
if snapshots is None:
|
|
206
|
+
raise CheckError(f"{key}[{index}] is a snapshot, which goes in expect")
|
|
207
|
+
if snapshots:
|
|
208
|
+
raise CheckError(f"{key}[{index}] is a second snapshot. A check ends on one screen and has one "
|
|
209
|
+
"snapshot; write another check for another screen.")
|
|
210
|
+
snapshots.append(parse_snapshot(item, f"{key}[{index}]"))
|
|
211
|
+
continue
|
|
212
|
+
if key == "expect" and wire.is_wire(item):
|
|
213
|
+
continue # wire.parse reads these
|
|
214
|
+
out.extend(expand_assertion(item, f"{key}[{index}]"))
|
|
215
|
+
if len(out) > LIMITS["expect"]:
|
|
216
|
+
raise CheckError(f"{key} has more than {LIMITS['expect']} assertions")
|
|
217
|
+
return tuple(out)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _expect(value):
|
|
221
|
+
"""(the assertions, the snapshot or None) of a check's ``expect``."""
|
|
222
|
+
snapshots = []
|
|
223
|
+
return _expect_list(value, "expect", snapshots), (snapshots[0] if snapshots else None)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _split_expect(value):
|
|
227
|
+
"""(the tree assertions, the sound names) of ``expect``: ``{sound: sent}`` is heard (sound.py), not read."""
|
|
228
|
+
from .sound import NAME
|
|
229
|
+
if not isinstance(value, list):
|
|
230
|
+
return value, ()
|
|
231
|
+
tree, sounds = [], []
|
|
232
|
+
for index, item in enumerate(value):
|
|
233
|
+
if not (isinstance(item, dict) and "sound" in item):
|
|
234
|
+
tree.append(item)
|
|
235
|
+
elif set(item) != {"sound"} or not isinstance(item["sound"], str) or not NAME.fullmatch(item["sound"]):
|
|
236
|
+
raise CheckError(f"expect[{index}] sound is a name such as {{sound: sent}}: lowercase letters, digits, "
|
|
237
|
+
"- and _")
|
|
238
|
+
else:
|
|
239
|
+
sounds.append(item["sound"])
|
|
240
|
+
return tree, tuple(dict.fromkeys(sounds))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
THROUGHOUT_KINDS = ("no_text", "absent")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _throughout(value):
|
|
247
|
+
"""``throughout``: assertions that must hold on every screen the run reads, so only the ones about what must never
|
|
248
|
+
show (no_text, absent)."""
|
|
249
|
+
out = _expect_list(value, "throughout")
|
|
250
|
+
for index, item in enumerate(out):
|
|
251
|
+
if item.kind not in THROUGHOUT_KINDS:
|
|
252
|
+
raise CheckError(f"throughout[{index}] is a {item.kind}; use no_text or absent, which can hold on "
|
|
253
|
+
"every screen")
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _audit(value):
|
|
258
|
+
from .a11y import NAME as A11Y
|
|
259
|
+
from .audit import AUDITS
|
|
260
|
+
from .device_audit import NAME as AX_DEVICE
|
|
261
|
+
names = (*AUDITS, A11Y, AX_DEVICE)
|
|
262
|
+
if value is None:
|
|
263
|
+
return ()
|
|
264
|
+
if isinstance(value, str):
|
|
265
|
+
value = [value]
|
|
266
|
+
if not isinstance(value, list):
|
|
267
|
+
raise CheckError("audit must be a list of names, such as [usage-descriptions]")
|
|
268
|
+
for index, name in enumerate(value):
|
|
269
|
+
if name not in names:
|
|
270
|
+
close = difflib.get_close_matches(str(name), list(names), n=1)
|
|
271
|
+
raise CheckError(f"audit[{index}] {name!r} is not an audit; "
|
|
272
|
+
+ (f"did you mean {close[0]}?" if close else f"use {', '.join(names)}"))
|
|
273
|
+
return tuple(dict.fromkeys(value))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _no_seed():
|
|
277
|
+
from .fixtures import Seed
|
|
278
|
+
return Seed()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _fixture(value, base_dir):
|
|
282
|
+
"""A check file's ``fixture``: a name or a list of names. With a project folder, each must exist."""
|
|
283
|
+
from . import fixtures
|
|
284
|
+
if value is None:
|
|
285
|
+
return ()
|
|
286
|
+
value = [value] if isinstance(value, str) else value
|
|
287
|
+
if not isinstance(value, list) or not value or len(value) > 10 or not all(
|
|
288
|
+
isinstance(item, str) and fixtures.NAME.fullmatch(item) for item in value):
|
|
289
|
+
raise CheckError("fixture is the name of a fixture in .mobster/fixtures/, or a list of names")
|
|
290
|
+
if base_dir is not None and (Path(base_dir) / ".mobster").is_dir():
|
|
291
|
+
for name in value:
|
|
292
|
+
if fixtures.path_of(base_dir, name) is None:
|
|
293
|
+
have = ", ".join(fixtures.names(base_dir)[:8])
|
|
294
|
+
raise CheckError(f"fixture {name!r} isn't in .mobster/fixtures/"
|
|
295
|
+
+ (f"; you have {have}" if have else ""))
|
|
296
|
+
return tuple(dict.fromkeys(value))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _a11y(value):
|
|
300
|
+
if value is None or value is False:
|
|
301
|
+
return None
|
|
302
|
+
if value is True:
|
|
303
|
+
return "warn"
|
|
304
|
+
if value not in ("warn", "strict"):
|
|
305
|
+
raise CheckError("a11y is warn, strict or false: warn reports findings and passes, strict fails the check")
|
|
306
|
+
return value
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _url(value, where):
|
|
310
|
+
url = _text(value, where, LIMITS["url"])
|
|
311
|
+
if url is not None and not URL_SCHEME.match(url):
|
|
312
|
+
raise CheckError(f"{where} must be a link with a scheme, such as yourapp://lesson")
|
|
313
|
+
return url
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _tags(value):
|
|
317
|
+
"""A check file's ``tags``: a list of short lowercase words (a-z, 0-9, - and _), or one such word."""
|
|
318
|
+
if value is None:
|
|
319
|
+
return ()
|
|
320
|
+
if isinstance(value, str):
|
|
321
|
+
value = [value]
|
|
322
|
+
if not isinstance(value, list):
|
|
323
|
+
raise CheckError("tags must be a list of words, such as [smoke, paywall]")
|
|
324
|
+
if len(value) > LIMITS["tags"]:
|
|
325
|
+
raise CheckError(f"tags has more than {LIMITS['tags']} tags")
|
|
326
|
+
out = []
|
|
327
|
+
for index, item in enumerate(value):
|
|
328
|
+
if isinstance(item, bool) or not isinstance(item, str) or not TAG.fullmatch(item.strip()):
|
|
329
|
+
raise CheckError(f"tags[{index}] must be a word of 1 to 40 of a-z, 0-9, - and _, such as smoke")
|
|
330
|
+
if item.strip() not in out:
|
|
331
|
+
out.append(item.strip())
|
|
332
|
+
return tuple(out)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _resolve(path, base_dir):
|
|
336
|
+
path = Path(os.path.expanduser(path))
|
|
337
|
+
if not path.is_absolute():
|
|
338
|
+
path = Path(base_dir or os.getcwd()) / path
|
|
339
|
+
return str(path.resolve())
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _business(value):
|
|
343
|
+
from .business import parse_business
|
|
344
|
+
return parse_business(value)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def check_from_dict(data, *, base_dir=None):
|
|
348
|
+
"""A Check from the file shape (§2.1) or the MCP tool arguments."""
|
|
349
|
+
if not isinstance(data, dict):
|
|
350
|
+
raise CheckError("a check must be an object (a YAML mapping)")
|
|
351
|
+
surfaces = data.get("surfaces")
|
|
352
|
+
data = {key: value for key, value in data.items() if key not in (*TOOL_ONLY, "surfaces")}
|
|
353
|
+
file_only = [key for key in FILE_SHAPE_KEYS if key in data]
|
|
354
|
+
tool_only = [key for key in TOOL_SHAPE_KEYS if key in data]
|
|
355
|
+
if file_only and tool_only:
|
|
356
|
+
raise CheckError(f"the check mixes the file format ({', '.join(file_only)}) and tool arguments "
|
|
357
|
+
f"({', '.join(tool_only)})")
|
|
358
|
+
if "tags" in data and tool_only:
|
|
359
|
+
raise CheckError("tags goes in a check file, not in tool arguments")
|
|
360
|
+
if "schedule" in data and tool_only:
|
|
361
|
+
raise CheckError("schedule goes in a check file, not in tool arguments")
|
|
362
|
+
if surfaces is not None and tool_only:
|
|
363
|
+
raise CheckError("surfaces goes in a check file, not in tool arguments")
|
|
364
|
+
if not file_only and not tool_only:
|
|
365
|
+
reject_unknown_keys(data, tuple(dict.fromkeys(FILE_KEYS + TOOL_KEYS)), "the check")
|
|
366
|
+
raise CheckError("the check needs an app: app.bundle or app.path in a check file, bundle_id or app_path "
|
|
367
|
+
"in tool arguments")
|
|
368
|
+
perf_limits = {}
|
|
369
|
+
if tool_only:
|
|
370
|
+
reject_unknown_keys(data, TOOL_KEYS, "the check")
|
|
371
|
+
fields = {"app_path": data.get("app_path"), "bundle": data.get("bundle_id"),
|
|
372
|
+
"args": data.get("launch_args"), "env": data.get("launch_env"), "url": data.get("open_url"),
|
|
373
|
+
"max_usd": data.get("max_usd"), "max_seconds": data.get("max_seconds"),
|
|
374
|
+
"where": {"path": "app_path", "bundle": "bundle_id", "args": "launch_args",
|
|
375
|
+
"env": "launch_env", "url": "open_url", "max_usd": "max_usd",
|
|
376
|
+
"max_seconds": "max_seconds"}}
|
|
377
|
+
else:
|
|
378
|
+
reject_unknown_keys(data, FILE_KEYS, "the check")
|
|
379
|
+
version = data.get("version", 1)
|
|
380
|
+
if version != 1 or isinstance(version, bool):
|
|
381
|
+
raise CheckError(f"version {version!r} is not supported; 1 is the only version")
|
|
382
|
+
app = {} if data.get("app") is None else data["app"]
|
|
383
|
+
if not isinstance(app, dict):
|
|
384
|
+
raise CheckError("app must be an object with bundle and/or path")
|
|
385
|
+
reject_unknown_keys(app, ("bundle", "path"), "app")
|
|
386
|
+
launch = {} if data.get("launch") is None else data["launch"]
|
|
387
|
+
if not isinstance(launch, dict):
|
|
388
|
+
raise CheckError("launch must be an object with args, env and/or url")
|
|
389
|
+
reject_unknown_keys(launch, ("args", "env", "url"), "launch")
|
|
390
|
+
budget = {} if data.get("budget") is None else data["budget"]
|
|
391
|
+
perf_limits = budget
|
|
392
|
+
if not isinstance(budget, dict):
|
|
393
|
+
raise CheckError("budget must be an object with max_seconds, max_usd, launch_ms, memory_mb and/or hangs")
|
|
394
|
+
reject_unknown_keys(budget, ("max_seconds", "max_usd", *perf.BUDGET_KEYS), "budget")
|
|
395
|
+
fields = {"app_path": app.get("path"), "bundle": app.get("bundle"), "args": launch.get("args"),
|
|
396
|
+
"env": launch.get("env"), "url": launch.get("url"), "max_usd": budget.get("max_usd"),
|
|
397
|
+
"max_seconds": budget.get("max_seconds"),
|
|
398
|
+
"where": {"path": "app.path", "bundle": "app.bundle", "args": "launch.args", "env": "launch.env",
|
|
399
|
+
"url": "launch.url", "max_usd": "budget.max_usd", "max_seconds": "budget.max_seconds"}}
|
|
400
|
+
where = fields["where"]
|
|
401
|
+
app_path = _text(fields["app_path"], where["path"], 4096)
|
|
402
|
+
if app_path is not None:
|
|
403
|
+
app_path = _resolve(app_path, base_dir)
|
|
404
|
+
bundle = _text(fields["bundle"], where["bundle"], 255)
|
|
405
|
+
if bundle is not None:
|
|
406
|
+
try:
|
|
407
|
+
bundle = validate_bundle_id(bundle)
|
|
408
|
+
except ValueError:
|
|
409
|
+
raise CheckError(f"{where['bundle']} {bundle!r} is not a bundle ID such as com.example.app") from None
|
|
410
|
+
if app_path is None and bundle is None:
|
|
411
|
+
raise CheckError(f"the check needs {where['bundle']} or {where['path']}: the app to check")
|
|
412
|
+
reset = data.get("reset")
|
|
413
|
+
if reset is not None and reset not in RESETS:
|
|
414
|
+
raise CheckError(f"reset must be one of {', '.join(RESETS)}, not {reset!r}")
|
|
415
|
+
steps = _steps(data.get("steps"))
|
|
416
|
+
name = _text(data.get("name"), "name", LIMITS["name"])
|
|
417
|
+
if name is None:
|
|
418
|
+
name = (steps[0][:LIMITS["name"] - 1] + "…" if len(steps[0]) > LIMITS["name"] else steps[0]) if steps \
|
|
419
|
+
else LAUNCH_ONLY_NAME
|
|
420
|
+
tree_expect, sounds = _split_expect(data.get("expect"))
|
|
421
|
+
expect, snapshot = _expect(tree_expect)
|
|
422
|
+
return Check(
|
|
423
|
+
name=name, steps=steps, expect=expect, snapshot=snapshot, sound=sounds, app_path=app_path, bundle_id=bundle,
|
|
424
|
+
device=_text(data.get("device"), "device", 100), runtime=_text(data.get("runtime"), "runtime", 100),
|
|
425
|
+
reset=reset or "auto", launch_args=_strings(fields["args"], where["args"], LIMITS["args"]),
|
|
426
|
+
launch_env=_env(fields["env"], where["env"]), open_url=_url(fields["url"], where["url"]),
|
|
427
|
+
max_seconds=_number(fields["max_seconds"], where["max_seconds"], 0, LIMITS["max_seconds"],
|
|
428
|
+
DEFAULT_MAX_SECONDS),
|
|
429
|
+
max_usd=_number(fields["max_usd"], where["max_usd"], 0, LIMITS["max_usd"], DEFAULT_MAX_USD),
|
|
430
|
+
tags=_tags(data.get("tags")), schedule=_text(data.get("schedule"), "schedule", 128),
|
|
431
|
+
throughout=_throughout(data.get("throughout")),
|
|
432
|
+
somewhere=_expect_list(data.get("somewhere"), "somewhere"),
|
|
433
|
+
audit=_audit(data.get("audit")), a11y=_a11y(data.get("a11y")), fixture=_fixture(data.get("fixture"), base_dir),
|
|
434
|
+
business=_business(data.get("business")),
|
|
435
|
+
conditions=conditions.parse(data.get("conditions"), base_dir), dialogs=dialogs.parse(data.get("dialogs")),
|
|
436
|
+
stubs=_stubs(data, base_dir), wire=wire.parse(data.get("expect")),
|
|
437
|
+
surfaces=surface_spec.parse(surfaces, base_dir), perf_budget=_perf_budget(perf_limits))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def project_root(path=None):
|
|
441
|
+
"""The directory holding ``.mobster/``, walking up from ``path`` (a file or folder; default the working directory);
|
|
442
|
+
else ``path``'s own folder."""
|
|
443
|
+
path = Path(path or Path.cwd()).resolve()
|
|
444
|
+
start = path if path.is_dir() else path.parent
|
|
445
|
+
for folder in (start, *start.parents):
|
|
446
|
+
if (folder / ".mobster").is_dir():
|
|
447
|
+
return folder
|
|
448
|
+
return start
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def load_check(path):
|
|
452
|
+
"""A check file (.yaml, .yml or .json). Raises CheckError with the file and line on a problem."""
|
|
453
|
+
path = Path(path).expanduser()
|
|
454
|
+
try:
|
|
455
|
+
size = path.stat().st_size
|
|
456
|
+
except FileNotFoundError:
|
|
457
|
+
raise CheckError(f"{path} does not exist") from None
|
|
458
|
+
except OSError as error:
|
|
459
|
+
raise CheckError(f"{path} can't be read: {error.strerror or error}") from None
|
|
460
|
+
if not path.is_file():
|
|
461
|
+
raise CheckError(f"{path} is not a file")
|
|
462
|
+
if size > FILE_LIMIT:
|
|
463
|
+
raise CheckError(f"{path} is larger than 1 MB")
|
|
464
|
+
try:
|
|
465
|
+
text = path.read_text(encoding="utf-8")
|
|
466
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
467
|
+
raise CheckError(f"{path} can't be read as UTF-8 text ({type(error).__name__})") from None
|
|
468
|
+
if path.suffix.lower() == ".json":
|
|
469
|
+
try:
|
|
470
|
+
data = json.loads(text)
|
|
471
|
+
except json.JSONDecodeError as error:
|
|
472
|
+
raise CheckError(f"{path} line {error.lineno}: {error.msg}") from None
|
|
473
|
+
except (RecursionError, ValueError):
|
|
474
|
+
raise CheckError(f"{path}: nested too deeply") from None
|
|
475
|
+
if _nests_too_deep(data):
|
|
476
|
+
raise CheckError(f"{path}: nested too deeply")
|
|
477
|
+
else:
|
|
478
|
+
import yaml
|
|
479
|
+
try:
|
|
480
|
+
data = yaml.safe_load(text)
|
|
481
|
+
except yaml.YAMLError as error:
|
|
482
|
+
mark = getattr(error, "problem_mark", None)
|
|
483
|
+
line = f" line {mark.line + 1}" if mark is not None else ""
|
|
484
|
+
problem = getattr(error, "problem", None) or "not valid YAML"
|
|
485
|
+
raise CheckError(f"{path}{line}: {problem}") from None
|
|
486
|
+
except (RecursionError, ValueError):
|
|
487
|
+
raise CheckError(f"{path}: nested too deeply") from None
|
|
488
|
+
if data is None:
|
|
489
|
+
raise CheckError(f"{path} is empty")
|
|
490
|
+
try:
|
|
491
|
+
check = check_from_dict(data, base_dir=project_root(path))
|
|
492
|
+
except CheckError as error:
|
|
493
|
+
raise CheckError(f"{path}: {error}") from None
|
|
494
|
+
return _with_source(check, str(path.resolve()))
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _nests_too_deep(value):
|
|
498
|
+
"""True when ``value``'s lists and objects nest more than MAX_NESTING deep. From Python 3.14 json.loads parses
|
|
499
|
+
nesting that 3.13 and earlier refused with RecursionError; YAML's parser still refuses it that way."""
|
|
500
|
+
pending = [(value, 0)]
|
|
501
|
+
while pending:
|
|
502
|
+
node, depth = pending.pop()
|
|
503
|
+
if isinstance(node, (dict, list)):
|
|
504
|
+
if depth == MAX_NESTING:
|
|
505
|
+
return True
|
|
506
|
+
pending.extend((child, depth + 1) for child in (node.values() if isinstance(node, dict) else node))
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _with_source(check, source):
|
|
511
|
+
from dataclasses import replace
|
|
512
|
+
return replace(check, source=source)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def check_to_dict(check, *, project_root=None):
|
|
516
|
+
"""The file shape of ``check``: ``app.path`` relative to ``project_root`` when it sits inside it."""
|
|
517
|
+
check = check.fixture_origin or check # a check with its fixtures flattened in is saved as it was written
|
|
518
|
+
out = {"version": 1, "name": check.name}
|
|
519
|
+
app = {}
|
|
520
|
+
if check.bundle_id:
|
|
521
|
+
app["bundle"] = check.bundle_id
|
|
522
|
+
if check.app_path:
|
|
523
|
+
path = Path(check.app_path)
|
|
524
|
+
if project_root is not None:
|
|
525
|
+
try:
|
|
526
|
+
path = path.resolve().relative_to(Path(project_root).resolve())
|
|
527
|
+
except ValueError:
|
|
528
|
+
pass
|
|
529
|
+
app["path"] = str(path)
|
|
530
|
+
out["app"] = app
|
|
531
|
+
if check.device:
|
|
532
|
+
out["device"] = check.device
|
|
533
|
+
if check.runtime:
|
|
534
|
+
out["runtime"] = check.runtime
|
|
535
|
+
if check.reset != "auto":
|
|
536
|
+
out["reset"] = check.reset
|
|
537
|
+
if check.tags:
|
|
538
|
+
out["tags"] = list(check.tags)
|
|
539
|
+
if check.schedule:
|
|
540
|
+
out["schedule"] = check.schedule
|
|
541
|
+
launch = {}
|
|
542
|
+
if check.launch_args:
|
|
543
|
+
launch["args"] = list(check.launch_args)
|
|
544
|
+
if check.launch_env:
|
|
545
|
+
launch["env"] = dict(check.launch_env)
|
|
546
|
+
if check.open_url:
|
|
547
|
+
launch["url"] = check.open_url
|
|
548
|
+
if launch:
|
|
549
|
+
out["launch"] = launch
|
|
550
|
+
if check.steps:
|
|
551
|
+
out["steps"] = list(check.steps)
|
|
552
|
+
out["expect"] = [assertion.to_dict() for assertion in (*check.expect, *check.wire)] + [
|
|
553
|
+
{"sound": name} for name in check.sound]
|
|
554
|
+
if check.snapshot:
|
|
555
|
+
out["expect"].append(check.snapshot.to_dict())
|
|
556
|
+
if check.throughout:
|
|
557
|
+
out["throughout"] = [assertion.to_dict() for assertion in check.throughout]
|
|
558
|
+
if check.somewhere:
|
|
559
|
+
out["somewhere"] = [assertion.to_dict() for assertion in check.somewhere]
|
|
560
|
+
if check.audit:
|
|
561
|
+
out["audit"] = list(check.audit)
|
|
562
|
+
if check.a11y:
|
|
563
|
+
out["a11y"] = check.a11y
|
|
564
|
+
if check.fixture:
|
|
565
|
+
out["fixture"] = list(check.fixture)
|
|
566
|
+
if check.business is not None:
|
|
567
|
+
out["business"] = check.business.to_dict()
|
|
568
|
+
if check.conditions:
|
|
569
|
+
out["conditions"] = conditions.to_dict(check.conditions, project_root)
|
|
570
|
+
if check.dialogs:
|
|
571
|
+
out["dialogs"] = dict(check.dialogs)
|
|
572
|
+
if check.stubs:
|
|
573
|
+
out["stubs"] = netstubs.to_dict(check.stubs, project_root)
|
|
574
|
+
if check.surfaces:
|
|
575
|
+
out["surfaces"] = surface_spec.to_dict(check.surfaces)
|
|
576
|
+
budget = {}
|
|
577
|
+
if check.max_seconds != DEFAULT_MAX_SECONDS:
|
|
578
|
+
budget["max_seconds"] = _plain_number(check.max_seconds)
|
|
579
|
+
if check.max_usd != DEFAULT_MAX_USD:
|
|
580
|
+
budget["max_usd"] = _plain_number(check.max_usd)
|
|
581
|
+
budget.update(check.perf_budget)
|
|
582
|
+
if budget:
|
|
583
|
+
out["budget"] = budget
|
|
584
|
+
return out
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _plain_number(value):
|
|
588
|
+
return int(value) if float(value).is_integer() else value
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def dump_check(check, *, project_root=None):
|
|
592
|
+
"""``check`` as a YAML check file that ``load_check`` reads back to the same check."""
|
|
593
|
+
import yaml
|
|
594
|
+
header = (f"# yaml-language-server: $schema={links.CHECK_SCHEMA}\n"
|
|
595
|
+
"# A Mobster check. Run it with: mobster verify --check <this file>\n")
|
|
596
|
+
return header + yaml.safe_dump(check_to_dict(check, project_root=project_root), sort_keys=False,
|
|
597
|
+
allow_unicode=True, default_flow_style=False, width=100)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
SAVE_NAME = re.compile(r"[a-z0-9][a-z0-9-]{0,59}")
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def save_check_file(check, name, checks_dir):
|
|
604
|
+
"""Write ``check`` as ``<checks_dir>/<name>.yaml``, with paths relative to that project. Returns the path."""
|
|
605
|
+
if not isinstance(name, str) or not SAVE_NAME.fullmatch(name):
|
|
606
|
+
raise CheckError("a check's name is 1 to 60 of a-z, 0-9 and -, starting with a letter or digit")
|
|
607
|
+
checks_dir = Path(checks_dir)
|
|
608
|
+
checks_dir.mkdir(parents=True, exist_ok=True)
|
|
609
|
+
root = checks_dir.parent.parent if checks_dir.parent.name == ".mobster" else None
|
|
610
|
+
path = checks_dir / f"{name}.yaml"
|
|
611
|
+
temporary = path.with_suffix(".yaml.tmp")
|
|
612
|
+
temporary.write_text(dump_check(check, project_root=root), encoding="utf-8")
|
|
613
|
+
os.replace(temporary, path)
|
|
614
|
+
return path
|