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,309 @@
|
|
|
1
|
+
"""Set or remove one table of a TOML file, or one entry of a YAML mapping, as a text edit."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import tomllib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EditError(ValueError):
|
|
10
|
+
"""The file can't be edited safely. The message says why."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------------------------------- TOML
|
|
14
|
+
|
|
15
|
+
HEADER = re.compile(r"^\s*(\[\[?)\s*(.+?)\s*(\]\]?)\s*(?:#.*)?$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _toml_key_path(raw):
|
|
19
|
+
"""``a."b c".d`` as ["a", "b c", "d"]; None when it can't be read."""
|
|
20
|
+
parts, pos = [], 0
|
|
21
|
+
raw = raw.strip()
|
|
22
|
+
while pos < len(raw):
|
|
23
|
+
if raw[pos] == '"':
|
|
24
|
+
end = pos + 1
|
|
25
|
+
while end < len(raw) and raw[end] != '"':
|
|
26
|
+
end += 2 if raw[end] == "\\" else 1
|
|
27
|
+
try:
|
|
28
|
+
parts.append(json.loads(raw[pos:end + 1]))
|
|
29
|
+
except ValueError:
|
|
30
|
+
return None
|
|
31
|
+
pos = end + 1
|
|
32
|
+
elif raw[pos] == "'":
|
|
33
|
+
end = raw.find("'", pos + 1)
|
|
34
|
+
if end < 0:
|
|
35
|
+
return None
|
|
36
|
+
parts.append(raw[pos + 1:end])
|
|
37
|
+
pos = end + 1
|
|
38
|
+
else:
|
|
39
|
+
match = re.match(r"[A-Za-z0-9_-]+", raw[pos:])
|
|
40
|
+
if not match:
|
|
41
|
+
return None
|
|
42
|
+
parts.append(match.group())
|
|
43
|
+
pos += match.end()
|
|
44
|
+
rest = re.match(r"\s*(\.)?\s*", raw[pos:])
|
|
45
|
+
pos += rest.end()
|
|
46
|
+
if not rest.group(1) and pos < len(raw):
|
|
47
|
+
return None
|
|
48
|
+
return parts
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def toml_string(value):
|
|
52
|
+
"""A TOML basic string."""
|
|
53
|
+
return json.dumps(str(value), ensure_ascii=False).replace("\x7f", "\\u007F")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def toml_value(value):
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
return "true" if value else "false"
|
|
59
|
+
if isinstance(value, (int, float)):
|
|
60
|
+
return repr(value)
|
|
61
|
+
if isinstance(value, list):
|
|
62
|
+
return "[" + ", ".join(toml_value(item) for item in value) + "]"
|
|
63
|
+
if isinstance(value, dict):
|
|
64
|
+
return "{ " + ", ".join(f"{_toml_key(k)} = {toml_value(v)}" for k, v in value.items()) + " }"
|
|
65
|
+
return toml_string(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _toml_key(key):
|
|
69
|
+
return key if re.fullmatch(r"[A-Za-z0-9_-]+", key) else toml_string(key)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def toml_table(path, value):
|
|
73
|
+
"""``[a.b]`` with ``value``'s scalar and list keys, then a sub-table for each dict value."""
|
|
74
|
+
lines = ["[" + ".".join(_toml_key(part) for part in path) + "]"]
|
|
75
|
+
tables = []
|
|
76
|
+
for key, item in value.items():
|
|
77
|
+
if isinstance(item, dict):
|
|
78
|
+
tables.append((key, item))
|
|
79
|
+
else:
|
|
80
|
+
lines.append(f"{_toml_key(key)} = {toml_value(item)}")
|
|
81
|
+
for key, item in tables:
|
|
82
|
+
lines.append("")
|
|
83
|
+
lines.extend(toml_table([*path, key], item).splitlines())
|
|
84
|
+
return "\n".join(lines) + "\n"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def toml_loads(text):
|
|
88
|
+
try:
|
|
89
|
+
return tomllib.loads(text)
|
|
90
|
+
except tomllib.TOMLDecodeError as error:
|
|
91
|
+
raise EditError(f"it isn't valid TOML ({error})") from None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _dig(data, path):
|
|
95
|
+
for part in path:
|
|
96
|
+
if not isinstance(data, dict) or part not in data:
|
|
97
|
+
return None
|
|
98
|
+
data = data[part]
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _toml_cut(text, path):
|
|
103
|
+
"""``text`` without the table at ``path`` and its sub-tables; and the line index where the first one began."""
|
|
104
|
+
lines = text.splitlines(keepends=True)
|
|
105
|
+
kept, cut_at, cutting, in_string = [], None, False, None
|
|
106
|
+
for line in lines:
|
|
107
|
+
# A header inside a multi-line string isn't a header.
|
|
108
|
+
if in_string is None:
|
|
109
|
+
match = HEADER.match(line)
|
|
110
|
+
if match:
|
|
111
|
+
key = _toml_key_path(match.group(2))
|
|
112
|
+
cutting = key is not None and match.group(1) == "[" and key[:len(path)] == list(path)
|
|
113
|
+
if cutting and cut_at is None:
|
|
114
|
+
cut_at = len(kept)
|
|
115
|
+
for quote in ('"""', "'''"):
|
|
116
|
+
count = line.count(quote)
|
|
117
|
+
if count % 2 == 1 and in_string in (None, quote):
|
|
118
|
+
in_string = None if in_string == quote else quote
|
|
119
|
+
if not cutting:
|
|
120
|
+
kept.append(line)
|
|
121
|
+
if cut_at is not None:
|
|
122
|
+
# Drop the blank lines the cut table left behind it.
|
|
123
|
+
while cut_at > 0 and not kept[cut_at - 1].strip() and (cut_at >= len(kept) or not kept[cut_at].strip()):
|
|
124
|
+
kept.pop(cut_at - 1)
|
|
125
|
+
cut_at -= 1
|
|
126
|
+
return "".join(kept), cut_at
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _pruned(data, path):
|
|
130
|
+
"""``data`` without the tables along ``path`` that are left empty."""
|
|
131
|
+
data = copy.deepcopy(data)
|
|
132
|
+
for depth in range(len(path), 0, -1):
|
|
133
|
+
parent = _dig(data, path[:depth - 1])
|
|
134
|
+
if isinstance(parent, dict) and parent.get(path[depth - 1]) == {}:
|
|
135
|
+
del parent[path[depth - 1]]
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def toml_set_table(text, path, value):
|
|
140
|
+
"""``text`` with the table at ``path`` (such as ("mcp_servers", "mobster")) holding exactly ``value``."""
|
|
141
|
+
data = toml_loads(text)
|
|
142
|
+
if _dig(data, path) == value:
|
|
143
|
+
return text
|
|
144
|
+
expected = copy.deepcopy(data)
|
|
145
|
+
target = expected
|
|
146
|
+
for part in path[:-1]:
|
|
147
|
+
target = target.setdefault(part, {})
|
|
148
|
+
if not isinstance(target, dict):
|
|
149
|
+
raise EditError(f"{part} is not a table")
|
|
150
|
+
target[path[-1]] = copy.deepcopy(value)
|
|
151
|
+
cut, cut_at = _toml_cut(text, path)
|
|
152
|
+
if _dig(toml_loads(cut), path) is not None:
|
|
153
|
+
raise EditError(f"{'.'.join(path)} is written inline or with dotted keys; remove it by hand first")
|
|
154
|
+
table = toml_table(path, value)
|
|
155
|
+
if cut_at is None:
|
|
156
|
+
body = cut.rstrip("\n")
|
|
157
|
+
new_text = (body + "\n\n" if body else "") + table
|
|
158
|
+
else:
|
|
159
|
+
lines = cut.splitlines(keepends=True)
|
|
160
|
+
before, after = "".join(lines[:cut_at]), "".join(lines[cut_at:])
|
|
161
|
+
if before and not before.endswith("\n"):
|
|
162
|
+
before += "\n"
|
|
163
|
+
if before.strip() and not before.endswith("\n\n"):
|
|
164
|
+
before += "\n"
|
|
165
|
+
new_text = before + table + ("\n" + after if after.strip() else after)
|
|
166
|
+
if toml_loads(new_text) != expected:
|
|
167
|
+
raise EditError("the edit would change more than Mobster's entry")
|
|
168
|
+
return new_text
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def toml_remove_table(text, path):
|
|
172
|
+
"""``text`` without the table at ``path``; ``text`` itself when there is none."""
|
|
173
|
+
data = toml_loads(text)
|
|
174
|
+
if _dig(data, path) is None:
|
|
175
|
+
return text
|
|
176
|
+
expected = copy.deepcopy(data)
|
|
177
|
+
del _dig(expected, path[:-1])[path[-1]]
|
|
178
|
+
new_text, _ = _toml_cut(text, path)
|
|
179
|
+
new_text = re.sub(r"\n{3,}", "\n\n", new_text)
|
|
180
|
+
try:
|
|
181
|
+
ok = toml_loads(new_text) in (expected, _pruned(expected, path[:-1]))
|
|
182
|
+
except EditError:
|
|
183
|
+
ok = False
|
|
184
|
+
if not ok:
|
|
185
|
+
raise EditError(f"{'.'.join(path)} is written inline or with dotted keys; remove it by hand")
|
|
186
|
+
return new_text
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------------------------------- YAML
|
|
190
|
+
|
|
191
|
+
def yaml_loads(text):
|
|
192
|
+
import yaml
|
|
193
|
+
try:
|
|
194
|
+
data = yaml.safe_load(text) if text.strip() else {}
|
|
195
|
+
except yaml.YAMLError as error:
|
|
196
|
+
raise EditError(f"it isn't valid YAML ({str(error).splitlines()[0]})") from None
|
|
197
|
+
if data is None:
|
|
198
|
+
return {}
|
|
199
|
+
if not isinstance(data, dict):
|
|
200
|
+
raise EditError("it holds something other than a YAML mapping")
|
|
201
|
+
return data
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _indent(line):
|
|
205
|
+
return len(line) - len(line.lstrip(" "))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _content(line):
|
|
209
|
+
stripped = line.strip()
|
|
210
|
+
return bool(stripped) and not stripped.startswith("#")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _yaml_entry(key, value, indent):
|
|
214
|
+
import yaml
|
|
215
|
+
dumped = yaml.safe_dump({key: value}, sort_keys=False, default_flow_style=False, allow_unicode=True, width=10_000)
|
|
216
|
+
return "".join(" " * indent + line + "\n" for line in dumped.splitlines())
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _yaml_find(lines, section, key):
|
|
220
|
+
"""(section line, end of the section, the child indent, the key's start, the key's end) by line index."""
|
|
221
|
+
top = re.compile(rf"^{re.escape(section)}\s*:\s*(?:#.*)?$")
|
|
222
|
+
head = next((i for i, line in enumerate(lines) if top.match(line.rstrip("\n"))), None)
|
|
223
|
+
if head is None:
|
|
224
|
+
return None
|
|
225
|
+
end, child = len(lines), None
|
|
226
|
+
for i in range(head + 1, len(lines)):
|
|
227
|
+
if _content(lines[i]) and _indent(lines[i]) == 0:
|
|
228
|
+
end = i
|
|
229
|
+
break
|
|
230
|
+
if child is None and _content(lines[i]):
|
|
231
|
+
child = _indent(lines[i])
|
|
232
|
+
start = stop = None
|
|
233
|
+
if child:
|
|
234
|
+
pattern = re.compile(rf"^ {{{child}}}{re.escape(key)}\s*:\s*(?:#.*)?$")
|
|
235
|
+
for i in range(head + 1, end):
|
|
236
|
+
if start is None and pattern.match(lines[i].rstrip("\n")):
|
|
237
|
+
start = i
|
|
238
|
+
elif start is not None and _content(lines[i]) and _indent(lines[i]) <= child:
|
|
239
|
+
stop = i
|
|
240
|
+
break
|
|
241
|
+
if start is not None and stop is None:
|
|
242
|
+
stop = end
|
|
243
|
+
while stop > start + 1 and not _content(lines[stop - 1]):
|
|
244
|
+
stop -= 1
|
|
245
|
+
return head, end, child, start, stop
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def yaml_set_entry(text, section, key, value):
|
|
249
|
+
"""``text`` with ``section: {key: value}`` at the top level (Goose's ``extensions:``)."""
|
|
250
|
+
data = yaml_loads(text)
|
|
251
|
+
existing = data.get(section)
|
|
252
|
+
if existing is not None and not isinstance(existing, dict):
|
|
253
|
+
raise EditError(f"{section} is not a mapping")
|
|
254
|
+
if isinstance(existing, dict) and existing.get(key) == value:
|
|
255
|
+
return text
|
|
256
|
+
expected = copy.deepcopy(data)
|
|
257
|
+
expected.setdefault(section, None)
|
|
258
|
+
expected[section] = dict(expected[section] or {})
|
|
259
|
+
expected[section][key] = copy.deepcopy(value)
|
|
260
|
+
lines = text.splitlines(keepends=True)
|
|
261
|
+
if lines and not lines[-1].endswith("\n"):
|
|
262
|
+
lines[-1] += "\n"
|
|
263
|
+
found = _yaml_find(lines, section, key)
|
|
264
|
+
empty = re.compile(rf"^{re.escape(section)}\s*:\s*\{{\s*\}}\s*(?:#.*)?$")
|
|
265
|
+
if found is None and data.get(section) == {}:
|
|
266
|
+
lines = [f"{section}:\n" if empty.match(line.rstrip("\n")) else line for line in lines]
|
|
267
|
+
found = _yaml_find(lines, section, key)
|
|
268
|
+
if found is None:
|
|
269
|
+
if section in data:
|
|
270
|
+
raise EditError(f"{section} is written in a form this installer doesn't edit; edit it by hand")
|
|
271
|
+
body = "".join(lines)
|
|
272
|
+
new_text = (body + ("" if not body or body.endswith("\n") else "\n") + f"{section}:\n"
|
|
273
|
+
+ _yaml_entry(key, value, 2))
|
|
274
|
+
else:
|
|
275
|
+
head, end, child, start, stop = found
|
|
276
|
+
indent = child or 2
|
|
277
|
+
entry = _yaml_entry(key, value, indent)
|
|
278
|
+
if start is not None:
|
|
279
|
+
lines[start:stop] = [entry]
|
|
280
|
+
else:
|
|
281
|
+
at = end
|
|
282
|
+
while at > head + 1 and not _content(lines[at - 1]):
|
|
283
|
+
at -= 1
|
|
284
|
+
lines[at:at] = [entry]
|
|
285
|
+
new_text = "".join(lines)
|
|
286
|
+
if yaml_loads(new_text) != expected:
|
|
287
|
+
raise EditError("the edit would change more than Mobster's entry")
|
|
288
|
+
return new_text
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def yaml_remove_entry(text, section, key):
|
|
292
|
+
"""``text`` without ``key`` under ``section``; ``text`` itself when there is none."""
|
|
293
|
+
data = yaml_loads(text)
|
|
294
|
+
if not isinstance(data.get(section), dict) or key not in data[section]:
|
|
295
|
+
return text
|
|
296
|
+
expected = copy.deepcopy(data)
|
|
297
|
+
del expected[section][key]
|
|
298
|
+
lines = text.splitlines(keepends=True)
|
|
299
|
+
found = _yaml_find(lines, section, key)
|
|
300
|
+
if found is None or found[3] is None:
|
|
301
|
+
raise EditError(f"{section}.{key} is written in a form this installer doesn't edit; remove it by hand")
|
|
302
|
+
head, _end, _child, start, stop = found
|
|
303
|
+
del lines[start:stop]
|
|
304
|
+
if not expected[section]:
|
|
305
|
+
lines[head] = f"{section}: {{}}\n"
|
|
306
|
+
new_text = "".join(lines)
|
|
307
|
+
if yaml_loads(new_text) != expected:
|
|
308
|
+
raise EditError("the edit would change more than Mobster's entry")
|
|
309
|
+
return new_text
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Deterministic path first: before tapping, ask whether the goal has a link that gets there (dark).
|
|
2
|
+
|
|
3
|
+
``atlas`` reads what the apps on a phone declare (URL schemes, associated domains) as "discovered" links, a layer
|
|
4
|
+
under ``deeplinks.LINKS``. ``callback`` builds and reads x-callback-url returns. ``plan`` proposes a link for a goal.
|
|
5
|
+
Everything is off unless ``MOBSTER_EXPERIMENTAL_INTENTS=1``. ``deeplinks.decide`` still governs every link; a
|
|
6
|
+
discovered link is never "verified" (that word is for links opened on a real iPhone, with a date).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
ENV_FLAG = "MOBSTER_EXPERIMENTAL_INTENTS"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def enabled():
|
|
15
|
+
"""Whether the experimental deterministic-path layer is on (``MOBSTER_EXPERIMENTAL_INTENTS=1``)."""
|
|
16
|
+
return os.environ.get(ENV_FLAG, "").strip().casefold() in ("1", "true", "yes", "on")
|
|
17
|
+
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""The link atlas: the schemes and universal-link paths the apps on one phone declare.
|
|
2
|
+
|
|
3
|
+
Sources, none of which needs the network here:
|
|
4
|
+
- an app's Info.plist ``CFBundleURLTypes`` (schemes) and, when the bundle carries it, its entitlements'
|
|
5
|
+
``com.apple.developer.associated-domains`` (``applinks:`` domains; a simulator build keeps
|
|
6
|
+
``archived-expanded-entitlements.xcent``);
|
|
7
|
+
- an apple-app-site-association document (the caller fetched it, when the network is allowed): the paths each
|
|
8
|
+
domain sends to the app.
|
|
9
|
+
A simulator is read through ``xcrun simctl listapps``. Every entry is "discovered" until a run opens it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import fnmatch
|
|
13
|
+
import json
|
|
14
|
+
import plistlib
|
|
15
|
+
import re
|
|
16
|
+
import tempfile
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from urllib.parse import urlsplit
|
|
20
|
+
|
|
21
|
+
from ..deeplinks import BLOCKED_SCHEMES, SCHEME
|
|
22
|
+
|
|
23
|
+
SCHEME_NAME = re.compile(r"^[a-z][a-z0-9+.-]{0,40}$")
|
|
24
|
+
DOMAIN = re.compile(r"^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$")
|
|
25
|
+
ENTITLEMENTS = "archived-expanded-entitlements.xcent"
|
|
26
|
+
STATE = "discovered"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Entry:
|
|
31
|
+
bundle: str
|
|
32
|
+
name: str
|
|
33
|
+
schemes: list = field(default_factory=list)
|
|
34
|
+
domains: list = field(default_factory=list)
|
|
35
|
+
paths: dict = field(default_factory=dict) # domain -> [{"pattern": "/invite/*", "exclude": False}]
|
|
36
|
+
|
|
37
|
+
def public(self):
|
|
38
|
+
return {"bundleId": self.bundle, "app": self.name, "schemes": self.schemes, "domains": self.domains,
|
|
39
|
+
"state": STATE}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _schemes(info):
|
|
43
|
+
found = []
|
|
44
|
+
for kind in info.get("CFBundleURLTypes") or ():
|
|
45
|
+
for scheme in (kind.get("CFBundleURLSchemes") or ()) if isinstance(kind, dict) else ():
|
|
46
|
+
scheme = str(scheme).strip().casefold()
|
|
47
|
+
if SCHEME_NAME.match(scheme) and scheme not in BLOCKED_SCHEMES and scheme not in found:
|
|
48
|
+
found.append(scheme)
|
|
49
|
+
return found
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _domains(entitlements):
|
|
53
|
+
found = []
|
|
54
|
+
for item in (entitlements or {}).get("com.apple.developer.associated-domains") or ():
|
|
55
|
+
kind, _, host = str(item).partition(":")
|
|
56
|
+
host = host.split("?", 1)[0].strip().casefold()
|
|
57
|
+
if kind == "applinks" and DOMAIN.match(host) and not host.startswith("*") and host not in found:
|
|
58
|
+
found.append(host)
|
|
59
|
+
return found
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def entry_from_bundle(bundle, name, info, entitlements=None):
|
|
63
|
+
"""An ``Entry`` from an app's Info.plist (a dict) and its entitlements; None when it declares no link."""
|
|
64
|
+
entry = Entry(bundle, str(info.get("CFBundleDisplayName") or info.get("CFBundleName") or name or bundle),
|
|
65
|
+
_schemes(info), _domains(entitlements))
|
|
66
|
+
return entry if entry.schemes or entry.domains else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_app_folder(folder):
|
|
70
|
+
"""An ``Entry`` from an unpacked ``.app`` folder; None when it is unreadable or declares nothing."""
|
|
71
|
+
folder = Path(folder)
|
|
72
|
+
try:
|
|
73
|
+
info = plistlib.loads((folder / "Info.plist").read_bytes())
|
|
74
|
+
bundle = str(info["CFBundleIdentifier"])
|
|
75
|
+
except (OSError, ValueError, KeyError, plistlib.InvalidFileException):
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
entitlements = plistlib.loads((folder / ENTITLEMENTS).read_bytes())
|
|
79
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
80
|
+
entitlements = None
|
|
81
|
+
return entry_from_bundle(bundle, folder.stem, info, entitlements)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_aasa(document, bundle):
|
|
85
|
+
"""The path rules an apple-app-site-association document (text or dict) gives ``bundle``: a list of
|
|
86
|
+
{"pattern", "exclude"}. Old ``paths`` ("NOT /x" excludes) and ``components`` ("/" patterns) are both read."""
|
|
87
|
+
try:
|
|
88
|
+
data = json.loads(document) if isinstance(document, (str, bytes)) else document
|
|
89
|
+
details = data["applinks"]["details"]
|
|
90
|
+
except (ValueError, KeyError, TypeError):
|
|
91
|
+
return []
|
|
92
|
+
rules = []
|
|
93
|
+
for detail in details if isinstance(details, list) else ():
|
|
94
|
+
ids = list(detail.get("appIDs") or ()) + ([detail["appID"]] if detail.get("appID") else [])
|
|
95
|
+
if not any(str(i).partition(".")[2] == bundle for i in ids):
|
|
96
|
+
continue
|
|
97
|
+
for path in detail.get("paths") or ():
|
|
98
|
+
text = str(path)
|
|
99
|
+
rules.append({"pattern": text.removeprefix("NOT ").strip(), "exclude": text.startswith("NOT ")})
|
|
100
|
+
for part in detail.get("components") or ():
|
|
101
|
+
if isinstance(part, dict) and isinstance(part.get("/"), str):
|
|
102
|
+
rules.append({"pattern": part["/"], "exclude": bool(part.get("exclude"))})
|
|
103
|
+
return [r for r in rules if r["pattern"].startswith(("/", "*"))]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def path_allowed(rules, path):
|
|
107
|
+
"""Whether the first rule that matches ``path`` includes it (a rule list that never matches refuses)."""
|
|
108
|
+
for rule in rules:
|
|
109
|
+
if fnmatch.fnmatchcase(path, rule["pattern"]):
|
|
110
|
+
return not rule["exclude"]
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Atlas:
|
|
115
|
+
def __init__(self, entries=()):
|
|
116
|
+
self.entries = {entry.bundle: entry for entry in entries}
|
|
117
|
+
|
|
118
|
+
def add_aasa(self, bundle, domain, document):
|
|
119
|
+
entry = self.entries.get(bundle)
|
|
120
|
+
if entry is not None and domain in entry.domains:
|
|
121
|
+
entry.paths[domain] = parse_aasa(document, bundle)
|
|
122
|
+
|
|
123
|
+
def find(self, url, installed=None):
|
|
124
|
+
"""The ``Entry`` that declares ``url``'s scheme, or whose associated domain and paths send it to the app;
|
|
125
|
+
None otherwise. ``installed``: bundle ids to limit to (an app that left the phone claims nothing)."""
|
|
126
|
+
text = str(url or "").strip()
|
|
127
|
+
found = SCHEME.match(text)
|
|
128
|
+
if not found:
|
|
129
|
+
return None
|
|
130
|
+
scheme = found.group(1).casefold()
|
|
131
|
+
for entry in self.entries.values():
|
|
132
|
+
if installed is not None and entry.bundle not in installed:
|
|
133
|
+
continue
|
|
134
|
+
if scheme in ("http", "https"):
|
|
135
|
+
parts = urlsplit(text)
|
|
136
|
+
host = (parts.hostname or "").casefold()
|
|
137
|
+
if host in entry.domains and path_allowed(entry.paths.get(host, ()), parts.path or "/"):
|
|
138
|
+
return entry
|
|
139
|
+
elif scheme in entry.schemes:
|
|
140
|
+
return entry
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def by_app(self, words):
|
|
144
|
+
"""Entries whose app name or bundle id holds every word of ``words``."""
|
|
145
|
+
wants = [w for w in re.split(r"\W+", str(words or "").casefold()) if w]
|
|
146
|
+
return [e for e in self.entries.values()
|
|
147
|
+
if wants and all(w in f"{e.name} {e.bundle}".casefold() for w in wants)]
|
|
148
|
+
|
|
149
|
+
def named_in(self, goal):
|
|
150
|
+
"""Entries whose app name is a whole word run in ``goal`` ("open Things and add milk" finds Things)."""
|
|
151
|
+
said = " " + " ".join(re.split(r"\W+", str(goal or "").casefold())) + " "
|
|
152
|
+
return [e for e in self.entries.values() if f" {' '.join(re.split(r'\W+', e.name.casefold()))} " in said]
|
|
153
|
+
|
|
154
|
+
def to_json(self):
|
|
155
|
+
return json.dumps({"entries": [vars(e) for e in self.entries.values()]}, indent=1)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_json(cls, text):
|
|
159
|
+
try:
|
|
160
|
+
rows = json.loads(text)["entries"]
|
|
161
|
+
return cls(Entry(**row) for row in rows)
|
|
162
|
+
except (ValueError, KeyError, TypeError):
|
|
163
|
+
return cls()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def path_for(key):
|
|
167
|
+
from ..paths import dev_data_dir
|
|
168
|
+
return dev_data_dir() / "link-atlas" / (re.sub(r"[^A-Za-z0-9._-]", "_", key) + ".json")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load(key):
|
|
172
|
+
try:
|
|
173
|
+
return Atlas.from_json(path_for(key).read_text())
|
|
174
|
+
except OSError:
|
|
175
|
+
return Atlas()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def save(key, atlas):
|
|
179
|
+
from ..paths import write_text_atomic
|
|
180
|
+
write_text_atomic(path_for(key), atlas.to_json())
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def harvest_simulator(udid, run):
|
|
184
|
+
"""The atlas of a simulator's apps. ``run(argv, timeout)`` is ``sim.simctl.run``'s shape (a ``Result``).
|
|
185
|
+
Reads Info.plist and entitlements of each app ``simctl listapps`` names; an unreadable app is skipped."""
|
|
186
|
+
listed = run(["xcrun", "simctl", "listapps", udid], timeout=60)
|
|
187
|
+
if not listed.ok:
|
|
188
|
+
return Atlas()
|
|
189
|
+
with tempfile.TemporaryDirectory(prefix="mobster-atlas-") as folder:
|
|
190
|
+
source = Path(folder) / "apps.plist"
|
|
191
|
+
source.write_text(listed.out)
|
|
192
|
+
converted = run(["plutil", "-convert", "json", "-o", "-", str(source)], timeout=30)
|
|
193
|
+
try:
|
|
194
|
+
apps = json.loads(converted.out) if converted.ok else {}
|
|
195
|
+
except ValueError:
|
|
196
|
+
apps = {}
|
|
197
|
+
entries = []
|
|
198
|
+
for bundle, app in apps.items() if isinstance(apps, dict) else ():
|
|
199
|
+
where = str(app.get("Path") or "") if isinstance(app, dict) else ""
|
|
200
|
+
entry = read_app_folder(where) if where else None
|
|
201
|
+
if entry is None and isinstance(app, dict):
|
|
202
|
+
entry = entry_from_bundle(bundle, app.get("CFBundleDisplayName") or "", app, None)
|
|
203
|
+
if entry is not None and entry.bundle == bundle:
|
|
204
|
+
entries.append(entry)
|
|
205
|
+
return Atlas(entries)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def discovered(args):
|
|
209
|
+
"""The atlas entries (as dicts) for an ``open_url`` listing's ``app`` and ``query`` on its ``device``."""
|
|
210
|
+
words = f"{args.get('app') or ''} {args.get('query') or ''}"
|
|
211
|
+
return [e.public() for e in load(args.get("device") or "default").by_app(words)]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def describe(rows):
|
|
215
|
+
lines = [f"{r['app']}: {', '.join(s + '://' for s in r['schemes']) or 'no scheme'}" for r in rows]
|
|
216
|
+
return "Discovered in the apps' own files, not verified. open_url still asks as usual:\n" + "\n".join(lines)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""x-callback-url: ask an app to hand control back to Mobster's own scheme, so the answer needs no screen read.
|
|
2
|
+
|
|
3
|
+
``with_callback(url, token)`` adds ``x-success``, ``x-error`` and ``x-cancel`` to a link that is already allowed by
|
|
4
|
+
``deeplinks.decide``; ``parse(url)`` reads what came back. Receiving the callback needs the runner to own the
|
|
5
|
+
``mobster-callback`` scheme; that part is pending a proof on a simulator. Mobster never opens a callback link
|
|
6
|
+
itself: it only reads one the runner reports.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from urllib.parse import parse_qsl, quote, urlsplit
|
|
11
|
+
|
|
12
|
+
from ..deeplinks import LinkError, decide
|
|
13
|
+
|
|
14
|
+
SCHEME = "mobster-callback"
|
|
15
|
+
TOKEN = re.compile(r"^[A-Za-z0-9_-]{6,64}$")
|
|
16
|
+
STATUSES = ("success", "error", "cancel")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def with_callback(url, token):
|
|
20
|
+
"""``url`` with the three return addresses added. LinkError for a link Mobster never opens, a web page (it has no
|
|
21
|
+
callback) or a bad token. The link keeps its decision: ``decide(result)`` asks exactly when ``decide(url)`` did."""
|
|
22
|
+
if not TOKEN.match(str(token or "")):
|
|
23
|
+
raise LinkError("A callback token is 6 to 64 letters, digits, - or _.")
|
|
24
|
+
decision = decide(url)
|
|
25
|
+
scheme = decision.url.partition(":")[0].casefold()
|
|
26
|
+
if scheme in ("http", "https"):
|
|
27
|
+
raise LinkError("A web page can't return to Mobster. Use an app's link.")
|
|
28
|
+
base, _, query = decision.url.partition("?")
|
|
29
|
+
returns = "&".join(f"x-{status}={quote(f'{SCHEME}://{token}/{status}', safe='')}" for status in STATUSES)
|
|
30
|
+
return f"{base}?{query}&{returns}" if query else f"{base}?{returns}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse(url):
|
|
34
|
+
"""{"token", "status", "params"} for a callback address, or None when it isn't one."""
|
|
35
|
+
parts = urlsplit(str(url or "").strip())
|
|
36
|
+
status = parts.path.strip("/")
|
|
37
|
+
if parts.scheme.casefold() != SCHEME or status not in STATUSES or not TOKEN.match(parts.netloc):
|
|
38
|
+
return None
|
|
39
|
+
return {"token": parts.netloc, "status": status, "params": dict(parse_qsl(parts.query))}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def shortcut_run(name, token):
|
|
43
|
+
"""The x-callback link that runs a shortcut and returns. It acts, so it asks every time (``decide``)."""
|
|
44
|
+
return with_callback("shortcuts://x-callback-url/run-shortcut?name=" + quote(str(name).strip(), safe=""), token)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""`mobster intents harvest`: read a simulator's apps and their associated domains into the link atlas.
|
|
2
|
+
|
|
3
|
+
Hidden unless MOBSTER_EXPERIMENTAL_INTENTS=1. The AASA fetch is the only network use; `--no-network` skips it.
|
|
4
|
+
Exit codes: 0 harvested, 3 couldn't (no simulator, no Xcode, usage too).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from ..devkit import DevError, main, say, subcommands
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_arguments(parser, helpers):
|
|
14
|
+
add = subcommands(parser, "intents_command")
|
|
15
|
+
harvest = add("harvest", help="read a simulator's apps and domains into the link atlas",
|
|
16
|
+
description="Read each app's URL schemes and associated domains on a simulator through simctl, "
|
|
17
|
+
"fetch each domain's apple-app-site-association (https), and keep the atlas under "
|
|
18
|
+
"Mobster's data folder. Discovered links are never marked verified.")
|
|
19
|
+
harvest.add_argument("--sim", metavar="UDID", help="the simulator (default: the one booted simulator)")
|
|
20
|
+
harvest.add_argument("--force", action="store_true", help="read again although the atlas is under a day old")
|
|
21
|
+
harvest.add_argument("--no-network", action="store_true", help="skip the AASA fetch")
|
|
22
|
+
harvest.add_argument("--json", action="store_true", help="print the atlas as JSON")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(args):
|
|
26
|
+
return main("intents", {"harvest": _harvest}, args)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def booted(run):
|
|
30
|
+
"""The UDID of the one booted simulator; DevError otherwise."""
|
|
31
|
+
result = run(["xcrun", "simctl", "list", "devices", "booted", "-j"], timeout=30)
|
|
32
|
+
try:
|
|
33
|
+
devices = [d["udid"] for group in json.loads(result.out)["devices"].values() for d in group
|
|
34
|
+
if d.get("state") == "Booted"] if result.ok else []
|
|
35
|
+
except (ValueError, KeyError, TypeError):
|
|
36
|
+
devices = []
|
|
37
|
+
if len(devices) != 1:
|
|
38
|
+
raise DevError("Mobster needs one booted simulator to read.", "Pass --sim UDID, or boot just one.")
|
|
39
|
+
return devices[0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _harvest(args):
|
|
43
|
+
from ..sim.simctl import run as simctl_run
|
|
44
|
+
from . import harvest
|
|
45
|
+
udid = args.sim or booted(simctl_run)
|
|
46
|
+
atlas = harvest.harvest(udid, simctl_run, fetch=None if args.no_network else harvest.fetch_aasa,
|
|
47
|
+
log=lambda line: say(line, sys.stderr), force=args.force)
|
|
48
|
+
if args.json:
|
|
49
|
+
say(atlas.to_json())
|
|
50
|
+
else:
|
|
51
|
+
for entry in atlas.entries.values():
|
|
52
|
+
say(f"{entry.name}: {', '.join(s + '://' for s in entry.schemes) or '-'}"
|
|
53
|
+
f" {', '.join(entry.domains) or '-'} discovered")
|
|
54
|
+
say(f"{len(atlas.entries)} apps declare links.")
|
|
55
|
+
return 0
|