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,204 @@
|
|
|
1
|
+
"""The diagnostics bundle a feedback report may carry (docs/feedback.mdx), built here so the Mac app can show it, word
|
|
2
|
+
for word, before anything is sent."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
from collections import deque
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .. import harness_api, secret_filter
|
|
11
|
+
|
|
12
|
+
EVENTS = 150
|
|
13
|
+
LOG_LINES = 200
|
|
14
|
+
STRING_MAX = 400
|
|
15
|
+
MAX_BYTES = 500_000
|
|
16
|
+
MASK = "[masked]"
|
|
17
|
+
# Event fields that are pictures or trees, never sent.
|
|
18
|
+
DROP_KEYS = {"screenshot", "image", "frame", "frames", "png", "jpeg", "thumbnail", "elements", "tree", "source", "xml",
|
|
19
|
+
"accessibility", "dataUrl", "data_url", "audio"}
|
|
20
|
+
|
|
21
|
+
_SCRUB = (
|
|
22
|
+
(re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)"), MASK),
|
|
23
|
+
(
|
|
24
|
+
re.compile(
|
|
25
|
+
r"https?://[^\s\"'<>]*(?:/webhooks?/|[?&](?:token|sig|signature|key|secret|code|access_token)=)[^\s\"'<>]*",
|
|
26
|
+
re.I), MASK),
|
|
27
|
+
(re.compile(r"\b(?:access|refresh|session|api|auth)[_-]?(?:token|key|secret)\b(\s*[:=]\s*)[\"']?[^\s,;\"']{6,}",
|
|
28
|
+
re.I), None),
|
|
29
|
+
(re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "<email>"),
|
|
30
|
+
(re.compile(r"\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{16}\b|\b[0-9a-fA-F]{40}\b"), "<UDID>"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scrub(text, emails=True):
|
|
35
|
+
"""``text`` masked: secret_filter.redact, then what a log holds besides: private key blocks, webhook and signed
|
|
36
|
+
URLs, email addresses (``emails`` False keeps them), device UDIDs and the home folder's name."""
|
|
37
|
+
if not isinstance(text, str) or not text:
|
|
38
|
+
return text
|
|
39
|
+
out = secret_filter.redact(text)
|
|
40
|
+
for pattern, to in _SCRUB:
|
|
41
|
+
if not emails and to == "<email>":
|
|
42
|
+
continue
|
|
43
|
+
out = pattern.sub(lambda m: m.group(0)[:m.start(1) - m.start(0)] + m.group(1) + MASK, out) if to is None \
|
|
44
|
+
else pattern.sub(to, out)
|
|
45
|
+
home = str(Path.home())
|
|
46
|
+
if len(home) > 1:
|
|
47
|
+
out = out.replace(home, "~")
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _clean(value, depth=0):
|
|
52
|
+
"""A JSON value with every string scrubbed and cut, pictures and trees dropped, and depth bounded."""
|
|
53
|
+
if isinstance(value, str):
|
|
54
|
+
if value.startswith("data:") or len(value) > 2000 and re.fullmatch(r"[A-Za-z0-9+/=\s]+", value[:2000]):
|
|
55
|
+
return "[omitted]"
|
|
56
|
+
text = scrub(value)
|
|
57
|
+
return text if len(text) <= STRING_MAX else text[:STRING_MAX] + "…"
|
|
58
|
+
if isinstance(value, bool) or value is None or isinstance(value, (int, float)):
|
|
59
|
+
return value
|
|
60
|
+
if depth >= 5:
|
|
61
|
+
return "[omitted]"
|
|
62
|
+
if isinstance(value, dict):
|
|
63
|
+
return {scrub(str(k))[:80]: _clean(v, depth + 1) for k, v in list(value.items())[:60]
|
|
64
|
+
if str(k) not in DROP_KEYS}
|
|
65
|
+
if isinstance(value, (list, tuple)):
|
|
66
|
+
return [_clean(v, depth + 1) for v in list(value)[:60]]
|
|
67
|
+
return scrub(str(value))[:STRING_MAX]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def data_dir(runtime):
|
|
71
|
+
"""The app's data folder when this agent was given one (the Mac app passes --data-dir), else None: a test or a CLI
|
|
72
|
+
agent never reads the real folder."""
|
|
73
|
+
config = getattr(runtime, "config", None)
|
|
74
|
+
value = getattr(config, "data_dir", None)
|
|
75
|
+
return Path(value) if value else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def connection_of(device):
|
|
79
|
+
if not isinstance(device, dict):
|
|
80
|
+
return None
|
|
81
|
+
if device.get("transport") == "wifi":
|
|
82
|
+
return "Wi-Fi"
|
|
83
|
+
return {"usb": "USB", "simulator": "Simulator", "wda": "WebDriverAgent address"}.get(device.get("kind"),
|
|
84
|
+
device.get("kind"))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def phone_of(runtime, run=None):
|
|
88
|
+
"""{model, ios, connection} of the run's phone, else the default phone, else None."""
|
|
89
|
+
fleet = getattr(runtime, "fleet", None)
|
|
90
|
+
try:
|
|
91
|
+
devices = fleet.public() if fleet is not None else []
|
|
92
|
+
except Exception: # noqa: BLE001 -- a fleet that can't answer leaves the phone out
|
|
93
|
+
devices = []
|
|
94
|
+
wanted = (run or {}).get("device")
|
|
95
|
+
device = next((d for d in devices if wanted and d.get("id") == wanted), None) \
|
|
96
|
+
or next((d for d in devices if d.get("default")), None) or (devices[0] if devices else None)
|
|
97
|
+
if not device:
|
|
98
|
+
return None
|
|
99
|
+
return {"model": device.get("modelName") or device.get("model"), "ios": device.get("ios"),
|
|
100
|
+
"connection": connection_of(device), "state": device.get("state")}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def macos():
|
|
104
|
+
version = platform.mac_ver()[0]
|
|
105
|
+
return f"macOS {version}" if version else platform.system() or None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def last_run(runtime, run_id=None):
|
|
109
|
+
"""The run's public dict (with events): ``run_id``'s, else the newest. A run that stays on this Mac
|
|
110
|
+
(harness_api.stays_on_mac: Network X-ray's report) is never in a bundle."""
|
|
111
|
+
lock = getattr(runtime, "lock", None)
|
|
112
|
+
runs = getattr(runtime, "runs", None) or {}
|
|
113
|
+
if lock is not None:
|
|
114
|
+
with lock:
|
|
115
|
+
items = dict(runs)
|
|
116
|
+
else:
|
|
117
|
+
items = dict(runs)
|
|
118
|
+
items = {key: run for key, run in items.items() if not harness_api.stays_on_mac(getattr(run, "extras", None))}
|
|
119
|
+
run = items.get(run_id) if run_id else max(items.values(), key=lambda r: getattr(r, "created_at", 0), default=None)
|
|
120
|
+
if run is None:
|
|
121
|
+
return None
|
|
122
|
+
return run.public(include_events=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def log_lines(directory, count=LOG_LINES):
|
|
126
|
+
"""The last ``count`` lines of agent.log, scrubbed; [] when there's no log."""
|
|
127
|
+
if directory is None:
|
|
128
|
+
return []
|
|
129
|
+
path = directory / "logs" / "agent.log"
|
|
130
|
+
try:
|
|
131
|
+
size = path.stat().st_size
|
|
132
|
+
with path.open("rb") as handle:
|
|
133
|
+
handle.seek(max(0, size - 400_000))
|
|
134
|
+
tail = deque(handle.read().decode("utf-8", "replace").splitlines(), maxlen=count)
|
|
135
|
+
except OSError:
|
|
136
|
+
return []
|
|
137
|
+
return [_clean(line) for line in tail if line.strip()]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build(runtime, run_id=None, app_version=None):
|
|
141
|
+
"""The bundle: app, phone, run and log, each masked, with no screenshot, frame or element tree. {text, bytes,
|
|
142
|
+
summary}."""
|
|
143
|
+
run = last_run(runtime, run_id)
|
|
144
|
+
if run_id and run is None:
|
|
145
|
+
from ..api_errors import APIError
|
|
146
|
+
raise APIError("That task isn't on this Mac anymore.", 404, "not_found")
|
|
147
|
+
bundle = {
|
|
148
|
+
"app": {"version": _clean(app_version) if app_version else None, "agent": _agent_version(),
|
|
149
|
+
"macos": macos(), "arch": platform.machine() or None},
|
|
150
|
+
"phone": phone_of(runtime, run),
|
|
151
|
+
"run": None,
|
|
152
|
+
"log": log_lines(data_dir(runtime)),
|
|
153
|
+
}
|
|
154
|
+
if run is not None:
|
|
155
|
+
events = list(run.get("events") or [])[-EVENTS:]
|
|
156
|
+
bundle["run"] = {"id": run.get("id"), "status": run.get("status"), "goal": _clean(run.get("goal")),
|
|
157
|
+
"app": _clean(run.get("appName")), "engine": run.get("engine"),
|
|
158
|
+
"createdAt": run.get("createdAt"), "finishedAt": run.get("finishedAt"),
|
|
159
|
+
"eventCount": run.get("eventCount"), "events": [_clean(e) for e in events]}
|
|
160
|
+
text = json.dumps(bundle, indent=2, ensure_ascii=False)
|
|
161
|
+
# Over the limit: drop log lines, then events, from the oldest, until it fits.
|
|
162
|
+
while len(text.encode("utf-8")) > MAX_BYTES and (bundle["log"] or (bundle["run"] and bundle["run"]["events"])):
|
|
163
|
+
if bundle["log"]:
|
|
164
|
+
del bundle["log"][: max(1, len(bundle["log"]) // 4)]
|
|
165
|
+
else:
|
|
166
|
+
del bundle["run"]["events"][: max(1, len(bundle["run"]["events"]) // 4)]
|
|
167
|
+
text = json.dumps(bundle, indent=2, ensure_ascii=False)
|
|
168
|
+
return {"text": text, "bytes": len(text.encode("utf-8")),
|
|
169
|
+
"summary": {"events": len(bundle["run"]["events"]) if bundle["run"] else 0, "logLines": len(bundle["log"]),
|
|
170
|
+
"runId": bundle["run"]["id"] if bundle["run"] else None, "phone": bundle["phone"],
|
|
171
|
+
"macos": bundle["app"]["macos"]}}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _agent_version():
|
|
175
|
+
from .. import __version__
|
|
176
|
+
return __version__
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def check(text):
|
|
180
|
+
"""The bundle text a report sends: the one the app showed, scrubbed again (idempotent), at most MAX_BYTES,
|
|
181
|
+
and a JSON object."""
|
|
182
|
+
if not isinstance(text, str) or not text:
|
|
183
|
+
raise ValueError("The diagnostics are the bundle Mobster showed")
|
|
184
|
+
try:
|
|
185
|
+
value = json.loads(text)
|
|
186
|
+
except ValueError:
|
|
187
|
+
raise ValueError("The diagnostics aren't the bundle Mobster showed") from None
|
|
188
|
+
if not isinstance(value, dict):
|
|
189
|
+
raise ValueError("The diagnostics aren't the bundle Mobster showed")
|
|
190
|
+
cleaned = json.dumps(_scrub_all(value), indent=2, ensure_ascii=False)
|
|
191
|
+
if len(cleaned.encode("utf-8")) > MAX_BYTES:
|
|
192
|
+
raise ValueError("The diagnostics are over 500 KB")
|
|
193
|
+
return cleaned
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _scrub_all(value):
|
|
197
|
+
"""Every string in a JSON value scrubbed again; nothing cut or dropped (the app showed exactly this)."""
|
|
198
|
+
if isinstance(value, str):
|
|
199
|
+
return scrub(value)
|
|
200
|
+
if isinstance(value, dict):
|
|
201
|
+
return {scrub(str(k)): _scrub_all(v) for k, v in value.items() if str(k) not in DROP_KEYS}
|
|
202
|
+
if isinstance(value, list):
|
|
203
|
+
return [_scrub_all(v) for v in value]
|
|
204
|
+
return value
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Sending a feedback report to the account service, and keeping it here when it can't go yet (docs/feedback.mdx)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..api_errors import APIError
|
|
11
|
+
|
|
12
|
+
TIMEOUT = 30.0
|
|
13
|
+
MAX_ITEMS = 20
|
|
14
|
+
MAX_REPORT_BYTES = 4_400_000
|
|
15
|
+
DRAFT_ID = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
|
|
16
|
+
QUEUED_CODES = {"unreachable", "untrusted_server", "busy", "not_configured", "rate_limited", "error"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SendError(Exception):
|
|
20
|
+
"""The service refused or couldn't be reached."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, code, status, message):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.code, self.status = code, status
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _post(path, token, body, timeout=TIMEOUT):
|
|
28
|
+
from ..share import ShareError, call
|
|
29
|
+
try:
|
|
30
|
+
status, value = call("POST", path, token, body, timeout, limit=64_000)
|
|
31
|
+
except ShareError as error:
|
|
32
|
+
if error.code == "untrusted_server":
|
|
33
|
+
raise SendError("untrusted_server", 503, "Mobster couldn't verify its server, so it didn't send.") from None
|
|
34
|
+
raise SendError("unreachable", 503, "Mobster couldn't reach its server.") from None
|
|
35
|
+
if 200 <= status < 300:
|
|
36
|
+
return value
|
|
37
|
+
message = value.get("message") if isinstance(value.get("message"), str) else None
|
|
38
|
+
code = value.get("error") if isinstance(value.get("error"), str) else "error"
|
|
39
|
+
if status == 401:
|
|
40
|
+
raise SendError("session", 401, "Sign in to Mobster again to send this.")
|
|
41
|
+
if status == 429:
|
|
42
|
+
raise SendError("rate_limited", 429, message or "Mobster's server is busy. It sends this later.")
|
|
43
|
+
if status == 413:
|
|
44
|
+
raise SendError("too_large", 413, message or "That report is too large. Leave out the screenshot.")
|
|
45
|
+
if status in (400, 415):
|
|
46
|
+
raise SendError("invalid", 400, message or "Mobster's server refused that report.")
|
|
47
|
+
# A server without the route yet (404) or not set up for it (503 not_configured): it waits and goes later.
|
|
48
|
+
if status == 404 or status == 503 and code == "not_configured":
|
|
49
|
+
raise SendError("not_configured", 503, "Mobster's server isn't taking reports yet. It sends this later.")
|
|
50
|
+
raise SendError("busy" if status in (502, 503, 504) else "error", 503,
|
|
51
|
+
message or "Mobster's server couldn't take the report. It sends this later.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def submit(report, token):
|
|
55
|
+
"""Posts one report: {reference, kind, createdAt, forwarded}."""
|
|
56
|
+
body = json.dumps(report, ensure_ascii=False).encode("utf-8")
|
|
57
|
+
if len(body) > MAX_REPORT_BYTES:
|
|
58
|
+
raise SendError("too_large", 413, "That report is over 4.4 MB. Leave out the screenshot or crop it.")
|
|
59
|
+
value = _post("/api/feedback", token, body)
|
|
60
|
+
feedback = value.get("feedback")
|
|
61
|
+
if not isinstance(feedback, dict) or not isinstance(feedback.get("reference"), str):
|
|
62
|
+
raise SendError("error", 502, "Mobster's server answered without a reference. It sends this again later.")
|
|
63
|
+
return feedback
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Outbox:
|
|
67
|
+
"""Reports waiting to be sent: a folder of JSON files, or memory."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, directory=None):
|
|
70
|
+
self.directory = Path(directory) if directory else None
|
|
71
|
+
self.memory = {}
|
|
72
|
+
self.lock = threading.Lock()
|
|
73
|
+
|
|
74
|
+
def _path(self, draft_id):
|
|
75
|
+
return self.directory / f"{draft_id}.json"
|
|
76
|
+
|
|
77
|
+
def _write(self, item):
|
|
78
|
+
if self.directory is None:
|
|
79
|
+
self.memory[item["draftId"]] = item
|
|
80
|
+
return
|
|
81
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
os.chmod(self.directory, 0o700)
|
|
83
|
+
path = self._path(item["draftId"])
|
|
84
|
+
tmp = path.with_suffix(".tmp")
|
|
85
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
86
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
87
|
+
json.dump(item, handle, ensure_ascii=False)
|
|
88
|
+
os.replace(tmp, path)
|
|
89
|
+
|
|
90
|
+
def items(self):
|
|
91
|
+
"""Every waiting report, oldest first."""
|
|
92
|
+
if self.directory is None:
|
|
93
|
+
found = list(self.memory.values())
|
|
94
|
+
else:
|
|
95
|
+
found = []
|
|
96
|
+
for path in sorted(self.directory.glob("*.json")) if self.directory.is_dir() else []:
|
|
97
|
+
try:
|
|
98
|
+
item = json.loads(path.read_text("utf-8"))
|
|
99
|
+
except (OSError, ValueError):
|
|
100
|
+
continue
|
|
101
|
+
if isinstance(item, dict) and DRAFT_ID.fullmatch(str(item.get("draftId", ""))):
|
|
102
|
+
found.append(item)
|
|
103
|
+
return sorted(found, key=lambda item: item.get("queuedAt", 0))
|
|
104
|
+
|
|
105
|
+
def put(self, report, error):
|
|
106
|
+
with self.lock:
|
|
107
|
+
waiting = self.items()
|
|
108
|
+
if len(waiting) >= MAX_ITEMS and not any(item["draftId"] == report["clientId"] for item in waiting):
|
|
109
|
+
raise APIError(f"{MAX_ITEMS} reports are already waiting to send. Delete one, or send them first.",
|
|
110
|
+
409, "outbox_full")
|
|
111
|
+
self._write({"draftId": report["clientId"], "queuedAt": time.time(), "attempts": 1, "lastError": error,
|
|
112
|
+
"report": report})
|
|
113
|
+
|
|
114
|
+
def touch(self, item, error):
|
|
115
|
+
with self.lock:
|
|
116
|
+
self._write({**item, "attempts": int(item.get("attempts", 0)) + 1, "lastError": error})
|
|
117
|
+
|
|
118
|
+
def remove(self, draft_id):
|
|
119
|
+
with self.lock:
|
|
120
|
+
if self.directory is None:
|
|
121
|
+
return self.memory.pop(draft_id, None) is not None
|
|
122
|
+
try:
|
|
123
|
+
self._path(draft_id).unlink()
|
|
124
|
+
return True
|
|
125
|
+
except FileNotFoundError:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def public(self):
|
|
129
|
+
"""What the app lists: no attachments, no description."""
|
|
130
|
+
return [{"draftId": item["draftId"], "kind": item["report"].get("kind"), "title": item["report"].get("title"),
|
|
131
|
+
"queuedAt": int(item.get("queuedAt", 0) * 1000), "attempts": item.get("attempts", 0),
|
|
132
|
+
"lastError": item.get("lastError"), "screenshot": bool(item["report"].get("screenshot")),
|
|
133
|
+
"diagnostics": bool(item["report"].get("diagnostics"))} for item in self.items()]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_outboxes = {}
|
|
137
|
+
_outboxes_lock = threading.Lock()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def outbox_for(runtime):
|
|
141
|
+
"""The runtime's outbox: under its data folder when it has one (bundle.data_dir), else in memory."""
|
|
142
|
+
from .bundle import data_dir
|
|
143
|
+
with _outboxes_lock:
|
|
144
|
+
box = _outboxes.get(id(runtime))
|
|
145
|
+
if box is None or box[0] is not runtime:
|
|
146
|
+
directory = data_dir(runtime)
|
|
147
|
+
box = (runtime, Outbox(directory / "feedback" / "outbox" if directory else None))
|
|
148
|
+
_outboxes[id(runtime)] = box
|
|
149
|
+
return box[1]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def send(runtime, report, token):
|
|
153
|
+
"""Sends ``report`` now, or keeps it in the outbox when the service can't take it yet. The service answers a second
|
|
154
|
+
send of the same draft (``clientId``) with the same reference, so sending again never makes a second report."""
|
|
155
|
+
box = outbox_for(runtime)
|
|
156
|
+
try:
|
|
157
|
+
feedback = submit(report, token)
|
|
158
|
+
except SendError as error:
|
|
159
|
+
if error.code in QUEUED_CODES:
|
|
160
|
+
box.put(report, error.code)
|
|
161
|
+
return {"queued": True, "draftId": report["clientId"], "reason": str(error)}
|
|
162
|
+
raise APIError(str(error), error.status, error.code) from None
|
|
163
|
+
finally:
|
|
164
|
+
token = None
|
|
165
|
+
box.remove(report["clientId"])
|
|
166
|
+
return {"feedback": feedback, "queued": False}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def flush(runtime, token):
|
|
170
|
+
"""Tries every waiting report, oldest first; stops at the first one the service can't take now."""
|
|
171
|
+
box = outbox_for(runtime)
|
|
172
|
+
sent, error = [], None
|
|
173
|
+
try:
|
|
174
|
+
for item in box.items():
|
|
175
|
+
try:
|
|
176
|
+
feedback = submit(item["report"], token)
|
|
177
|
+
except SendError as problem:
|
|
178
|
+
if problem.code in QUEUED_CODES:
|
|
179
|
+
box.touch(item, problem.code)
|
|
180
|
+
error = str(problem)
|
|
181
|
+
break
|
|
182
|
+
if problem.code == "session":
|
|
183
|
+
error = str(problem)
|
|
184
|
+
break
|
|
185
|
+
# The service refuses this report for good (a field it won't take): it would wait forever.
|
|
186
|
+
box.remove(item["draftId"])
|
|
187
|
+
error = f"Mobster couldn't send “{item['report'].get('title', '')}”: {problem}"
|
|
188
|
+
continue
|
|
189
|
+
box.remove(item["draftId"])
|
|
190
|
+
sent.append({"draftId": item["draftId"], "reference": feedback["reference"]})
|
|
191
|
+
finally:
|
|
192
|
+
token = None
|
|
193
|
+
out = {"sent": sent, "waiting": len(box.items())}
|
|
194
|
+
if error:
|
|
195
|
+
out["error"] = error
|
|
196
|
+
return out
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Track ``feedback``'s registrations (tracks.load calls ``register(api)`` once per process, staged): the routes under
|
|
2
|
+
/api/feedback (routes.py)."""
|
|
3
|
+
|
|
4
|
+
from . import routes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(api):
|
|
8
|
+
"""``api`` is mobile_agent.harness_api."""
|
|
9
|
+
routes.register_routes()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""The local API for in-app feedback, under /api/feedback (api_routes, owner "feedback")."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from ..api_errors import APIError
|
|
8
|
+
from ..api_routes import Response, Route, register
|
|
9
|
+
from ..service_support import needs_app
|
|
10
|
+
from . import bundle, outbox
|
|
11
|
+
|
|
12
|
+
MAX_BODY = 6_000_000
|
|
13
|
+
KINDS = ("bug", "feature", "support")
|
|
14
|
+
FIELDS = {"draftId", "kind", "title", "description", "email", "runId", "app", "screenshot", "diagnostics",
|
|
15
|
+
"accessToken"}
|
|
16
|
+
APP_FIELDS = ("version", "macos", "connection", "phone", "ios")
|
|
17
|
+
APP_ONLY = "Sending feedback is in Mobster for Mac."
|
|
18
|
+
MASKS = re.compile(r"\[masked\]|••••••")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _json(request):
|
|
22
|
+
body = request.body
|
|
23
|
+
if isinstance(body, (bytes, bytearray)):
|
|
24
|
+
try:
|
|
25
|
+
body = json.loads(body)
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise ValueError("The body must be a JSON object") from None
|
|
28
|
+
if not isinstance(body, dict):
|
|
29
|
+
raise ValueError("The body must be a JSON object")
|
|
30
|
+
return body
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _token(body):
|
|
34
|
+
token = body.pop("accessToken", None)
|
|
35
|
+
if not isinstance(token, str) or not 16 <= len(token) <= 8192:
|
|
36
|
+
raise APIError("Sign in to Mobster to send feedback.", 401, "session")
|
|
37
|
+
return token
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _masked(text, emails=True):
|
|
41
|
+
"""(masked text, how many values it masked)."""
|
|
42
|
+
if not isinstance(text, str):
|
|
43
|
+
return text, 0
|
|
44
|
+
out = bundle.scrub(text, emails=emails)
|
|
45
|
+
return out, max(0, len(MASKS.findall(out)) - len(MASKS.findall(text)))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def report_of(body):
|
|
49
|
+
"""The report as it goes to the account service: checked, masked."""
|
|
50
|
+
unknown = set(body) - FIELDS
|
|
51
|
+
if unknown:
|
|
52
|
+
raise ValueError(f"{sorted(unknown)[0][:40]} isn't a feedback field")
|
|
53
|
+
draft = body.get("draftId")
|
|
54
|
+
if not isinstance(draft, str) or not outbox.DRAFT_ID.fullmatch(draft):
|
|
55
|
+
raise ValueError("draftId is the draft's id, a lowercase UUID")
|
|
56
|
+
if body.get("kind") not in KINDS:
|
|
57
|
+
raise ValueError("kind is bug, feature or support")
|
|
58
|
+
title = body.get("title")
|
|
59
|
+
if not isinstance(title, str) or not 1 <= len(title.strip()) <= 120:
|
|
60
|
+
raise ValueError("Add a title of 1 to 120 characters")
|
|
61
|
+
description = body.get("description") or ""
|
|
62
|
+
if not isinstance(description, str) or len(description) > 5000:
|
|
63
|
+
raise ValueError("Keep the description under 5,000 characters")
|
|
64
|
+
masked = 0
|
|
65
|
+
title, count = _masked(" ".join(title.split()), emails=False)
|
|
66
|
+
masked += count
|
|
67
|
+
description, count = _masked(description.strip(), emails=False)
|
|
68
|
+
masked += count
|
|
69
|
+
report = {"clientId": draft, "kind": body["kind"], "title": title[:120], "description": description}
|
|
70
|
+
email = body.get("email")
|
|
71
|
+
if email not in (None, ""):
|
|
72
|
+
if (not isinstance(email, str) or len(email) > 254
|
|
73
|
+
or not re.fullmatch(r"[^\s@]+@[^\s@]+\.[A-Za-z]{2,}", email.strip())):
|
|
74
|
+
raise ValueError("That reply-to email doesn't look right")
|
|
75
|
+
report["email"] = email.strip()
|
|
76
|
+
run_id = body.get("runId")
|
|
77
|
+
if run_id is not None:
|
|
78
|
+
if not isinstance(run_id, str) or not re.fullmatch(r"[a-f0-9]{12}", run_id):
|
|
79
|
+
raise ValueError("runId is a task's id")
|
|
80
|
+
report["runId"] = run_id
|
|
81
|
+
app = body.get("app")
|
|
82
|
+
if app is not None:
|
|
83
|
+
if not isinstance(app, dict) or set(app) - set(APP_FIELDS):
|
|
84
|
+
raise ValueError("app is {version, macos, connection, phone, ios}")
|
|
85
|
+
facts = {}
|
|
86
|
+
for key in APP_FIELDS:
|
|
87
|
+
value = app.get(key)
|
|
88
|
+
if value is None:
|
|
89
|
+
continue
|
|
90
|
+
if not isinstance(value, str):
|
|
91
|
+
raise ValueError(f"app.{key} is text")
|
|
92
|
+
text, count = _masked(" ".join(value.split())[:80])
|
|
93
|
+
masked += count
|
|
94
|
+
facts[key] = text
|
|
95
|
+
report["app"] = facts
|
|
96
|
+
shot = body.get("screenshot")
|
|
97
|
+
if shot is not None:
|
|
98
|
+
if (not isinstance(shot, dict) or set(shot) != {"type", "data"}
|
|
99
|
+
or shot["type"] not in ("image/png", "image/jpeg")
|
|
100
|
+
or not isinstance(shot["data"], str)):
|
|
101
|
+
raise ValueError("screenshot is {type: image/png or image/jpeg, data: base64}")
|
|
102
|
+
try:
|
|
103
|
+
raw = base64.b64decode(shot["data"], validate=True)
|
|
104
|
+
except ValueError:
|
|
105
|
+
raise ValueError("The screenshot isn't base64") from None
|
|
106
|
+
if len(raw) > 2_500_000:
|
|
107
|
+
raise APIError("That screenshot is over 2.5 MB. Crop it, or send the report without one.", 413, "too_large")
|
|
108
|
+
png, jpeg = raw[:8] == b"\x89PNG\r\n\x1a\n", raw[:3] == b"\xff\xd8\xff"
|
|
109
|
+
if not (png if shot["type"] == "image/png" else jpeg):
|
|
110
|
+
raise ValueError("The screenshot isn't the image it says it is")
|
|
111
|
+
report["screenshot"] = {"type": shot["type"], "data": shot["data"]}
|
|
112
|
+
diagnostics = body.get("diagnostics")
|
|
113
|
+
if diagnostics is not None:
|
|
114
|
+
before = len(MASKS.findall(diagnostics)) if isinstance(diagnostics, str) else 0
|
|
115
|
+
report["diagnostics"] = bundle.check(diagnostics)
|
|
116
|
+
masked += max(0, len(MASKS.findall(report["diagnostics"])) - before)
|
|
117
|
+
return report, masked
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def diagnostics(request):
|
|
121
|
+
body = request.body or {}
|
|
122
|
+
if set(body) - {"runId", "appVersion"}:
|
|
123
|
+
raise ValueError("The diagnostics take runId and appVersion")
|
|
124
|
+
run_id = body.get("runId")
|
|
125
|
+
if run_id is not None and (not isinstance(run_id, str) or not re.fullmatch(r"[a-f0-9]{12}", run_id)):
|
|
126
|
+
raise ValueError("runId is a task's id")
|
|
127
|
+
version = body.get("appVersion")
|
|
128
|
+
if version is not None and not isinstance(version, str):
|
|
129
|
+
raise ValueError("appVersion is text")
|
|
130
|
+
return Response.json(bundle.build(request.runtime, run_id=run_id, app_version=(version or "")[:80] or None))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def send(request):
|
|
134
|
+
needs_app(request, APP_ONLY)
|
|
135
|
+
body = _json(request)
|
|
136
|
+
token = _token(body)
|
|
137
|
+
try:
|
|
138
|
+
report, masked = report_of(body)
|
|
139
|
+
answer = outbox.send(request.runtime, report, token)
|
|
140
|
+
finally:
|
|
141
|
+
token = None
|
|
142
|
+
return Response.json({**answer, "masked": masked}, 202 if answer["queued"] else 201)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def waiting(request):
|
|
146
|
+
return Response.json({"items": outbox.outbox_for(request.runtime).public()})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def flush(request):
|
|
150
|
+
needs_app(request, APP_ONLY)
|
|
151
|
+
body = request.body or {}
|
|
152
|
+
token = _token(body)
|
|
153
|
+
if body:
|
|
154
|
+
raise ValueError("Sending waiting reports takes only the account's accessToken")
|
|
155
|
+
try:
|
|
156
|
+
return Response.json(outbox.flush(request.runtime, token))
|
|
157
|
+
finally:
|
|
158
|
+
token = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def delete(request):
|
|
162
|
+
needs_app(request, APP_ONLY)
|
|
163
|
+
draft = request.params["draft"]
|
|
164
|
+
if not outbox.outbox_for(request.runtime).remove(draft):
|
|
165
|
+
raise APIError("That report isn't waiting to send.", 404, "not_found")
|
|
166
|
+
return Response.json({"deleted": draft})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def register_routes():
|
|
170
|
+
for route in (Route("feedback", "POST", "/api/feedback/diagnostics", diagnostics),
|
|
171
|
+
Route("feedback", "POST", "/api/feedback", send, body="raw", max_bytes=MAX_BODY),
|
|
172
|
+
Route("feedback", "GET", "/api/feedback/outbox", waiting, body="none"),
|
|
173
|
+
Route("feedback", "POST", "/api/feedback/outbox/flush", flush),
|
|
174
|
+
Route("feedback", "DELETE",
|
|
175
|
+
"/api/feedback/outbox/(?P<draft>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
|
|
176
|
+
delete, body="none")):
|
|
177
|
+
register(route)
|