mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""The X-ray crawler: explores an app with no model, and every tap, replayed ones too, goes through the policy."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..packs.core import BACK_LABEL
|
|
10
|
+
from . import paywall as paywalls
|
|
11
|
+
from . import policy, screens
|
|
12
|
+
|
|
13
|
+
SCHEMA = "mobster.xray/1"
|
|
14
|
+
SETTLE_SECONDS = .5
|
|
15
|
+
SETTLE_TIMEOUT = 5.0
|
|
16
|
+
POLL_SECONDS = .1
|
|
17
|
+
THUMB_WIDTH = 300
|
|
18
|
+
BOXES_KEPT = 60
|
|
19
|
+
STUCK_LIMIT = 3
|
|
20
|
+
TOP_STEPS = 6
|
|
21
|
+
# A list whose detail pages look alike (fonts, products, contacts) is sampled: after this many look-alike pages, its
|
|
22
|
+
# other rows are left closed.
|
|
23
|
+
CATALOG_ALIKE = 2
|
|
24
|
+
# The depth the first pass stops at; screens there are explored deeper afterwards while the caps allow.
|
|
25
|
+
FIRST_PASS_DEPTH = 2
|
|
26
|
+
ALIKE = .6
|
|
27
|
+
# A read older than this is read again before a tap on it (WebDriverAgent refuses taps on reads over 1 s old).
|
|
28
|
+
FRESH_SECONDS = .7
|
|
29
|
+
QUIET_ROLES = frozenset({"StaticText", "Image", "Icon", "Other", "NavigationBar", "Toolbar", "TabBar", "Text",
|
|
30
|
+
"Window", "ScrollView", "Table", "CollectionView", "Keyboard", "Key"})
|
|
31
|
+
QUIET_REASONS = ("outside the content area", "under a bar", "not tappable", "no label")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Stop(Exception):
|
|
35
|
+
"""The crawl can't go on: the message says why, in plain words."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Gone(Exception):
|
|
39
|
+
"""The control to tap is no longer on screen; nothing was tapped. ``snapshot``: the fresh read."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, snapshot):
|
|
42
|
+
super().__init__("the control is gone")
|
|
43
|
+
self.snapshot = snapshot
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Caps:
|
|
48
|
+
screens: int = 60
|
|
49
|
+
depth: int = 6
|
|
50
|
+
seconds: float = 300.0
|
|
51
|
+
pages: int = 4 # pages of a long list it scrolls through per screen
|
|
52
|
+
taps: int = 800
|
|
53
|
+
children: int = 8 # new screens opened from one screen two or more taps in (the first two levels: all)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Screen:
|
|
58
|
+
id: str
|
|
59
|
+
title: str # the screen's own title (its identity)
|
|
60
|
+
tokens: frozenset
|
|
61
|
+
union: set
|
|
62
|
+
lines: list
|
|
63
|
+
depth: int
|
|
64
|
+
path: list # [{"label", "kind"}] from the app's first screen
|
|
65
|
+
parent: str = ""
|
|
66
|
+
via: str = ""
|
|
67
|
+
seen_s: float = 0.0
|
|
68
|
+
thumb: str = ""
|
|
69
|
+
boxes: list = field(default_factory=list)
|
|
70
|
+
paywall: object = None
|
|
71
|
+
frame: bytes = None # the full capture until announce() hands it to the run
|
|
72
|
+
frame_id: str = None # the run's stored frame of this screen (the result's gallery)
|
|
73
|
+
image_only: bool = False
|
|
74
|
+
done: set = field(default_factory=set)
|
|
75
|
+
refused: dict = field(default_factory=dict)
|
|
76
|
+
noop: list = field(default_factory=list)
|
|
77
|
+
changed: list = field(default_factory=list) # taps that changed the screen in place: the rest is left alone
|
|
78
|
+
choice: bool = False # seen as a list of options: its rows are never tapped
|
|
79
|
+
kids: list = field(default_factory=list) # the identity tokens of each new screen opened from it
|
|
80
|
+
alike: int = 0 # how many of those look like an earlier one (a catalog's detail pages)
|
|
81
|
+
held_back: int = 0 # rows not opened because of the children cap or a catalog
|
|
82
|
+
pending: bool = False # reached at the first pass's depth with something left to open
|
|
83
|
+
unreachable: bool = False
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def name(self):
|
|
87
|
+
"""What the report calls it: its title, or the row that opens it when the title is missing or a badge."""
|
|
88
|
+
if self.title and not (self.title.isupper() and len(self.title) <= 6):
|
|
89
|
+
return self.title
|
|
90
|
+
return self.via or self.title or "Untitled"
|
|
91
|
+
|
|
92
|
+
def to_json(self):
|
|
93
|
+
return {"id": self.id, "title": self.title, "name": self.name, "depth": self.depth, "path": self.path,
|
|
94
|
+
"parent": self.parent,
|
|
95
|
+
"via": self.via, "seen_s": round(self.seen_s, 2), "thumb": self.thumb, "frame_id": self.frame_id,
|
|
96
|
+
"lines": self.lines,
|
|
97
|
+
"tokens": sorted(self.union), "boxes": self.boxes,
|
|
98
|
+
"paywall": self.paywall.to_json() if self.paywall else None,
|
|
99
|
+
"refused": [{"label": label, "reason": reason} for label, reason in self.refused.items()],
|
|
100
|
+
"no_change": self.noop, "changed_in_place": self.changed, "choice_list": self.choice,
|
|
101
|
+
"catalog": self.alike >= CATALOG_ALIKE, "held_back": self.held_back,
|
|
102
|
+
"unreachable": self.unreachable}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Candidate:
|
|
107
|
+
key: str
|
|
108
|
+
label: str
|
|
109
|
+
element: object
|
|
110
|
+
reason: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def key_of(label):
|
|
114
|
+
"""A control's identity on its screen: its label, case folded, without a trailing state (", Off")."""
|
|
115
|
+
return screens.STATES.sub("", policy.plain(label)).casefold()[:120]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def boxes(snapshot):
|
|
119
|
+
"""The screen's layout for a wireframe: [x, y, w, h, kind, text] for its structural elements."""
|
|
120
|
+
out = []
|
|
121
|
+
for element in sorted(snapshot.elements, key=lambda e: (e.rect[1], e.rect[0])):
|
|
122
|
+
if element.role in ("Image", "Icon") or not policy.plain(element.label):
|
|
123
|
+
continue
|
|
124
|
+
kind = ("row" if element.role in ("Cell", "Button") and element.rect[2] > .5 else
|
|
125
|
+
"control" if element.role in ("Button", "Link", "Switch", "Toggle", "SearchField", "TextField")
|
|
126
|
+
else "bar" if element.role in ("NavigationBar", "TabBar", "Toolbar") else "text")
|
|
127
|
+
out.append([round(v, 4) for v in element.rect] + [kind, policy.plain(element.label)[:60]])
|
|
128
|
+
if len(out) >= BOXES_KEPT:
|
|
129
|
+
break
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
CHROME = ("XCUIElementTypeToolbar", "XCUIElementTypeTabBar", "XCUIElementTypeKeyboard", "XCUIElementTypeSearchField",
|
|
134
|
+
"XCUIElementTypeNavigationBar")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def more_below(snapshot):
|
|
138
|
+
"""Whether content runs past the bottom of the content area (a long list): only then is a scroll worth it."""
|
|
139
|
+
for element in snapshot.elements:
|
|
140
|
+
if element.role in policy.CONTROL_ROLES or any(part in (element.locator or "") for part in CHROME):
|
|
141
|
+
continue
|
|
142
|
+
if element.rect[1] + element.rect[3] > policy.CONTENT_BOTTOM - .02 and element.rect[1] < .995:
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def state(snapshot):
|
|
148
|
+
"""What the screen shows, controls' values and checkmarks included, for telling whether a tap changed it."""
|
|
149
|
+
return frozenset((e.role, policy.plain(e.label), e.value, e.selected) for e in snapshot.elements
|
|
150
|
+
if e.rect[1] >= screens.STATUS_BAR)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_dialog_element(element):
|
|
154
|
+
return element.role in ("Alert", "Sheet") or "XCUIElementTypeAlert" in (element.locator or "")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _Timer:
|
|
158
|
+
def __init__(self, spent, kind, clock):
|
|
159
|
+
self.spent, self.kind, self.clock = spent, kind, clock
|
|
160
|
+
|
|
161
|
+
def __enter__(self):
|
|
162
|
+
self.started = self.clock()
|
|
163
|
+
|
|
164
|
+
def __exit__(self, *exc):
|
|
165
|
+
self.spent[self.kind] = self.spent.get(self.kind, 0.0) + self.clock() - self.started
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Crawler:
|
|
169
|
+
def __init__(self, driver, bundle, *, caps=None, out_dir=None, shots=True, clock=time.monotonic,
|
|
170
|
+
sleep=time.sleep, log=None, emit=None, cancelled=None):
|
|
171
|
+
self.driver, self.bundle = driver, bundle
|
|
172
|
+
self.emit, self.cancelled = emit, cancelled
|
|
173
|
+
self.caps = caps or Caps()
|
|
174
|
+
self.out = Path(out_dir) if out_dir else None
|
|
175
|
+
self.shots = shots and self.out is not None and callable(getattr(driver, "capture_preview", None))
|
|
176
|
+
self.clock, self.sleep = clock, sleep
|
|
177
|
+
self.log = log or (lambda message: None)
|
|
178
|
+
self.screens = []
|
|
179
|
+
self.edges = []
|
|
180
|
+
self.stats = {"taps": 0, "scrolls": 0, "backs": {"control": 0, "close": 0, "swipe": 0, "relaunch": 0},
|
|
181
|
+
"dialogs_dismissed": 0, "left_app": 0, "no_change": 0, "changed_in_place": 0,
|
|
182
|
+
"relaunch_failures": 0}
|
|
183
|
+
self.stuck = 0
|
|
184
|
+
self.before = None
|
|
185
|
+
self.limit = self.caps.depth
|
|
186
|
+
self.spent = {}
|
|
187
|
+
self.settles = {}
|
|
188
|
+
self.stopped = ""
|
|
189
|
+
self.started = 0.0
|
|
190
|
+
|
|
191
|
+
def timed(self, kind):
|
|
192
|
+
return _Timer(self.spent, kind, self.clock)
|
|
193
|
+
|
|
194
|
+
def note(self, event):
|
|
195
|
+
if self.emit is not None:
|
|
196
|
+
self.emit(event)
|
|
197
|
+
|
|
198
|
+
def sent(self, operation, label, send):
|
|
199
|
+
# Journaled before it is sent and acknowledged after; a send that raises is neither acknowledged nor retried.
|
|
200
|
+
self.note({"event": "action_started", "operation": operation,
|
|
201
|
+
"target": policy.plain(label)[:120] if label else None})
|
|
202
|
+
began = self.clock()
|
|
203
|
+
try:
|
|
204
|
+
result = send()
|
|
205
|
+
except Exception as error:
|
|
206
|
+
if "stale_revision" in str(error):
|
|
207
|
+
self.note({"event": "action_not_dispatched", "operation": operation}) # the driver sent nothing
|
|
208
|
+
raise
|
|
209
|
+
self.note({"event": "action_acknowledged", "act_ms": round((self.clock() - began) * 1000)})
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
def elapsed(self):
|
|
213
|
+
return self.clock() - self.started
|
|
214
|
+
|
|
215
|
+
def capped(self):
|
|
216
|
+
if self.stopped:
|
|
217
|
+
return True
|
|
218
|
+
if self.cancelled is not None and self.cancelled():
|
|
219
|
+
self.stopped = "stopped by you"
|
|
220
|
+
elif len(self.screens) >= self.caps.screens:
|
|
221
|
+
self.stopped = f"reached {self.caps.screens} screens"
|
|
222
|
+
elif self.elapsed() >= self.caps.seconds:
|
|
223
|
+
self.stopped = f"reached the time limit ({round(self.caps.seconds)} s)"
|
|
224
|
+
elif self.stats["taps"] >= self.caps.taps:
|
|
225
|
+
self.stopped = f"reached {self.caps.taps} taps"
|
|
226
|
+
return bool(self.stopped)
|
|
227
|
+
|
|
228
|
+
def observe(self):
|
|
229
|
+
with self.timed("observe"):
|
|
230
|
+
return self.driver.observe(timeout=10)
|
|
231
|
+
|
|
232
|
+
def settle(self, before, kind="tap"):
|
|
233
|
+
started = self.clock()
|
|
234
|
+
with self.timed("settle"):
|
|
235
|
+
try:
|
|
236
|
+
return self._settle(before)
|
|
237
|
+
finally:
|
|
238
|
+
detail = self.settles.setdefault(kind, {"n": 0, "seconds": 0.0})
|
|
239
|
+
detail["n"] += 1
|
|
240
|
+
detail["seconds"] += self.clock() - started
|
|
241
|
+
|
|
242
|
+
def _settle(self, before):
|
|
243
|
+
wait = getattr(self.driver, "wait_for_change", None)
|
|
244
|
+
if callable(wait):
|
|
245
|
+
try:
|
|
246
|
+
# ``early``: the first changed read that agrees with the read before it, without the driver's extra
|
|
247
|
+
# quiet period. Two agreeing reads are still required, so a moving screen is not returned.
|
|
248
|
+
try:
|
|
249
|
+
return wait(before, timeout=SETTLE_TIMEOUT, wait_seconds=SETTLE_SECONDS, early=True)
|
|
250
|
+
except TypeError:
|
|
251
|
+
return wait(before, timeout=SETTLE_TIMEOUT, wait_seconds=SETTLE_SECONDS)
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
return self._poll_settle(before)
|
|
255
|
+
|
|
256
|
+
def _poll_settle(self, before):
|
|
257
|
+
started = self.clock()
|
|
258
|
+
previous = None
|
|
259
|
+
while True:
|
|
260
|
+
read_at = self.clock()
|
|
261
|
+
current = self.observe()
|
|
262
|
+
now = self.clock()
|
|
263
|
+
if current.content_fingerprint == before.content_fingerprint:
|
|
264
|
+
previous = None
|
|
265
|
+
if now - started >= SETTLE_SECONDS:
|
|
266
|
+
return current
|
|
267
|
+
else:
|
|
268
|
+
if previous is not None and previous.content_fingerprint == current.content_fingerprint:
|
|
269
|
+
return current
|
|
270
|
+
previous = current
|
|
271
|
+
if now - started >= SETTLE_TIMEOUT:
|
|
272
|
+
return current
|
|
273
|
+
pause = POLL_SECONDS - (now - read_at)
|
|
274
|
+
if pause > 0:
|
|
275
|
+
self.sleep(pause)
|
|
276
|
+
|
|
277
|
+
def regrounded(self, element, snapshot):
|
|
278
|
+
if time.monotonic() - getattr(snapshot, "captured_at", 0) <= FRESH_SECONDS:
|
|
279
|
+
return element, snapshot
|
|
280
|
+
now = self.observe()
|
|
281
|
+
key, (cx, cy) = key_of(element.label), element.center
|
|
282
|
+
same = [e for e in now.elements if e.role == element.role and key_of(e.label) == key]
|
|
283
|
+
same.sort(key=lambda e: abs(e.center[1] - cy) + abs(e.center[0] - cx))
|
|
284
|
+
return (same[0] if same else None), now
|
|
285
|
+
|
|
286
|
+
def tap(self, element, snapshot):
|
|
287
|
+
"""Taps ``element`` (checked by the policy here, every time) and returns the settled screen after."""
|
|
288
|
+
element, snapshot = self.regrounded(element, snapshot)
|
|
289
|
+
if element is None:
|
|
290
|
+
raise Gone(snapshot)
|
|
291
|
+
verdict = policy.decide(element, snapshot)
|
|
292
|
+
if not verdict.ok: # the one place every tap passes, replayed taps included
|
|
293
|
+
# The fresh read says no: nothing was tapped, so the control is skipped like one that has gone.
|
|
294
|
+
self.log(f"refused at the tap: {policy_refusal(element, verdict)}")
|
|
295
|
+
raise Gone(snapshot)
|
|
296
|
+
try:
|
|
297
|
+
with self.timed("tap"):
|
|
298
|
+
self.sent("TAP", element.label, lambda: self.driver.execute("TAP", element, snapshot, timeout=10))
|
|
299
|
+
except Exception as error:
|
|
300
|
+
# The driver refused before sending anything (the screen moved since the read): nothing happened.
|
|
301
|
+
if "stale_revision" in str(error):
|
|
302
|
+
raise Gone(self.observe()) from None
|
|
303
|
+
raise
|
|
304
|
+
self.stats["taps"] += 1
|
|
305
|
+
self.before = snapshot # the read the tap was made on, for telling what it changed
|
|
306
|
+
return self.settle(snapshot)
|
|
307
|
+
|
|
308
|
+
def tap_back(self, element, snapshot):
|
|
309
|
+
"""A back or close control, checked on its own: its words must name no act."""
|
|
310
|
+
if policy.act_words(element.label) and not BACK_LABEL.match(policy.plain(element.label)):
|
|
311
|
+
return None
|
|
312
|
+
element, snapshot = self.regrounded(element, snapshot)
|
|
313
|
+
if element is None:
|
|
314
|
+
return None
|
|
315
|
+
try:
|
|
316
|
+
self.sent("TAP", element.label, lambda: self.driver.execute("TAP", element, snapshot, timeout=10))
|
|
317
|
+
except Exception as error:
|
|
318
|
+
if "stale_revision" in str(error):
|
|
319
|
+
return None
|
|
320
|
+
raise
|
|
321
|
+
return self.settle(snapshot, "back")
|
|
322
|
+
|
|
323
|
+
def scroll(self, snapshot, op="SWIPE_UP"):
|
|
324
|
+
self.sent(op, None, lambda: self.driver.execute(op, None, snapshot, timeout=10))
|
|
325
|
+
self.stats["scrolls"] += 1
|
|
326
|
+
return self.settle(snapshot, "scroll")
|
|
327
|
+
|
|
328
|
+
def edge_back(self, snapshot):
|
|
329
|
+
"""An edge swipe from the left (iOS's back gesture), when the driver can make one; None when it can't."""
|
|
330
|
+
gesture = getattr(self.driver, "edge_back", None)
|
|
331
|
+
if callable(gesture):
|
|
332
|
+
swipe = lambda: gesture(snapshot) # noqa: E731
|
|
333
|
+
elif callable(getattr(self.driver, "call", None)) and snapshot.source == "wda":
|
|
334
|
+
w, h = snapshot.width, snapshot.height
|
|
335
|
+
swipe = lambda: self.driver.call("POST", "/actions", {"actions": [{ # noqa: E731
|
|
336
|
+
"type": "pointer", "id": "finger", "parameters": {"pointerType": "touch"},
|
|
337
|
+
"actions": [{"type": "pointerMove", "duration": 0, "x": 1, "y": round(h * .45)},
|
|
338
|
+
{"type": "pointerDown"},
|
|
339
|
+
{"type": "pointerMove", "duration": 250, "x": round(w * .8), "y": round(h * .45)},
|
|
340
|
+
{"type": "pointerUp"}]}]}, timeout=10)
|
|
341
|
+
elif hasattr(getattr(self.driver, "helper", None), "request"):
|
|
342
|
+
swipe = lambda: self.driver.helper.request("drag", x1=.005, y1=.45, x2=.8, y2=.45, ms=250) # noqa: E731
|
|
343
|
+
else:
|
|
344
|
+
return None
|
|
345
|
+
try:
|
|
346
|
+
self.sent("EDGE_BACK", None, swipe)
|
|
347
|
+
except Exception as error:
|
|
348
|
+
self.log(f"edge swipe failed: {error}")
|
|
349
|
+
return None
|
|
350
|
+
return self.settle(snapshot, "swipe")
|
|
351
|
+
|
|
352
|
+
def launch(self, fresh=False):
|
|
353
|
+
"""The app's first screen. ``fresh``: closed and reopened (back to its start), when the driver can."""
|
|
354
|
+
self.note({"event": "app_launch_started", "app": self.bundle})
|
|
355
|
+
with self.timed("launch"):
|
|
356
|
+
snapshot = self._launch(fresh)
|
|
357
|
+
self.note({"event": "app_launch_acknowledged", "app": self.bundle})
|
|
358
|
+
return snapshot
|
|
359
|
+
|
|
360
|
+
def _launch(self, fresh):
|
|
361
|
+
if fresh:
|
|
362
|
+
relaunch = getattr(self.driver, "relaunch_app", None)
|
|
363
|
+
if callable(relaunch):
|
|
364
|
+
if relaunch(self.bundle) is False:
|
|
365
|
+
raise Stop("the connection stopped answering")
|
|
366
|
+
ready = getattr(self.driver, "observe_ready", None)
|
|
367
|
+
return ready(timeout=15) if callable(ready) else self.observe()
|
|
368
|
+
launch = getattr(self.driver, "launch", None)
|
|
369
|
+
if callable(launch):
|
|
370
|
+
result = launch(self.bundle, timeout=30)
|
|
371
|
+
return result[0] if isinstance(result, tuple) else result
|
|
372
|
+
before = self.observe()
|
|
373
|
+
self.driver.execute("LAUNCH_APP", self.bundle, before, timeout=20)
|
|
374
|
+
self.sleep(1.5)
|
|
375
|
+
return self.observe()
|
|
376
|
+
|
|
377
|
+
def thumbnail(self, screen):
|
|
378
|
+
if not self.shots:
|
|
379
|
+
return
|
|
380
|
+
with self.timed("frames"):
|
|
381
|
+
self._thumbnail(screen)
|
|
382
|
+
|
|
383
|
+
def _thumbnail(self, screen):
|
|
384
|
+
try:
|
|
385
|
+
url = self.driver.capture_preview(timeout=5)
|
|
386
|
+
from PIL import Image
|
|
387
|
+
data = base64.b64decode(url.split(",", 1)[1])
|
|
388
|
+
screen.frame = data # announce() hands it to the run, which stores it beside the run
|
|
389
|
+
with Image.open(BytesIO(data)) as image:
|
|
390
|
+
image = image.convert("RGB")
|
|
391
|
+
height = max(1, round(image.height * THUMB_WIDTH / image.width))
|
|
392
|
+
image = image.resize((THUMB_WIDTH, height), Image.LANCZOS)
|
|
393
|
+
frames = self.out / "frames"
|
|
394
|
+
frames.mkdir(parents=True, exist_ok=True)
|
|
395
|
+
name = f"frames/{screen.id}.jpg"
|
|
396
|
+
image.save(self.out / name, "JPEG", quality=78)
|
|
397
|
+
screen.thumb = name
|
|
398
|
+
except Exception as error:
|
|
399
|
+
self.log(f"no frame for {screen.id}: {error}")
|
|
400
|
+
|
|
401
|
+
def dialog(self, snapshot, system=False):
|
|
402
|
+
"""Gets a dialog out of the way with Not Now, Cancel or Close; the screen after."""
|
|
403
|
+
inside = [e for e in snapshot.elements if is_dialog_element(e)]
|
|
404
|
+
if inside:
|
|
405
|
+
alert = inside[0]
|
|
406
|
+
buttons = [e for e in snapshot.elements if e.role == "Button" and (
|
|
407
|
+
e.locator.startswith(alert.locator + "/") if alert.locator else policy._contains(alert.rect, e.rect))]
|
|
408
|
+
safe = policy.safe_dismiss([e.label for e in buttons])
|
|
409
|
+
if safe is None:
|
|
410
|
+
raise Stop(f"a dialog needs you: “{policy.plain(alert.label)[:80]}”")
|
|
411
|
+
button = next(e for e in buttons if e.label == safe)
|
|
412
|
+
self.sent("TAP", button.label, lambda: self.driver.execute("TAP", button, snapshot, timeout=10))
|
|
413
|
+
self.stats["dialogs_dismissed"] += 1
|
|
414
|
+
return self.settle(snapshot, "dialog")
|
|
415
|
+
if system and callable(getattr(self.driver, "call", None)) and snapshot.source == "wda":
|
|
416
|
+
from ..wda import system_dialogs
|
|
417
|
+
with self.timed("dialog_checks"):
|
|
418
|
+
found = system_dialogs.read(self.driver, timeout=2)
|
|
419
|
+
if found is not None:
|
|
420
|
+
safe = policy.safe_dismiss(found.buttons)
|
|
421
|
+
if safe is None:
|
|
422
|
+
raise Stop(f"iOS asks something only you can answer: “{found.text[:80]}”")
|
|
423
|
+
self.sent("TAP", safe, lambda: self.driver.call("POST", "/alert/accept", {"name": safe}, timeout=10))
|
|
424
|
+
self.stats["dialogs_dismissed"] += 1
|
|
425
|
+
return self.settle(snapshot, "dialog")
|
|
426
|
+
return snapshot
|
|
427
|
+
|
|
428
|
+
def identify(self, reading):
|
|
429
|
+
for screen in self.screens:
|
|
430
|
+
if screens.same(reading.title, reading.tokens, screen.title, screen.tokens, b_union=screen.union):
|
|
431
|
+
return screen
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
def is_on(self, screen, snapshot):
|
|
435
|
+
reading = screens.read(snapshot)
|
|
436
|
+
return screens.same(reading.title, reading.tokens, screen.title, screen.tokens, b_union=screen.union)
|
|
437
|
+
|
|
438
|
+
def register(self, snapshot, reading, depth, path, parent="", via=""):
|
|
439
|
+
screen = Screen(id=f"s{len(self.screens) + 1:03d}", title=reading.title, tokens=reading.tokens,
|
|
440
|
+
union=set(reading.tokens), lines=list(reading.lines), depth=depth, path=path, parent=parent,
|
|
441
|
+
via=via, seen_s=self.elapsed(), boxes=boxes(snapshot), image_only=reading.image_only)
|
|
442
|
+
screen.paywall = paywalls.detect(reading.lines)
|
|
443
|
+
self.screens.append(screen)
|
|
444
|
+
self.thumbnail(screen)
|
|
445
|
+
self.announce(screen)
|
|
446
|
+
self.log(f"{screen.id} depth {depth} “{screen.title or '(untitled)'}”"
|
|
447
|
+
+ (f" via “{via}”" if via else "") + (" PAYWALL" if screen.paywall else ""))
|
|
448
|
+
return screen
|
|
449
|
+
|
|
450
|
+
def announce(self, screen):
|
|
451
|
+
"""A ``step`` in the run for a screen just mapped, with its capture as the step's frame."""
|
|
452
|
+
frame, screen.frame = screen.frame, None
|
|
453
|
+
if self.emit is None:
|
|
454
|
+
return
|
|
455
|
+
event = {"event": "step", "n": len(self.screens), "step": len(self.screens),
|
|
456
|
+
"text": f"Mapped {screen.name}" + (" (a paywall)" if screen.paywall else ""),
|
|
457
|
+
**({"_frame": frame} if frame else {})}
|
|
458
|
+
self.emit(event)
|
|
459
|
+
screen.frame_id = event.get("frameId") # Run.emit stored the frame and put its id in the event
|
|
460
|
+
|
|
461
|
+
def learn(self, screen, snapshot):
|
|
462
|
+
"""Adds what a scrolled read of ``screen`` shows to it."""
|
|
463
|
+
reading = screens.read(snapshot)
|
|
464
|
+
new = reading.tokens - screen.union
|
|
465
|
+
screen.union |= reading.tokens
|
|
466
|
+
for line in reading.lines:
|
|
467
|
+
if line not in screen.lines and len(screen.lines) < screens.LINES_KEPT * 2:
|
|
468
|
+
screen.lines.append(line)
|
|
469
|
+
return new
|
|
470
|
+
|
|
471
|
+
def saturated(self, screen):
|
|
472
|
+
"""Whether ``screen`` has opened enough: a catalog sampled, or the children cap reached two levels in."""
|
|
473
|
+
return (screen.alike >= CATALOG_ALIKE
|
|
474
|
+
or screen.depth >= 2 and len(screen.kids) >= self.caps.children)
|
|
475
|
+
|
|
476
|
+
def candidates(self, screen, snapshot):
|
|
477
|
+
found, keys = [], set()
|
|
478
|
+
if screen.changed:
|
|
479
|
+
return found
|
|
480
|
+
if policy.choice_list(snapshot):
|
|
481
|
+
screen.choice = True
|
|
482
|
+
for element in sorted(snapshot.elements, key=lambda e: (round(e.rect[1], 3), -e.rect[2])):
|
|
483
|
+
verdict = policy.decide(element, snapshot)
|
|
484
|
+
key = key_of(element.label)
|
|
485
|
+
if not verdict.ok:
|
|
486
|
+
if (key and key not in screen.refused and verdict.reason not in QUIET_REASONS
|
|
487
|
+
and not (element.role in QUIET_ROLES and not policy.act_words(element.label)
|
|
488
|
+
and not screen.image_only)):
|
|
489
|
+
screen.refused[policy.plain(element.label)[:80]] = verdict.reason
|
|
490
|
+
continue
|
|
491
|
+
if not key or key in keys or key in screen.done:
|
|
492
|
+
continue
|
|
493
|
+
if verdict.reason == "tab" and screen.depth > 0:
|
|
494
|
+
continue # tabs are top-level: taken from a tab's first screen only
|
|
495
|
+
if screen.choice and verdict.reason == "cell":
|
|
496
|
+
screen.refused.setdefault(policy.plain(element.label)[:80], "an option in a choice list")
|
|
497
|
+
continue
|
|
498
|
+
keys.add(key)
|
|
499
|
+
label = policy.plain(element.label)
|
|
500
|
+
if element.role in ("Cell", "Button") and ", " in label:
|
|
501
|
+
label = label.split(", ")[0] # "Apple Account, Sign in to access..." is the row "Apple Account"
|
|
502
|
+
found.append(Candidate(key, label, element, verdict.reason))
|
|
503
|
+
return found
|
|
504
|
+
|
|
505
|
+
def back_controls(self, snapshot):
|
|
506
|
+
from ..frontier import back_control
|
|
507
|
+
control = back_control(snapshot)
|
|
508
|
+
if control is not None:
|
|
509
|
+
yield "control", control
|
|
510
|
+
for element in snapshot.elements:
|
|
511
|
+
label = policy.plain(element.label)
|
|
512
|
+
if (element is not control and BACK_LABEL.match(label) and element.center[1] < .2
|
|
513
|
+
and (element.role == "Button" or policy.image_only(snapshot) and element.center[0] < .3)):
|
|
514
|
+
yield ("control" if label.casefold() in ("back", "<", "‹") else "close"), element
|
|
515
|
+
|
|
516
|
+
def go_back(self, snapshot):
|
|
517
|
+
"""One step back by a back or close control, else an edge swipe; the new snapshot,
|
|
518
|
+
or None when neither moved."""
|
|
519
|
+
for kind, control in self.back_controls(snapshot):
|
|
520
|
+
moved = self.tap_back(control, snapshot)
|
|
521
|
+
if moved is not None and moved.content_fingerprint != snapshot.content_fingerprint:
|
|
522
|
+
self.stats["backs"][kind] += 1
|
|
523
|
+
return moved
|
|
524
|
+
swiped = self.edge_back(snapshot)
|
|
525
|
+
if swiped is not None and swiped.content_fingerprint != snapshot.content_fingerprint:
|
|
526
|
+
self.stats["backs"]["swipe"] += 1
|
|
527
|
+
return swiped
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
def to_top(self, snapshot, root=None):
|
|
531
|
+
"""After the app opens: its first screen. Some apps (Settings on an iPhone) reopen on the last one shown."""
|
|
532
|
+
for _ in range(TOP_STEPS):
|
|
533
|
+
if root is not None and self.is_on(root, snapshot):
|
|
534
|
+
break
|
|
535
|
+
if root is None and not any(kind == "control" for kind, _ in self.back_controls(snapshot)):
|
|
536
|
+
break
|
|
537
|
+
moved = self.go_back(snapshot)
|
|
538
|
+
if moved is None:
|
|
539
|
+
break
|
|
540
|
+
snapshot = self.dialog(moved)
|
|
541
|
+
if self.bundle_left(snapshot):
|
|
542
|
+
break
|
|
543
|
+
return snapshot
|
|
544
|
+
|
|
545
|
+
def return_to(self, screen, snapshot):
|
|
546
|
+
"""``snapshot`` on ``screen`` by Back, an edge swipe, or a relaunch and the path again; None when it can't."""
|
|
547
|
+
if self.is_on(screen, snapshot):
|
|
548
|
+
return snapshot
|
|
549
|
+
for _ in range(4):
|
|
550
|
+
moved = None
|
|
551
|
+
for kind, control in self.back_controls(snapshot):
|
|
552
|
+
moved = self.tap_back(control, snapshot)
|
|
553
|
+
if moved is not None:
|
|
554
|
+
self.stats["backs"][kind] += 1
|
|
555
|
+
break
|
|
556
|
+
if moved is None or moved.content_fingerprint == snapshot.content_fingerprint:
|
|
557
|
+
swiped = self.edge_back(snapshot)
|
|
558
|
+
if swiped is not None and swiped.content_fingerprint != snapshot.content_fingerprint:
|
|
559
|
+
self.stats["backs"]["swipe"] += 1
|
|
560
|
+
moved = swiped
|
|
561
|
+
else:
|
|
562
|
+
moved = None
|
|
563
|
+
if moved is None:
|
|
564
|
+
break
|
|
565
|
+
snapshot = self.dialog(moved)
|
|
566
|
+
if self.is_on(screen, snapshot):
|
|
567
|
+
return snapshot
|
|
568
|
+
if self.bundle_left(snapshot):
|
|
569
|
+
break
|
|
570
|
+
self.log(f"relaunching to reach {screen.id}: no back control or swipe brought it up")
|
|
571
|
+
return self.replay(screen)
|
|
572
|
+
|
|
573
|
+
def bundle_left(self, snapshot):
|
|
574
|
+
bundle = str(getattr(snapshot, "bundle_id", "") or "")
|
|
575
|
+
return bool(bundle) and bundle != self.bundle
|
|
576
|
+
|
|
577
|
+
def find(self, snapshot, key, pages):
|
|
578
|
+
"""The control ``key`` names on this screen, scrolling down for it; (element, snapshot) or (None, snapshot)."""
|
|
579
|
+
for page in range(pages + 1):
|
|
580
|
+
for element in snapshot.elements:
|
|
581
|
+
if key_of(element.label) == key and policy.decide(element, snapshot).ok:
|
|
582
|
+
return element, snapshot
|
|
583
|
+
if page < pages:
|
|
584
|
+
after = self.scroll(snapshot)
|
|
585
|
+
if after.content_fingerprint == snapshot.content_fingerprint:
|
|
586
|
+
break
|
|
587
|
+
snapshot = after
|
|
588
|
+
return None, snapshot
|
|
589
|
+
|
|
590
|
+
def replay(self, screen):
|
|
591
|
+
"""Relaunch the app and tap the path to ``screen`` again; the snapshot on it, or None."""
|
|
592
|
+
if self.capped():
|
|
593
|
+
return None
|
|
594
|
+
self.stats["backs"]["relaunch"] += 1
|
|
595
|
+
try:
|
|
596
|
+
snapshot = self.dialog(self.launch(fresh=True), system=True)
|
|
597
|
+
snapshot = self.to_top(snapshot, self.screens[0] if self.screens else None)
|
|
598
|
+
for step in screen.path:
|
|
599
|
+
element, snapshot = self.find(snapshot, step["key"], self.caps.pages)
|
|
600
|
+
if element is None:
|
|
601
|
+
raise Stop(f"“{step['label']}” is gone")
|
|
602
|
+
snapshot = self.dialog(self.tap(element, snapshot))
|
|
603
|
+
except Gone:
|
|
604
|
+
snapshot = None
|
|
605
|
+
except Stop as error:
|
|
606
|
+
if "needs you" in str(error) or "only you" in str(error) or "stopped answering" in str(error):
|
|
607
|
+
raise
|
|
608
|
+
self.log(f"couldn't get back to {screen.id}: {error}")
|
|
609
|
+
snapshot = None
|
|
610
|
+
if snapshot is not None and self.is_on(screen, snapshot):
|
|
611
|
+
self.stuck = 0
|
|
612
|
+
return snapshot
|
|
613
|
+
self.stats["relaunch_failures"] += 1
|
|
614
|
+
self.stuck += 1
|
|
615
|
+
if self.stuck >= STUCK_LIMIT:
|
|
616
|
+
raise Stop(f"couldn't get back to a screen {STUCK_LIMIT} times in a row")
|
|
617
|
+
return None
|
|
618
|
+
|
|
619
|
+
def run(self):
|
|
620
|
+
self.started = self.clock()
|
|
621
|
+
try:
|
|
622
|
+
refusal = policy.app_refusal(self.bundle)
|
|
623
|
+
if refusal:
|
|
624
|
+
raise Stop(refusal) # before anything is launched or tapped
|
|
625
|
+
snapshot = self.to_top(self.dialog(self.launch(fresh=True), system=True))
|
|
626
|
+
reading = screens.read(snapshot)
|
|
627
|
+
root = self.register(snapshot, reading, 0, [])
|
|
628
|
+
if root.paywall is None:
|
|
629
|
+
# The first levels first: a long list deep in one branch (Settings' fonts) can't use up the screens
|
|
630
|
+
# before the app's other sections are mapped. Then each screen left at that depth, deeper.
|
|
631
|
+
self.limit = min(FIRST_PASS_DEPTH, self.caps.depth)
|
|
632
|
+
self.explore(root, snapshot)
|
|
633
|
+
if self.caps.depth > self.limit:
|
|
634
|
+
frontier = [s for s in self.screens if s.depth == self.limit and s.pending]
|
|
635
|
+
self.limit = self.caps.depth
|
|
636
|
+
for screen in frontier:
|
|
637
|
+
if self.capped():
|
|
638
|
+
break
|
|
639
|
+
found = self.replay(screen)
|
|
640
|
+
if found is not None:
|
|
641
|
+
self.explore(screen, found)
|
|
642
|
+
if not self.stopped:
|
|
643
|
+
self.stopped = "explored every screen it could reach"
|
|
644
|
+
except Stop as error:
|
|
645
|
+
self.stopped = str(error)
|
|
646
|
+
except KeyboardInterrupt:
|
|
647
|
+
self.stopped = "stopped by you"
|
|
648
|
+
return self.map()
|
|
649
|
+
|
|
650
|
+
def explore(self, screen, snapshot):
|
|
651
|
+
"""Explores ``screen`` (``snapshot`` shows it); the last snapshot on it, or None when it lost the screen."""
|
|
652
|
+
scrolls = 0
|
|
653
|
+
while not self.capped():
|
|
654
|
+
found = self.candidates(screen, snapshot)
|
|
655
|
+
if found and self.saturated(screen):
|
|
656
|
+
screen.held_back += len(found)
|
|
657
|
+
break
|
|
658
|
+
if not found:
|
|
659
|
+
if scrolls >= self.caps.pages - 1 or screen.changed or not more_below(snapshot):
|
|
660
|
+
break
|
|
661
|
+
after = self.scroll(snapshot)
|
|
662
|
+
scrolls += 1
|
|
663
|
+
moved = after.content_fingerprint != snapshot.content_fingerprint
|
|
664
|
+
self.learn(screen, after)
|
|
665
|
+
snapshot = after
|
|
666
|
+
if not moved:
|
|
667
|
+
break # the end of the list
|
|
668
|
+
continue
|
|
669
|
+
pick = found[0]
|
|
670
|
+
screen.done.add(pick.key)
|
|
671
|
+
snapshot = self.visit(screen, snapshot, pick)
|
|
672
|
+
if snapshot is None:
|
|
673
|
+
screen.unreachable = True
|
|
674
|
+
return None
|
|
675
|
+
return snapshot
|
|
676
|
+
|
|
677
|
+
def visit(self, screen, snapshot, pick):
|
|
678
|
+
"""Taps ``pick`` on ``screen``, maps where it leads, explores it when new, and comes back."""
|
|
679
|
+
try:
|
|
680
|
+
after = self.tap(pick.element, snapshot)
|
|
681
|
+
after = self.dialog(after, system=state(after) == state(self.before))
|
|
682
|
+
except Gone as gone:
|
|
683
|
+
return gone.snapshot # it moved; the next pass reads the screen again
|
|
684
|
+
if self.bundle_left(after):
|
|
685
|
+
self.stats["left_app"] += 1
|
|
686
|
+
self.edges.append({"from": screen.id, "to": "", "label": pick.label, "kind": "leaves app",
|
|
687
|
+
"app": after.bundle_id})
|
|
688
|
+
return self.return_to(screen, after)
|
|
689
|
+
if self.is_on(screen, after):
|
|
690
|
+
if state(after) != state(self.before):
|
|
691
|
+
# The tap changed this screen in place: it may have set something, so nothing else on it is tapped.
|
|
692
|
+
screen.changed.append(pick.label)
|
|
693
|
+
self.stats["changed_in_place"] = self.stats.get("changed_in_place", 0) + 1
|
|
694
|
+
self.log(f"“{pick.label}” changed {screen.id} in place: leaving the rest of it alone")
|
|
695
|
+
else:
|
|
696
|
+
self.stats["no_change"] += 1
|
|
697
|
+
screen.noop.append(pick.label)
|
|
698
|
+
self.learn(screen, after)
|
|
699
|
+
return after
|
|
700
|
+
reading = screens.read(after)
|
|
701
|
+
target = self.identify(reading)
|
|
702
|
+
kind = "tab" if pick.reason == "tab" else "tap"
|
|
703
|
+
if target is None:
|
|
704
|
+
depth = 0 if kind == "tab" else screen.depth + 1
|
|
705
|
+
path = ([] if kind == "tab" else list(screen.path)) + [{"label": pick.label, "key": pick.key,
|
|
706
|
+
"kind": kind}]
|
|
707
|
+
target = self.register(after, reading, depth, path, parent=screen.id, via=pick.label)
|
|
708
|
+
shape = target.tokens - {screens.token(target.title)}
|
|
709
|
+
if any(screens.jaccard(shape, other) >= ALIKE for other in screen.kids):
|
|
710
|
+
screen.alike += 1
|
|
711
|
+
screen.kids.append(shape)
|
|
712
|
+
self.edges.append({"from": screen.id, "to": target.id, "label": pick.label, "kind": kind})
|
|
713
|
+
if target.paywall is None and target.depth < self.limit and not self.capped():
|
|
714
|
+
explored = self.explore(target, after)
|
|
715
|
+
after = explored if explored is not None else self.observe()
|
|
716
|
+
elif target.paywall is None:
|
|
717
|
+
target.pending = bool(self.candidates(target, after)) or more_below(after)
|
|
718
|
+
else:
|
|
719
|
+
self.edges.append({"from": screen.id, "to": target.id, "label": pick.label, "kind": kind})
|
|
720
|
+
if self.stopped:
|
|
721
|
+
return after
|
|
722
|
+
return self.return_to(screen, after)
|
|
723
|
+
|
|
724
|
+
def map(self):
|
|
725
|
+
wall = max(self.elapsed(), 1e-6)
|
|
726
|
+
refused = [{"screen": s.id, "label": label, "reason": reason}
|
|
727
|
+
for s in self.screens for label, reason in s.refused.items()]
|
|
728
|
+
safety = [item for item in refused if item["reason"] not in QUIET_REASONS]
|
|
729
|
+
paywalled = [s for s in self.screens if s.paywall]
|
|
730
|
+
linked = {edge["from"] for edge in self.edges}
|
|
731
|
+
stats = {
|
|
732
|
+
"screens": len(self.screens), "edges": len(self.edges),
|
|
733
|
+
"max_depth": max((s.depth for s in self.screens), default=0),
|
|
734
|
+
"wall_seconds": round(wall, 1),
|
|
735
|
+
"screens_per_minute": round(len(self.screens) / (wall / 60), 1),
|
|
736
|
+
"refused": len(safety),
|
|
737
|
+
"paywalls": len(paywalled),
|
|
738
|
+
"dead_ends": sum(1 for s in self.screens if s.id not in linked and not s.paywall),
|
|
739
|
+
"model_usd": 0.0,
|
|
740
|
+
**self.stats,
|
|
741
|
+
"settles": {kind: {"n": d["n"], "seconds": round(d["seconds"], 1)}
|
|
742
|
+
for kind, d in sorted(self.settles.items())},
|
|
743
|
+
"seconds_in": {kind: round(value, 1) for kind, value in sorted(self.spent.items())},
|
|
744
|
+
}
|
|
745
|
+
return {"schema": SCHEMA, "app": {"bundle": self.bundle}, "created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
746
|
+
"connection": "pixels" if any(s.image_only for s in self.screens) else "tree",
|
|
747
|
+
"caps": {"screens": self.caps.screens, "depth": self.caps.depth, "seconds": self.caps.seconds,
|
|
748
|
+
"pages": self.caps.pages},
|
|
749
|
+
"stopped": self.stopped, "stats": stats,
|
|
750
|
+
"screens": [s.to_json() for s in self.screens], "edges": self.edges}
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def policy_refusal(element, verdict):
|
|
754
|
+
return Stop(f"the policy refused “{policy.plain(element.label)[:60]}”: {verdict.reason}")
|