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,392 @@
|
|
|
1
|
+
"""Wire assertions in ``expect:``: what the app sent over the network and what it stored on disk.
|
|
2
|
+
|
|
3
|
+
``request`` and ``no_request`` are judged on the requests the run's proxy saw (netproxy.py); ``storage`` on the app's
|
|
4
|
+
container after the steps (wirestore.py). They run on a simulator, dark behind MOBSTER_EXPERIMENTAL_NETWORK=1. The
|
|
5
|
+
evidence in a result is the matched request line or the storage diff, with secrets masked and bodies cut short."""
|
|
6
|
+
|
|
7
|
+
import fnmatch
|
|
8
|
+
import gzip
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import zlib
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from .. import secret_filter
|
|
17
|
+
from . import netstubs, wirestore
|
|
18
|
+
from .assertions import AssertionResult, CheckError, Matcher, parse_matcher
|
|
19
|
+
|
|
20
|
+
FLAG = "MOBSTER_EXPERIMENTAL_NETWORK"
|
|
21
|
+
KINDS = ("request", "no_request", "storage")
|
|
22
|
+
REQUEST_KEYS = ("method", "host", "path", "status", "body", "response")
|
|
23
|
+
STORAGE_KEYS = ("file", "sqlite", "query", "changed", "contains", "equals", "at_least", "at_most")
|
|
24
|
+
STATUS = re.compile(r"[1-5]xx")
|
|
25
|
+
SHORTHAND = re.compile(r"(?:(?P<method>[A-Za-z]+)\s+)?(?P<host>[^/\s]+)?(?P<path>/\S*)?$")
|
|
26
|
+
SECRET_HEADERS = re.compile(r"authorization|cookie|token|secret|api[-_]?key|session|password|signature", re.I)
|
|
27
|
+
SHOWN_BODY = 200
|
|
28
|
+
SHOWN_SEEN = 4
|
|
29
|
+
OFF = (f"expect: request, no_request and storage are experimental. Set {FLAG}=1 to use them, or remove them from "
|
|
30
|
+
"the check.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def enabled(environ=None):
|
|
34
|
+
return (os.environ if environ is None else environ).get(FLAG) == "1"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_wire(item):
|
|
38
|
+
return isinstance(item, dict) and any(kind in item for kind in KINDS)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class WireAssertion:
|
|
43
|
+
kind: str
|
|
44
|
+
method: Optional[str] = None
|
|
45
|
+
host: Optional[str] = None
|
|
46
|
+
path: Optional[Matcher] = None
|
|
47
|
+
status: Any = None # a number, or "2xx"
|
|
48
|
+
body: Optional[Matcher] = None # of the request's body
|
|
49
|
+
response: Optional[Matcher] = None # of the response's body
|
|
50
|
+
file: Optional[str] = None
|
|
51
|
+
sqlite: Optional[str] = None
|
|
52
|
+
query: Optional[str] = None
|
|
53
|
+
changed: bool = True
|
|
54
|
+
contains: Optional[Matcher] = None
|
|
55
|
+
equals: Optional[int] = None
|
|
56
|
+
at_least: Optional[int] = None
|
|
57
|
+
at_most: Optional[int] = None
|
|
58
|
+
name: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def needs_open(self):
|
|
61
|
+
"""Whether the proxy must open the host's https to judge this: a path, status or body is inside it."""
|
|
62
|
+
return bool(self.host) and self.kind != "storage" and bool(self.path or self.status or self.body
|
|
63
|
+
or self.response)
|
|
64
|
+
|
|
65
|
+
def render(self):
|
|
66
|
+
if self.kind == "storage":
|
|
67
|
+
target = f"file {self.file}" if self.file else f"sqlite {self.sqlite}"
|
|
68
|
+
if self.file:
|
|
69
|
+
said = "changed" if self.changed else "unchanged"
|
|
70
|
+
return f"storage {target} {said}" + (f' containing "{self.contains.render()}"' if self.contains else "")
|
|
71
|
+
bound = (f"== {self.equals}" if self.equals is not None else
|
|
72
|
+
f">= {self.at_least}" if self.at_least is not None else f"<= {self.at_most}")
|
|
73
|
+
return f"storage {target} rows {bound}"
|
|
74
|
+
line = " ".join(part for part in (self.method, (self.host or "") + (self.path.render() if self.path else ""))
|
|
75
|
+
if part) or "any request"
|
|
76
|
+
extra = "".join((f" -> {self.status}" if self.status else "",
|
|
77
|
+
f" body {self.body.render()}" if self.body else "",
|
|
78
|
+
f" response {self.response.render()}" if self.response else ""))
|
|
79
|
+
return f"{self.kind} {line}{extra}"
|
|
80
|
+
|
|
81
|
+
def to_dict(self):
|
|
82
|
+
out = {}
|
|
83
|
+
if self.kind == "storage":
|
|
84
|
+
for key in STORAGE_KEYS:
|
|
85
|
+
value = getattr(self, key)
|
|
86
|
+
if key == "changed":
|
|
87
|
+
if self.file and not value:
|
|
88
|
+
out[key] = False
|
|
89
|
+
elif value is not None:
|
|
90
|
+
out[key] = value.raw if isinstance(value, Matcher) else value
|
|
91
|
+
else:
|
|
92
|
+
for key in REQUEST_KEYS:
|
|
93
|
+
value = getattr(self, key)
|
|
94
|
+
if value is not None:
|
|
95
|
+
out[key] = value.raw if isinstance(value, Matcher) else value
|
|
96
|
+
result = {self.kind: out}
|
|
97
|
+
if self.name:
|
|
98
|
+
result["name"] = self.name
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _check_keys(body, allowed, where):
|
|
103
|
+
for key in body:
|
|
104
|
+
if key not in allowed:
|
|
105
|
+
raise CheckError(f"{where}.{key} isn't a setting. Use {', '.join(allowed)}.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _shorthand(text, where):
|
|
109
|
+
found = SHORTHAND.match(text.strip())
|
|
110
|
+
if not text.strip() or found is None or not (found["host"] or found["path"]):
|
|
111
|
+
raise CheckError(f"{where} needs a method, host and path such as POST api.example.com/v1/cart, or a "
|
|
112
|
+
"mapping.")
|
|
113
|
+
out = {}
|
|
114
|
+
method, host = found["method"], found["host"]
|
|
115
|
+
if method:
|
|
116
|
+
out["method"] = method
|
|
117
|
+
if host:
|
|
118
|
+
out["host"] = host
|
|
119
|
+
if found["path"]:
|
|
120
|
+
out["path"] = found["path"]
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_one(obj, where):
|
|
125
|
+
kinds = [kind for kind in KINDS if kind in obj]
|
|
126
|
+
if len(kinds) != 1:
|
|
127
|
+
raise CheckError(f"{where} needs exactly one of {', '.join(KINDS)}; it has {', '.join(kinds)}")
|
|
128
|
+
kind = kinds[0]
|
|
129
|
+
_check_keys(obj, KINDS + ("name",), where)
|
|
130
|
+
body = obj[kind]
|
|
131
|
+
if isinstance(body, str) and kind != "storage":
|
|
132
|
+
body = _shorthand(body, f"{where}.{kind}")
|
|
133
|
+
if not isinstance(body, dict):
|
|
134
|
+
raise CheckError(f"{where}.{kind} must be a mapping such as "
|
|
135
|
+
+ ("{file: Library/Preferences/app.plist}." if kind == "storage"
|
|
136
|
+
else "{method: POST, host: api.example.com, path: /v1/cart}."))
|
|
137
|
+
name = obj.get("name")
|
|
138
|
+
if name is not None and (not isinstance(name, str) or not name.strip() or len(name) > 120):
|
|
139
|
+
raise CheckError(f"{where}.name must be text of 1 to 120 characters")
|
|
140
|
+
fields = {"kind": kind, "name": name.strip() if name else None}
|
|
141
|
+
here = f"{where}.{kind}"
|
|
142
|
+
if kind == "storage":
|
|
143
|
+
fields.update(_parse_storage(body, here))
|
|
144
|
+
else:
|
|
145
|
+
fields.update(_parse_request(body, here))
|
|
146
|
+
if kind == "no_request" and (fields.get("status") or fields.get("response")):
|
|
147
|
+
raise CheckError(f"{here} takes no status or response: a request that never left has none.")
|
|
148
|
+
return WireAssertion(**fields)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse_request(body, where):
|
|
152
|
+
_check_keys(body, REQUEST_KEYS, where)
|
|
153
|
+
out = {}
|
|
154
|
+
if "method" in body:
|
|
155
|
+
method = body["method"]
|
|
156
|
+
if not isinstance(method, str) or method.upper() not in netstubs.METHODS:
|
|
157
|
+
raise CheckError(f"{where}.method must be one of {', '.join(netstubs.METHODS)}.")
|
|
158
|
+
out["method"] = method.upper()
|
|
159
|
+
if "host" in body:
|
|
160
|
+
out["host"] = netstubs._host(body["host"], f"{where}.host")
|
|
161
|
+
if "path" in body:
|
|
162
|
+
path = body["path"]
|
|
163
|
+
if isinstance(path, str) and not path.startswith(("/", "*")):
|
|
164
|
+
raise CheckError(f"{where}.path must start with /, such as /v1/cart or /v1/*, or be a /regex/.")
|
|
165
|
+
out["path"] = parse_matcher(path, f"{where}.path")
|
|
166
|
+
if "status" in body:
|
|
167
|
+
status = body["status"]
|
|
168
|
+
if isinstance(status, str) and STATUS.fullmatch(status):
|
|
169
|
+
out["status"] = status
|
|
170
|
+
elif isinstance(status, int) and not isinstance(status, bool) and 100 <= status <= 599:
|
|
171
|
+
out["status"] = status
|
|
172
|
+
else:
|
|
173
|
+
raise CheckError(f"{where}.status must be a number from 100 to 599 or a class such as 2xx.")
|
|
174
|
+
for key in ("body", "response"):
|
|
175
|
+
if key in body:
|
|
176
|
+
out[key] = parse_matcher(body[key], f"{where}.{key}")
|
|
177
|
+
if not out:
|
|
178
|
+
raise CheckError(f"{where} needs at least a method, host or path.")
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _parse_storage(body, where):
|
|
183
|
+
_check_keys(body, STORAGE_KEYS, where)
|
|
184
|
+
if ("file" in body) == ("sqlite" in body):
|
|
185
|
+
raise CheckError(f"{where} needs one of file (a path or glob under the app's container) or sqlite.")
|
|
186
|
+
out = {}
|
|
187
|
+
for key in ("file", "sqlite"):
|
|
188
|
+
if key in body:
|
|
189
|
+
value = body[key]
|
|
190
|
+
if not isinstance(value, str) or not value.strip() or value.startswith("/") or ".." in value.split("/"):
|
|
191
|
+
raise CheckError(f"{where}.{key} must be a path inside the app's container, such as "
|
|
192
|
+
"Library/Preferences/app.plist, without a leading / or ..")
|
|
193
|
+
out[key] = value.strip()
|
|
194
|
+
if "file" in body:
|
|
195
|
+
if set(body) - {"file", "changed", "contains"}:
|
|
196
|
+
raise CheckError(f"{where}: a file takes changed and contains only.")
|
|
197
|
+
if not isinstance(body.get("changed", True), bool):
|
|
198
|
+
raise CheckError(f"{where}.changed must be true or false.")
|
|
199
|
+
out["changed"] = body.get("changed", True)
|
|
200
|
+
if "contains" in body:
|
|
201
|
+
out["contains"] = parse_matcher(body["contains"], f"{where}.contains")
|
|
202
|
+
else:
|
|
203
|
+
query = body.get("query")
|
|
204
|
+
if not isinstance(query, str) or not wirestore.read_only_query(query):
|
|
205
|
+
raise CheckError(f"{where}.query must be one SELECT or WITH statement, such as "
|
|
206
|
+
"SELECT 1 FROM cart WHERE sku = 'a1'.")
|
|
207
|
+
bounds = {key: body[key] for key in ("equals", "at_least", "at_most") if key in body}
|
|
208
|
+
if set(body) - {"sqlite", "query", *bounds} or len(bounds) != 1:
|
|
209
|
+
raise CheckError(f"{where}: a sqlite query needs exactly one of equals, at_least and at_most (rows).")
|
|
210
|
+
(key, number), = bounds.items()
|
|
211
|
+
if isinstance(number, bool) or not isinstance(number, int) or number < 0:
|
|
212
|
+
raise CheckError(f"{where}.{key} must be a whole number of at least 0.")
|
|
213
|
+
out.update(query=query.strip(), **{key: number})
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def parse(expect):
|
|
218
|
+
"""The wire assertions of a check file's ``expect`` list: () when it has none. Raises CheckError."""
|
|
219
|
+
found = [(index, item) for index, item in enumerate(expect or ()) if is_wire(item)]
|
|
220
|
+
if found and not enabled():
|
|
221
|
+
raise CheckError(OFF)
|
|
222
|
+
return tuple(_parse_one(item, f"expect[{index}]") for index, item in found)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def watch_hosts(assertions):
|
|
226
|
+
"""The hosts whose https the proxy must open, or None when no assertion reads the proxy."""
|
|
227
|
+
wire = [item for item in assertions if item.kind != "storage"]
|
|
228
|
+
return tuple(dict.fromkeys(item.host for item in wire if item.needs_open())) if wire else None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# evidence
|
|
232
|
+
|
|
233
|
+
def _mask_header(name, value):
|
|
234
|
+
return "••••••" if SECRET_HEADERS.search(name) else secret_filter.redact(value)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _text(raw, limit=SHOWN_BODY):
|
|
238
|
+
text = secret_filter.redact(raw.decode("utf-8", "replace").strip())
|
|
239
|
+
return text if len(text) <= limit else text[:limit] + f"… ({len(raw)} bytes)"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _response_body(raw):
|
|
243
|
+
"""A captured response's body as bytes: de-chunked and inflated where it says so, else what was kept."""
|
|
244
|
+
head, _, body = raw.partition(b"\r\n\r\n")
|
|
245
|
+
lowered = head.lower()
|
|
246
|
+
if b"transfer-encoding: chunked" in lowered:
|
|
247
|
+
out, rest = b"", body
|
|
248
|
+
while rest:
|
|
249
|
+
size_line, _, rest = rest.partition(b"\r\n")
|
|
250
|
+
try:
|
|
251
|
+
size = int(size_line.split(b";")[0].strip() or b"0", 16)
|
|
252
|
+
except ValueError:
|
|
253
|
+
break
|
|
254
|
+
if not size:
|
|
255
|
+
break
|
|
256
|
+
out, rest = out + rest[:size], rest[size + 2:]
|
|
257
|
+
body = out
|
|
258
|
+
try:
|
|
259
|
+
if b"content-encoding: gzip" in lowered:
|
|
260
|
+
body = gzip.decompress(body)
|
|
261
|
+
elif b"content-encoding: deflate" in lowered:
|
|
262
|
+
body = zlib.decompress(body)
|
|
263
|
+
except (OSError, EOFError, zlib.error):
|
|
264
|
+
pass
|
|
265
|
+
return body
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def request_line(item):
|
|
269
|
+
"""``POST api.example.com/v1/cart -> 200 at 4.2 s``: one request, safe to show."""
|
|
270
|
+
where = secret_filter.redact(f"{item['host']}{item.get('path', '')}")
|
|
271
|
+
status = f" -> {item['status']}" if item.get("status") else ""
|
|
272
|
+
return f"{item['method']} {where}{status} at {item['atMs'] / 1000:.1f} s"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def evidence(item):
|
|
276
|
+
"""The matched request for wire.json: its line, headers with secrets masked, and bodies cut short."""
|
|
277
|
+
return {"line": request_line(item),
|
|
278
|
+
"headers": [[name, _mask_header(name, value)] for name, value in item.get("headers", [])],
|
|
279
|
+
"body": _text(item["body"]) if item.get("body") else "",
|
|
280
|
+
"response": _text(_response_body(item["response"])) if item.get("response") else ""}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# judging
|
|
284
|
+
|
|
285
|
+
def _path_matches(matcher, path):
|
|
286
|
+
if matcher.is_regex:
|
|
287
|
+
return bool(matcher.pattern.search(path))
|
|
288
|
+
return fnmatch.fnmatchcase(path.split("?")[0], matcher.text) or fnmatch.fnmatchcase(path, matcher.text)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _status_matches(want, got):
|
|
292
|
+
if got is None:
|
|
293
|
+
return False
|
|
294
|
+
return got // 100 == int(want[0]) if isinstance(want, str) else got == want
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def request_matches(assertion, item):
|
|
298
|
+
if assertion.method and item["method"] != assertion.method:
|
|
299
|
+
return False
|
|
300
|
+
if assertion.host and not netstubs.host_matches(assertion.host, item["host"]):
|
|
301
|
+
return False
|
|
302
|
+
if assertion.path and not _path_matches(assertion.path, item.get("path", "")):
|
|
303
|
+
return False
|
|
304
|
+
if assertion.status and not _status_matches(assertion.status, item.get("status")):
|
|
305
|
+
return False
|
|
306
|
+
if assertion.body and not assertion.body.contains(item["body"].decode("utf-8", "replace")):
|
|
307
|
+
return False
|
|
308
|
+
return not assertion.response or assertion.response.contains(
|
|
309
|
+
_response_body(item["response"]).decode("utf-8", "replace"))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _opaque(rows):
|
|
313
|
+
"""Connections the proxy relayed or refused without opening: they name a host and nothing more."""
|
|
314
|
+
return [{"method": row["method"], "host": row["host"], "path": "", "atMs": row["atMs"], "headers": [],
|
|
315
|
+
"body": b"", "status": None, "response": b"", "opaque": True}
|
|
316
|
+
for row in rows if row.get("via") in ("tunnel", "refused")]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _judge_request(assertion, captures, rows):
|
|
320
|
+
text = assertion.render()
|
|
321
|
+
hidden = _opaque(rows)
|
|
322
|
+
if assertion.kind == "request":
|
|
323
|
+
hits = [item for item in captures if request_matches(assertion, item)]
|
|
324
|
+
if hits:
|
|
325
|
+
first = hits[0]
|
|
326
|
+
return AssertionResult(True, text, request_line(first) + (f" and {len(hits) - 1} more"
|
|
327
|
+
if len(hits) > 1 else ""), (evidence(first),))
|
|
328
|
+
seen = ", ".join(request_line(item) for item in captures[:SHOWN_SEEN])
|
|
329
|
+
tunnels = sorted({item["host"] for item in hidden})
|
|
330
|
+
more = (f"; https to {', '.join(tunnels[:SHOWN_SEEN])} wasn't opened, so name the host to see its paths"
|
|
331
|
+
if tunnels and not assertion.host else "")
|
|
332
|
+
if not captures and not hidden:
|
|
333
|
+
return AssertionResult(False, text, "no request from the app reached the proxy" + more)
|
|
334
|
+
return AssertionResult(False, text, f"no request matched; saw {seen or 'none opened'}{more}")
|
|
335
|
+
hits = [item for item in (*captures, *hidden) if request_matches(assertion, item)]
|
|
336
|
+
if hits:
|
|
337
|
+
return AssertionResult(False, text, "left the app: " + request_line(hits[0]), (evidence(hits[0]),))
|
|
338
|
+
if not captures and not hidden:
|
|
339
|
+
return AssertionResult(False, text, "no request from the app reached the proxy, so Mobster can't tell "
|
|
340
|
+
"whether this one left")
|
|
341
|
+
undecided = [item for item in hidden if assertion.path or assertion.body]
|
|
342
|
+
if undecided:
|
|
343
|
+
hosts = ", ".join(sorted({item["host"] for item in undecided})[:SHOWN_SEEN])
|
|
344
|
+
return AssertionResult(False, text, f"https to {hosts} wasn't opened, so its paths are unknown; name the "
|
|
345
|
+
"host in this assertion")
|
|
346
|
+
return AssertionResult(True, text, f"none of {len(captures) + len(hidden)} requests matched")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class Judge:
|
|
350
|
+
"""One run's wire assertions: ``baseline`` before the flow, ``judge`` after it. Never raises."""
|
|
351
|
+
|
|
352
|
+
def __init__(self, assertions, *, run, udid, bundle, container=wirestore.container):
|
|
353
|
+
self.assertions = tuple(assertions)
|
|
354
|
+
self._find = lambda: container(run, udid, bundle)
|
|
355
|
+
self._before = {}
|
|
356
|
+
self.evidence = []
|
|
357
|
+
|
|
358
|
+
def baseline(self):
|
|
359
|
+
try:
|
|
360
|
+
root = self._find()
|
|
361
|
+
except wirestore.StoreError:
|
|
362
|
+
return
|
|
363
|
+
for index, item in enumerate(self.assertions):
|
|
364
|
+
if item.kind == "storage":
|
|
365
|
+
self._before[index] = wirestore.take(root, item)
|
|
366
|
+
|
|
367
|
+
def judge(self, net):
|
|
368
|
+
captures, rows = net.seen() if net is not None else ([], [])
|
|
369
|
+
out = []
|
|
370
|
+
root, problem = None, None
|
|
371
|
+
if any(item.kind == "storage" for item in self.assertions):
|
|
372
|
+
try:
|
|
373
|
+
root = self._find()
|
|
374
|
+
except wirestore.StoreError as error:
|
|
375
|
+
problem = str(error)
|
|
376
|
+
for index, item in enumerate(self.assertions):
|
|
377
|
+
if item.kind != "storage":
|
|
378
|
+
out.append(_judge_request(item, captures, rows) if net is not None else AssertionResult(
|
|
379
|
+
False, item.render(), "the run had no proxy, so no request was seen"))
|
|
380
|
+
elif root is None:
|
|
381
|
+
out.append(AssertionResult(False, item.render(), problem))
|
|
382
|
+
else:
|
|
383
|
+
out.append(wirestore.judge(item, root, self._before.get(index)))
|
|
384
|
+
self.evidence = [{"assertion": item.render(), "ok": result.ok, "observed": result.observed,
|
|
385
|
+
"matches": [dict(m) for m in result.matches]}
|
|
386
|
+
for item, result in zip(self.assertions, out)]
|
|
387
|
+
return out
|
|
388
|
+
|
|
389
|
+
def write(self, directory):
|
|
390
|
+
if self.evidence:
|
|
391
|
+
with open(os.path.join(directory, "wire.json"), "w", encoding="utf-8") as handle:
|
|
392
|
+
json.dump(self.evidence, handle, indent=1, default=str)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""``expect: storage``: what the app wrote under its own container, read on the Mac after the steps.
|
|
2
|
+
|
|
3
|
+
A simulator's app container is a folder on this Mac, so nothing is pulled over a wire: ``take`` hashes the files an
|
|
4
|
+
assertion names before the flow, ``judge`` compares them after, and a sqlite query runs on a copy so the app's
|
|
5
|
+
database is never opened for writing. Evidence is a short diff or the first rows, with secrets masked."""
|
|
6
|
+
|
|
7
|
+
import difflib
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import plistlib
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import sqlite3
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .. import secret_filter
|
|
19
|
+
from .assertions import AssertionResult
|
|
20
|
+
|
|
21
|
+
FILES_LIMIT = 2000
|
|
22
|
+
HASH_LIMIT = 16 << 20
|
|
23
|
+
TEXT_LIMIT = 64 << 10
|
|
24
|
+
DIFF_LINES = 8
|
|
25
|
+
ROWS_SHOWN = 3
|
|
26
|
+
QUERY_SECONDS = 5
|
|
27
|
+
READ_ONLY = re.compile(r"\s*(select|with)\b", re.I)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StoreError(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_only_query(sql):
|
|
35
|
+
"""One SELECT or WITH statement (the authorizer in ``rows`` refuses writes whatever this accepts)."""
|
|
36
|
+
body = sql.strip().rstrip(";").strip()
|
|
37
|
+
return bool(READ_ONLY.match(body)) and ";" not in body and len(body) <= 2000
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def container(run, udid, bundle):
|
|
41
|
+
"""The app's data container on this Mac, from ``simctl get_app_container <udid> <bundle> data``."""
|
|
42
|
+
code, output = run(["get_app_container", udid, bundle, "data"])
|
|
43
|
+
path = Path(output.splitlines()[-1].strip()) if output.strip() else None
|
|
44
|
+
if code != 0 or path is None or not path.is_dir():
|
|
45
|
+
raise StoreError("Mobster couldn't find the app's container on the simulator"
|
|
46
|
+
+ (f" ({output.splitlines()[-1][:120]})" if output.strip() else "") + ".")
|
|
47
|
+
return path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _files(root, pattern):
|
|
51
|
+
"""Files under ``root`` matching a glob, as {relative path: (size, sha256, text or None)}."""
|
|
52
|
+
found = {}
|
|
53
|
+
for path in sorted(root.glob(pattern)):
|
|
54
|
+
if len(found) >= FILES_LIMIT:
|
|
55
|
+
break
|
|
56
|
+
if not path.is_file() or path.is_symlink():
|
|
57
|
+
continue
|
|
58
|
+
size = path.stat().st_size
|
|
59
|
+
digest = hashlib.sha256()
|
|
60
|
+
with path.open("rb") as handle:
|
|
61
|
+
kept = handle.read(HASH_LIMIT)
|
|
62
|
+
digest.update(kept)
|
|
63
|
+
found[path.relative_to(root).as_posix()] = (size, digest.hexdigest(),
|
|
64
|
+
_readable(path, kept) if size <= TEXT_LIMIT else None)
|
|
65
|
+
return found
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _readable(path, raw):
|
|
69
|
+
"""A file's content as text for a diff: a plist as JSON-like lines, text as is, anything else None."""
|
|
70
|
+
if raw[:6] == b"bplist" or raw[:5] == b"<?xml":
|
|
71
|
+
try:
|
|
72
|
+
return json.dumps(plistlib.loads(raw), indent=1, sort_keys=True, default=str)
|
|
73
|
+
except (plistlib.InvalidFileException, ValueError, OverflowError):
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
return raw.decode("utf-8")
|
|
77
|
+
except UnicodeDecodeError:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def take(root, assertion):
|
|
82
|
+
"""What the assertion looks at before the flow: its files, or its query's row count."""
|
|
83
|
+
if assertion.file:
|
|
84
|
+
return {"files": _files(root, assertion.file)}
|
|
85
|
+
try:
|
|
86
|
+
return {"rows": len(rows(root, assertion))}
|
|
87
|
+
except StoreError:
|
|
88
|
+
return {"rows": None}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def rows(root, assertion):
|
|
92
|
+
"""The query's rows from a copy of the database (and its -wal and -shm), never the file itself."""
|
|
93
|
+
source = root / assertion.sqlite
|
|
94
|
+
if not source.is_file():
|
|
95
|
+
raise StoreError(f"{assertion.sqlite} isn't in the app's container.")
|
|
96
|
+
with tempfile.TemporaryDirectory(prefix="mobster-wire-") as scratch:
|
|
97
|
+
copy = Path(scratch, "db.sqlite")
|
|
98
|
+
shutil.copyfile(source, copy)
|
|
99
|
+
for suffix in ("-wal", "-shm"):
|
|
100
|
+
if Path(f"{source}{suffix}").is_file():
|
|
101
|
+
shutil.copyfile(f"{source}{suffix}", f"{copy}{suffix}")
|
|
102
|
+
deadline = time.monotonic() + QUERY_SECONDS
|
|
103
|
+
connection = sqlite3.connect(str(copy))
|
|
104
|
+
try:
|
|
105
|
+
connection.execute("PRAGMA query_only = ON")
|
|
106
|
+
connection.set_authorizer(lambda action, *_: sqlite3.SQLITE_OK if action in (
|
|
107
|
+
sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ, sqlite3.SQLITE_FUNCTION, sqlite3.SQLITE_RECURSIVE)
|
|
108
|
+
else sqlite3.SQLITE_DENY)
|
|
109
|
+
connection.set_progress_handler(lambda: int(time.monotonic() > deadline), 10000)
|
|
110
|
+
return connection.execute(assertion.query).fetchall()
|
|
111
|
+
except sqlite3.Error as error:
|
|
112
|
+
raise StoreError(f"the query on {assertion.sqlite} failed: {str(error)[:120]}.") from None
|
|
113
|
+
finally:
|
|
114
|
+
connection.close()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _show(text, limit=160):
|
|
118
|
+
text = secret_filter.redact(str(text))
|
|
119
|
+
return text if len(text) <= limit else text[:limit] + "…"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _diff(before, after):
|
|
123
|
+
old = (before[2] or "").splitlines() if before else []
|
|
124
|
+
new = (after[2] or "").splitlines() if after else []
|
|
125
|
+
if (before and before[2] is None) or (after and after[2] is None):
|
|
126
|
+
return f"{before[0] if before else 0} -> {after[0] if after else 0} bytes (not text)"
|
|
127
|
+
lines = [line for line in difflib.unified_diff(old, new, lineterm="", n=0) if line[:1] in "+-" and line[:3] not in
|
|
128
|
+
("+++", "---")]
|
|
129
|
+
shown = "; ".join(_show(line, 80) for line in lines[:DIFF_LINES])
|
|
130
|
+
return shown + (f"; and {len(lines) - DIFF_LINES} more lines" if len(lines) > DIFF_LINES else "")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def judge(assertion, root, before):
|
|
134
|
+
text = assertion.render()
|
|
135
|
+
if assertion.sqlite:
|
|
136
|
+
return _judge_rows(assertion, text, root, before)
|
|
137
|
+
after = _files(root, assertion.file)
|
|
138
|
+
if before is None:
|
|
139
|
+
return AssertionResult(False, text, "Mobster took no baseline before the flow, so it can't tell what changed")
|
|
140
|
+
old = before["files"]
|
|
141
|
+
moved = [(name, old.get(name), after.get(name)) for name in sorted({*old, *after})
|
|
142
|
+
if (old.get(name) or (0, ""))[1] != (after.get(name) or (0, ""))[1]]
|
|
143
|
+
if assertion.contains:
|
|
144
|
+
holds = [name for name, new in sorted(after.items()) if new[2] and assertion.contains.contains(new[2])]
|
|
145
|
+
if not assertion.changed:
|
|
146
|
+
if moved:
|
|
147
|
+
name, was, now = moved[0]
|
|
148
|
+
return AssertionResult(False, text, f"{name} changed: {_diff(was, now)}")
|
|
149
|
+
return AssertionResult(True, text, f"{len(after)} matching files unchanged" if after else
|
|
150
|
+
"no file matches, so none changed")
|
|
151
|
+
if not moved:
|
|
152
|
+
return AssertionResult(False, text, f"none of {len(after)} matching files changed" if after
|
|
153
|
+
else f"no file in the container matches {assertion.file}")
|
|
154
|
+
name, was, now = moved[0]
|
|
155
|
+
kind = "added" if was is None else "deleted" if now is None else "changed"
|
|
156
|
+
if assertion.contains and not holds:
|
|
157
|
+
return AssertionResult(False, text, f"{name} {kind}, but no matching file contains "
|
|
158
|
+
f'"{assertion.contains.render()}"')
|
|
159
|
+
return AssertionResult(True, text, f"{name} {kind}: {_diff(was, now)}" + (f" and {len(moved) - 1} more files"
|
|
160
|
+
if len(moved) > 1 else ""),
|
|
161
|
+
({"file": name, "diff": _diff(was, now)},))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _judge_rows(assertion, text, root, before):
|
|
165
|
+
try:
|
|
166
|
+
found = rows(root, assertion)
|
|
167
|
+
except StoreError as error:
|
|
168
|
+
return AssertionResult(False, text, str(error))
|
|
169
|
+
count = len(found)
|
|
170
|
+
ok = (count == assertion.equals if assertion.equals is not None
|
|
171
|
+
else count >= assertion.at_least if assertion.at_least is not None else count <= assertion.at_most)
|
|
172
|
+
was = (before or {}).get("rows")
|
|
173
|
+
shown = "; ".join(_show(row) for row in found[:ROWS_SHOWN])
|
|
174
|
+
observed = f"{count} rows" + (f" (was {was})" if was is not None else "") + (f": {shown}" if shown else "")
|
|
175
|
+
shown_rows = [[_show(cell) for cell in row] for row in found[:ROWS_SHOWN]]
|
|
176
|
+
return AssertionResult(ok, text, observed, ({"rows": shown_rows},))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Who's waiting on me: the people you left on read, with drafts you approve (Mobster for Mac)."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Names for the people in Messages, from this Mac's own Contacts database, read-only."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
ROOT = Path.home() / "Library" / "Application Support" / "AddressBook"
|
|
9
|
+
MATCH_DIGITS = 10
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def database_paths(root=None):
|
|
13
|
+
base = Path(root) if root else ROOT
|
|
14
|
+
found = sorted(base.glob("Sources/*/AddressBook-v22.abcddb"))
|
|
15
|
+
top = base / "AddressBook-v22.abcddb"
|
|
16
|
+
return ([top] if top.is_file() else []) + found
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def digits(value):
|
|
20
|
+
return re.sub(r"\D", "", value or "")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def phone_key(value):
|
|
24
|
+
"""The last 10 digits of a phone number, or None when it has fewer than 7 digits."""
|
|
25
|
+
number = digits(value)
|
|
26
|
+
return number[-MATCH_DIGITS:] if len(number) >= 7 else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _name(first, last, nick, org):
|
|
30
|
+
full = " ".join(part.strip() for part in (first, last) if part and part.strip())
|
|
31
|
+
return full or (nick or "").strip() or (org or "").strip() or None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ContactBook:
|
|
35
|
+
"""Name lookup by handle. ``ContactBook.load(paths)``; an unreadable database is skipped."""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.by_phone, self.by_email = {}, {}
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def load(cls, paths=None):
|
|
42
|
+
book = cls()
|
|
43
|
+
for path in (database_paths() if paths is None else paths):
|
|
44
|
+
try:
|
|
45
|
+
book._read(path)
|
|
46
|
+
except sqlite3.Error:
|
|
47
|
+
continue
|
|
48
|
+
return book
|
|
49
|
+
|
|
50
|
+
def _read(self, path):
|
|
51
|
+
connection = sqlite3.connect(f"file:{quote(str(path))}?mode=ro", uri=True, timeout=3)
|
|
52
|
+
try:
|
|
53
|
+
connection.execute("PRAGMA query_only = ON")
|
|
54
|
+
names = {row[0]: _name(*row[1:]) for row in connection.execute(
|
|
55
|
+
"SELECT Z_PK, ZFIRSTNAME, ZLASTNAME, ZNICKNAME, ZORGANIZATION FROM ZABCDRECORD")}
|
|
56
|
+
for owner, number in connection.execute("SELECT ZOWNER, ZFULLNUMBER FROM ZABCDPHONENUMBER"):
|
|
57
|
+
key, name = phone_key(number), names.get(owner)
|
|
58
|
+
if key and name:
|
|
59
|
+
self.by_phone.setdefault(key, name)
|
|
60
|
+
for owner, address in connection.execute("SELECT ZOWNER, ZADDRESS FROM ZABCDEMAILADDRESS"):
|
|
61
|
+
name = names.get(owner)
|
|
62
|
+
if address and name:
|
|
63
|
+
self.by_email.setdefault(address.strip().lower(), name)
|
|
64
|
+
finally:
|
|
65
|
+
connection.close()
|
|
66
|
+
|
|
67
|
+
def name(self, handle):
|
|
68
|
+
"""The contact's name for a Messages handle, or None."""
|
|
69
|
+
handle = (handle or "").strip()
|
|
70
|
+
if "@" in handle:
|
|
71
|
+
return self.by_email.get(handle.lower())
|
|
72
|
+
key = phone_key(handle)
|
|
73
|
+
return self.by_phone.get(key) if key else None
|