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
mobile_agent/drivers.py
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
"""Device adapters: the driver contract and WebDriverAgent (WDA). Extensions may register more."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from .state import ADJUSTABLE_ROLES, HOST_KEY_OPERATIONS, WDA_HOST_KEY_NAMES, validate_bundle_id
|
|
10
|
+
from .transport import HTTP, Deadline, TransportError, error_name
|
|
11
|
+
from .wda import timeouts as wda_timeouts
|
|
12
|
+
from .wda import watchdog as wda_watchdog
|
|
13
|
+
from .wda.gestures import (
|
|
14
|
+
WDA_GLIDE_SPAN,
|
|
15
|
+
WDA_SWIPE_SECONDS,
|
|
16
|
+
WDAGestures,
|
|
17
|
+
_swipe_points,
|
|
18
|
+
clear_stroke_start,
|
|
19
|
+
switch_point,
|
|
20
|
+
)
|
|
21
|
+
from .wda.guards import finite_positive, grounded_target
|
|
22
|
+
from .wda.observe import WDA_STATE_SECONDS, WDAObserve
|
|
23
|
+
from .wda.text import WDAText
|
|
24
|
+
|
|
25
|
+
# Snapshot depth. 25 was chosen when a full-depth widget home screen took
|
|
26
|
+
# 40.5 s with WDA's `visible` attribute; without it the home screen reads in
|
|
27
|
+
# ~270 ms at depth 80. Depth 25 then blinded apps with deep view trees
|
|
28
|
+
# (Spotify: text recall 0.23 at 25 vs 0.77 at 50, +70 ms). 60 stays within the
|
|
29
|
+
# parser's 64-level nesting limit.
|
|
30
|
+
WDA_SNAPSHOT_MAX_DEPTH = 60
|
|
31
|
+
# WDA's default idle wait held every tap for ~1.2 s; with both at 0 a W3C tap
|
|
32
|
+
# returns in ~470 ms. Mid-animation reads are then possible, so settling
|
|
33
|
+
# requires consecutive identical reads instead (WDA.wait_for_change).
|
|
34
|
+
# A read that cannot finish fails in seconds instead of hanging: WDA serves one request
|
|
35
|
+
# at a time, and on 2026-09-23 stuck TikTok reads (default 15 s snapshot timeouts,
|
|
36
|
+
# stacked by retries) froze it for 21 minutes. Normal reads take 0.13-0.21 s.
|
|
37
|
+
WDA_SNAPSHOT_TIMEOUT = 6
|
|
38
|
+
WDA_SESSION_SETTINGS = {"snapshotMaxDepth": WDA_SNAPSHOT_MAX_DEPTH,
|
|
39
|
+
"waitForIdleTimeout": 0, "animationCoolOffTimeout": 0,
|
|
40
|
+
"snapshotTimeout": WDA_SNAPSHOT_TIMEOUT, "customSnapshotTimeout": WDA_SNAPSHOT_TIMEOUT}
|
|
41
|
+
# Requests after which the page shown may be another one.
|
|
42
|
+
# (Switching to an app and back keeps its tab: only a fresh launch or a URL opens another.)
|
|
43
|
+
PAGE_SWITCHING_PATHS = frozenset({"/wda/apps/launch", "/url"})
|
|
44
|
+
# /window/size resolves the active application, which can hang on a system overlay.
|
|
45
|
+
WDA_WINDOW_SIZE_SECONDS = 3
|
|
46
|
+
# Until the exact screen center is known: a point inside every iPhone screen
|
|
47
|
+
# (the smallest is 320x568 pt), below the banner area and away from the edges.
|
|
48
|
+
WDA_FALLBACK_DETECTION_POINT = "160,320"
|
|
49
|
+
# snapshotMaxChildren (WDA's default is unbounded): a bound on one node's children in a snapshot.
|
|
50
|
+
# iOSWorld mem-046's runner died on CityRide's map screens in 2 of 4 runs (ConnectionReset, then refused).
|
|
51
|
+
WDA_MAX_CHILDREN = 300
|
|
52
|
+
# maxTypingFrequency on a simulator (WDA's default 60 types ~57 characters a second). Measured on an
|
|
53
|
+
# iOSWorld simulator (F2, 26 Sep; 20 bodies of 500 characters typed into a Notes body at each rate, read
|
|
54
|
+
# back whole): 60, 120, 180, 240, 360, 480, 720 and 960 all dropped no character; 500 characters took
|
|
55
|
+
# 8.8 / 4.6 / 3.3 / 2.6 / 1.9 / 1.6 / 1.2 / 1.1 s. The phone keeps WDA's default until measured there.
|
|
56
|
+
WDA_SIMULATOR_TYPING_FREQUENCY = 960
|
|
57
|
+
# Text at least this long goes in through the pasteboard and the edit menu's Paste, not keystrokes, at
|
|
58
|
+
# WDA's typing rate (~57 characters a second: 1,000 take ~18 s, a paste ~2.4 s). A faster rate moves the
|
|
59
|
+
# line (WDA.tune): at 960 a simulator typed 1,000 characters in 1.5 s, faster than the paste (F2, 26 Sep).
|
|
60
|
+
WDA_PASTE_MIN_CHARS = 200
|
|
61
|
+
# Characters typed per second per unit of maxTypingFrequency at high rates (450 a second at 960), and a
|
|
62
|
+
# paste's usual seconds: typing is kept while it is expected to be quicker.
|
|
63
|
+
WDA_TYPED_PER_RATE, WDA_PASTE_SECONDS = .47, 2.5
|
|
64
|
+
# Recovery: how long a restarted or re-supervised runner has to answer /status again.
|
|
65
|
+
WDA_RECOVER_SECONDS = 90
|
|
66
|
+
# Opening or closing a USB iPhone's screen-capture device (WATCH hears and sees through it) resets the phone's USB
|
|
67
|
+
# network link: a blip of 0.4-1.1 s at once, then xcodebuild, which hosts the runner over that link, exits 6-16 s
|
|
68
|
+
# later and the runner with it, so the session is gone (iPhone 15 Pro, iOS 26.0.1, 14 Oct 2026: 2 of 2 opens and
|
|
69
|
+
# 2 of 2 closes; finding the device without opening it changed nothing in 60 s). after_capture waits out the blip,
|
|
70
|
+
# then restarts the runner at once (runner_restart) instead of waiting for the old one to die.
|
|
71
|
+
WDA_LINK_BLIP_SECONDS = 2.0
|
|
72
|
+
# Without runner_restart (a runner Mobster doesn't manage): how long the doomed runner may keep answering.
|
|
73
|
+
WDA_LINK_DOOMED_SECONDS = 25.0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _env_int(name):
|
|
77
|
+
"""A positive integer from the environment, else None."""
|
|
78
|
+
try:
|
|
79
|
+
value = int(os.environ.get(name, "") or 0)
|
|
80
|
+
except ValueError:
|
|
81
|
+
return None
|
|
82
|
+
return value if value > 0 else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _Declared(abc.ABCMeta):
|
|
86
|
+
"""Driver's metaclass: a driver is checked against its declared capabilities once its ``__init__`` has run."""
|
|
87
|
+
|
|
88
|
+
def __call__(cls, *args, **kwargs):
|
|
89
|
+
driver = super().__call__(*args, **kwargs)
|
|
90
|
+
check_capabilities(driver)
|
|
91
|
+
return driver
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Driver(abc.ABC, metaclass=_Declared):
|
|
95
|
+
"""Required device contract. Instantiation fails when a method is missing.
|
|
96
|
+
|
|
97
|
+
Optional capabilities are probed with ``getattr`` at the call site, because each has a load-bearing fallback.
|
|
98
|
+
``OPTIONAL`` lists them with what their absence means, and ``capabilities`` names the ones a driver has:
|
|
99
|
+
instantiation fails when it names one the new driver lacks or leaves one out, and ``operations_for`` derives
|
|
100
|
+
what a loop may offer the driver from it. tests/test_driver_contract.py
|
|
101
|
+
fails when code probes a driver for a name that isn't listed.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
OPTIONAL = {
|
|
105
|
+
# Reading
|
|
106
|
+
"observe_ready": "a settled read; without it, observe",
|
|
107
|
+
"wait_for_change": "a native wait for the screen to change; without it, observe with the whole budget",
|
|
108
|
+
"capture_preview": "an operator preview frame; without it, another capture route (no default)",
|
|
109
|
+
"active_app": "the bundle id in front; without it, the snapshot's bundle_id",
|
|
110
|
+
"slow_front": "the app in front when its tree is too slow to wait for, confirmed without the tree; without it, "
|
|
111
|
+
"the front app comes from active_app",
|
|
112
|
+
"pixel_snapshot": "the screen in front as a tree read from a screenshot, for an app whose tree is too slow; "
|
|
113
|
+
"without it, the evidence read waits for the tree",
|
|
114
|
+
"frame_clock": "a FrameClock on the phone's video (frame_clock.attach_frame_clock sets it)",
|
|
115
|
+
"settled_by": "how the last settle was proven (one_read, agreeing_reads...); without it, unproven",
|
|
116
|
+
"predicted_settle": "the last settle's prediction and outcome (MOBSTER_EXPERIMENTAL_SETTLE); without it, none",
|
|
117
|
+
"settles_early": "the last settle ended before its quiet period, on a frame-clock proof",
|
|
118
|
+
"settles_on_one_read": "a settle may end on one read (wait_for_change's single_read: True, or \"rest\" for one "
|
|
119
|
+
"read only when the pixels already rest)",
|
|
120
|
+
"ax_only_steps": "steps left that read the tree with no screenshot",
|
|
121
|
+
"next_step": "called at the top of each turn; returns the steps left of ax_only_steps",
|
|
122
|
+
"source_reads": "how many screen reads this driver made (the run's timing)",
|
|
123
|
+
"source_seconds": "the seconds those reads took",
|
|
124
|
+
"rich_rows": "reads disabled and selected controls (WDA.tune(rich=True))",
|
|
125
|
+
"tune": "set the driver up for the agent (rich rows, glides, typing rate); without it, as it is",
|
|
126
|
+
"evidence_source": "the screen's tree as WebDriverAgent-shaped XML, read by the connection itself (Android); "
|
|
127
|
+
"without it, verify reads WebDriverAgent's /source through call",
|
|
128
|
+
# Acting beyond execute
|
|
129
|
+
"tap_point": "tap a screen point (TAP_XY, DISMISS), with informational purpose; without it, those actions fail",
|
|
130
|
+
"dismiss_at": "where DISMISS taps; without it, tap_point",
|
|
131
|
+
"long_press": "LONG_PRESS; without it, the action fails",
|
|
132
|
+
"set_value": "set an adjustable control (a picker wheel); without it, SET_TEXT on one fails",
|
|
133
|
+
"write_text": "type with a read-back and one repair; without it, execute TYPE, which types at the end",
|
|
134
|
+
"replace_text": "clear and type through one element reference; without it, clear_text then TYPE",
|
|
135
|
+
"clear_text": "empty a field; without it, SET_TEXT to nothing fails",
|
|
136
|
+
"relaunch_app": "quit and reopen an app (a fresh start); without it, LAUNCH_APP",
|
|
137
|
+
"launch": "open an app and return its first screen (App X-ray); without it, LAUNCH_APP, a pause and a read",
|
|
138
|
+
"edge_back": "the left-edge swipe that goes back (App X-ray); without it, a raw drag on "
|
|
139
|
+
"WebDriverAgent, else none",
|
|
140
|
+
"open_url": "open a link on the phone; without it, WebDriverAgent's /url through call",
|
|
141
|
+
"drag_next": "a slow short drag that reveals a list row's buttons",
|
|
142
|
+
"last_swipe_glided": "the last swipe was a glide, so it can't have flung past rows",
|
|
143
|
+
"last_relaunch": "when the driver last relaunched the app",
|
|
144
|
+
"recover": "restart a stuck connection; without it, the run ends on the error",
|
|
145
|
+
"after_capture": "bring the connection back after a USB iPhone's capture device closed, which resets its link "
|
|
146
|
+
"(WATCH, MCP listen); without it, the next step's own recovery does",
|
|
147
|
+
# WebDriverAgent only: the skills that read Notification Center and codes send raw requests
|
|
148
|
+
"call": "a raw WebDriverAgent request (method, path, body); without it, those skills aren't offered",
|
|
149
|
+
"call_trace": "the WebDriverAgent requests of the last step, for timing reports",
|
|
150
|
+
"url": "the WebDriverAgent address",
|
|
151
|
+
"prefix": "the WebDriverAgent session path",
|
|
152
|
+
"http": "the WebDriverAgent HTTP client (ios.system reads the iOS version from /status); without it, unknown",
|
|
153
|
+
# Image-only connections
|
|
154
|
+
"helper": "the native helper that captures and acts (iPhone Mirroring, Cable + Bluetooth)",
|
|
155
|
+
# iOS's own moves (ios.system), on any connection
|
|
156
|
+
"recent_dialogs": "system_dialogs.Recent owned by this driver; absence starts with no "
|
|
157
|
+
"clipboard or link evidence",
|
|
158
|
+
"opens_by_bundle": "False: apps can't open by bundle id, so open_app goes by name (Spotlight, App Library)",
|
|
159
|
+
"reads_front_app": "False: active_app is a guess, so an app counts as opened when iOS's own screens left",
|
|
160
|
+
"system_shortcut": "send the connection's key for home, spotlight or app_switcher; without it, Home and taps",
|
|
161
|
+
"type_focused": "type into the field that has the keyboard; without it, TYPE on the field element",
|
|
162
|
+
"clear_focused": "empty the field that has the keyboard; without it, its Clear text button when shown",
|
|
163
|
+
"ios_version": "the phone's iOS version when the connection knows it; without it, WebDriverAgent's /status",
|
|
164
|
+
"effects": "the methods that change the device, on a driver with no WebDriverAgent transport, so a verify run "
|
|
165
|
+
"journals each call; without it, the run journals WebDriverAgent's HTTP",
|
|
166
|
+
# VerifyRun's private wrappers: absence means the driver is not owned by that run layer yet.
|
|
167
|
+
"_verify_action_driver_journal": "the action journal owning recovery callbacks; without it, this run may "
|
|
168
|
+
"guard them",
|
|
169
|
+
"_verify_source_owner": "the run observing accessibility source reads; without it, this run may attach",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
capabilities: frozenset[str] = frozenset()
|
|
173
|
+
can_type = False
|
|
174
|
+
last_image = None
|
|
175
|
+
|
|
176
|
+
@abc.abstractmethod
|
|
177
|
+
def observe(self, timeout=10):
|
|
178
|
+
"""Read the current screen. Never mutates the device."""
|
|
179
|
+
|
|
180
|
+
@abc.abstractmethod
|
|
181
|
+
def execute(self, operation, target, snapshot, text=None, timeout=10):
|
|
182
|
+
"""Dispatch one grounded action. At most once; never retried here."""
|
|
183
|
+
|
|
184
|
+
@abc.abstractmethod
|
|
185
|
+
def close(self):
|
|
186
|
+
"""Release driver resources. Never terminates the caller's session."""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def check_capabilities(driver):
|
|
190
|
+
"""Raise TypeError unless ``driver.capabilities`` names exactly the Driver.OPTIONAL attributes ``driver`` has."""
|
|
191
|
+
declared = frozenset(driver.capabilities)
|
|
192
|
+
present = frozenset(name for name in Driver.OPTIONAL if hasattr(driver, name))
|
|
193
|
+
if declared != present:
|
|
194
|
+
raise TypeError(f"{type(driver).__name__}.capabilities declares {sorted(declared - present)} it doesn't have "
|
|
195
|
+
f"and leaves out {sorted(present - declared)} it has. Set capabilities to the Driver.OPTIONAL "
|
|
196
|
+
"names the driver has.")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# What an operation needs beyond execute: any one of these Driver.OPTIONAL names. An operation not listed needs
|
|
200
|
+
# nothing more (READ_LIST and SCROLL_TO swipe and read through execute and observe).
|
|
201
|
+
OPERATION_NEEDS = {"LONG_PRESS": frozenset({"long_press"}), "TAP_XY": frozenset({"tap_point"}),
|
|
202
|
+
"DISMISS": frozenset({"dismiss_at", "tap_point"})}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def operations_for(capabilities, base_operations, role=None):
|
|
206
|
+
"""The operations of ``base_operations``, in order, that a loop may offer a driver with ``capabilities``: each
|
|
207
|
+
one whose OPERATION_NEEDS the driver meets. With ``role``, those it may offer on an element of that role:
|
|
208
|
+
SET_TEXT on an adjustable control (a picker wheel) needs set_value, while on a field it clears and types
|
|
209
|
+
through execute."""
|
|
210
|
+
def offered(op):
|
|
211
|
+
needs = frozenset({"set_value"}) if op == "SET_TEXT" and role in ADJUSTABLE_ROLES else OPERATION_NEEDS.get(op)
|
|
212
|
+
return needs is None or not needs.isdisjoint(capabilities)
|
|
213
|
+
return tuple(op for op in base_operations if offered(op))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def call_kind(method, path):
|
|
217
|
+
"""A WDA request's kind for timing reports: "GET source", "POST element/*/value"."""
|
|
218
|
+
parts = path.split("?", 1)[0].strip("/").split("/")
|
|
219
|
+
if len(parts) > 1 and parts[0] == "element" and parts[1] != "active":
|
|
220
|
+
parts[1] = "*"
|
|
221
|
+
return method + " " + "/".join(parts)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def resolve_wda_session(url, preferred=None, timeout=5, create=True):
|
|
225
|
+
"""The WDA session to drive: WDA serves one at a time, so its own report wins.
|
|
226
|
+
|
|
227
|
+
A configured id goes stale whenever the runner restarts (measured: every
|
|
228
|
+
restart issued a new id and the server failed until restarted by hand).
|
|
229
|
+
Preference: the session WDA reports as current, then ``preferred`` only if
|
|
230
|
+
WDA reports none and it is still accepted, else a newly created session.
|
|
231
|
+
Returns None when WDA is unreachable or has no session and ``create`` is off.
|
|
232
|
+
"""
|
|
233
|
+
client = HTTP(url)
|
|
234
|
+
try:
|
|
235
|
+
status = client.request("GET", "/status", timeout=timeout)
|
|
236
|
+
current = status.get("sessionId") if isinstance(status, dict) else None
|
|
237
|
+
if isinstance(current, str) and current:
|
|
238
|
+
return _settled_session(client, url, current, timeout)
|
|
239
|
+
if preferred:
|
|
240
|
+
try:
|
|
241
|
+
# The session itself, not /window/size: that waits for the app to go idle,
|
|
242
|
+
# which an autoplaying feed (TikTok) never does.
|
|
243
|
+
client.request("GET", "/session/" + quote(preferred, safe=""), timeout=timeout)
|
|
244
|
+
return _settled_session(client, url, preferred, timeout)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
if not create:
|
|
248
|
+
return None
|
|
249
|
+
created = client.request("POST", "/session", {"capabilities": {"alwaysMatch": {
|
|
250
|
+
"platformName": "iOS", "shouldWaitForQuiescence": False, "waitForIdleTimeout": 0}}},
|
|
251
|
+
timeout=max(timeout, 30))
|
|
252
|
+
session = created.get("sessionId") or (created.get("value") or {}).get("sessionId")
|
|
253
|
+
if not isinstance(session, str) or not session:
|
|
254
|
+
raise TransportError("WDA did not create a session")
|
|
255
|
+
return _settled_session(client, url, session, timeout)
|
|
256
|
+
finally:
|
|
257
|
+
client.close()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Sessions already given the no-idle-wait settings, per WDA URL. WDA by default waits
|
|
261
|
+
# for the app to go idle before every read and action; an app playing video never
|
|
262
|
+
# does, so reads hung until timeout (measured on TikTok, 2026-09-23) and each stuck
|
|
263
|
+
# call blocked WDA's single request queue for everything after it.
|
|
264
|
+
_settled_sessions = {}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _settled_session(client, url, session, timeout):
|
|
268
|
+
if _settled_sessions.get(url) != session:
|
|
269
|
+
try:
|
|
270
|
+
client.request("POST", "/session/" + quote(session, safe="") + "/appium/settings",
|
|
271
|
+
{"settings": dict(WDA_SESSION_SETTINGS)}, timeout=timeout)
|
|
272
|
+
_settled_sessions[url] = session
|
|
273
|
+
except Exception:
|
|
274
|
+
pass # Retried on the next resolve; the driver's configure() applies them too.
|
|
275
|
+
return session
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def wait_out_capture(status_ok, runner_restart=None, timeout=WDA_RECOVER_SECONDS, *, sleep=time.sleep,
|
|
280
|
+
clock=time.monotonic):
|
|
281
|
+
"""After a USB iPhone's screen-capture device closed (WDA_LINK_BLIP_SECONDS), wait until the runner that serves
|
|
282
|
+
next is the one to attach to: wait out the link's blip, then restart the runner now (``runner_restart``, which
|
|
283
|
+
returns a check that the new one serves) and wait for it to say so, or, without one, wait for the old runner to
|
|
284
|
+
stop answering (``status_ok(timeout)``), since its host is about to exit. The old runner may answer a while longer:
|
|
285
|
+
measured 4 s after a restart, through the restarted relay, and attaching to it would lose the session again.
|
|
286
|
+
Returns whether it restarted the runner. The caller then waits for /status and attaches."""
|
|
287
|
+
started = clock()
|
|
288
|
+
sleep(WDA_LINK_BLIP_SECONDS)
|
|
289
|
+
fresh = None
|
|
290
|
+
if callable(runner_restart):
|
|
291
|
+
try:
|
|
292
|
+
fresh = runner_restart()
|
|
293
|
+
except Exception: # noqa: BLE001 -- not this phone's managed runner any more: wait for its own end
|
|
294
|
+
fresh = None
|
|
295
|
+
if callable(fresh):
|
|
296
|
+
while clock() - started < timeout and not fresh():
|
|
297
|
+
sleep(.5)
|
|
298
|
+
return True
|
|
299
|
+
doomed = clock() + WDA_LINK_DOOMED_SECONDS
|
|
300
|
+
while clock() < doomed and status_ok(1.5):
|
|
301
|
+
sleep(.5)
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
class WDA(WDAObserve, WDAGestures, WDAText, Driver):
|
|
305
|
+
capabilities = frozenset({
|
|
306
|
+
"observe_ready", "wait_for_change", "capture_preview", "active_app", "slow_front", "pixel_snapshot",
|
|
307
|
+
"frame_clock", "settled_by", "predicted_settle", "settles_early", "settles_on_one_read", "ax_only_steps",
|
|
308
|
+
"next_step",
|
|
309
|
+
"source_reads", "source_seconds", "rich_rows", "tune", "tap_point", "long_press", "set_value", "write_text",
|
|
310
|
+
"replace_text", "clear_text", "relaunch_app", "launch", "drag_next", "last_swipe_glided", "last_relaunch",
|
|
311
|
+
"recover", "after_capture", "call", "call_trace", "url", "prefix", "http"})
|
|
312
|
+
can_type = True
|
|
313
|
+
# wait_for_change(on_settling=...) reports a likely-final screen before it is proven.
|
|
314
|
+
reports_settling = True
|
|
315
|
+
|
|
316
|
+
# Screen size in points per WDA URL: fixed for a phone, read once per process.
|
|
317
|
+
_screen_sizes = {}
|
|
318
|
+
# Apps whose reads needed the foreground hint, per WDA URL, for this process:
|
|
319
|
+
# every run builds a new driver, and each failed read costs ~8 s.
|
|
320
|
+
_overlay_apps = {}
|
|
321
|
+
# Optional JSON file that keeps _overlay_apps across processes (the app's
|
|
322
|
+
# state directory). Measured 2026-09-23: with the invisible Siri overlay up,
|
|
323
|
+
# the first read of every new process took 8.3 s (a failed read, then the
|
|
324
|
+
# hint) and 0.2 s afterwards; a remembered app is hinted at activation.
|
|
325
|
+
overlay_hint_path = None
|
|
326
|
+
# A list to collect every WDA request and read as (kind, started monotonic, ms): the bench's
|
|
327
|
+
# per-action cost report. None (the default) records nothing.
|
|
328
|
+
call_trace = None
|
|
329
|
+
# WDA.tune (the frontier): rich rows (Other leaves, disabled and selected controls), glides in
|
|
330
|
+
# every app (FrameClock demotes one that keeps moving), and the settings below.
|
|
331
|
+
rich_rows = False
|
|
332
|
+
glide_everywhere = False
|
|
333
|
+
tuned_settings = None
|
|
334
|
+
paste_min_chars = WDA_PASTE_MIN_CHARS
|
|
335
|
+
last_swipe_glided = False
|
|
336
|
+
# A callable that restarts this WDA runner (a simulator's xcodebuild).
|
|
337
|
+
# None: recover() waits for a supervisor to bring it back (the USB runner is supervised).
|
|
338
|
+
restarter = None
|
|
339
|
+
# runner_restart() -> None: restarts this phone's runner now (the device manager's start_runner), for
|
|
340
|
+
# after_capture; None for a runner Mobster doesn't manage.
|
|
341
|
+
runner_restart = None
|
|
342
|
+
# killer(bundle_id, timeout) -> bool: closes an app without WDA (app_killer.py), when WDA's terminate fails.
|
|
343
|
+
killer = None
|
|
344
|
+
last_relaunch = None
|
|
345
|
+
last_recovery = None
|
|
346
|
+
# Simulators by WDA URL (None: not asked yet).
|
|
347
|
+
_simulators = {}
|
|
348
|
+
|
|
349
|
+
def __init__(self, url, session):
|
|
350
|
+
if not session:
|
|
351
|
+
raise ValueError("An existing WDA session ID is required")
|
|
352
|
+
self.url = url
|
|
353
|
+
self.http = HTTP(url)
|
|
354
|
+
# Operator screenshots must not contend for the action client's in-flight lock.
|
|
355
|
+
self.preview_http = HTTP(url)
|
|
356
|
+
self.prefix = "/session/" + quote(session, safe="")
|
|
357
|
+
# pressButton is a core WDA endpoint; a WDA too old to serve it
|
|
358
|
+
# rejects the dispatch, which surfaces as a TransportError.
|
|
359
|
+
self.host_operations = frozenset(HOST_KEY_OPERATIONS)
|
|
360
|
+
self.last_image = None
|
|
361
|
+
self._stable = None
|
|
362
|
+
self._settled = {}
|
|
363
|
+
# When the last action was dispatched: a layer visibility answer from before it is asked again.
|
|
364
|
+
self._dispatched_at = float("-inf")
|
|
365
|
+
self._swiped_at = float("-inf")
|
|
366
|
+
self._detection_point_pending = False
|
|
367
|
+
self._pages_stale = False
|
|
368
|
+
self._page_groups = ()
|
|
369
|
+
self._stalls_seen = 0
|
|
370
|
+
self._drag_once = False
|
|
371
|
+
self._write_rank = None
|
|
372
|
+
self._foreground_hint = None
|
|
373
|
+
self._pane_visibility = {}
|
|
374
|
+
self._last_bundle = ""
|
|
375
|
+
self._hinted = None
|
|
376
|
+
self._frame_watch = None
|
|
377
|
+
self._frame_action = None
|
|
378
|
+
self._frame_followup = None
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def load_overlay_hints(cls, path):
|
|
382
|
+
"""Remember apps that needed the foreground hint across processes."""
|
|
383
|
+
cls.overlay_hint_path = path
|
|
384
|
+
try:
|
|
385
|
+
with open(path, encoding="utf-8") as handle:
|
|
386
|
+
saved = json.load(handle)
|
|
387
|
+
except (OSError, ValueError):
|
|
388
|
+
return
|
|
389
|
+
if isinstance(saved, dict):
|
|
390
|
+
for url, bundles in saved.items():
|
|
391
|
+
if isinstance(url, str) and isinstance(bundles, list):
|
|
392
|
+
for bundle in bundles[:64]:
|
|
393
|
+
try:
|
|
394
|
+
cls._overlay_apps.setdefault(url, set()).add(validate_bundle_id(bundle))
|
|
395
|
+
except ValueError:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
def _save_overlay_hints(self):
|
|
399
|
+
path = WDA.overlay_hint_path
|
|
400
|
+
if not path:
|
|
401
|
+
return
|
|
402
|
+
try:
|
|
403
|
+
data = {url: sorted(bundles) for url, bundles in WDA._overlay_apps.items()}
|
|
404
|
+
temporary = f"{path}.tmp"
|
|
405
|
+
with open(temporary, "w", encoding="utf-8") as handle:
|
|
406
|
+
json.dump(data, handle)
|
|
407
|
+
os.replace(temporary, path)
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
def call(self, method, path, body=None, timeout=None):
|
|
412
|
+
# None: the bound for this class of call (wda_timeouts.WDA_CALL_TIMEOUTS).
|
|
413
|
+
# Any value is capped at MAX_WDA_TIMEOUT.
|
|
414
|
+
timeout = wda_timeouts.bounded(timeout, method, path)
|
|
415
|
+
if method == "POST" and path in PAGE_SWITCHING_PATHS:
|
|
416
|
+
self._pages_stale = True # another app or screen: which tab page shows is asked again
|
|
417
|
+
started = time.monotonic()
|
|
418
|
+
try:
|
|
419
|
+
# An action is recorded while it is in flight;
|
|
420
|
+
# one that ends unanswered is an unknown outcome, never re-sent.
|
|
421
|
+
result = wda_watchdog.send(self.url, method, path,
|
|
422
|
+
lambda: self.http.request(method, self.prefix + path, body, timeout))
|
|
423
|
+
finally:
|
|
424
|
+
if self.call_trace is not None:
|
|
425
|
+
self.call_trace.append((call_kind(method, path), started, (time.monotonic() - started) * 1000))
|
|
426
|
+
if path in ("/wda/apps/activate", "/url") and isinstance(body, dict) and body.get("bundleId"):
|
|
427
|
+
# The app a caller brought forward is WDA's active application from now on.
|
|
428
|
+
# Measured 2026-09-24 (iOS 26.0.1): with detection on "auto", every Safari read
|
|
429
|
+
# failed after 8 s (HTTP 404, the Siri overlay as active app), including right
|
|
430
|
+
# after a /url open; naming Safari read the page in 0.5-2.8 s. One settings
|
|
431
|
+
# call (~50 ms) is far cheaper than a single failed read.
|
|
432
|
+
self._foreground_hint = body.get("bundleId")
|
|
433
|
+
self._hint_app(self._foreground_hint, timeout)
|
|
434
|
+
return self.response_value(result)
|
|
435
|
+
|
|
436
|
+
def relaunch_app(self, bundle, timeout=40):
|
|
437
|
+
"""Close and reopen ``bundle`` when WDA answers and the app does not; False when WDA doesn't (recovery's job).
|
|
438
|
+
A stuck accessibility server blocks every read until its app closes; a WDA restart doesn't clear it (10 Oct)."""
|
|
439
|
+
deadline = Deadline(timeout)
|
|
440
|
+
try:
|
|
441
|
+
self.http.request("GET", "/status", None, min(5, deadline.remaining()))
|
|
442
|
+
except Exception:
|
|
443
|
+
return False
|
|
444
|
+
self.last_relaunch = "wda"
|
|
445
|
+
try:
|
|
446
|
+
self.call("POST", "/wda/apps/terminate", {"bundleId": bundle}, min(15, deadline.remaining()))
|
|
447
|
+
except Exception:
|
|
448
|
+
# A stuck app can block WDA's own terminate too (ab1, 11 Oct): close it out of band when this device has
|
|
449
|
+
# a way to (app_killer.py), else the failure stands.
|
|
450
|
+
if not callable(self.killer) or not self.killer(bundle, max(1.0, min(15, deadline.remaining()))):
|
|
451
|
+
raise
|
|
452
|
+
self.last_relaunch = "out_of_band"
|
|
453
|
+
self.call("POST", "/wda/apps/activate", {"bundleId": bundle}, min(20, deadline.remaining()))
|
|
454
|
+
self._pages_stale = True # the app comes back on its default tab: which page shows is asked again
|
|
455
|
+
return True
|
|
456
|
+
|
|
457
|
+
def _hint_app(self, bundle, timeout):
|
|
458
|
+
self.call("POST", "/appium/settings", {"settings": {"defaultActiveApplication": bundle}}, timeout)
|
|
459
|
+
self._hinted = bundle
|
|
460
|
+
|
|
461
|
+
@staticmethod
|
|
462
|
+
def response_value(result):
|
|
463
|
+
# WDA answers every command with a value, null included. With none, nothing says the command ran or didn't.
|
|
464
|
+
if "value" not in result:
|
|
465
|
+
raise TransportError("WDA answered with no value; request outcome unknown; not retried")
|
|
466
|
+
value = result.get("value")
|
|
467
|
+
if (isinstance(value, dict) and value.get("error")) or result.get("status", 0) != 0:
|
|
468
|
+
raise TransportError("WDA rejected command; not retried", status=200, wda_error=error_name(value))
|
|
469
|
+
return value
|
|
470
|
+
|
|
471
|
+
def configure(self, timeout=10):
|
|
472
|
+
"""Apply measured session settings to the caller's session.
|
|
473
|
+
|
|
474
|
+
The settings go first, before anything that WDA resolves through the
|
|
475
|
+
"active application": a fresh session otherwise pays WDA's default idle
|
|
476
|
+
wait on that first call. A rejection fails the run here rather than
|
|
477
|
+
silently observing at 40-second snapshots.
|
|
478
|
+
|
|
479
|
+
WDA decides the foreground app by hit-testing one point, default
|
|
480
|
+
(64, 64): under a notification banner. On a real phone an incoming
|
|
481
|
+
message then made SpringBoard "active" mid-task and put the banner's
|
|
482
|
+
text in the observation. The screen center is where the app is.
|
|
483
|
+
Measured 2026-09-23: /window/size on a fresh session hung 8-10 s and
|
|
484
|
+
failed ("stale element ... com.apple.siri.IntelligentLight",
|
|
485
|
+
kAXErrorServerNotFound) while a system overlay was the active app. It
|
|
486
|
+
is asked briefly; if it cannot answer, the first observation's
|
|
487
|
+
Application frame supplies the center instead.
|
|
488
|
+
"""
|
|
489
|
+
deadline = Deadline(timeout)
|
|
490
|
+
# The screen size is fixed per phone: after the first run the centre goes
|
|
491
|
+
# out with the other settings in one request (the desktop app lost ~3 s per
|
|
492
|
+
# run to a /window/size timeout here).
|
|
493
|
+
known = WDA._screen_sizes.get(self.url)
|
|
494
|
+
if known:
|
|
495
|
+
self._set_detection_point(*known, deadline.remaining())
|
|
496
|
+
return
|
|
497
|
+
# Never leave WDA's (64, 64) default in force, even briefly: it sits
|
|
498
|
+
# under notification banners and the iOS 26 edge-glow overlay.
|
|
499
|
+
settings = {**WDA_SESSION_SETTINGS, "activeAppDetectionPoint": WDA_FALLBACK_DETECTION_POINT}
|
|
500
|
+
self.call("POST", "/appium/settings", {"settings": settings}, deadline.remaining())
|
|
501
|
+
self._detection_point_pending = True
|
|
502
|
+
size = None
|
|
503
|
+
try:
|
|
504
|
+
# /wda/screen measures through SpringBoard, never the "active" app that
|
|
505
|
+
# a system overlay can hijack; /window/size is the fallback.
|
|
506
|
+
screen = self.response_value(self.http.request("GET", "/wda/screen", None,
|
|
507
|
+
min(WDA_WINDOW_SIZE_SECONDS, deadline.remaining())))
|
|
508
|
+
size = screen.get("screenSize") if isinstance(screen, dict) else None
|
|
509
|
+
except (TransportError, TimeoutError, ValueError):
|
|
510
|
+
size = None
|
|
511
|
+
if not (isinstance(size, dict) and all(finite_positive(size.get(k)) for k in ("width", "height"))):
|
|
512
|
+
try:
|
|
513
|
+
size = self.call("GET", "/window/size", timeout=min(WDA_WINDOW_SIZE_SECONDS, deadline.remaining()))
|
|
514
|
+
except (TransportError, TimeoutError, ValueError):
|
|
515
|
+
return
|
|
516
|
+
if isinstance(size, dict) and all(finite_positive(size.get(k)) for k in ("width", "height")):
|
|
517
|
+
WDA._screen_sizes[self.url] = (size["width"], size["height"])
|
|
518
|
+
self._set_detection_point(size["width"], size["height"], deadline.remaining())
|
|
519
|
+
|
|
520
|
+
def _set_detection_point(self, width, height, timeout):
|
|
521
|
+
settings = {**WDA_SESSION_SETTINGS, "activeAppDetectionPoint": f"{width / 2:.0f},{height / 2:.0f}"}
|
|
522
|
+
self.call("POST", "/appium/settings", {"settings": settings}, timeout)
|
|
523
|
+
self._detection_point_pending = False
|
|
524
|
+
|
|
525
|
+
def launch(self, bundle_id, timeout=20):
|
|
526
|
+
"""Bring an app forward and return its first settled observation.
|
|
527
|
+
|
|
528
|
+
Activation applies the remembered foreground hint (overlay_hint_path),
|
|
529
|
+
which is what makes the first read fast (measured: 9.0 s -> 0.16 s in a
|
|
530
|
+
fresh process under the Siri overlay). The read then settles with
|
|
531
|
+
``observe_ready`` (the FrameClock rule in "on" mode). Waiting for the
|
|
532
|
+
pixels before reading was measured and rejected: iOS's zoom-in
|
|
533
|
+
animation outlasts the app's AX tree, so warm launches got slower
|
|
534
|
+
(Settings 0.70 -> 1.49 s, Safari 1.5-1.8 -> 3.0 s). Returns
|
|
535
|
+
``(snapshot, {"activate_ms", "ready_ms"})``.
|
|
536
|
+
"""
|
|
537
|
+
bundle = validate_bundle_id(bundle_id)
|
|
538
|
+
deadline = Deadline(timeout)
|
|
539
|
+
started = time.monotonic()
|
|
540
|
+
self._dispatching()
|
|
541
|
+
self.call("POST", "/wda/apps/activate", {"bundleId": bundle}, min(10, deadline.remaining()))
|
|
542
|
+
info = {"activate_ms": round((time.monotonic() - started) * 1000, 1)}
|
|
543
|
+
snapshot = self.observe_ready(timeout=deadline.remaining())
|
|
544
|
+
info["ready_ms"] = round((time.monotonic() - started) * 1000, 1)
|
|
545
|
+
if self.frame_clock is not None:
|
|
546
|
+
self.frame_clock.record({"kind": "launch", "bundle": bundle, **info})
|
|
547
|
+
return snapshot, info
|
|
548
|
+
|
|
549
|
+
def active_app(self, timeout=5):
|
|
550
|
+
"""The foreground bundle, read device-wide within the probe bound: a TransportError means it is unknown.
|
|
551
|
+
A slow-tree app is confirmed by slow_front: activeAppInfo there took 17-50 s, then WDA died (14 Oct, TikTok)."""
|
|
552
|
+
timeout = min(float(timeout), wda_timeouts.WDA_CALL_TIMEOUTS["probe"])
|
|
553
|
+
started = time.monotonic()
|
|
554
|
+
slow = self.slow_front(min(WDA_STATE_SECONDS, timeout))
|
|
555
|
+
if slow:
|
|
556
|
+
return slow
|
|
557
|
+
timeout = max(.25, timeout - (time.monotonic() - started))
|
|
558
|
+
value = self.response_value(self.http.request("GET", "/wda/activeAppInfo", timeout=timeout))
|
|
559
|
+
bundle = value.get("bundleId") if isinstance(value, dict) else None
|
|
560
|
+
if not isinstance(bundle, str) or not bundle:
|
|
561
|
+
raise TransportError("WDA did not report a foreground app")
|
|
562
|
+
return bundle
|
|
563
|
+
|
|
564
|
+
def capture_preview(self, timeout=3):
|
|
565
|
+
try:
|
|
566
|
+
result = self.preview_http.request("GET", self.prefix + "/screenshot", timeout=timeout)
|
|
567
|
+
except (TransportError, TimeoutError) as error:
|
|
568
|
+
if isinstance(error, TimeoutError) or "timeout" in str(error).casefold():
|
|
569
|
+
self.note_screenshot_timeout()
|
|
570
|
+
raise
|
|
571
|
+
value = result.get("value") if isinstance(result, dict) else None
|
|
572
|
+
if isinstance(value, dict) and "screenshot" in str(value.get("message") or "").casefold() \
|
|
573
|
+
and "timeout" in str(value.get("message") or "").casefold():
|
|
574
|
+
self.note_screenshot_timeout()
|
|
575
|
+
image = self.response_value(result)
|
|
576
|
+
self.screenshot_timeouts = 0
|
|
577
|
+
if not isinstance(image, str) or not image:
|
|
578
|
+
raise TransportError("WDA screenshot unavailable")
|
|
579
|
+
return "data:image/png;base64," + image
|
|
580
|
+
|
|
581
|
+
def execute(self, operation, target, snapshot, text=None, timeout=10):
|
|
582
|
+
deadline = Deadline(timeout)
|
|
583
|
+
if operation in HOST_KEY_OPERATIONS:
|
|
584
|
+
# WDA is device-level, so no bundle scoping applies: HOME leaves
|
|
585
|
+
# the app and later observations simply describe the new screen.
|
|
586
|
+
# pressButton (not the sessionless /wda/homescreen) carries all
|
|
587
|
+
# three keys; verified live against upstream WDA on iOS 26.
|
|
588
|
+
self._dispatching()
|
|
589
|
+
self._frame_mark(operation)
|
|
590
|
+
self.call("POST", "/wda/pressButton",
|
|
591
|
+
{"name": WDA_HOST_KEY_NAMES[operation]}, deadline.remaining())
|
|
592
|
+
self._frame_dispatched()
|
|
593
|
+
return {"dispatch_attempted": True}
|
|
594
|
+
if operation == "LAUNCH_APP":
|
|
595
|
+
# The switch is trusted like any dispatched action; the next
|
|
596
|
+
# snapshot reports the foreground bundle from its Application node.
|
|
597
|
+
bundle = validate_bundle_id(target)
|
|
598
|
+
self._dispatching()
|
|
599
|
+
self._frame_mark(operation)
|
|
600
|
+
self.call("POST", "/wda/apps/activate", {"bundleId": bundle}, deadline.remaining())
|
|
601
|
+
self._frame_dispatched()
|
|
602
|
+
return {}
|
|
603
|
+
if operation == "SUBMIT":
|
|
604
|
+
grounded_target(operation, target, snapshot)
|
|
605
|
+
if "SUBMIT" not in target.actions:
|
|
606
|
+
raise ValueError("SUBMIT needs a keyboard and an editable field")
|
|
607
|
+
self._check_current(snapshot, deadline)
|
|
608
|
+
self._dispatching()
|
|
609
|
+
self._frame_mark(operation)
|
|
610
|
+
# Return on the focused field: Go in Safari, Search in search bars.
|
|
611
|
+
self.call("POST", "/wda/keys", {"value": ["\n"]}, deadline.remaining())
|
|
612
|
+
self._frame_dispatched()
|
|
613
|
+
elif operation == "TAP":
|
|
614
|
+
grounded_target(operation, target, snapshot)
|
|
615
|
+
self._check_current(snapshot, deadline)
|
|
616
|
+
self._note_tap(target.locator)
|
|
617
|
+
# A coordinate tap on the observed, occlusion-filtered frame. No
|
|
618
|
+
# XPath resolution means no index-based locator that can silently
|
|
619
|
+
# resolve to a replacement cell after a reload.
|
|
620
|
+
x, y = switch_point(target, snapshot.width)
|
|
621
|
+
self._frame_mark(operation)
|
|
622
|
+
self._tap(x * snapshot.width, y * snapshot.height, deadline)
|
|
623
|
+
self._frame_dispatched()
|
|
624
|
+
elif operation in {"TYPE", "TYPE_SUBMIT"}:
|
|
625
|
+
grounded_target(operation, target, snapshot)
|
|
626
|
+
return self._type_text(operation, target, snapshot, text, deadline)
|
|
627
|
+
elif operation in {"SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT"}:
|
|
628
|
+
aimed, reveal = None, False
|
|
629
|
+
if target is not None and operation in {"SWIPE_LEFT", "SWIPE_RIGHT"}:
|
|
630
|
+
grounded = any(target is element or target == element for element in snapshot.elements)
|
|
631
|
+
if not grounded:
|
|
632
|
+
raise ValueError("Action target is not part of the supplied observation")
|
|
633
|
+
aimed, reveal = target.rect, target.role == "Cell"
|
|
634
|
+
x1, y1, x2, y2 = _swipe_points(operation, snapshot.width, snapshot.height, aimed, reveal)
|
|
635
|
+
self._dispatching()
|
|
636
|
+
glide = self._glide_ok(operation, snapshot)
|
|
637
|
+
if glide and operation in WDA_GLIDE_SPAN:
|
|
638
|
+
# A glide's time is its fixed segments (~480 ms), not its distance: the
|
|
639
|
+
# longer span moves ~0.68 screens per swipe instead of ~0.47 for the same
|
|
640
|
+
# cost, and still overlaps the previous screen by about a third.
|
|
641
|
+
start, end = WDA_GLIDE_SPAN[operation]
|
|
642
|
+
y1, y2 = snapshot.height * start, snapshot.height * end
|
|
643
|
+
y1 = clear_stroke_start(snapshot, operation, x1, y1, snapshot.width, snapshot.height,
|
|
644
|
+
fixed=glide and self.glide_everywhere)
|
|
645
|
+
self._frame_mark(operation, glide=glide)
|
|
646
|
+
self._swiped_at = time.monotonic()
|
|
647
|
+
self.last_swipe_glided = bool(glide)
|
|
648
|
+
if reveal:
|
|
649
|
+
self._reveal(x1, y1, x2, y2, deadline)
|
|
650
|
+
elif glide:
|
|
651
|
+
self._glide(x1, y1, x2, y2, deadline)
|
|
652
|
+
elif operation in {"SWIPE_LEFT", "SWIPE_RIGHT"}:
|
|
653
|
+
# A flick: a paging view turns on speed, and XCTest's slow half-screen drag snapped
|
|
654
|
+
# back (iOSWorld Weather's city pages: 3-7 SWIPE_LEFTs with no effect a run, 25 Sep;
|
|
655
|
+
# live, the flick turned each page in 0.7 s against no turn in 1.4 s). Same distance
|
|
656
|
+
# as before: a Mail row's Delete and Archive were revealed, not triggered.
|
|
657
|
+
self._flick(x1, y1, x2, y2, deadline)
|
|
658
|
+
else:
|
|
659
|
+
self.call("POST", "/wda/dragfromtoforduration", {
|
|
660
|
+
"fromX": x1, "fromY": y1,
|
|
661
|
+
"toX": x2, "toY": y2, "duration": WDA_SWIPE_SECONDS}, deadline.remaining())
|
|
662
|
+
self._frame_dispatched()
|
|
663
|
+
else:
|
|
664
|
+
raise ValueError("Unsupported WDA operation")
|
|
665
|
+
|
|
666
|
+
# The frontier's options: Jev and the plain driver never call these.
|
|
667
|
+
|
|
668
|
+
def is_simulator(self, timeout=3):
|
|
669
|
+
"""True when this WDA runs on a simulator, asked once per URL; no answer is False and is asked again."""
|
|
670
|
+
known = WDA._simulators.get(self.url)
|
|
671
|
+
if known is None:
|
|
672
|
+
try:
|
|
673
|
+
status = self.http.request("GET", "/status", timeout=timeout)
|
|
674
|
+
ios = (status.get("value") or {}).get("ios") if isinstance(status, dict) else None
|
|
675
|
+
known = bool(isinstance(ios, dict) and ios.get("simulatorVersion"))
|
|
676
|
+
except (TransportError, TimeoutError, ValueError, AttributeError):
|
|
677
|
+
return False
|
|
678
|
+
WDA._simulators[self.url] = known
|
|
679
|
+
return known
|
|
680
|
+
|
|
681
|
+
def tune(self, rich=True, glide=True, max_children=WDA_MAX_CHILDREN, typing_frequency=None, timeout=5):
|
|
682
|
+
"""The frontier policy's driver: rich rows (state.from_wda_root), glides in every app, and WDA
|
|
683
|
+
settings: a bound on a snapshot's children and, on a simulator, the measured typing rate
|
|
684
|
+
(MOBSTER_TYPING_FREQUENCY sets one on any device). Returns the settings applied; they are
|
|
685
|
+
applied again after a recovery."""
|
|
686
|
+
self.rich_rows, self.glide_everywhere = bool(rich), bool(glide)
|
|
687
|
+
settings = {}
|
|
688
|
+
if max_children:
|
|
689
|
+
settings["snapshotMaxChildren"] = int(max_children)
|
|
690
|
+
rate = typing_frequency or _env_int("MOBSTER_TYPING_FREQUENCY")
|
|
691
|
+
if not rate and WDA_SIMULATOR_TYPING_FREQUENCY and self.is_simulator():
|
|
692
|
+
rate = WDA_SIMULATOR_TYPING_FREQUENCY
|
|
693
|
+
if rate:
|
|
694
|
+
settings["maxTypingFrequency"] = int(rate)
|
|
695
|
+
self.paste_min_chars = max(WDA_PASTE_MIN_CHARS, round(WDA_PASTE_SECONDS * WDA_TYPED_PER_RATE * (rate or 0)))
|
|
696
|
+
self.tuned_settings = settings
|
|
697
|
+
if settings:
|
|
698
|
+
self.call("POST", "/appium/settings", {"settings": settings}, timeout)
|
|
699
|
+
return settings
|
|
700
|
+
|
|
701
|
+
def _status_ok(self, timeout=3):
|
|
702
|
+
try:
|
|
703
|
+
status = self.http.request("GET", "/status", timeout=timeout)
|
|
704
|
+
except (TransportError, TimeoutError, ValueError):
|
|
705
|
+
self.http.close()
|
|
706
|
+
return False
|
|
707
|
+
value = status.get("value") if isinstance(status, dict) else None
|
|
708
|
+
return isinstance(value, dict) and value.get("ready", True) is not False
|
|
709
|
+
|
|
710
|
+
def recover(self, timeout=WDA_RECOVER_SECONDS):
|
|
711
|
+
"""Bring back a runner that reset or refused the connection and attach; TransportError past ``timeout``."""
|
|
712
|
+
started = time.monotonic()
|
|
713
|
+
deadline = Deadline(timeout)
|
|
714
|
+
phases = self.last_recovery = {"restarted": False}
|
|
715
|
+
if not self._status_ok(min(3, deadline.remaining())) and callable(self.restarter):
|
|
716
|
+
self.restarter(deadline.remaining())
|
|
717
|
+
phases["restarted"] = True
|
|
718
|
+
while not self._status_ok(min(3, deadline.remaining())):
|
|
719
|
+
if deadline.end - time.monotonic() < 1.5:
|
|
720
|
+
raise TransportError("WDA did not come back")
|
|
721
|
+
time.sleep(.5)
|
|
722
|
+
phases["ready_ms"] = round((time.monotonic() - started) * 1000)
|
|
723
|
+
self.reattach(timeout=min(30, deadline.remaining()))
|
|
724
|
+
phases["total_ms"] = round((time.monotonic() - started) * 1000)
|
|
725
|
+
return time.monotonic() - started
|
|
726
|
+
|
|
727
|
+
def after_capture(self, timeout=WDA_RECOVER_SECONDS, sleep=time.sleep, clock=time.monotonic):
|
|
728
|
+
"""Bring the runner back after the phone's screen-capture device closed (see WDA_LINK_BLIP_SECONDS).
|
|
729
|
+
Nothing is sent to the phone meanwhile, so no action is in doubt; TransportError past ``timeout``."""
|
|
730
|
+
started = clock()
|
|
731
|
+
restarted = wait_out_capture(self._status_ok, self.runner_restart, timeout, sleep=sleep, clock=clock)
|
|
732
|
+
self.recover(timeout=max(1.0, timeout - (clock() - started)))
|
|
733
|
+
self.last_recovery = {**(self.last_recovery or {}), "after_capture": True, "restarted": restarted}
|
|
734
|
+
return clock() - started
|
|
735
|
+
|
|
736
|
+
def reattach(self, timeout=30):
|
|
737
|
+
"""Drive the runner's current session (a new one when it has none), set up as configure() does."""
|
|
738
|
+
for client in (self.http, self.preview_http):
|
|
739
|
+
client.close()
|
|
740
|
+
_settled_sessions.pop(self.url, None)
|
|
741
|
+
factory = getattr(self, "new_session", None)
|
|
742
|
+
session = factory() if callable(factory) else resolve_wda_session(self.url, timeout=min(10, timeout))
|
|
743
|
+
if not session:
|
|
744
|
+
raise TransportError("WDA has no session")
|
|
745
|
+
self.prefix = "/session/" + quote(session, safe="")
|
|
746
|
+
self._stable, self._settled, self._pane_visibility = None, {}, {}
|
|
747
|
+
self._dispatched_at = float("-inf")
|
|
748
|
+
self._hinted, self._frame_watch, self._frame_action, self._frame_followup = None, None, None, None
|
|
749
|
+
self._pages_stale = True
|
|
750
|
+
self.configure(timeout=min(10, timeout))
|
|
751
|
+
if self.tuned_settings:
|
|
752
|
+
self.call("POST", "/appium/settings", {"settings": dict(self.tuned_settings)}, min(5, timeout))
|
|
753
|
+
return session
|
|
754
|
+
|
|
755
|
+
def close(self):
|
|
756
|
+
# The session belongs to the caller; do not terminate its app.
|
|
757
|
+
clock, self.frame_clock = self.frame_clock, None
|
|
758
|
+
try:
|
|
759
|
+
if clock is not None:
|
|
760
|
+
try:
|
|
761
|
+
clock.close() # Releases the relay subscription (or its own stream).
|
|
762
|
+
except Exception:
|
|
763
|
+
pass
|
|
764
|
+
self.http.close()
|
|
765
|
+
finally:
|
|
766
|
+
self.preview_http.close()
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
# Driver kinds by name. The core registers WDA; mirroring.py registers "mirroring" (experimental) when imported, and an
|
|
770
|
+
# optional private package (harness_api.hooks) may register more before ``build_driver`` looks one up.
|
|
771
|
+
DRIVERS = {"wda": WDA}
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def register_driver(kind, cls):
|
|
775
|
+
"""Add a driver kind. The class must subclass ``Driver``."""
|
|
776
|
+
if not isinstance(kind, str) or not kind or not (isinstance(cls, type) and issubclass(cls, Driver)):
|
|
777
|
+
raise TypeError("register_driver needs a name and a Driver subclass")
|
|
778
|
+
DRIVERS[kind] = cls
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def build_driver(kind, **kwargs):
|
|
782
|
+
"""One validated construction site: an unknown kind is a ValueError, a class missing the contract a TypeError."""
|
|
783
|
+
if isinstance(kind, str) and kind not in DRIVERS:
|
|
784
|
+
from . import harness_api
|
|
785
|
+
harness_api.hooks()
|
|
786
|
+
try:
|
|
787
|
+
cls = DRIVERS[kind]
|
|
788
|
+
except (KeyError, TypeError):
|
|
789
|
+
raise ValueError(f"Unknown driver {kind!r}; expected one of {sorted(DRIVERS)}") from None
|
|
790
|
+
driver = cls(**kwargs)
|
|
791
|
+
if not isinstance(driver, Driver):
|
|
792
|
+
raise TypeError(f"Driver {kind!r} does not implement the device contract")
|
|
793
|
+
return driver
|