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,332 @@
|
|
|
1
|
+
"""The transport monitor: the service that keeps each phone on the cable or on Wi-Fi (SPEC §3.7 items 2, 5-7)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from . import devicectl, store, transport
|
|
8
|
+
from .relay import check_target
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("mobster.wireless")
|
|
11
|
+
|
|
12
|
+
TICK = 10.0
|
|
13
|
+
SLOW = 60.0 # devicectl at most this often while every phone with Wi-Fi on is on the cable
|
|
14
|
+
START_TIMEOUT = 120.0 # a runner started over Wi-Fi answers within this, or it is stopped and retried
|
|
15
|
+
LOST_AFTER = 30.0 # a Wi-Fi runner that stops answering for this long (and no task) is restarted
|
|
16
|
+
NETWORK_EVERY = 60.0 # the network check, at most this often while a phone with Wi-Fi on can't be found
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def task_on(runtime, phone, udid):
|
|
20
|
+
# when it can't be told, it counts as busy
|
|
21
|
+
try:
|
|
22
|
+
keys = dict.fromkeys(key for key in (udid, getattr(phone, "key", None)) if key)
|
|
23
|
+
if any(runtime.device_busy(key) for key in keys):
|
|
24
|
+
return True
|
|
25
|
+
except Exception: # noqa: BLE001
|
|
26
|
+
return True
|
|
27
|
+
in_use = getattr(getattr(runtime, "fleet", None), "in_use", None)
|
|
28
|
+
if not callable(in_use):
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
return bool(in_use(phone))
|
|
32
|
+
except Exception: # noqa: BLE001
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Monitor:
|
|
37
|
+
def __init__(self, runtime, *, lister=None, bring_up=None, attached=None, nwi=None, clock=None, publish=None,
|
|
38
|
+
through=None, network_check=None):
|
|
39
|
+
from ..mac_tools import usb_attached
|
|
40
|
+
from . import network
|
|
41
|
+
self.runtime = runtime
|
|
42
|
+
self.through = through or network.through_tunnel
|
|
43
|
+
self.network_check = network_check or network.peer_check
|
|
44
|
+
self.network = None # the last network.peer_check result, or None before one ran
|
|
45
|
+
self.network_at = None
|
|
46
|
+
self.lister = lister or devicectl.list_devices
|
|
47
|
+
self.bring_up = bring_up or devicectl.bring_up
|
|
48
|
+
self.attached = attached or usb_attached
|
|
49
|
+
self.nwi = nwi or (lambda: network.signature(network.nwi()))
|
|
50
|
+
self.clock = clock or time.monotonic
|
|
51
|
+
self.publish = publish or getattr(runtime, "publish", None) or (lambda topic, event: None)
|
|
52
|
+
self.backoffs = {} # UDID -> transport.Backoff
|
|
53
|
+
self.starting = {} # UDID -> when a Wi-Fi start began
|
|
54
|
+
self.silent = {} # UDID -> since when a Wi-Fi runner stopped answering
|
|
55
|
+
self.peers = {} # UDID -> devicectl.Peer
|
|
56
|
+
self.peers_at = None
|
|
57
|
+
self.devicectl_problem = None
|
|
58
|
+
self.net = None
|
|
59
|
+
self.known = set()
|
|
60
|
+
self.lock = threading.Lock()
|
|
61
|
+
self.stopping = threading.Event()
|
|
62
|
+
self.kicked = threading.Event()
|
|
63
|
+
self.thread = None
|
|
64
|
+
|
|
65
|
+
# -- the service ----------------------------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def running(self):
|
|
69
|
+
return self.thread is not None and self.thread.is_alive()
|
|
70
|
+
|
|
71
|
+
def start(self):
|
|
72
|
+
if self.running:
|
|
73
|
+
return
|
|
74
|
+
self.stopping.clear()
|
|
75
|
+
self.thread = threading.Thread(target=self._loop, name="mobster-wifi-monitor", daemon=True)
|
|
76
|
+
self.thread.start()
|
|
77
|
+
|
|
78
|
+
def stop(self, timeout=2.0):
|
|
79
|
+
self.stopping.set()
|
|
80
|
+
self.kicked.set()
|
|
81
|
+
if self.thread is not None:
|
|
82
|
+
self.thread.join(timeout)
|
|
83
|
+
|
|
84
|
+
def kick(self, udid=None):
|
|
85
|
+
"""Look again now; with ``udid``, forget its backoff first (Try again, Wi-Fi just turned on)."""
|
|
86
|
+
if udid:
|
|
87
|
+
self.backoff(udid).reset()
|
|
88
|
+
self.peers_at = None
|
|
89
|
+
self.kicked.set()
|
|
90
|
+
|
|
91
|
+
def _loop(self):
|
|
92
|
+
while not self.stopping.is_set() and not getattr(self.runtime, "closing", False):
|
|
93
|
+
try:
|
|
94
|
+
self.tick()
|
|
95
|
+
except Exception: # noqa: BLE001 -- the monitor never stops Mobster
|
|
96
|
+
log.exception("the Wi-Fi monitor's check failed")
|
|
97
|
+
self.kicked.wait(self.pause())
|
|
98
|
+
self.kicked.clear()
|
|
99
|
+
|
|
100
|
+
def pause(self):
|
|
101
|
+
now = self.clock()
|
|
102
|
+
due = [backoff.next_at - now for backoff in list(self.backoffs.values()) if backoff.failures]
|
|
103
|
+
return max(0.5, min([TICK, *due]))
|
|
104
|
+
|
|
105
|
+
def backoff(self, udid):
|
|
106
|
+
return self.backoffs.setdefault(str(udid).upper(), transport.Backoff())
|
|
107
|
+
|
|
108
|
+
# -- one look at every phone ----------------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def tick(self):
|
|
111
|
+
with self.lock:
|
|
112
|
+
if getattr(self.runtime, "closing", False):
|
|
113
|
+
return
|
|
114
|
+
fleet = getattr(self.runtime, "fleet", None)
|
|
115
|
+
if fleet is None or fleet.data_dir is None:
|
|
116
|
+
return
|
|
117
|
+
now = self.clock()
|
|
118
|
+
phones = fleet.usb_managers()
|
|
119
|
+
enabled = store.enabled_udids(fleet.data_dir)
|
|
120
|
+
signature = self.nwi()
|
|
121
|
+
if signature is not None and self.net is not None and signature != self.net:
|
|
122
|
+
# Another Wi-Fi, a new address, a VPN: the tunnel is set up again, so look again at once.
|
|
123
|
+
for backoff in list(self.backoffs.values()):
|
|
124
|
+
backoff.reset()
|
|
125
|
+
self.peers_at = None
|
|
126
|
+
self.network_at = None
|
|
127
|
+
if signature is not None:
|
|
128
|
+
self.net = signature
|
|
129
|
+
usb = {udid.upper(): self._usb(udid) for _, udid, _ in phones}
|
|
130
|
+
off_cable = [udid for _, udid, _ in phones if udid.upper() in enabled and usb[udid.upper()] is not True]
|
|
131
|
+
due = SLOW if not off_cable else TICK * 0.9
|
|
132
|
+
if enabled and (self.peers_at is None or now - self.peers_at >= due):
|
|
133
|
+
self._refresh(now)
|
|
134
|
+
away = [udid for udid in off_cable if not self.peers.get(udid.upper()) or
|
|
135
|
+
not self.peers[udid.upper()].network]
|
|
136
|
+
if away and (self.network_at is None or now - self.network_at >= NETWORK_EVERY):
|
|
137
|
+
self._check_network(now)
|
|
138
|
+
managed = set()
|
|
139
|
+
for phone, udid, manager in phones:
|
|
140
|
+
managed.add(udid.upper())
|
|
141
|
+
try:
|
|
142
|
+
self._step(phone, udid, manager, udid.upper() in enabled, usb[udid.upper()], now)
|
|
143
|
+
except Exception: # noqa: BLE001 -- one phone never stops the others
|
|
144
|
+
log.exception("the Wi-Fi monitor's check of one phone failed")
|
|
145
|
+
for gone in self.known - managed:
|
|
146
|
+
transport.forget(gone)
|
|
147
|
+
self.known = managed
|
|
148
|
+
|
|
149
|
+
def busy(self, phone, udid):
|
|
150
|
+
return task_on(self.runtime, phone, udid)
|
|
151
|
+
|
|
152
|
+
def _usb(self, udid):
|
|
153
|
+
try:
|
|
154
|
+
return self.attached(udid)
|
|
155
|
+
except Exception:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _refresh(self, now):
|
|
159
|
+
self.peers_at = now
|
|
160
|
+
try:
|
|
161
|
+
self.peers = self.lister()
|
|
162
|
+
self.devicectl_problem = None
|
|
163
|
+
except devicectl.DevicectlError as error:
|
|
164
|
+
self.peers = {}
|
|
165
|
+
if error.code == "hang":
|
|
166
|
+
self.devicectl_problem = "devicectl_hang"
|
|
167
|
+
else:
|
|
168
|
+
self.devicectl_problem = "no_xcode" if error.code == "no_xcode" else "not_seen"
|
|
169
|
+
except RuntimeError:
|
|
170
|
+
self.peers, self.devicectl_problem = {}, "not_seen" # MOBSTER_NO_DEVICES=1
|
|
171
|
+
|
|
172
|
+
def _check_network(self, now):
|
|
173
|
+
self.network_at = now
|
|
174
|
+
try:
|
|
175
|
+
self.network = self.network_check()
|
|
176
|
+
except Exception: # noqa: BLE001 -- a check that fails says nothing
|
|
177
|
+
log.exception("the Wi-Fi monitor's network check failed")
|
|
178
|
+
self.network = None
|
|
179
|
+
|
|
180
|
+
def network_problem(self):
|
|
181
|
+
from .words import NETWORK_PROBLEMS
|
|
182
|
+
return NETWORK_PROBLEMS.get((self.network or {}).get("state"))
|
|
183
|
+
|
|
184
|
+
def _tunnel(self, udid):
|
|
185
|
+
peer = self.peers.get(udid.upper())
|
|
186
|
+
if peer is None or not peer.network:
|
|
187
|
+
return ("unknown" if peer is None else "down"), None, False
|
|
188
|
+
address = None
|
|
189
|
+
if peer.tunnel == "up" and peer.address:
|
|
190
|
+
try:
|
|
191
|
+
address = check_target(peer.address)
|
|
192
|
+
except ValueError:
|
|
193
|
+
return "up", None, True
|
|
194
|
+
# only an address this Mac reaches through its CoreDevice tunnel, never through Wi-Fi, Ethernet or a VPN
|
|
195
|
+
if not self.through(address):
|
|
196
|
+
return "up", None, True
|
|
197
|
+
return peer.tunnel, address, True
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _now(manager):
|
|
201
|
+
"""(what the phone's runner uses now, or None when it isn't running)."""
|
|
202
|
+
running = manager.runner is not None and manager.runner.running
|
|
203
|
+
return getattr(manager, "transport", None) if running else None
|
|
204
|
+
|
|
205
|
+
def _step(self, phone, udid, manager, enabled, usb, now):
|
|
206
|
+
key = udid.upper()
|
|
207
|
+
current = self._now(manager)
|
|
208
|
+
tunnel, address, seen = self._tunnel(udid) if enabled else ("unknown", None, False)
|
|
209
|
+
reachable = bool(current == "wifi" and address is None and manager.wda_ready(timeout=1.5))
|
|
210
|
+
route_lost = False
|
|
211
|
+
if current == "wifi" and enabled and usb is not True and reachable and address is None:
|
|
212
|
+
# Answering over Wi-Fi: devicectl not answering (or not listing it this time) doesn't end a working link,
|
|
213
|
+
# while this Mac still reaches its address through the tunnel.
|
|
214
|
+
route_lost = not self.through(manager.wifi_address)
|
|
215
|
+
if not route_lost:
|
|
216
|
+
address, tunnel = manager.wifi_address, "up"
|
|
217
|
+
desired = transport.choose(usb is True, enabled, address is not None)
|
|
218
|
+
busy = self.busy(phone, udid)
|
|
219
|
+
step = transport.next_step(current, desired, busy)
|
|
220
|
+
if step == "stay" and current == "wifi" and address and manager.wifi_address != address and not busy:
|
|
221
|
+
step = "switch" # the tunnel came back with another address: start again on it
|
|
222
|
+
backoff = self.backoff(udid)
|
|
223
|
+
problem, waiting = None, step == "wait"
|
|
224
|
+
|
|
225
|
+
if step == "switch" and desired == "wifi":
|
|
226
|
+
problem, _started = self._start_wifi(udid, manager, address, backoff, now)
|
|
227
|
+
elif step == "switch" and current == "wifi":
|
|
228
|
+
manager.stop_runner()
|
|
229
|
+
self.starting.pop(key, None)
|
|
230
|
+
self.silent.pop(key, None)
|
|
231
|
+
if desired == "usb":
|
|
232
|
+
manager.autostart() # the cable's runner, as at launch
|
|
233
|
+
else:
|
|
234
|
+
backoff.fail(now)
|
|
235
|
+
problem = "route" if route_lost else "tunnel_down" if seen else (self.devicectl_problem or "not_seen")
|
|
236
|
+
elif desired is None and enabled and usb is not True and current != "wifi" and not busy:
|
|
237
|
+
problem = self._bring_up(udid, tunnel, seen, backoff, now)
|
|
238
|
+
|
|
239
|
+
current = self._now(manager)
|
|
240
|
+
reachable = bool(current is not None and manager.wda_ready(timeout=1.5))
|
|
241
|
+
if current == "wifi":
|
|
242
|
+
problem = self._watch_wifi(udid, manager, reachable, busy, backoff, now) or problem
|
|
243
|
+
current = self._now(manager)
|
|
244
|
+
reachable = reachable and current == "wifi"
|
|
245
|
+
# Switching: a change waits for a task, or a start over Wi-Fi hasn't answered yet.
|
|
246
|
+
switching = waiting or (current == "wifi" and key in self.starting and not reachable)
|
|
247
|
+
if not enabled:
|
|
248
|
+
problem = None
|
|
249
|
+
elif problem == "not_seen":
|
|
250
|
+
problem = self.network_problem() or problem
|
|
251
|
+
before, after = transport.update(udid, transport=current, tunnel=tunnel if enabled else "unknown",
|
|
252
|
+
reachable=reachable, problem=problem, switching=bool(switching))
|
|
253
|
+
if before is None or (before.transport, before.reachable) != (after.transport, after.reachable):
|
|
254
|
+
self.publish("devices", {"event": "transport_changed", "device": udid, "transport": after.transport,
|
|
255
|
+
"reachable": after.reachable})
|
|
256
|
+
|
|
257
|
+
def _start_wifi(self, udid, manager, address, backoff, now):
|
|
258
|
+
if not manager.settings().get("autostart"):
|
|
259
|
+
return None, False # stopped in Setup: stays stopped on Wi-Fi too
|
|
260
|
+
if not backoff.ready(now):
|
|
261
|
+
return ("runner" if backoff.failures else None), False
|
|
262
|
+
try:
|
|
263
|
+
manager.start_wifi_runner(address, self.peers[udid.upper()].udid)
|
|
264
|
+
except LookupError as error:
|
|
265
|
+
backoff.fail(now)
|
|
266
|
+
text = str(error)
|
|
267
|
+
return ("identity" if "different iPhone" in text else "address" if "isn't available" in text else
|
|
268
|
+
"port_busy" if "port" in text else "not_set_up" if "Build" in text or "Setup" in text else
|
|
269
|
+
"runner"), False
|
|
270
|
+
self.starting[udid.upper()] = now
|
|
271
|
+
self.silent.pop(udid.upper(), None)
|
|
272
|
+
return None, True
|
|
273
|
+
|
|
274
|
+
def _watch_wifi(self, udid, manager, reachable, busy, backoff, now):
|
|
275
|
+
key = udid.upper()
|
|
276
|
+
if reachable:
|
|
277
|
+
self.starting.pop(key, None)
|
|
278
|
+
self.silent.pop(key, None)
|
|
279
|
+
backoff.reset()
|
|
280
|
+
return None
|
|
281
|
+
if busy:
|
|
282
|
+
return None
|
|
283
|
+
started = self.starting.get(key)
|
|
284
|
+
if started is not None:
|
|
285
|
+
if now - started < START_TIMEOUT:
|
|
286
|
+
return None
|
|
287
|
+
else:
|
|
288
|
+
since = self.silent.setdefault(key, now)
|
|
289
|
+
if now - since < LOST_AFTER:
|
|
290
|
+
return None
|
|
291
|
+
manager.stop_runner()
|
|
292
|
+
self.starting.pop(key, None)
|
|
293
|
+
self.silent.pop(key, None)
|
|
294
|
+
backoff.fail(now)
|
|
295
|
+
return "runner"
|
|
296
|
+
|
|
297
|
+
def _bring_up(self, udid, tunnel, seen, backoff, now):
|
|
298
|
+
"""Unplugged with Wi-Fi on and no usable tunnel: ask CoreDevice to bring it up, at the backoff's pace."""
|
|
299
|
+
if self.devicectl_problem in ("no_xcode", "devicectl_hang"):
|
|
300
|
+
return self.devicectl_problem
|
|
301
|
+
if not seen:
|
|
302
|
+
return "not_seen"
|
|
303
|
+
if tunnel == "up":
|
|
304
|
+
return "address" # up, but its address isn't a tunnel address: never used
|
|
305
|
+
if not backoff.ready(now):
|
|
306
|
+
return "tunnel_down"
|
|
307
|
+
try:
|
|
308
|
+
self.peers.update(self.bring_up(udid))
|
|
309
|
+
self.peers_at = None # read the list again next time
|
|
310
|
+
except devicectl.DevicectlError as error:
|
|
311
|
+
backoff.fail(now)
|
|
312
|
+
if error.code == "hang":
|
|
313
|
+
return "devicectl_hang"
|
|
314
|
+
return "tunnel_down"
|
|
315
|
+
except (RuntimeError, ValueError):
|
|
316
|
+
backoff.fail(now)
|
|
317
|
+
return "tunnel_down"
|
|
318
|
+
self.kicked.set() # look again soon: the tunnel may be up now
|
|
319
|
+
return "tunnel_down"
|
|
320
|
+
|
|
321
|
+
def retire(self):
|
|
322
|
+
with self.lock:
|
|
323
|
+
fleet = getattr(self.runtime, "fleet", None)
|
|
324
|
+
for phone, udid, manager in (fleet.usb_managers() if fleet is not None else ()):
|
|
325
|
+
if getattr(manager, "transport", None) == "wifi" and not self.busy(phone, udid):
|
|
326
|
+
try:
|
|
327
|
+
manager.stop_runner()
|
|
328
|
+
manager.autostart()
|
|
329
|
+
except Exception: # noqa: BLE001
|
|
330
|
+
log.exception("couldn't stop a runner on Wi-Fi")
|
|
331
|
+
transport.forget(udid)
|
|
332
|
+
self.known = set()
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""This Mac's network, read without changing it: changes, a VPN, Local Network permission, peers and routes."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import hashlib
|
|
5
|
+
import ipaddress
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import select
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
SCUTIL = "/usr/sbin/scutil"
|
|
15
|
+
ROUTE = "/sbin/route"
|
|
16
|
+
IFCONFIG = "/sbin/ifconfig"
|
|
17
|
+
DNS_SD = "/usr/bin/dns-sd"
|
|
18
|
+
VPN_INTERFACE = re.compile(r"^(utun|ipsec|ppp|tun|tap|wg)\d*$")
|
|
19
|
+
# A network this Mac can share with an iPhone: Wi-Fi or Ethernet (en0, en7, ...), or a bridge.
|
|
20
|
+
LAN_INTERFACE = re.compile(r"^(en|bridge)\d+$")
|
|
21
|
+
# Apple's CoreDevice tunnel to a phone is a utun interface on this Mac that holds this Mac's end of the link: an
|
|
22
|
+
# address on the phone's own /64 (or a narrower network), or a point-to-point address whose peer is the phone. A VPN
|
|
23
|
+
# is a utun too, so the route must also be more than a default route, and the utun not one macOS marks as a VPN.
|
|
24
|
+
TUNNEL_INTERFACE = re.compile(r"^utun\d+$")
|
|
25
|
+
TUNNEL_PREFIX = 64
|
|
26
|
+
# `ifconfig -v`'s marks of a VPN: the IS_VPN interface flag, or a NetworkExtension VPN agent.
|
|
27
|
+
VPN_MARK = re.compile(r"\bIS_VPN\b|agent domain:NetworkExtension type:VPN\b")
|
|
28
|
+
INET6 = re.compile(r"^\s*inet6\s+([0-9a-fA-F:.]+)(?:%\S+)?(?:\s+-->\s+([0-9a-fA-F:.]+))?\s+prefixlen\s+(\d+)", re.M)
|
|
29
|
+
# What phones, TVs, speakers, printers and computers advertise. Info.plist's NSBonjourServices lists the same.
|
|
30
|
+
PEER_TYPES = ("_companion-link._tcp", "_airplay._tcp", "_raop._tcp", "_apple-mobdev2._tcp", "_remotepairing._tcp",
|
|
31
|
+
"_googlecast._tcp", "_hap._tcp", "_ipp._tcp", "_sleep-proxy._udp", "_spotify-connect._tcp")
|
|
32
|
+
BROWSE_SECONDS = 2.5
|
|
33
|
+
# mDNSResponder's "policy denied": macOS's Local Network privacy refused the browse.
|
|
34
|
+
POLICY_DENIED = "-65570"
|
|
35
|
+
LOOPBACK_INDEX = 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_output(argv, timeout=3):
|
|
39
|
+
try:
|
|
40
|
+
completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
|
|
41
|
+
except (OSError, subprocess.SubprocessError):
|
|
42
|
+
return None
|
|
43
|
+
return completed.stdout if completed.returncode == 0 else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def nwi(reader=None):
|
|
47
|
+
"""`scutil --nwi`: the network state macOS routes by (interfaces, addresses, reachability), or None."""
|
|
48
|
+
return (reader or read_output)([SCUTIL, "--nwi"])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def signature(text):
|
|
52
|
+
if text is None:
|
|
53
|
+
return None
|
|
54
|
+
lines = [" ".join(line.split()) for line in text.splitlines() if line.strip()]
|
|
55
|
+
return hashlib.sha256("\n".join(lines).encode()).hexdigest()[:16]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ipv4_interfaces(text):
|
|
59
|
+
"""The interfaces `scutil --nwi` lists under IPv4, in its order (the primary first)."""
|
|
60
|
+
interfaces, section = [], None
|
|
61
|
+
for line in (text or "").splitlines():
|
|
62
|
+
stripped = line.strip()
|
|
63
|
+
if stripped.startswith("IPv4 network interface information"):
|
|
64
|
+
section = "v4"
|
|
65
|
+
continue
|
|
66
|
+
if stripped.startswith("IPv6 network interface information") or stripped.startswith("Network interfaces:"):
|
|
67
|
+
section = None
|
|
68
|
+
continue
|
|
69
|
+
if section == "v4":
|
|
70
|
+
match = re.match(r"^([a-z]+\d+)\s*:\s*flags", stripped)
|
|
71
|
+
if match:
|
|
72
|
+
interfaces.append(match.group(1))
|
|
73
|
+
return interfaces
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def vpn_interface(text):
|
|
77
|
+
"""The IPv4 interface of a VPN when one carries this Mac's traffic (utun, ipsec, ppp, ...), else None."""
|
|
78
|
+
return next((name for name in ipv4_interfaces(text) if VPN_INTERFACE.match(name)), None)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def default_gateway(reader=None):
|
|
82
|
+
"""The IPv4 default gateway (`route -n get default`), or None."""
|
|
83
|
+
text = (reader or read_output)([ROUTE, "-n", "get", "default"])
|
|
84
|
+
match = re.search(r"^\s*gateway:\s*(\S+)\s*$", text or "", re.M)
|
|
85
|
+
if not match:
|
|
86
|
+
return None
|
|
87
|
+
try:
|
|
88
|
+
return str(ipaddress.IPv4Address(match.group(1)))
|
|
89
|
+
except ValueError:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# macOS's Local Network privacy refuses with EHOSTUNREACH to a gateway that is there; any other answer means allowed.
|
|
94
|
+
def local_network_allowed(gateway, timeout=0.6, connect=None):
|
|
95
|
+
if not gateway:
|
|
96
|
+
return None
|
|
97
|
+
attempt = connect or socket.create_connection
|
|
98
|
+
try:
|
|
99
|
+
attempt((gateway, 9), timeout).close()
|
|
100
|
+
return True
|
|
101
|
+
except ConnectionRefusedError:
|
|
102
|
+
return True
|
|
103
|
+
except (socket.timeout, TimeoutError):
|
|
104
|
+
return True
|
|
105
|
+
except OSError as error:
|
|
106
|
+
if error.errno in (errno.EHOSTUNREACH, errno.EPERM, errno.EACCES):
|
|
107
|
+
return False
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve(hostnames, timeout=4.0, lookup=None):
|
|
112
|
+
lookup = lookup or socket.getaddrinfo
|
|
113
|
+
found = {"ipv4": [], "ipv6": []}
|
|
114
|
+
for name in hostnames:
|
|
115
|
+
result = []
|
|
116
|
+
|
|
117
|
+
def ask(name=name, result=result):
|
|
118
|
+
try:
|
|
119
|
+
result.extend(lookup(name, None, 0, socket.SOCK_STREAM))
|
|
120
|
+
except (OSError, UnicodeError):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
worker = threading.Thread(target=ask, name="mobster-wifi-resolve", daemon=True)
|
|
124
|
+
worker.start()
|
|
125
|
+
worker.join(timeout)
|
|
126
|
+
for family, *_rest, sockaddr in list(result):
|
|
127
|
+
address = sockaddr[0].split("%", 1)[0]
|
|
128
|
+
try:
|
|
129
|
+
parsed = ipaddress.ip_address(address)
|
|
130
|
+
except ValueError:
|
|
131
|
+
continue
|
|
132
|
+
# the tunnel's fd00::/8 end is not an address on the Wi-Fi network
|
|
133
|
+
if parsed.version == 6 and parsed in ipaddress.IPv6Network("fd00::/8"):
|
|
134
|
+
continue
|
|
135
|
+
key = "ipv4" if parsed.version == 4 else "ipv6"
|
|
136
|
+
if sockaddr[0] not in found[key]:
|
|
137
|
+
found[key].append(sockaddr[0])
|
|
138
|
+
return found
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def port_open(host, port, timeout=2.0, connect=None):
|
|
142
|
+
attempt = connect or socket.create_connection
|
|
143
|
+
try:
|
|
144
|
+
attempt((host, port), timeout).close()
|
|
145
|
+
return True
|
|
146
|
+
except OSError:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def primary_interface(reader=None):
|
|
151
|
+
text = (reader or read_output)([ROUTE, "-n", "get", "default"])
|
|
152
|
+
match = re.search(r"^\s*interface:\s*(\S+)\s*$", text or "", re.M)
|
|
153
|
+
return match.group(1) if match else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _field(text, key):
|
|
157
|
+
"""One ``key: value`` line of `route -n get`'s answer, or None."""
|
|
158
|
+
match = re.search(rf"^\s*{key}:\s*(\S+)\s*$", text or "", re.M)
|
|
159
|
+
return match.group(1) if match else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def tunnel_route(address, reader=None):
|
|
163
|
+
"""(interface, None) when this Mac reaches ``address``, the phone's end of its CoreDevice tunnel, through that
|
|
164
|
+
tunnel; else (the interface or None, why not). Whatever it can't tell apart from a VPN is refused: no route, a
|
|
165
|
+
route through Wi-Fi or Ethernet, only a default route, a utun macOS marks as a VPN, or a utun that holds no
|
|
166
|
+
address on a link to ``address``."""
|
|
167
|
+
read = reader or read_output
|
|
168
|
+
try:
|
|
169
|
+
target = ipaddress.IPv6Address(str(address).strip("[]"))
|
|
170
|
+
except ValueError:
|
|
171
|
+
return None, f"{address} isn't an IPv6 address"
|
|
172
|
+
shown = target.compressed
|
|
173
|
+
route = read([ROUTE, "-n", "get", "-inet6", shown])
|
|
174
|
+
name = _field(route, "interface")
|
|
175
|
+
if not (name and TUNNEL_INTERFACE.match(name)):
|
|
176
|
+
return name, f"this Mac reaches {shown} through {name or 'no interface'}, not a tunnel (utun)"
|
|
177
|
+
if _field(route, "destination") == "default":
|
|
178
|
+
return name, f"this Mac reaches {shown} only through {name}'s default route, which a VPN sets, not a tunnel"
|
|
179
|
+
facts = read([IFCONFIG, "-v", name])
|
|
180
|
+
if facts is None:
|
|
181
|
+
return name, f"this Mac couldn't read {name}'s addresses"
|
|
182
|
+
if VPN_MARK.search(facts):
|
|
183
|
+
return name, f"this Mac reaches {shown} through {name}, which is a VPN, not the tunnel"
|
|
184
|
+
if not link_end(facts, target):
|
|
185
|
+
held = ", ".join(f"{own}/{prefix}" + (f" to {peer}" if peer else "")
|
|
186
|
+
for own, peer, prefix in INET6.findall(facts)
|
|
187
|
+
if not own.lower().startswith("fe80:")) or "only link-local addresses"
|
|
188
|
+
return name, f"{name} holds no address on a link to {shown} ({held}), so it isn't the tunnel to the phone"
|
|
189
|
+
return name, None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def link_end(facts, target):
|
|
193
|
+
"""Whether an address `ifconfig` lists in ``facts`` is this Mac's end of a link to ``target``: one whose
|
|
194
|
+
point-to-point peer is ``target``, or whose network, /64 or narrower, holds it."""
|
|
195
|
+
for own, peer, prefix in INET6.findall(facts or ""):
|
|
196
|
+
try:
|
|
197
|
+
own, peer = ipaddress.IPv6Address(own), ipaddress.IPv6Address(peer) if peer else None
|
|
198
|
+
except ValueError:
|
|
199
|
+
continue
|
|
200
|
+
if own.is_link_local or own == target:
|
|
201
|
+
continue
|
|
202
|
+
if peer == target or (TUNNEL_PREFIX <= int(prefix) < 128
|
|
203
|
+
and target in ipaddress.IPv6Network(f"{own}/{prefix}", strict=False)):
|
|
204
|
+
return True
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def through_tunnel(address, reader=None):
|
|
209
|
+
return tunnel_route(address, reader)[1] is None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def parse_browse(text):
|
|
213
|
+
found, denied = [], POLICY_DENIED in (text or "")
|
|
214
|
+
for line in (text or "").splitlines():
|
|
215
|
+
parts = line.split(None, 6)
|
|
216
|
+
if len(parts) == 7 and parts[1] == "Add" and parts[3].isdigit():
|
|
217
|
+
found.append((int(parts[3]), parts[6].strip()))
|
|
218
|
+
return found, denied
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _browse_one(service, seconds, stop, sink):
|
|
222
|
+
# dns-sd buffers its output unless it writes to a terminal, so it gets a pseudo-terminal
|
|
223
|
+
import pty
|
|
224
|
+
try:
|
|
225
|
+
master, slave = pty.openpty()
|
|
226
|
+
except OSError:
|
|
227
|
+
return False
|
|
228
|
+
try:
|
|
229
|
+
process = subprocess.Popen([DNS_SD, "-B", service, "local."], stdin=subprocess.DEVNULL, stdout=slave,
|
|
230
|
+
stderr=slave, start_new_session=True)
|
|
231
|
+
except OSError:
|
|
232
|
+
os.close(master)
|
|
233
|
+
os.close(slave)
|
|
234
|
+
return False
|
|
235
|
+
os.close(slave)
|
|
236
|
+
deadline = time.monotonic() + seconds
|
|
237
|
+
try:
|
|
238
|
+
while time.monotonic() < deadline and not stop.is_set():
|
|
239
|
+
ready, _, _ = select.select([master], [], [], 0.1)
|
|
240
|
+
if not ready:
|
|
241
|
+
continue
|
|
242
|
+
try:
|
|
243
|
+
data = os.read(master, 4096)
|
|
244
|
+
except OSError:
|
|
245
|
+
break
|
|
246
|
+
if not data:
|
|
247
|
+
break
|
|
248
|
+
sink.append(data)
|
|
249
|
+
finally:
|
|
250
|
+
try:
|
|
251
|
+
os.killpg(process.pid, 9)
|
|
252
|
+
except (ProcessLookupError, PermissionError):
|
|
253
|
+
pass
|
|
254
|
+
try:
|
|
255
|
+
process.wait(timeout=2)
|
|
256
|
+
except subprocess.TimeoutExpired:
|
|
257
|
+
pass
|
|
258
|
+
os.close(master)
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def browse(interface_index, types=PEER_TYPES, seconds=BROWSE_SECONDS, settle=1.0):
|
|
263
|
+
if not os.access(DNS_SD, os.X_OK):
|
|
264
|
+
return {service: None for service in types}
|
|
265
|
+
stop = threading.Event()
|
|
266
|
+
sinks = {service: [] for service in types}
|
|
267
|
+
ran = {}
|
|
268
|
+
|
|
269
|
+
def run(service):
|
|
270
|
+
ran[service] = _browse_one(service, seconds, stop, sinks[service])
|
|
271
|
+
|
|
272
|
+
def outputs():
|
|
273
|
+
return {service: b"".join(list(sinks[service])).decode("utf-8", "replace") if ran.get(service, True)
|
|
274
|
+
else None for service in types}
|
|
275
|
+
|
|
276
|
+
workers = [threading.Thread(target=run, args=(service,), name="mobster-wifi-bonjour", daemon=True)
|
|
277
|
+
for service in types]
|
|
278
|
+
began = time.monotonic()
|
|
279
|
+
for worker in workers:
|
|
280
|
+
worker.start()
|
|
281
|
+
while any(worker.is_alive() for worker in workers) and time.monotonic() - began < seconds + 3:
|
|
282
|
+
time.sleep(0.1)
|
|
283
|
+
# after `settle`, this Mac's own services have shown on the loopback interface too
|
|
284
|
+
if time.monotonic() - began >= settle and peers_heard(outputs(), interface_index)[0]:
|
|
285
|
+
stop.set()
|
|
286
|
+
stop.set()
|
|
287
|
+
for worker in workers:
|
|
288
|
+
worker.join(3)
|
|
289
|
+
return outputs()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def peers_heard(outputs, interface_index):
|
|
293
|
+
own, others, denied, ran = set(), [], False, False
|
|
294
|
+
for text in outputs.values():
|
|
295
|
+
if text is None:
|
|
296
|
+
continue
|
|
297
|
+
ran = True
|
|
298
|
+
found, refused = parse_browse(text)
|
|
299
|
+
denied = denied or refused
|
|
300
|
+
# macOS also lists this Mac's own services on the loopback interface: they aren't other devices
|
|
301
|
+
own.update(name for index, name in found if index == LOOPBACK_INDEX)
|
|
302
|
+
others += [name for index, name in found if index == interface_index]
|
|
303
|
+
return sorted({name for name in others if name not in own}), denied, ran
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def peer_check(*, interface=None, gateway=None, allowed=None, outputs=None, index_of=None):
|
|
307
|
+
from .. import native_helpers
|
|
308
|
+
if outputs is None and not native_helpers.devices_allowed():
|
|
309
|
+
return {"state": "unknown", "interface": interface, "peers": 0}
|
|
310
|
+
interface = primary_interface() if interface is None else interface
|
|
311
|
+
if not interface:
|
|
312
|
+
return {"state": "offline", "interface": None, "peers": 0}
|
|
313
|
+
if VPN_INTERFACE.match(interface):
|
|
314
|
+
return {"state": "vpn", "interface": interface, "peers": 0}
|
|
315
|
+
if not LAN_INTERFACE.match(interface):
|
|
316
|
+
return {"state": "unknown", "interface": interface, "peers": 0}
|
|
317
|
+
gateway = default_gateway() if gateway is None else gateway
|
|
318
|
+
allowed = local_network_allowed(gateway) if allowed is None else allowed
|
|
319
|
+
if allowed is False:
|
|
320
|
+
return {"state": "no_permission", "interface": interface, "peers": 0}
|
|
321
|
+
try:
|
|
322
|
+
index = (index_of or socket.if_nametoindex)(interface)
|
|
323
|
+
except OSError:
|
|
324
|
+
return {"state": "unknown", "interface": interface, "peers": 0}
|
|
325
|
+
if outputs is None:
|
|
326
|
+
outputs = browse(index)
|
|
327
|
+
names, denied, ran = peers_heard(outputs, index)
|
|
328
|
+
if names:
|
|
329
|
+
return {"state": "ok", "interface": interface, "peers": len(names)}
|
|
330
|
+
if denied:
|
|
331
|
+
return {"state": "no_permission", "interface": interface, "peers": 0}
|
|
332
|
+
if not ran:
|
|
333
|
+
return {"state": "unknown", "interface": interface, "peers": 0}
|
|
334
|
+
return {"state": "blocked", "interface": interface, "peers": 0}
|